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