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