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 }