1 /*****************************************************************************
2 * Copyright (C) 2016-2019 Krusader Krew [https://krusader.org] *
3 * *
4 * This file is part of Krusader [https://krusader.org]. *
5 * *
6 * Krusader is free software: you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation, either version 2 of the License, or *
9 * (at your option) any later version. *
10 * *
11 * Krusader is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU General Public License *
17 * along with Krusader. If not, see [http://www.gnu.org/licenses/]. *
18 *****************************************************************************/
19
20 #include "jobman.h"
21
22 // QtCore
23 #include <QDebug>
24 #include <QUrl>
25 // QtWidgets
26 #include <QComboBox>
27 #include <QLabel>
28 #include <QMenu>
29 #include <QPushButton>
30 #include <QVBoxLayout>
31 #include <QWidgetAction>
32
33 #include <KConfigCore/KSharedConfig>
34 #include <KI18n/KLocalizedString>
35 #include <KIOWidgets/KIO/FileUndoManager>
36
37 #include "krjob.h"
38 #include "../krglobal.h"
39 #include "../icon.h"
40
41 const int MAX_OLD_MENU_ACTIONS = 10;
42
43 /** The menu action entry for a job in the popup menu.*/
44 class JobMenuAction : public QWidgetAction
45 {
46 Q_OBJECT
47 public:
JobMenuAction(KrJob * krJob,QObject * parent,KJob * kJob=nullptr)48 JobMenuAction(KrJob *krJob, QObject *parent, KJob *kJob = nullptr)
49 : QWidgetAction(parent), m_krJob(krJob)
50 {
51 QWidget *container = new QWidget();
52 QGridLayout *layout = new QGridLayout(container);
53 m_description = new QLabel(krJob->description());
54 m_progressBar = new QProgressBar();
55 layout->addWidget(m_description, 0, 0, 1, 3);
56 layout->addWidget(m_progressBar, 1, 0);
57
58 m_pauseResumeButton = new QPushButton();
59 updatePauseResumeButton();
60 connect(m_pauseResumeButton, &QPushButton::clicked, this,
61 &JobMenuAction::slotPauseResumeButtonClicked);
62 layout->addWidget(m_pauseResumeButton, 1, 1);
63
64 m_cancelButton = new QPushButton();
65 m_cancelButton->setIcon(Icon("remove"));
66 m_cancelButton->setToolTip(i18n("Cancel Job"));
67 connect(m_cancelButton, &QPushButton::clicked,
68 this, &JobMenuAction::slotCancelButtonClicked);
69 layout->addWidget(m_cancelButton, 1, 2);
70
71 setDefaultWidget(container);
72
73 if (kJob) {
74 slotStarted(kJob);
75 } else {
76 connect(krJob, &KrJob::started, this, &JobMenuAction::slotStarted);
77 }
78
79 connect(krJob, &KrJob::terminated, this, &JobMenuAction::slotTerminated);
80 }
81
isDone()82 bool isDone() { return !m_krJob; }
83
84 protected slots:
slotDescription(KJob *,const QString & description,const QPair<QString,QString> & field1,const QPair<QString,QString> & field2)85 void slotDescription(KJob *, const QString &description,
86 const QPair<QString, QString> &field1,
87 const QPair<QString, QString> &field2)
88 {
89 const QPair<QString, QString> textField = !field2.first.isEmpty() ? field2 : field1;
90 QString text = description;
91 if (!textField.first.isEmpty()) {
92 text += QString(" - %1: %2").arg(textField.first, textField.second);
93 }
94 m_description->setText(text);
95
96 if (!field2.first.isEmpty() && !field1.first.isEmpty()) {
97 // NOTE: tooltips for QAction items in menu are not shown
98 m_progressBar->setToolTip(QString("%1: %2").arg(field1.first, field1.second));
99 }
100 }
101
slotPercent(KJob *,unsigned long percent)102 void slotPercent(KJob *, unsigned long percent) { m_progressBar->setValue(percent); }
103
updatePauseResumeButton()104 void updatePauseResumeButton()
105 {
106 m_pauseResumeButton->setIcon(Icon(
107 m_krJob->isRunning() ? "media-playback-pause" :
108 m_krJob->isPaused() ? "media-playback-start" : "chronometer-start"));
109 m_pauseResumeButton->setToolTip(m_krJob->isRunning() ? i18n("Pause Job") :
110 m_krJob->isPaused() ? i18n("Resume Job") :
111 i18n("Start Job"));
112 }
113
slotResult(KJob * job)114 void slotResult(KJob *job)
115 {
116 // NOTE: m_job may already set to NULL now
117 if(!job->error()) {
118 // percent signal is not reliable, set manually
119 m_progressBar->setValue(100);
120 }
121 }
122
slotTerminated()123 void slotTerminated()
124 {
125 qDebug() << "job description=" << m_krJob->description();
126 m_pauseResumeButton->setEnabled(false);
127 m_cancelButton->setIcon(Icon("edit-clear"));
128 m_cancelButton->setToolTip(i18n("Clear"));
129
130 m_krJob = nullptr;
131 }
132
slotPauseResumeButtonClicked()133 void slotPauseResumeButtonClicked()
134 {
135 if (!m_krJob)
136 return;
137
138 if (m_krJob->isRunning())
139 m_krJob->pause();
140 else
141 m_krJob->start();
142 }
143
slotCancelButtonClicked()144 void slotCancelButtonClicked()
145 {
146 if (m_krJob) {
147 m_krJob->cancel();
148 } else {
149 deleteLater();
150 }
151 }
152
153 private slots:
slotStarted(KJob * job)154 void slotStarted(KJob *job)
155 {
156 connect(job, &KJob::description, this, &JobMenuAction::slotDescription);
157 connect(job, SIGNAL(percent(KJob *, ulong)), this, SLOT(slotPercent(KJob *, ulong)));
158 connect(job, &KJob::suspended, this, &JobMenuAction::updatePauseResumeButton);
159 connect(job, &KJob::resumed, this, &JobMenuAction::updatePauseResumeButton);
160 connect(job, &KJob::result, this, &JobMenuAction::slotResult);
161 connect(job, &KJob::warning, this, [](KJob *, const QString &plain, const QString &) {
162 qWarning() << "unexpected job warning: " << plain;
163 });
164
165 updatePauseResumeButton();
166 }
167
168 private:
169 KrJob *m_krJob;
170
171 QLabel *m_description;
172 QProgressBar *m_progressBar;
173 QPushButton *m_pauseResumeButton;
174 QPushButton *m_cancelButton;
175 };
176
177 #include "jobman.moc" // required for class definitions with Q_OBJECT macro in implementation files
178
179
180 const QString JobMan::sDefaultToolTip = i18n("No jobs");
181
JobMan(QObject * parent)182 JobMan::JobMan(QObject *parent) : QObject(parent), m_messageBox(0)
183 {
184 // job control action
185 m_controlAction = new KToolBarPopupAction(Icon("media-playback-pause"),
186 i18n("Play/Pause &Job"), this);
187 m_controlAction->setEnabled(false);
188 connect(m_controlAction, &QAction::triggered, this, &JobMan::slotControlActionTriggered);
189
190 QMenu *menu = new QMenu(krMainWindow);
191 menu->setMinimumWidth(300);
192 // make scrollable if menu is too long
193 menu->setStyleSheet("QMenu { menu-scrollable: 1; }");
194 m_controlAction->setMenu(menu);
195
196 // progress bar action
197 m_progressBar = new QProgressBar();
198 m_progressBar->setToolTip(sDefaultToolTip);
199 m_progressBar->setEnabled(false);
200 // listen to clicks on progress bar
201 m_progressBar->installEventFilter(this);
202
203 QWidgetAction *progressAction = new QWidgetAction(krMainWindow);
204 progressAction->setText(i18n("Job Progress Bar"));
205 progressAction->setDefaultWidget(m_progressBar);
206 m_progressAction = progressAction;
207
208 // job queue mode action
209 KConfigGroup cfg(krConfig, "JobManager");
210 m_queueMode = cfg.readEntry("Queue Mode", false);
211 m_modeAction = new QAction(Icon("media-playlist-repeat"), i18n("Job Queue Mode"),
212 krMainWindow);
213 m_modeAction->setToolTip(i18n("Run only one job in parallel"));
214 m_modeAction->setCheckable(true);
215 m_modeAction->setChecked(m_queueMode);
216 connect(m_modeAction, &QAction::toggled, this, [=](bool checked) mutable {
217 m_queueMode = checked;
218 cfg.writeEntry("Queue Mode", m_queueMode);
219 });
220
221 // undo action
222 KIO::FileUndoManager *undoManager = KIO::FileUndoManager::self();
223 undoManager->uiInterface()->setParentWidget(krMainWindow);
224
225 m_undoAction = new QAction(Icon("edit-undo"), i18n("Undo Last Job"), krMainWindow);
226 m_undoAction->setEnabled(false);
227 connect(m_undoAction, &QAction::triggered, undoManager, &KIO::FileUndoManager::undo);
228 connect(undoManager, static_cast<void(KIO::FileUndoManager::*)(bool)>(&KIO::FileUndoManager::undoAvailable),
229 m_undoAction, &QAction::setEnabled);
230 connect(undoManager, &KIO::FileUndoManager::undoTextChanged, this, &JobMan::slotUndoTextChange);
231 }
232
waitForJobs(bool waitForUserInput)233 bool JobMan::waitForJobs(bool waitForUserInput)
234 {
235 if (m_jobs.isEmpty() && !waitForUserInput)
236 return true;
237
238 // attempt to get all job threads does not work
239 //QList<QThread *> threads = krMainWindow->findChildren<QThread *>();
240
241 m_autoCloseMessageBox = !waitForUserInput;
242
243 m_messageBox = new QMessageBox(krMainWindow);
244 m_messageBox->setWindowTitle(i18n("Warning"));
245 m_messageBox->setIconPixmap(Icon("dialog-warning")
246 .pixmap(QMessageBox::standardIcon(QMessageBox::Information).size()));
247 m_messageBox->setText(i18n("Are you sure you want to quit?"));
248 m_messageBox->addButton(QMessageBox::Abort);
249 m_messageBox->addButton(QMessageBox::Cancel);
250 m_messageBox->setDefaultButton(QMessageBox::Cancel);
251 for (KrJob *job: m_jobs)
252 connect(job, &KrJob::terminated, this, &JobMan::slotUpdateMessageBox);
253 slotUpdateMessageBox();
254
255 int result = m_messageBox->exec(); // blocking
256 m_messageBox->deleteLater();
257 m_messageBox = 0;
258
259 // accepted -> cancel all jobs
260 if (result == QMessageBox::Abort) {
261 for (KrJob *job: m_jobs) {
262 job->cancel();
263 }
264 return true;
265 }
266 // else:
267 return false;
268 }
269
manageJob(KrJob * job,StartMode startMode)270 void JobMan::manageJob(KrJob *job, StartMode startMode)
271 {
272 qDebug() << "new job, startMode=" << startMode;
273 managePrivate(job);
274
275 connect(job, &KrJob::started, this, &JobMan::slotKJobStarted);
276
277 const bool enqueue = startMode == Enqueue || (startMode == Default && m_queueMode);
278 if (startMode == Start || (startMode == Default && !m_queueMode) ||
279 (enqueue && !jobsAreRunning())) {
280 job->start();
281 }
282
283 updateUI();
284 }
285
manageStartedJob(KrJob * krJob,KJob * kJob)286 void JobMan::manageStartedJob(KrJob *krJob, KJob *kJob)
287 {
288 managePrivate(krJob, kJob);
289 slotKJobStarted(kJob);
290 updateUI();
291 }
292
293 // #### protected slots
294
slotKJobStarted(KJob * job)295 void JobMan::slotKJobStarted(KJob *job)
296 {
297 // KJob has two percent() functions
298 connect(job, SIGNAL(percent(KJob*,ulong)), this,
299 SLOT(slotPercent(KJob*,ulong)));
300 connect(job, &KJob::description, this, &JobMan::slotDescription);
301 connect(job, &KJob::suspended, this, &JobMan::updateUI);
302 connect(job, &KJob::resumed, this, &JobMan::updateUI);
303 }
304
slotControlActionTriggered()305 void JobMan::slotControlActionTriggered()
306 {
307 if (m_jobs.isEmpty()) {
308 m_controlAction->menu()->clear();
309 m_controlAction->setEnabled(false);
310 return;
311 }
312
313 const bool anyRunning = jobsAreRunning();
314 if (!anyRunning && m_queueMode) {
315 m_jobs.first()->start();
316 } else {
317 for (KrJob *job : m_jobs) {
318 if (anyRunning)
319 job->pause();
320 else
321 job->start();
322 }
323 }
324 }
325
slotPercent(KJob *,unsigned long)326 void JobMan::slotPercent(KJob *, unsigned long)
327 {
328 updateUI();
329 }
330
slotDescription(KJob *,const QString & description,const QPair<QString,QString> & field1,const QPair<QString,QString> & field2)331 void JobMan::slotDescription(KJob*,const QString &description, const QPair<QString,QString> &field1,
332 const QPair<QString,QString> &field2)
333 {
334 // TODO cache all descriptions
335 if (m_jobs.length() > 1)
336 return;
337
338 m_progressBar->setToolTip(
339 QString("%1\n%2: %3\n%4: %5")
340 .arg(description, field1.first, field1.second, field2.first, field2.second));
341 }
342
slotTerminated(KrJob * krJob)343 void JobMan::slotTerminated(KrJob *krJob)
344 {
345 qDebug() << "terminated, job description: " << krJob->description();
346
347 m_jobs.removeAll(krJob);
348
349 // NOTE: ignoring queue mode here. We assume that if queue mode is turned off, the user created
350 // jobs which were not already started with a "queue" option and still wants queue behaviour.
351 if (!m_jobs.isEmpty() && !jobsAreRunning()) {
352 foreach (KrJob *job, m_jobs) {
353 if (!job->isPaused()) {
354 // start next job
355 job->start();
356 break;
357 }
358 }
359 }
360
361 updateUI();
362 cleanupMenu();
363 }
364
slotUpdateControlAction()365 void JobMan::slotUpdateControlAction()
366 {
367 m_controlAction->setEnabled(!m_controlAction->menu()->isEmpty());
368 }
369
slotUndoTextChange(const QString & text)370 void JobMan::slotUndoTextChange(const QString &text)
371 {
372 m_undoAction->setToolTip(KIO::FileUndoManager::self()->undoAvailable() ? text :
373 i18n("Undo Last Job"));
374 }
375
slotUpdateMessageBox()376 void JobMan::slotUpdateMessageBox()
377 {
378 if (!m_messageBox)
379 return;
380
381 if (m_jobs.isEmpty() && m_autoCloseMessageBox) {
382 m_messageBox->done(QMessageBox::Abort);
383 return;
384 }
385
386 if (m_jobs.isEmpty()) {
387 m_messageBox->setInformativeText("");
388 m_messageBox->setButtonText(QMessageBox::Abort, "Quit");
389 return;
390 }
391
392 m_messageBox->setInformativeText(i18np("There is one job operation left.",
393 "There are %1 job operations left.", m_jobs.length()));
394 m_messageBox->setButtonText(QMessageBox::Abort, "Abort Jobs and Quit");
395 }
396
397 // #### private
398
managePrivate(KrJob * job,KJob * kJob)399 void JobMan::managePrivate(KrJob *job, KJob *kJob)
400 {
401 JobMenuAction *menuAction = new JobMenuAction(job, m_controlAction, kJob);
402 connect(menuAction, &QObject::destroyed, this, &JobMan::slotUpdateControlAction);
403 m_controlAction->menu()->addAction(menuAction);
404 cleanupMenu();
405
406 slotUpdateControlAction();
407
408 connect(job, &KrJob::terminated, this, &JobMan::slotTerminated);
409
410 m_jobs.append(job);
411 }
412
cleanupMenu()413 void JobMan::cleanupMenu() {
414 const QList<QAction *> actions = m_controlAction->menu()->actions();
415 for (QAction *action : actions) {
416 if (m_controlAction->menu()->actions().count() <= MAX_OLD_MENU_ACTIONS)
417 break;
418 JobMenuAction *jobAction = static_cast<JobMenuAction *>(action);
419 if (jobAction->isDone()) {
420 m_controlAction->menu()->removeAction(action);
421 action->deleteLater();
422 }
423 }
424 }
425
updateUI()426 void JobMan::updateUI()
427 {
428 int totalPercent = 0;
429 for (KrJob *job: m_jobs) {
430 totalPercent += job->percent();
431 }
432 const bool hasJobs = !m_jobs.isEmpty();
433 m_progressBar->setEnabled(hasJobs);
434 if (hasJobs) {
435 m_progressBar->setValue(totalPercent / m_jobs.length());
436 } else {
437 m_progressBar->reset();
438 }
439 if (!hasJobs)
440 m_progressBar->setToolTip(i18n("No Jobs"));
441 if (m_jobs.length() > 1)
442 m_progressBar->setToolTip(i18np("%1 Job", "%1 Jobs", m_jobs.length()));
443
444 const bool running = jobsAreRunning();
445 m_controlAction->setIcon(Icon(
446 !hasJobs ? "edit-clear" : running ? "media-playback-pause" : "media-playback-start"));
447 m_controlAction->setToolTip(!hasJobs ? i18n("Clear Job List") : running ?
448 i18n("Pause All Jobs") :
449 i18n("Resume Job List"));
450 }
451
jobsAreRunning()452 bool JobMan::jobsAreRunning()
453 {
454 for (KrJob *job: m_jobs)
455 if (job->isRunning())
456 return true;
457
458 return false;
459 }
460