1 #include "common/common_pch.h"
2
3 #include <QCursor>
4 #include <QDebug>
5 #include <QFileDialog>
6 #include <QMenu>
7 #include <QPushButton>
8 #include <QtGlobal>
9
10 #include "common/list_utils.h"
11 #include "common/qt.h"
12 #include "common/strings/formatting.h"
13 #include "mkvtoolnix-gui/app.h"
14 #include "mkvtoolnix-gui/forms/watch_jobs/tab.h"
15 #include "mkvtoolnix-gui/jobs/mux_job.h"
16 #include "mkvtoolnix-gui/jobs/tool.h"
17 #include "mkvtoolnix-gui/main_window/main_window.h"
18 #include "mkvtoolnix-gui/util/date_time.h"
19 #include "mkvtoolnix-gui/util/file.h"
20 #include "mkvtoolnix-gui/util/file_dialog.h"
21 #include "mkvtoolnix-gui/util/message_box.h"
22 #include "mkvtoolnix-gui/util/settings.h"
23 #include "mkvtoolnix-gui/util/widget.h"
24 #include "mkvtoolnix-gui/watch_jobs/tab.h"
25 #include "mkvtoolnix-gui/watch_jobs/tool.h"
26
27 constexpr auto MTX_RUN_PROGRAM_CONFIGURATION_ADDRESS = "mtxRunProgramConfigurationAddress";
28 constexpr auto MTX_RUN_PROGRAM_CONFIGURATION_CONDITION = "mtxRunProgramConfigurationCondition";
29
30 using namespace mtx::gui;
31
32 namespace mtx::gui::WatchJobs {
33
34 class TabPrivate {
35 friend class Tab;
36
37 // UI stuff:
38 std::unique_ptr<Ui::Tab> ui;
39 QStringList m_fullOutput;
40 std::optional<uint64_t> m_id;
41 uint64_t m_currentJobProgress, m_queueProgress;
42 QHash<Jobs::Job::LineType, bool> m_currentJobLineTypeSeen;
43 Jobs::Job::Status m_currentJobStatus;
44 QDateTime m_currentJobStartTime;
45 QString m_currentJobDescription;
46 QMenu *m_whenFinished, *m_moreActions;
47 bool m_forCurrentJob;
48
49 // Only use this variable for determining whether or not to ignore
50 // certain Q_SIGNALS.
51 QObject const *m_currentlyConnectedJob;
52
53 QAction *m_saveOutputAction, *m_clearOutputAction, *m_openFolderAction;
54
TabPrivate(Tab * tab,bool forCurrentJob)55 explicit TabPrivate(Tab *tab, bool forCurrentJob)
56 : ui{new Ui::Tab}
57 , m_currentJobProgress{}
58 , m_queueProgress{}
59 , m_currentJobStatus{Jobs::Job::PendingManual}
60 , m_whenFinished{new QMenu{tab}}
61 , m_moreActions{new QMenu{tab}}
62 , m_forCurrentJob{forCurrentJob}
63 , m_currentlyConnectedJob{}
64 , m_saveOutputAction{new QAction{tab}}
65 , m_clearOutputAction{new QAction{tab}}
66 , m_openFolderAction{new QAction{tab}}
67 {
68 }
69 };
70
Tab(QWidget * parent,bool forCurrentJob)71 Tab::Tab(QWidget *parent,
72 bool forCurrentJob)
73 : QWidget{parent}
74 , p_ptr{new TabPrivate{this, forCurrentJob}}
75 {
76 setupUi();
77 }
78
~Tab()79 Tab::~Tab() {
80 }
81
82 void
setupUi()83 Tab::setupUi() {
84 auto p = p_func();
85
86 // Setup UI controls.
87 p->ui->setupUi(this);
88
89 p->m_saveOutputAction->setEnabled(false);
90
91 Util::preventScrollingWithoutFocus(this);
92
93 auto &cfg = Util::Settings::get();
94 for (auto const &splitter : findChildren<QSplitter *>())
95 cfg.handleSplitterSizes(splitter);
96
97 setupMoreActionsMenu();
98
99 retranslateUi();
100
101 p->ui->acknowledgeWarningsAndErrorsButton->setIcon(QIcon{Q(":/icons/16x16/dialog-ok-apply.png")});
102 p->ui->abortButton->setIcon(QIcon{Q(":/icons/16x16/dialog-cancel.png")});
103
104 auto model = MainWindow::jobTool()->model();
105
106 connect(p->ui->abortButton, &QPushButton::clicked, this, &Tab::onAbort);
107 connect(p->ui->acknowledgeWarningsAndErrorsButton, &QPushButton::clicked, this, &Tab::acknowledgeWarningsAndErrors);
108 connect(model, &Jobs::Model::progressChanged, this, &Tab::onQueueProgressChanged);
109 connect(model, &Jobs::Model::queueStatusChanged, this, &Tab::updateRemainingTime);
110 connect(p->m_whenFinished, &QMenu::aboutToShow, this, &Tab::setupWhenFinishedActions);
111 connect(p->m_moreActions, &QMenu::aboutToShow, this, &Tab::enableMoreActionsActions);
112 connect(p->m_saveOutputAction, &QAction::triggered, this, &Tab::onSaveOutput);
113 connect(p->m_clearOutputAction, &QAction::triggered, this, &Tab::clearOutput);
114 connect(p->m_openFolderAction, &QAction::triggered, this, &Tab::openFolder);
115 connect(MainWindow::jobTool()->model(), &Jobs::Model::numUnacknowledgedWarningsOrErrorsChanged, this, &Tab::disableButtonIfAllWarningsAndErrorsButtonAcknowledged);
116 }
117
118 void
setupMoreActionsMenu()119 Tab::setupMoreActionsMenu() {
120 auto p = p_func();
121
122 p->ui->whenFinishedButton->setMenu(p->m_whenFinished);
123
124 // Setup the "more actions" menu.
125 p->m_moreActions->addAction(p->m_openFolderAction);
126 p->m_moreActions->addSeparator();
127 p->m_moreActions->addAction(p->m_saveOutputAction);
128
129 if (isCurrentJobTab())
130 p->m_moreActions->addAction(p->m_clearOutputAction);
131
132 p->ui->moreActionsButton->setMenu(p->m_moreActions);
133
134 p->m_openFolderAction->setIcon(QIcon{Q(":/icons/16x16/document-open-folder.png")});
135 p->m_saveOutputAction->setIcon(QIcon{Q(":/icons/16x16/document-save.png")});
136 }
137
138 void
retranslateUi()139 Tab::retranslateUi() {
140 auto p = p_func();
141
142 p->ui->retranslateUi(this);
143
144 p->ui->description->setText(p->m_currentJobDescription.isEmpty() ? QY("No job has been started yet.") : p->m_currentJobDescription);
145 p->m_saveOutputAction->setText(QY("&Save output"));
146 p->m_clearOutputAction->setText(QY("&Clear output and reset progress"));
147 p->m_openFolderAction->setText(QY("&Open folder"));
148
149 p->ui->output->setPlaceholderText(QY("No output yet"));
150 p->ui->warnings->setPlaceholderText(QY("No warnings yet"));
151 p->ui->errors->setPlaceholderText(QY("No errors yet"));
152 }
153
154 void
connectToJob(Jobs::Job const & job)155 Tab::connectToJob(Jobs::Job const &job) {
156 auto p = p_func();
157 p->m_currentlyConnectedJob = &job;
158 p->m_id = job.id();
159 auto connType = static_cast<Qt::ConnectionType>(Qt::AutoConnection | Qt::UniqueConnection);
160
161 connect(&job, &Jobs::Job::statusChanged, this, &Tab::onStatusChanged, connType);
162 connect(&job, &Jobs::Job::progressChanged, this, &Tab::onJobProgressChanged, connType);
163 connect(&job, &Jobs::Job::lineRead, this, &Tab::onLineRead, connType);
164 }
165
166 void
disconnectFromJob(Jobs::Job const & job)167 Tab::disconnectFromJob(Jobs::Job const &job) {
168 auto p = p_func();
169
170 if (p->m_currentlyConnectedJob == &job) {
171 p->m_currentlyConnectedJob = nullptr;
172 p->m_id.reset();
173 }
174
175 disconnect(&job, &Jobs::Job::statusChanged, this, &Tab::onStatusChanged);
176 disconnect(&job, &Jobs::Job::progressChanged, this, &Tab::onJobProgressChanged);
177 disconnect(&job, &Jobs::Job::lineRead, this, &Tab::onLineRead);
178 }
179
180 uint64_t
queueProgress() const181 Tab::queueProgress()
182 const {
183 return p_func()->m_queueProgress;
184 }
185
186 void
onAbort()187 Tab::onAbort() {
188 auto p = p_func();
189
190 if (!p->m_id)
191 return;
192
193 if ( Util::Settings::get().m_warnBeforeAbortingJobs
194 && (Util::MessageBox::question(this)
195 ->title(QY("Abort running job"))
196 .text(QY("Do you really want to abort this job?"))
197 .buttonLabel(QMessageBox::Yes, QY("&Abort job"))
198 .buttonLabel(QMessageBox::No, QY("Cancel"))
199 .exec() == QMessageBox::No))
200 return;
201
202 MainWindow::jobTool()->model()->withJob(*p->m_id, [](Jobs::Job &job) { job.abort(); });
203 }
204
205 void
onStatusChanged(uint64_t id,mtx::gui::Jobs::Job::Status,mtx::gui::Jobs::Job::Status newStatus)206 Tab::onStatusChanged(uint64_t id,
207 mtx::gui::Jobs::Job::Status,
208 mtx::gui::Jobs::Job::Status newStatus) {
209 auto p = p_func();
210
211 if (QObject::sender() != p->m_currentlyConnectedJob)
212 return;
213
214 auto job = MainWindow::jobTool()->model()->fromId(id);
215 if (!job) {
216 p->ui->abortButton->setEnabled(false);
217 p->m_saveOutputAction->setEnabled(false);
218 MainWindow::watchJobTool()->enableMenuActions();
219
220 p->m_id.reset();
221 p->m_currentJobProgress = 0;
222 p->m_currentJobProgress = Jobs::Job::Aborted;
223 updateRemainingTime();
224
225 return;
226 }
227
228 job->action([this, p, job, newStatus]() {
229 p->m_currentJobStatus = job->status();
230
231 p->ui->abortButton->setEnabled(Jobs::Job::Running == p->m_currentJobStatus);
232 p->m_saveOutputAction->setEnabled(true);
233 p->ui->status->setText(Jobs::Job::displayableStatus(p->m_currentJobStatus));
234 MainWindow::watchJobTool()->enableMenuActions();
235
236 // Check for the signalled status, not the current one, in order to
237 // detect a change from "not running" to "running" only once, no
238 // matter which order the Q_SIGNALS arrive in.
239 if (Jobs::Job::Running == newStatus)
240 setInitialDisplay(*job);
241
242 else if (mtx::included_in(p->m_currentJobStatus, Jobs::Job::DoneOk, Jobs::Job::DoneWarnings, Jobs::Job::Failed, Jobs::Job::Aborted))
243 p->ui->finishedAt->setText(Util::displayableDate(job->dateFinished()));
244 });
245
246 updateRemainingTime();
247 }
248
249 void
updateOneRemainingTimeLabel(QLabel * label,QDateTime const & startTime,uint64_t progress)250 Tab::updateOneRemainingTimeLabel(QLabel *label,
251 QDateTime const &startTime,
252 uint64_t progress) {
253 if (!progress)
254 return;
255
256 auto elapsedDuration = startTime.msecsTo(QDateTime::currentDateTime());
257 if (5000 > elapsedDuration)
258 label->setText(Q("–"));
259
260 else {
261 auto totalDuration = elapsedDuration * 100 / progress;
262 auto remainingDuration = totalDuration - elapsedDuration;
263 label->setText(Q(mtx::string::create_minutes_seconds_time_string(remainingDuration / 1000)));
264 }
265 }
266
267 void
updateRemainingTime()268 Tab::updateRemainingTime() {
269 auto p = p_func();
270
271 if ((Jobs::Job::Running != p->m_currentJobStatus) || !p->m_currentJobProgress)
272 p->ui->remainingTimeCurrentJob->setText(Q("–"));
273
274 else
275 updateOneRemainingTimeLabel(p->ui->remainingTimeCurrentJob, p->m_currentJobStartTime, p->m_currentJobProgress);
276
277 auto model = MainWindow::jobTool()->model();
278 if (!model->isRunning())
279 p->ui->remainingTimeQueue->setText(Q("–"));
280
281 else
282 updateOneRemainingTimeLabel(p->ui->remainingTimeQueue, model->queueStartTime(), p->m_queueProgress);
283 }
284
285 void
onQueueProgressChanged(int,int totalProgress)286 Tab::onQueueProgressChanged(int,
287 int totalProgress) {
288 p_func()->m_queueProgress = totalProgress;
289 updateRemainingTime();
290 }
291
292 void
onJobProgressChanged(uint64_t,unsigned int progress)293 Tab::onJobProgressChanged(uint64_t,
294 unsigned int progress) {
295 auto p = p_func();
296
297 if (QObject::sender() != p->m_currentlyConnectedJob)
298 return;
299
300 p->ui->progressBar->setValue(progress);
301 p->m_currentJobProgress = progress;
302 updateRemainingTime();
303 }
304
305 void
onLineRead(QString const & line,Jobs::Job::LineType type)306 Tab::onLineRead(QString const &line,
307 Jobs::Job::LineType type) {
308 auto p = p_func();
309
310 if ((QObject::sender() != p->m_currentlyConnectedJob) || line.isEmpty())
311 return;
312
313 auto &storage = Jobs::Job::InfoLine == type ? p->ui->output
314 : Jobs::Job::WarningLine == type ? p->ui->warnings
315 : p->ui->errors;
316
317 auto prefix = Jobs::Job::InfoLine == type ? Q("")
318 : Jobs::Job::WarningLine == type ? Q("%1 ").arg(QY("Warning:"))
319 : Q("%1 ").arg(QY("Error:"));
320
321 if (mtx::included_in(type, Jobs::Job::WarningLine, Jobs::Job::ErrorLine)) {
322 p->ui->acknowledgeWarningsAndErrorsButton->setEnabled(true);
323
324 if (isCurrentJobTab() && Util::Settings::get().m_showOutputOfAllJobs && !p->m_currentJobLineTypeSeen[type]) {
325 p->m_currentJobLineTypeSeen[type] = true;
326 auto dateStarted = Util::displayableDate(p->m_currentJobStartTime);
327 auto separator = Jobs::Job::WarningLine == type ? QY("--- Warnings emitted by job '%1' started on %2 ---").arg(p->m_currentJobDescription).arg(dateStarted)
328 : QY("--- Errors emitted by job '%1' started on %2 ---" ).arg(p->m_currentJobDescription).arg(dateStarted);
329
330 storage->appendPlainText(separator);
331 }
332 }
333
334 p->m_fullOutput << Q("%1%2").arg(prefix).arg(line);
335 storage->appendPlainText(line);
336
337 }
338
339 void
setInitialDisplay(Jobs::Job const & job)340 Tab::setInitialDisplay(Jobs::Job const &job) {
341 auto p = p_func();
342 auto dateStarted = Util::displayableDate(job.dateStarted());
343 p->m_currentJobDescription = job.description();
344
345 if (isCurrentJobTab() && Util::Settings::get().m_showOutputOfAllJobs) {
346 auto outputOfJobLine = QY("--- Output of job '%1' started on %2 ---").arg(p->m_currentJobDescription).arg(dateStarted);
347 p->m_fullOutput << outputOfJobLine << job.fullOutput();
348
349 p->ui->output->appendPlainText(outputOfJobLine);
350
351 } else {
352 p->m_fullOutput = job.fullOutput();
353
354 p->ui->output ->setPlainText(!job.output().isEmpty() ? Q("%1\n").arg(job.output().join("\n")) : Q(""));
355 p->ui->warnings->setPlainText(!job.warnings().isEmpty() ? Q("%1\n").arg(job.warnings().join("\n")) : Q(""));
356 p->ui->errors ->setPlainText(!job.errors().isEmpty() ? Q("%1\n").arg(job.errors().join("\n")) : Q(""));
357 }
358
359 p->m_currentJobLineTypeSeen.clear();
360
361 p->m_currentJobStatus = job.status();
362 p->m_currentJobProgress = job.progress();
363 p->m_currentJobStartTime = job.dateStarted();
364 p->m_queueProgress = MainWindow::watchCurrentJobTab()->queueProgress();
365
366 p->ui->description->setText(p->m_currentJobDescription);
367 p->ui->status->setText(Jobs::Job::displayableStatus(job.status()));
368 p->ui->progressBar->setValue(job.progress());
369
370 p->ui->startedAt ->setText(job.dateStarted() .isValid() ? Util::displayableDate(job.dateStarted()) : QY("Not started yet"));
371 p->ui->finishedAt->setText(job.dateFinished().isValid() ? Util::displayableDate(job.dateFinished()) : QY("Not finished yet"));
372
373 p->ui->abortButton->setEnabled(Jobs::Job::Running == job.status());
374 p->m_saveOutputAction->setEnabled(!mtx::included_in(job.status(), Jobs::Job::PendingManual, Jobs::Job::PendingAuto, Jobs::Job::Disabled));
375
376 p->ui->acknowledgeWarningsAndErrorsButton->setEnabled(job.numUnacknowledgedWarnings() || job.numUnacknowledgedErrors());
377
378 updateRemainingTime();
379 }
380
381 void
disableButtonIfAllWarningsAndErrorsButtonAcknowledged(int numWarnings,int numErrors)382 Tab::disableButtonIfAllWarningsAndErrorsButtonAcknowledged(int numWarnings,
383 int numErrors) {
384 auto p = p_func();
385
386 if (!numWarnings && !numErrors)
387 p->ui->acknowledgeWarningsAndErrorsButton->setEnabled(false);
388 }
389
390 void
onSaveOutput()391 Tab::onSaveOutput() {
392 auto p = p_func();
393 auto &cfg = Util::Settings::get();
394 auto txtName = Util::replaceInvalidFileNameCharacters(p->m_currentJobDescription) + Q(".txt");
395 auto fileName = Util::getSaveFileName(this, QY("Save job output"), cfg.lastOpenDirPath(), txtName, QY("Text files") + Q(" (*.txt);;") + QY("All files") + Q(" (*)"), Q("txt"));
396
397 if (fileName.isEmpty())
398 return;
399
400 QFile out{fileName};
401 if (out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
402 out.write(Q("%1\n").arg(p->m_fullOutput.join(Q("\n"))).toUtf8());
403 out.flush();
404 out.close();
405 }
406
407 cfg.m_lastOpenDir.setPath(QFileInfo{fileName}.path());
408 cfg.save();
409 }
410
411 std::optional<uint64_t>
id() const412 Tab::id()
413 const {
414 return p_func()->m_id;
415 }
416
417 bool
isSaveOutputEnabled() const418 Tab::isSaveOutputEnabled()
419 const {
420 return p_func()->m_saveOutputAction->isEnabled();
421 }
422
423 bool
isCurrentJobTab() const424 Tab::isCurrentJobTab()
425 const {
426 return p_func()->m_forCurrentJob;
427 }
428
429 void
acknowledgeWarningsAndErrors()430 Tab::acknowledgeWarningsAndErrors() {
431 auto p = p_func();
432
433 if (p->m_id)
434 MainWindow::jobTool()->acknowledgeWarningsAndErrors(*p->m_id);
435 p->ui->acknowledgeWarningsAndErrorsButton->setEnabled(false);
436 }
437
438 void
clearOutput()439 Tab::clearOutput() {
440 auto p = p_func();
441
442 p->ui->output->clear();
443 p->ui->warnings->clear();
444 p->ui->errors->clear();
445 p->ui->acknowledgeWarningsAndErrorsButton->setEnabled(false);
446
447 if (MainWindow::jobTool()->model()->isRunning())
448 return;
449
450 p->m_currentJobDescription.clear();
451 p->m_id.reset();
452
453 p->ui->progressBar->reset();
454 p->ui->status->setText(QY("No job started yet"));
455 p->ui->description->setText(QY("No job has been started yet."));
456 p->ui->startedAt->setText(QY("Not started yet"));
457 p->ui->finishedAt->setText(QY("Not finished yet"));
458 p->ui->remainingTimeCurrentJob->setText(Q("–"));
459 p->ui->remainingTimeQueue->setText(Q("–"));
460
461 Q_EMIT watchCurrentJobTabCleared();
462 }
463
464 void
openFolder()465 Tab::openFolder() {
466 auto p = p_func();
467
468 if (p->m_id)
469 MainWindow::jobTool()->model()->withJob(*p->m_id, [](Jobs::Job &job) { job.openOutputFolder(); });
470 }
471
472 void
enableMoreActionsActions()473 Tab::enableMoreActionsActions() {
474 auto p = p_func();
475 auto hasJob = !!p->m_id;
476
477 p->m_openFolderAction->setEnabled(hasJob);
478 p->m_saveOutputAction->setEnabled(!p->ui->output->toPlainText().isEmpty() || !p->ui->warnings->toPlainText().isEmpty() || !p->ui->errors->toPlainText().isEmpty());
479 }
480
481 void
setupWhenFinishedActions()482 Tab::setupWhenFinishedActions() {
483 auto p = p_func();
484
485 p->m_whenFinished->clear();
486
487 auto afterCurrentJobMenu = new QMenu{QY("Execute action after next &job completion")};
488 auto afterJobQueueMenu = new QMenu{QY("Execute action when the &queue completes")};
489 auto editRunProgramConfigurationsAction = new QAction{p->m_whenFinished};
490 auto menus = QVector<QMenu *>{} << afterCurrentJobMenu << afterJobQueueMenu;
491 auto &programRunner = App::programRunner();
492
493 editRunProgramConfigurationsAction->setText(QY("&Edit available actions to execute"));
494
495 p->m_whenFinished->addMenu(afterCurrentJobMenu);
496 p->m_whenFinished->addMenu(afterJobQueueMenu);
497 p->m_whenFinished->addSeparator();
498 p->m_whenFinished->addAction(editRunProgramConfigurationsAction);
499
500 connect(editRunProgramConfigurationsAction, &QAction::triggered, MainWindow::get(), &MainWindow::editRunProgramConfigurations);
501
502 for (auto const &config : Util::Settings::get().m_runProgramConfigurations) {
503 if (!config->m_active)
504 continue;
505
506 for (auto const &menu : menus) {
507 auto action = menu->addAction(config->name());
508 auto forQueue = menu == afterJobQueueMenu;
509 auto condition = forQueue ? Jobs::ProgramRunner::ExecuteActionCondition::AfterQueueFinishes : Jobs::ProgramRunner::ExecuteActionCondition::AfterJobFinishes;
510
511 action->setProperty(MTX_RUN_PROGRAM_CONFIGURATION_ADDRESS, reinterpret_cast<quint64>(config.get()));
512 action->setProperty(MTX_RUN_PROGRAM_CONFIGURATION_CONDITION, static_cast<int>(condition));
513
514 action->setCheckable(true);
515 action->setChecked(programRunner.isActionToExecuteEnabled(*config, condition));
516
517 connect(action, &QAction::triggered, this, &Tab::toggleActionToExecute);
518 }
519 }
520
521 afterCurrentJobMenu->setEnabled(!afterCurrentJobMenu->isEmpty());
522 afterJobQueueMenu->setEnabled(!afterJobQueueMenu->isEmpty());
523 }
524
525 void
toggleActionToExecute()526 Tab::toggleActionToExecute() {
527 auto &action = *qobject_cast<QAction *>(sender());
528 auto config = reinterpret_cast<Util::Settings::RunProgramConfig *>(action.property(MTX_RUN_PROGRAM_CONFIGURATION_ADDRESS).value<quint64>());
529 auto conditionIdx = action.property(MTX_RUN_PROGRAM_CONFIGURATION_CONDITION).value<int>();
530 auto condition = conditionIdx == static_cast<int>(Jobs::ProgramRunner::ExecuteActionCondition::AfterQueueFinishes) ? Jobs::ProgramRunner::ExecuteActionCondition::AfterQueueFinishes
531 : Jobs::ProgramRunner::ExecuteActionCondition::AfterJobFinishes;
532 auto enable = action.isChecked();
533
534 App::programRunner().enableActionToExecute(*config, condition, enable);
535 }
536
537 }
538