File indexing completed on 2024-05-26 04:32:39

0001 /*
0002  *  SPDX-FileCopyrightText: 2020 Dmitrii Utkin <loentar@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: LGPL-2.1-only
0005  */
0006 
0007 #include "recorder_export.h"
0008 #include "ui_recorder_export.h"
0009 #include "recorder_export_config.h"
0010 #include "recorder_export_settings.h"
0011 #include "recorder_profile_settings.h"
0012 #include "recorder_directory_cleaner.h"
0013 #include "animation/KisFFMpegWrapper.h"
0014 
0015 #include <klocalizedstring.h>
0016 #include <kis_icon_utils.h>
0017 #include "kis_config.h"
0018 
0019 #include <QAction>
0020 #include <QDesktopServices>
0021 #include <QDir>
0022 #include <QDirIterator>
0023 #include <QFileDialog>
0024 #include <QUrl>
0025 #include <QDebug>
0026 #include <QCloseEvent>
0027 #include <QMessageBox>
0028 #include <QJsonObject>
0029 #include <QImageReader>
0030 #include <QElapsedTimer>
0031 
0032 #include "kis_debug.h"
0033 
0034 
0035 namespace
0036 {
0037 enum ExportPageIndex
0038 {
0039     PageSettings = 0,
0040     PageProgress = 1,
0041     PageDone = 2
0042 };
0043 }
0044 
0045 
0046 class RecorderExport::Private
0047 {
0048 public:
0049     RecorderExport *q;
0050     QScopedPointer<Ui::RecorderExport> ui;
0051     RecorderExportSettings *settings;
0052 
0053     QScopedPointer<KisFFMpegWrapper> ffmpeg;
0054     RecorderDirectoryCleaner *cleaner = nullptr;
0055 
0056     QElapsedTimer elapsedTimer;
0057 
0058     int spinInputFPSMinValue = 0;
0059     int spinInputFPSMaxValue = 0;
0060 
0061     Private(RecorderExport *q_ptr)
0062         : q(q_ptr)
0063         , ui(new Ui::RecorderExport)
0064         , settings(q_ptr->settings)
0065     {
0066     }
0067 
0068     void checkFfmpeg()
0069     {
0070         const QJsonObject ffmpegJson = KisFFMpegWrapper::findFFMpeg(settings->ffmpegPath);
0071         const bool success = ffmpegJson["enabled"].toBool();
0072         const QIcon &icon = KisIconUtils::loadIcon(success ? "dialog-ok" : "window-close");
0073         const QList<QAction *> &actions = ui->editFfmpegPath->actions();
0074         QAction *action;
0075 
0076         if (!actions.isEmpty()) {
0077             action = actions.first();
0078             action->setIcon(icon);
0079         } else {
0080             action = ui->editFfmpegPath->addAction(icon, QLineEdit::TrailingPosition);
0081         }
0082         if (success) {
0083             settings->ffmpegPath = ffmpegJson["path"].toString();
0084             ui->editFfmpegPath->setText(settings->ffmpegPath);
0085             action->setToolTip("Version: "+ffmpegJson["version"].toString()
0086                                 +(ffmpegJson["codecs"].toObject()["h264"].toObject()["encoding"].toBool() ? "":" (MP4/MKV UNSUPPORTED)")
0087             );
0088         } else {
0089             ui->editFfmpegPath->setText(i18nc("This text is displayed instead of path to external tool in case of external tool is not found", "[NOT FOUND]"));
0090             action->setToolTip(i18n("FFmpeg executable location couldn't be detected, please install it or select its location manually"));
0091         }
0092         ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(success);
0093     }
0094 
0095     void fillComboProfiles()
0096     {
0097         QSignalBlocker blocker(ui->comboProfile);
0098         ui->comboProfile->clear();
0099         for (const RecorderProfile &profile : settings->profiles) {
0100             ui->comboProfile->addItem(profile.name);
0101         }
0102         blocker.unblock();
0103         ui->comboProfile->setCurrentIndex(settings->profileIndex);
0104     }
0105 
0106     void updateFrameInfo()
0107     {
0108         QDir dir(settings->inputDirectory, "*." % RecorderFormatInfo::fileExtension(settings->format),
0109                 QDir::Name, QDir::Files | QDir::NoDotAndDotDot);
0110         const QStringList &frames = dir.entryList(); // dir.count() calls entryList().count() internally
0111         settings->framesCount = frames.count();
0112         if (settings->framesCount != 0) {
0113             const QString &fileName = settings->inputDirectory % QDir::separator() % frames.last();
0114             settings->imageSize = QImageReader(fileName).size();
0115             settings->imageSize.rwidth() &= ~1;
0116             settings->imageSize.rheight() &= ~1;
0117         }
0118     }
0119 
0120     void updateVideoFilePath()
0121     {
0122         if (settings->videoDirectory.isEmpty())
0123             settings->videoDirectory = RecorderExportConfig(true).videoDirectory();
0124 
0125         settings->videoFilePath = settings->videoDirectory
0126             % QDir::separator()
0127             % settings->videoFileName
0128             % "."
0129             % settings->profiles[settings->profileIndex].extension;
0130         QSignalBlocker blocker(ui->editVideoFilePath);
0131         ui->editVideoFilePath->setText(settings->videoFilePath);
0132     }
0133 
0134     void updateRatio(bool widthToHeight)
0135     {
0136         const float ratio = static_cast<float>(settings->imageSize.width()) / static_cast<float>(settings->imageSize.height());
0137         if (widthToHeight) {
0138             settings->size.setHeight(static_cast<int>(settings->size.width() / ratio));
0139         } else {
0140             settings->size.setWidth(static_cast<int>(settings->size.height() * ratio));
0141         }
0142         // make width and height even
0143         settings->size.rwidth() &= ~1;
0144         settings->size.rheight() &= ~1;
0145         QSignalBlocker blockerWidth(ui->spinScaleHeight);
0146         QSignalBlocker blockerHeight(ui->spinScaleWidth);
0147         ui->spinScaleHeight->setValue(settings->size.height());
0148         ui->spinScaleWidth->setValue(settings->size.width());
0149     }
0150 
0151     void updateFps(RecorderExportConfig &config, bool takeFromInputFps = false)
0152     {
0153         if (!settings->lockFps)
0154             return;
0155 
0156         if (takeFromInputFps) {
0157             settings->fps = settings->inputFps;
0158             config.setFps(settings->fps);
0159             ui->spinFps->setValue(settings->fps);
0160         } else {
0161             settings->inputFps = settings->fps;
0162             config.setInputFps(settings->inputFps);
0163             ui->spinInputFps->setValue(settings->inputFps);
0164         }
0165         updateVideoDuration();
0166     }
0167 
0168     bool tryAbortExport()
0169     {
0170         if (!ffmpeg)
0171             return true;
0172 
0173         if (QMessageBox::question(q, q->windowTitle(), i18n("Abort encoding the timelapse video?"))
0174             == QMessageBox::Yes) {
0175             ffmpeg->reset();
0176             cleanupFFMpeg();
0177             return true;
0178         }
0179 
0180         return false;
0181     }
0182 
0183     QStringList splitCommand(const QString &command)
0184     {
0185         QStringList args;
0186         QString tmp;
0187         int quoteCount = 0;
0188         bool inQuote = false;
0189 
0190         // handle quoting. tokens can be surrounded by double quotes
0191         // "hello world". three consecutive double quotes represent
0192         // the quote character itself.
0193         for (int i = 0; i < command.size(); ++i) {
0194             if (command.at(i) == QLatin1Char('"')) {
0195                 ++quoteCount;
0196                 if (quoteCount == 3) {
0197                     // third consecutive quote
0198                     quoteCount = 0;
0199                     tmp += command.at(i);
0200                 }
0201                 continue;
0202             }
0203             if (quoteCount) {
0204                 if (quoteCount == 1)
0205                     inQuote = !inQuote;
0206                 quoteCount = 0;
0207             }
0208             if (!inQuote && command.at(i).isSpace()) {
0209                 if (!tmp.isEmpty()) {
0210                     args += tmp;
0211                     tmp.clear();
0212                 }
0213             } else {
0214                 tmp += command.at(i);
0215             }
0216         }
0217         if (!tmp.isEmpty())
0218             args += tmp;
0219 
0220         return args;
0221     }
0222 
0223     void startExport()
0224     {
0225         Q_ASSERT(ffmpeg == nullptr);
0226 
0227         updateFrameInfo();
0228 
0229         const QString &arguments = applyVariables(settings->profiles[settings->profileIndex].arguments);
0230 
0231         ffmpeg.reset(new KisFFMpegWrapper(q));
0232         QObject::connect(ffmpeg.data(), SIGNAL(sigStarted()), q, SLOT(onFFMpegStarted()));
0233         QObject::connect(ffmpeg.data(), SIGNAL(sigFinished()), q, SLOT(onFFMpegFinished()));
0234         QObject::connect(ffmpeg.data(), SIGNAL(sigFinishedWithError(QString)), q, SLOT(onFFMpegFinishedWithError(QString)));
0235         QObject::connect(ffmpeg.data(), SIGNAL(sigProgressUpdated(int)), q, SLOT(onFFMpegProgressUpdated(int)));
0236 
0237         KisFFMpegWrapperSettings FFmpegSettings;
0238         KisConfig cfg(true);
0239         FFmpegSettings.processPath = settings->ffmpegPath;
0240         FFmpegSettings.args = splitCommand(arguments);
0241         FFmpegSettings.outputFile = settings->videoFilePath;
0242         FFmpegSettings.batchMode = true; //TODO: Consider renaming to 'silent' mode, meaning no window for extra window handling...
0243 
0244         ffmpeg->startNonBlocking(FFmpegSettings);
0245         ui->labelStatus->setText(i18nc("Status for the export of the video record", "Starting FFmpeg..."));
0246         ui->buttonCancelExport->setEnabled(false);
0247         ui->progressExport->setValue(0);
0248         elapsedTimer.start();
0249     }
0250 
0251     void cleanupFFMpeg()
0252     {
0253         ffmpeg.reset();
0254     }
0255 
0256     QString applyVariables(const QString &templateArguments)
0257     {
0258         const QSize &outSize = settings->resize ? settings->size : settings->imageSize;
0259         const int previewLength = settings->resultPreview ? settings->firstFrameSec : 0;
0260         const int resultLength = settings->extendResult ? settings->lastFrameSec : 0;
0261         return QString(templateArguments)
0262                .replace("$IN_FPS", QString::number(settings->inputFps))
0263                .replace("$OUT_FPS", QString::number(settings->fps))
0264                .replace("$WIDTH", QString::number(outSize.width()))
0265                .replace("$HEIGHT", QString::number(outSize.height()))
0266                .replace("$FRAMES", QString::number(settings->framesCount))
0267                .replace("$INPUT_DIR", settings->inputDirectory)
0268                .replace("$FIRST_FRAME_SEC", QString::number(previewLength))
0269                .replace("$LAST_FRAME_SEC", QString::number(resultLength))
0270                .replace("$EXT", RecorderFormatInfo::fileExtension(settings->format));
0271     }
0272 
0273     void updateVideoDuration()
0274     {
0275         long ms = (settings->framesCount * 1000L / (settings->inputFps ? settings->inputFps : 30));
0276 
0277         if (settings->resultPreview) {
0278             ms += (settings->firstFrameSec * 1000L);
0279         }
0280 
0281         if (settings->extendResult) {
0282             ms += (settings->lastFrameSec * 1000L);
0283         }
0284 
0285         ui->labelVideoDuration->setText(formatDuration(ms));
0286     }
0287 
0288     QString formatDuration(long durationMs)
0289     {
0290         QString result;
0291         const long ms = (durationMs % 1000) / 10;
0292 
0293         result += QString(".%1").arg(ms, 2, 10, QLatin1Char('0'));
0294 
0295         long duration = durationMs / 1000;
0296         const long seconds = duration % 60;
0297         result = QString("%1%2").arg(seconds, 2, 10, QLatin1Char('0')).arg(result);
0298 
0299         duration = duration / 60;
0300         const long minutes = duration % 60;
0301         if (minutes != 0) {
0302             result = QString("%1:%2").arg(minutes, 2, 10, QLatin1Char('0')).arg(result);
0303 
0304             duration = duration / 60;
0305             if (duration != 0)
0306                 result = QString("%1:%2").arg(duration, 2, 10, QLatin1Char('0')).arg(result);
0307         }
0308 
0309         return result;
0310     }
0311 };
0312 
0313 
0314 RecorderExport::RecorderExport(RecorderExportSettings *s, QWidget *parent)
0315     : QDialog(parent)
0316     , settings(s)
0317     , d(new Private(this))
0318 {
0319     d->ui->setupUi(this);
0320     d->spinInputFPSMaxValue = d->ui->spinInputFps->minimum();
0321     d->spinInputFPSMaxValue = d->ui->spinInputFps->maximum();
0322     d->ui->buttonBrowseDirectory->setIcon(KisIconUtils::loadIcon("view-preview"));
0323     d->ui->buttonBrowseFfmpeg->setIcon(KisIconUtils::loadIcon("folder"));
0324     d->ui->buttonEditProfile->setIcon(KisIconUtils::loadIcon("document-edit"));
0325     d->ui->buttonBrowseExport->setIcon(KisIconUtils::loadIcon("folder"));
0326     d->ui->buttonLockRatio->setIcon(settings->lockRatio ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
0327     d->ui->buttonLockFps->setIcon(settings->lockFps ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
0328     d->ui->buttonWatchIt->setIcon(KisIconUtils::loadIcon("media-playback-start"));
0329     d->ui->buttonShowInFolder->setIcon(KisIconUtils::loadIcon("folder"));
0330     d->ui->buttonRemoveSnapshots->setIcon(KisIconUtils::loadIcon("edit-delete"));
0331     d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageSettings);
0332     d->ui->spinLastFrameSec->setEnabled(d->ui->extendResultCheckBox->isChecked());
0333     d->ui->spinFirstFrameSec->setEnabled(d->ui->resultPreviewCheckBox->isChecked());
0334 
0335     connect(d->ui->buttonBrowseDirectory, SIGNAL(clicked()), SLOT(onButtonBrowseDirectoryClicked()));
0336     connect(d->ui->spinInputFps, SIGNAL(valueChanged(int)), SLOT(onSpinInputFpsValueChanged(int)));
0337     connect(d->ui->spinFps, SIGNAL(valueChanged(int)), SLOT(onSpinFpsValueChanged(int)));
0338     connect(d->ui->resultPreviewCheckBox, SIGNAL(toggled(bool)), SLOT(onCheckResultPreviewToggled(bool)));
0339     connect(d->ui->spinFirstFrameSec, SIGNAL(valueChanged(int)), SLOT(onFirstFrameSecValueChanged(int)));
0340     connect(d->ui->extendResultCheckBox, SIGNAL(toggled(bool)), SLOT(onCheckExtendResultToggled(bool)));
0341     connect(d->ui->spinLastFrameSec, SIGNAL(valueChanged(int)), SLOT(onLastFrameSecValueChanged(int)));
0342     connect(d->ui->checkResize, SIGNAL(toggled(bool)), SLOT(onCheckResizeToggled(bool)));
0343     connect(d->ui->spinScaleWidth, SIGNAL(valueChanged(int)), SLOT(onSpinScaleWidthValueChanged(int)));
0344     connect(d->ui->spinScaleHeight, SIGNAL(valueChanged(int)), SLOT(onSpinScaleHeightValueChanged(int)));
0345     connect(d->ui->buttonLockRatio, SIGNAL(toggled(bool)), SLOT(onButtonLockRatioToggled(bool)));
0346     connect(d->ui->buttonLockFps, SIGNAL(toggled(bool)), SLOT(onButtonLockFpsToggled(bool)));
0347     connect(d->ui->buttonBrowseFfmpeg, SIGNAL(clicked()), SLOT(onButtonBrowseFfmpegClicked()));
0348     connect(d->ui->comboProfile, SIGNAL(currentIndexChanged(int)), SLOT(onComboProfileIndexChanged(int)));
0349     connect(d->ui->buttonEditProfile, SIGNAL(clicked()), SLOT(onButtonEditProfileClicked()));
0350     connect(d->ui->editVideoFilePath, SIGNAL(textChanged(QString)), SLOT(onEditVideoPathChanged(QString)));
0351     connect(d->ui->buttonBrowseExport, SIGNAL(clicked()), SLOT(onButtonBrowseExportClicked()));
0352     connect(d->ui->buttonBox->button(QDialogButtonBox::Save), SIGNAL(clicked()), this, SLOT(onButtonExportClicked()));
0353     connect(d->ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0354     connect(d->ui->buttonCancelExport, SIGNAL(clicked()), SLOT(onButtonCancelClicked()));
0355     connect(d->ui->buttonWatchIt, SIGNAL(clicked()), SLOT(onButtonWatchItClicked()));
0356     connect(d->ui->buttonShowInFolder, SIGNAL(clicked()), SLOT(onButtonShowInFolderClicked()));
0357     connect(d->ui->buttonRemoveSnapshots, SIGNAL(clicked()), SLOT(onButtonRemoveSnapshotsClicked()));
0358     connect(d->ui->buttonRestart, SIGNAL(clicked()), SLOT(onButtonRestartClicked()));
0359     connect(d->ui->resultPreviewCheckBox, SIGNAL(toggled(bool)), d->ui->spinFirstFrameSec, SLOT(setEnabled(bool)));
0360     connect(d->ui->extendResultCheckBox, SIGNAL(toggled(bool)), d->ui->spinLastFrameSec, SLOT(setEnabled(bool)));
0361 
0362     if (settings->realTimeCaptureMode)
0363         d->ui->buttonBox->button(QDialogButtonBox::Close)->setText("OK");
0364     d->ui->buttonBox->button(QDialogButtonBox::Save)->setText(i18n("Export"));
0365     d->ui->editVideoFilePath->installEventFilter(this);
0366 }
0367 
0368 RecorderExport::~RecorderExport()
0369 {
0370 }
0371 
0372 void RecorderExport::setup()
0373 {
0374     RecorderExportConfig config(true);
0375     d->updateFps(config);
0376     d->updateFrameInfo();
0377 
0378     if (settings->framesCount == 0) {
0379         d->ui->labelRecordInfo->setText(i18nc("Can't export recording because nothing to export", "No frames to export"));
0380         d->ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false);
0381     } else {
0382         d->ui->labelRecordInfo->setText(QString("%1: %2x%3 %4, %5 %6")
0383                                         .arg(i18nc("General information about recording", "Recording info"))
0384                                         .arg(settings->imageSize.width())
0385                                         .arg(settings->imageSize.height())
0386                                         .arg(i18nc("Pixel dimension suffix", "px"))
0387                                         .arg(settings->framesCount)
0388                                         .arg(i18nc("The suffix after number of frames", "frame(s)"))
0389                                        );
0390     }
0391 
0392 
0393     // Don't load lockFps flag from config, if liveCaptureMode was just set by the user
0394     config.loadConfiguration(settings, !settings->realTimeCaptureModeWasSet);
0395     settings->realTimeCaptureModeWasSet = false;
0396 
0397     d->ui->spinInputFps->setValue(settings->inputFps);
0398     d->ui->spinFps->setValue(settings->fps);
0399     d->ui->resultPreviewCheckBox->setChecked(settings->resultPreview);
0400     d->ui->spinFirstFrameSec->setValue(settings->firstFrameSec);
0401     d->ui->extendResultCheckBox->setChecked(settings->extendResult);
0402     d->ui->spinLastFrameSec->setValue(settings->lastFrameSec);
0403     d->ui->checkResize->setChecked(settings->resize);
0404     d->ui->spinScaleWidth->setValue(settings->size.width());
0405     d->ui->spinScaleHeight->setValue(settings->size.height());
0406     d->ui->buttonLockRatio->setChecked(settings->lockRatio);
0407     d->ui->buttonLockRatio->setIcon(settings->lockRatio ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
0408     d->ui->labelRealTimeCaptureNotion->setVisible(settings->realTimeCaptureMode);
0409     d->ui->buttonLockFps->setChecked(settings->lockFps);
0410     d->ui->buttonLockFps->setIcon(settings->lockFps ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
0411     d->fillComboProfiles();
0412     d->checkFfmpeg();
0413     d->updateVideoFilePath();
0414     d->updateVideoDuration();
0415 }
0416 
0417 void RecorderExport::closeEvent(QCloseEvent *event)
0418 {
0419     if (!d->tryAbortExport())
0420         event->ignore();
0421 }
0422 
0423 void RecorderExport::reject()
0424 {
0425     if (d->tryAbortExport())
0426         QDialog::reject();
0427 }
0428 
0429 void RecorderExport::onButtonBrowseDirectoryClicked()
0430 {
0431     if (settings->framesCount != 0) {
0432         QDesktopServices::openUrl(QUrl::fromLocalFile(settings->inputDirectory));
0433     } else {
0434         QMessageBox::warning(this, windowTitle(), i18nc("Can't browse frames of recording because no frames have been recorded", "No frames to browse."));
0435         return;
0436     }
0437 }
0438 
0439 void RecorderExport::onSpinInputFpsValueChanged(int value)
0440 {
0441     settings->inputFps = value;
0442     RecorderExportConfig config(false);
0443     config.setInputFps(value);
0444     d->updateFps(config, true);
0445 }
0446 
0447 void RecorderExport::onSpinFpsValueChanged(int value)
0448 {
0449     settings->fps = value;
0450     RecorderExportConfig config(false);
0451     config.setFps(value);
0452     d->updateFps(config, false);
0453 }
0454 
0455 void RecorderExport::onCheckResultPreviewToggled(bool checked)
0456 {
0457     settings->resultPreview = checked;
0458     RecorderExportConfig(false).setResultPreview(checked);
0459     d->updateVideoDuration();
0460 }
0461 
0462 void RecorderExport::onFirstFrameSecValueChanged(int value)
0463 {
0464     settings->firstFrameSec = value;
0465     RecorderExportConfig(false).setFirstFrameSec(value);
0466     d->updateVideoDuration();
0467 }
0468 
0469 void RecorderExport::onCheckExtendResultToggled(bool checked)
0470 {
0471     settings->extendResult = checked;
0472     RecorderExportConfig(false).setExtendResult(checked);
0473     d->updateVideoDuration();
0474 }
0475 
0476 void RecorderExport::onLastFrameSecValueChanged(int value)
0477 {
0478     settings->lastFrameSec = value;
0479     RecorderExportConfig(false).setLastFrameSec(value);
0480     d->updateVideoDuration();
0481 }
0482 
0483 void RecorderExport::onCheckResizeToggled(bool checked)
0484 {
0485     settings->resize = checked;
0486     RecorderExportConfig(false).setResize(checked);
0487 }
0488 
0489 void RecorderExport::onSpinScaleWidthValueChanged(int value)
0490 {
0491     settings->size.setWidth(value);
0492     if (settings->lockRatio)
0493         d->updateRatio(true);
0494     RecorderExportConfig(false).setSize(settings->size);
0495 }
0496 
0497 void RecorderExport::onSpinScaleHeightValueChanged(int value)
0498 {
0499     settings->size.setHeight(value);
0500     if (settings->lockRatio)
0501         d->updateRatio(false);
0502     RecorderExportConfig(false).setSize(settings->size);
0503 }
0504 
0505 void RecorderExport::onButtonLockRatioToggled(bool checked)
0506 {
0507     settings->lockRatio = checked;
0508     RecorderExportConfig config(false);
0509     config.setLockRatio(checked);
0510     if (settings->lockRatio) {
0511         d->updateRatio(true);
0512         config.setSize(settings->size);
0513     }
0514     d->ui->buttonLockRatio->setIcon(settings->lockRatio ? KisIconUtils::loadIcon("locked") : KisIconUtils::loadIcon("unlocked"));
0515 }
0516 
0517 void RecorderExport::onButtonLockFpsToggled(bool checked)
0518 {
0519     settings->lockFps = checked;
0520     RecorderExportConfig config(false);
0521     config.setLockFps(checked);
0522     d->updateFps(config);
0523     if (settings->lockFps) {
0524         d->ui->buttonLockFps->setIcon(KisIconUtils::loadIcon("locked"));
0525         d->ui->spinInputFps->setMinimum(d->ui->spinFps->minimum());
0526         d->ui->spinInputFps->setMaximum(d->ui->spinFps->maximum());
0527     } else {
0528         d->ui->buttonLockFps->setIcon(KisIconUtils::loadIcon("unlocked"));
0529         d->ui->spinInputFps->setMinimum(d->spinInputFPSMinValue);
0530         d->ui->spinInputFps->setMaximum(d->spinInputFPSMaxValue);
0531     }
0532 
0533 }
0534 
0535 void RecorderExport::onButtonBrowseFfmpegClicked()
0536 {
0537     QFileDialog dialog(this);
0538     dialog.setFileMode(QFileDialog::ExistingFile);
0539     dialog.setOption(QFileDialog::DontUseNativeDialog, true);
0540     dialog.setFilter(QDir::Executable | QDir::Files);
0541 
0542     const QString &file = dialog.getOpenFileName(this,
0543                           i18n("Select FFmpeg Executable File"),
0544                           settings->ffmpegPath);
0545     if (!file.isEmpty()) {
0546         settings->ffmpegPath = file;
0547         RecorderExportConfig(false).setFfmpegPath(file);
0548         d->checkFfmpeg();
0549     }
0550 }
0551 
0552 void RecorderExport::onComboProfileIndexChanged(int index)
0553 {
0554     settings->profileIndex = index;
0555     d->updateVideoFilePath();
0556     RecorderExportConfig(false).setProfileIndex(index);
0557 }
0558 
0559 void RecorderExport::onButtonEditProfileClicked()
0560 {
0561     RecorderProfileSettings settingsDialog(this);
0562 
0563     connect(&settingsDialog, &RecorderProfileSettings::requestPreview, [&](const QString & arguments) {
0564         settingsDialog.setPreview(settings->ffmpegPath % " -y " % d->applyVariables(arguments).replace("\n", " ")
0565                                   % " \"" % settings->videoFilePath % "\"");
0566     });
0567 
0568     if (settingsDialog.editProfile(
0569             &settings->profiles[settings->profileIndex], settings->defaultProfiles[settings->profileIndex])) {
0570         d->fillComboProfiles();
0571         d->updateVideoFilePath();
0572         RecorderExportConfig(false).setProfiles(settings->profiles);
0573     }
0574 }
0575 
0576 void RecorderExport::onEditVideoPathChanged(const QString &videoFilePath)
0577 {
0578     QFileInfo fileInfo(videoFilePath);
0579     if (!fileInfo.isRelative())
0580         settings->videoDirectory = fileInfo.absolutePath();
0581     settings->videoFileName = fileInfo.completeBaseName();
0582 }
0583 
0584 void RecorderExport::onButtonBrowseExportClicked()
0585 {
0586     QFileDialog dialog(this);
0587 
0588     const QString &extension = settings->profiles[settings->profileIndex].extension;
0589     const QString &videoFileName = dialog.getSaveFileName(this,
0590                                    i18n("Export Timelapse Video As"),
0591                                    settings->videoDirectory, "*." % extension);
0592     if (!videoFileName.isEmpty()) {
0593         QFileInfo fileInfo(videoFileName);
0594         settings->videoDirectory = fileInfo.absolutePath();
0595         settings->videoFileName = fileInfo.completeBaseName();
0596         d->updateVideoFilePath();
0597         RecorderExportConfig(false).setVideoDirectory(settings->videoDirectory);
0598     }
0599 }
0600 
0601 void RecorderExport::onButtonExportClicked()
0602 {
0603     if (QFile::exists(settings->videoFilePath)) {
0604         if (settings->framesCount != 0) {
0605             if (QMessageBox::question(this, windowTitle(),
0606                                       i18n("The video file already exists. Do you wish to overwrite it?"))
0607                 != QMessageBox::Yes) {
0608                 return;
0609             }
0610         } else {
0611             QMessageBox::warning(this, windowTitle(), i18n("No frames to export."));
0612             return;
0613         }
0614     }
0615 
0616 
0617     d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageProgress);
0618     d->startExport();
0619 }
0620 
0621 void RecorderExport::onButtonCancelClicked()
0622 {
0623     if (d->cleaner) {
0624         d->cleaner->stop();
0625         d->cleaner->deleteLater();
0626         d->cleaner = nullptr;
0627         return;
0628     }
0629 
0630     if (d->tryAbortExport())
0631         d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageSettings);
0632 }
0633 
0634 
0635 void RecorderExport::onFFMpegStarted()
0636 {
0637     d->ui->buttonCancelExport->setEnabled(true);
0638     d->ui->labelStatus->setText(i18n("The timelapse video is being encoded..."));
0639 }
0640 
0641 void RecorderExport::onFFMpegFinished()
0642 {
0643     quint64 elapsed = d->elapsedTimer.elapsed();
0644     d->ui->labelRenderTime->setText(d->formatDuration(elapsed));
0645     d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageDone);
0646     d->ui->labelVideoPathDone->setText(settings->videoFilePath);
0647     d->cleanupFFMpeg();
0648 }
0649 
0650 void RecorderExport::onFFMpegFinishedWithError(QString error)
0651 {
0652     d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageSettings);
0653     QMessageBox::critical(this, windowTitle(), i18n("Export failed. FFmpeg message:") % "\n\n" % error);
0654     d->cleanupFFMpeg();
0655 }
0656 
0657 void RecorderExport::onFFMpegProgressUpdated(int frameNo)
0658 {
0659     d->ui->progressExport->setValue(frameNo * 100 / (settings->framesCount * settings->fps / static_cast<float>(settings->inputFps)));
0660 }
0661 
0662 void RecorderExport::onButtonWatchItClicked()
0663 {
0664     QDesktopServices::openUrl(QUrl::fromLocalFile(settings->videoFilePath));
0665 }
0666 
0667 void RecorderExport::onButtonShowInFolderClicked()
0668 {
0669     QDesktopServices::openUrl(QUrl::fromLocalFile(settings->videoDirectory));
0670 }
0671 
0672 void RecorderExport::onButtonRemoveSnapshotsClicked()
0673 {
0674     const QString confirmation(i18n("The recordings for this document will be deleted"
0675                                     " and you will not be able to export a timelapse for it again"
0676                                     ". Note that already exported timelapses will still be preserved."
0677                                     "\n\nDo you wish to continue?"));
0678     if (QMessageBox::question(this, windowTitle(), confirmation) != QMessageBox::Yes)
0679         return;
0680 
0681     d->ui->labelStatus->setText(i18nc("Label title, Snapshot directory deleting is in progress", "Cleaning up..."));
0682     d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageProgress);
0683 
0684     Q_ASSERT(d->cleaner == nullptr);
0685     d->cleaner = new RecorderDirectoryCleaner({d->settings->inputDirectory});
0686     connect(d->cleaner, SIGNAL(finished()), this, SLOT(onCleanUpFinished()));
0687     d->cleaner->start();
0688 }
0689 
0690 void RecorderExport::onButtonRestartClicked()
0691 {
0692     d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageSettings);
0693 }
0694 
0695 void RecorderExport::onCleanUpFinished()
0696 {
0697     d->cleaner->deleteLater();
0698     d->cleaner = nullptr;
0699 
0700     d->ui->stackedWidget->setCurrentIndex(ExportPageIndex::PageDone);
0701     d->ui->buttonRestart->hide();
0702     d->ui->buttonRemoveSnapshots->hide();
0703 }
0704 
0705 bool RecorderExport::eventFilter(QObject *obj, QEvent *event)
0706 {
0707     if (obj == d->ui->editVideoFilePath && event->type() == QEvent::FocusOut)
0708         d->updateVideoFilePath();
0709 
0710     return QDialog::eventFilter(obj, event);
0711 }