Warning, file /multimedia/kdenlive/src/dialogs/renderwidget.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2008 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003     SPDX-FileCopyrightText: 2022 Julius Künzel <jk.kdedev@smartlab.uber.space>
0004 
0005     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "renderwidget.h"
0009 #include "bin/bin.h"
0010 #include "bin/projectitemmodel.h"
0011 #include "core.h"
0012 #include "dialogs/renderpresetdialog.h"
0013 #include "doc/kdenlivedoc.h"
0014 #include "kdenlivesettings.h"
0015 #include "mainwindow.h"
0016 #include "monitor/monitor.h"
0017 #include "profiles/profilemodel.hpp"
0018 #include "profiles/profilerepository.hpp"
0019 #include "project/projectmanager.h"
0020 #include "utils/timecode.h"
0021 #include "xml/xml.hpp"
0022 
0023 #include "renderpresets/renderpresetmodel.hpp"
0024 #include "renderpresets/renderpresetrepository.hpp"
0025 
0026 #include <KColorScheme>
0027 #include <KIO/DesktopExecParser>
0028 #include <kio_version.h>
0029 #if KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
0030 #include <KIO/JobUiDelegateFactory>
0031 #else
0032 #include <KIO/JobUiDelegate>
0033 #endif
0034 #include "utils/KMessageBox_KdenliveCompat.h"
0035 #include <KIO/OpenFileManagerWindowJob>
0036 #include <KIO/OpenUrlJob>
0037 #include <KLocalizedString>
0038 #include <KMessageBox>
0039 #include <KNotification>
0040 #include <knewstuff_version.h>
0041 #include <knotifications_version.h>
0042 
0043 #include "kdenlive_debug.h"
0044 #include <QDBusConnectionInterface>
0045 #include <QDir>
0046 #include <QDomDocument>
0047 #include <QFileIconProvider>
0048 #include <QHeaderView>
0049 #include <QInputDialog>
0050 #include <QJsonArray>
0051 #include <QJsonObject>
0052 #include <QKeyEvent>
0053 #include <QMenu>
0054 #include <QMimeDatabase>
0055 #include <QProcess>
0056 #include <QScreen>
0057 #include <QScrollBar>
0058 #include <QStandardPaths>
0059 #include <QString>
0060 #include <QTemporaryFile>
0061 #include <QThread>
0062 #include <QTreeWidgetItem>
0063 #include <QtGlobal>
0064 
0065 #ifdef KF5_USE_PURPOSE
0066 #include <Purpose/AlternativesModel>
0067 #include <PurposeWidgets/Menu>
0068 #endif
0069 
0070 #include <locale>
0071 #ifdef Q_OS_MAC
0072 #include <xlocale.h>
0073 #endif
0074 
0075 // Render job roles
0076 enum {
0077     ParametersRole = Qt::UserRole + 1,
0078     StartTimeRole,
0079     ProgressRole,
0080     ExtraInfoRole = ProgressRole + 2, // vpinon: don't understand why, else spurious message displayed
0081     LastTimeRole,
0082     LastFrameRole,
0083     OpenBrowserRole,
0084     PlayAfterRole
0085 };
0086 
0087 // Running job status
0088 enum JOBSTATUS { WAITINGJOB = 0, STARTINGJOB, RUNNINGJOB, FINISHEDJOB, FAILEDJOB, ABORTEDJOB };
0089 
0090 RenderJobItem::RenderJobItem(QTreeWidget *parent, const QStringList &strings, int type)
0091     : QTreeWidgetItem(parent, strings, type)
0092     , m_status(-1)
0093 {
0094     setSizeHint(1, QSize(parent->columnWidth(1), parent->fontMetrics().height() * 3));
0095     setStatus(WAITINGJOB);
0096 }
0097 
0098 void RenderJobItem::setStatus(int status)
0099 {
0100     if (m_status == status) {
0101         return;
0102     }
0103     m_status = status;
0104     switch (status) {
0105     case WAITINGJOB:
0106         setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause")));
0107         setData(1, Qt::UserRole, i18n("Waiting…"));
0108         break;
0109     case FINISHEDJOB:
0110         setData(1, Qt::UserRole, i18n("Rendering finished"));
0111         setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok")));
0112         setData(1, ProgressRole, 100);
0113         break;
0114     case FAILEDJOB:
0115         setData(1, Qt::UserRole, i18n("Rendering crashed"));
0116         setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close")));
0117         setData(1, ProgressRole, 100);
0118         break;
0119     case ABORTEDJOB:
0120         setData(1, Qt::UserRole, i18n("Rendering aborted"));
0121         setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-cancel")));
0122         setData(1, ProgressRole, 100);
0123         break;
0124     default:
0125         break;
0126     }
0127 }
0128 
0129 int RenderJobItem::status() const
0130 {
0131     return m_status;
0132 }
0133 
0134 void RenderJobItem::setMetadata(const QString &data)
0135 {
0136     m_data = data;
0137 }
0138 
0139 const QString RenderJobItem::metadata() const
0140 {
0141     return m_data;
0142 }
0143 
0144 RenderWidget::RenderWidget(bool enableProxy, QWidget *parent)
0145     : QDialog(parent)
0146     , m_blockProcessing(false)
0147 {
0148     m_view.setupUi(this);
0149     int size = style()->pixelMetric(QStyle::PM_SmallIconSize);
0150     QSize iconSize(size, size);
0151 
0152     // ===== "Render Project" tab =====
0153     m_view.buttonDelete->setIconSize(iconSize);
0154     m_view.buttonEdit->setIconSize(iconSize);
0155     m_view.buttonNew->setIconSize(iconSize);
0156     m_view.buttonSaveAs->setIconSize(iconSize);
0157     m_view.m_knsbutton->setIconSize(iconSize);
0158 
0159     m_view.buttonRender->setEnabled(false);
0160     m_view.buttonGenerateScript->setEnabled(false);
0161 
0162     connect(m_view.profileTree, &QTreeView::doubleClicked, this, [&](const QModelIndex &index) {
0163         if (m_treeModel->parent(index) == QModelIndex()) {
0164             // This is a top level item - group - don't edit
0165             return;
0166         }
0167         slotEditPreset();
0168     });
0169     connect(m_view.buttonNew, &QAbstractButton::clicked, this, &RenderWidget::slotNewPreset);
0170     connect(m_view.buttonEdit, &QAbstractButton::clicked, this, &RenderWidget::slotEditPreset);
0171     connect(m_view.buttonDelete, &QAbstractButton::clicked, this, [this]() {
0172         RenderPresetRepository::get()->deletePreset(m_currentProfile);
0173         m_currentProfile = QString();
0174         parseProfiles();
0175         refreshView();
0176     });
0177     connect(m_view.buttonSaveAs, &QAbstractButton::clicked, this, &RenderWidget::slotSavePresetAs);
0178 
0179     connect(m_view.m_knsbutton, &KNSWidgets::Button::dialogFinished, this, [&](const QList<KNSCore::Entry> &changedEntries) {
0180         if (changedEntries.count() > 0) {
0181             parseProfiles();
0182         }
0183     });
0184 
0185     m_view.optionsGroup->setVisible(m_view.options->isChecked());
0186     m_view.optionsGroup->setMinimumWidth(m_view.optionsGroup->width() + m_view.optionsGroup->verticalScrollBar()->width());
0187     connect(m_view.options, &QAbstractButton::toggled, m_view.optionsGroup, &QWidget::setVisible);
0188 
0189     connect(m_view.out_file, &KUrlRequester::textChanged, this, static_cast<void (RenderWidget::*)()>(&RenderWidget::slotUpdateButtons));
0190     connect(m_view.out_file, &KUrlRequester::urlSelected, this, static_cast<void (RenderWidget::*)(const QUrl &)>(&RenderWidget::slotUpdateButtons));
0191 
0192     connect(m_view.render_multi, &QAbstractButton::clicked, this, &RenderWidget::slotRenderModeChanged);
0193     connect(m_view.render_guide, &QAbstractButton::clicked, this, &RenderWidget::slotRenderModeChanged);
0194     connect(m_view.render_zone, &QAbstractButton::clicked, this, &RenderWidget::slotRenderModeChanged);
0195     connect(m_view.render_full, &QAbstractButton::clicked, this, &RenderWidget::slotRenderModeChanged);
0196 
0197     connect(m_view.guide_end, static_cast<void (KComboBox::*)(int)>(&KComboBox::activated), this, &RenderWidget::slotCheckStartGuidePosition);
0198     connect(m_view.guide_start, static_cast<void (KComboBox::*)(int)>(&KComboBox::activated), this, &RenderWidget::slotCheckEndGuidePosition);
0199 
0200     // === "More Options" widget ===
0201     setRescaleEnabled(false);
0202     m_view.guide_zone_box->setVisible(false);
0203     m_view.guide_multi_box->setVisible(false);
0204     m_view.error_box->setVisible(false);
0205     m_view.tc_type->addItem(i18n("None"));
0206     m_view.tc_type->addItem(i18n("Timecode"), QStringLiteral("#timecode#"));
0207     m_view.tc_type->addItem(i18n("Timecode Non Drop Frame"), QStringLiteral("#smtpe_ndf#"));
0208     m_view.tc_type->addItem(i18n("Frame Number"), QStringLiteral("#frame#"));
0209     m_view.checkTwoPass->setEnabled(false);
0210     m_view.proxy_render->setHidden(!enableProxy);
0211     connect(m_view.proxy_render, &QCheckBox::toggled, this,
0212             [&](bool enabled) { errorMessage(ProxyWarning, enabled ? i18n("Rendering using low quality proxy") : QString()); });
0213 
0214     connect(m_view.quality, &QAbstractSlider::valueChanged, this, &RenderWidget::refreshParams);
0215     connect(m_view.qualityGroup, &QGroupBox::toggled, this, &RenderWidget::refreshParams);
0216     connect(m_view.speed, &QAbstractSlider::valueChanged, this, &RenderWidget::adjustSpeed);
0217 
0218     m_view.encoder_threads->setMaximum(QThread::idealThreadCount());
0219     m_view.encoder_threads->setValue(KdenliveSettings::encodethreads());
0220     connect(m_view.encoder_threads, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &KdenliveSettings::setEncodethreads);
0221     connect(m_view.encoder_threads, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &RenderWidget::refreshParams);
0222 
0223     connect(m_view.video_box, &QGroupBox::toggled, this, &RenderWidget::refreshParams);
0224     connect(m_view.audio_box, &QGroupBox::toggled, this, &RenderWidget::refreshParams);
0225     m_view.rescale_keep->setChecked(KdenliveSettings::rescalekeepratio());
0226     connect(m_view.rescale, &QAbstractButton::toggled, this, &RenderWidget::setRescaleEnabled);
0227     connect(m_view.rescale, &QAbstractButton::toggled, this, &RenderWidget::refreshParams);
0228     connect(m_view.rescale_width, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &RenderWidget::slotUpdateRescaleWidth);
0229     connect(m_view.rescale_height, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &RenderWidget::slotUpdateRescaleHeight);
0230     connect(m_view.rescale_keep, &QAbstractButton::clicked, this, &RenderWidget::slotSwitchAspectRatio);
0231     m_view.processing_threads->setMaximum(QThread::idealThreadCount() - 1);
0232     m_view.processing_threads->setValue(KdenliveSettings::processingthreads());
0233     connect(m_view.processing_threads, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &KdenliveSettings::setProcessingthreads);
0234     connect(m_view.processing_threads, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &RenderWidget::refreshParams);
0235     m_view.processing_box->setChecked(KdenliveSettings::parallelrender());
0236     connect(m_view.processing_box, &QGroupBox::toggled, [&](int state) {
0237         KdenliveSettings::setParallelrender(state == Qt::Checked);
0238         refreshParams();
0239     });
0240     if (KdenliveSettings::gpu_accel()) {
0241         // Disable parallel rendering for movit
0242         m_view.processing_box->setChecked(false);
0243         m_view.processing_box->setEnabled(false);
0244     }
0245     connect(m_view.export_meta, &QCheckBox::stateChanged, this, &RenderWidget::refreshParams);
0246     connect(m_view.checkTwoPass, &QCheckBox::stateChanged, this, &RenderWidget::refreshParams);
0247 
0248     connect(m_view.buttonRender, &QAbstractButton::clicked, this, [&]() { slotPrepareExport(); });
0249     connect(m_view.buttonGenerateScript, &QAbstractButton::clicked, this, [&]() { slotPrepareExport(true); });
0250     updateMetadataToolTip();
0251     connect(m_view.edit_metadata, &QLabel::linkActivated, []() { pCore->window()->slotEditProjectSettings(3); });
0252 
0253     m_view.infoMessage->hide();
0254     m_view.jobInfo->hide();
0255     parseProfiles();
0256 
0257     // ===== "Job Queue" tab =====
0258     parseScriptFiles();
0259     m_view.running_jobs->setUniformRowHeights(false);
0260     m_view.running_jobs->setContextMenuPolicy(Qt::CustomContextMenu);
0261     connect(m_view.running_jobs, &QTreeWidget::customContextMenuRequested, this, &RenderWidget::prepareJobContextMenu);
0262     m_view.scripts_list->setUniformRowHeights(false);
0263     connect(m_view.start_script, &QAbstractButton::clicked, this, &RenderWidget::slotStartScript);
0264     connect(m_view.delete_script, &QAbstractButton::clicked, this, &RenderWidget::slotDeleteScript);
0265     connect(m_view.scripts_list, &QTreeWidget::itemSelectionChanged, this, &RenderWidget::slotCheckScript);
0266     connect(m_view.running_jobs, &QTreeWidget::itemSelectionChanged, this, &RenderWidget::slotCheckJob);
0267     connect(m_view.running_jobs, &QTreeWidget::itemDoubleClicked, this, &RenderWidget::slotPlayRendering);
0268 
0269     connect(m_view.abort_job, &QAbstractButton::clicked, this, &RenderWidget::slotAbortCurrentJob);
0270     connect(m_view.start_job, &QAbstractButton::clicked, this, &RenderWidget::slotStartCurrentJob);
0271     connect(m_view.clean_up, &QAbstractButton::clicked, this, &RenderWidget::slotCleanUpJobs);
0272     connect(m_view.hide_log, &QAbstractButton::clicked, this, &RenderWidget::slotHideLog);
0273 
0274     connect(m_view.buttonClose, &QAbstractButton::clicked, this, &QWidget::hide);
0275     connect(m_view.buttonClose2, &QAbstractButton::clicked, this, &QWidget::hide);
0276     connect(m_view.buttonClose3, &QAbstractButton::clicked, this, &QWidget::hide);
0277 
0278     m_jobsDelegate = new RenderViewDelegate(this);
0279     m_view.running_jobs->setHeaderLabels(QStringList() << QString() << i18n("File"));
0280     m_view.running_jobs->setItemDelegate(m_jobsDelegate);
0281 
0282     QHeaderView *header = m_view.running_jobs->header();
0283     header->setSectionResizeMode(0, QHeaderView::Fixed);
0284     header->resizeSection(0, size + 4);
0285     header->setSectionResizeMode(1, QHeaderView::Interactive);
0286 
0287     // ===== "Scripts" tab =====
0288     m_view.scripts_list->setHeaderLabels(QStringList() << QString() << i18n("Stored Playlists"));
0289     m_scriptsDelegate = new RenderViewDelegate(this);
0290     m_view.scripts_list->setItemDelegate(m_scriptsDelegate);
0291     header = m_view.scripts_list->header();
0292     header->setSectionResizeMode(0, QHeaderView::Fixed);
0293     header->resizeSection(0, size + 4);
0294 
0295     // Find path for Kdenlive renderer
0296 #ifdef Q_OS_WIN
0297     m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render.exe");
0298 #else
0299     m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render");
0300 #endif
0301     if (!QFile::exists(m_renderer)) {
0302         m_renderer = QStandardPaths::findExecutable(QStringLiteral("kdenlive_render"));
0303         if (m_renderer.isEmpty()) {
0304             KMessageBox::error(this,
0305                                i18n("Could not find the kdenlive_render application, something is wrong with your installation. Rendering will not work"));
0306         }
0307     }
0308 
0309     QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface();
0310     if ((interface == nullptr) ||
0311         (!interface->isServiceRegistered(QStringLiteral("org.kde.ksmserver")) && !interface->isServiceRegistered(QStringLiteral("org.gnome.SessionManager")))) {
0312         m_view.shutdown->setEnabled(false);
0313     }
0314 
0315 #ifdef KF5_USE_PURPOSE
0316     m_shareMenu = new Purpose::Menu();
0317     m_view.shareButton->setMenu(m_shareMenu);
0318     m_view.shareButton->setIcon(QIcon::fromTheme(QStringLiteral("document-share")));
0319     connect(m_shareMenu, &Purpose::Menu::finished, this, &RenderWidget::slotShareActionFinished);
0320 #else
0321     m_view.shareButton->setEnabled(false);
0322 #endif
0323 
0324     loadConfig();
0325     refreshView();
0326     focusItem();
0327     adjustSize();
0328     m_view.embed_subtitles->setToolTip(i18n("Only works for the matroska (mkv) format"));
0329 }
0330 
0331 void RenderWidget::slotShareActionFinished(const QJsonObject &output, int error, const QString &message)
0332 {
0333 #ifdef KF5_USE_PURPOSE
0334     m_view.jobInfo->hide();
0335     if (error) {
0336         KMessageBox::error(this, i18n("There was a problem sharing the document: %1", message), i18n("Share"));
0337     } else {
0338         const QString url = output["url"].toString();
0339         if (url.isEmpty()) {
0340             m_view.jobInfo->setMessageType(KMessageWidget::Positive);
0341             m_view.jobInfo->setText(i18n("Document shared successfully"));
0342             m_view.jobInfo->show();
0343         } else {
0344             KMessageBox::information(this, i18n("You can find the shared document at: <a href=\"%1\">%1</a>", url), i18n("Share"), QString(),
0345                                      KMessageBox::Notify | KMessageBox::AllowLink);
0346         }
0347     }
0348 #else
0349     Q_UNUSED(output);
0350     Q_UNUSED(error);
0351     Q_UNUSED(message);
0352 #endif
0353 }
0354 
0355 QSize RenderWidget::sizeHint() const
0356 {
0357     // Make sure the widget has minimum size on opening
0358     return {200, qMax(200, screen()->availableGeometry().height())};
0359 }
0360 
0361 RenderWidget::~RenderWidget()
0362 {
0363     saveConfig();
0364     m_view.running_jobs->blockSignals(true);
0365     m_view.scripts_list->blockSignals(true);
0366     m_view.running_jobs->clear();
0367     m_view.scripts_list->clear();
0368     delete m_jobsDelegate;
0369     delete m_scriptsDelegate;
0370 }
0371 
0372 void RenderWidget::saveConfig()
0373 {
0374     KSharedConfigPtr config = KSharedConfig::openConfig();
0375     KConfigGroup resourceConfig(config, "RenderWidget");
0376     resourceConfig.writeEntry(QStringLiteral("showoptions"), m_view.options->isChecked());
0377     config->sync();
0378 }
0379 
0380 void RenderWidget::loadConfig()
0381 {
0382     KSharedConfigPtr config = KSharedConfig::openConfig();
0383     KConfigGroup resourceConfig(config, "RenderWidget");
0384     m_view.options->setChecked(resourceConfig.readEntry("showoptions", false));
0385 }
0386 
0387 void RenderWidget::updateDocumentPath()
0388 {
0389     if (m_view.out_file->url().isEmpty()) {
0390         return;
0391     }
0392     const QString fileName = m_view.out_file->url().fileName();
0393     m_view.out_file->setUrl(QUrl::fromLocalFile(QDir(pCore->currentDoc()->projectDataFolder()).absoluteFilePath(fileName)));
0394     parseScriptFiles();
0395 }
0396 
0397 void RenderWidget::slotRenderModeChanged()
0398 {
0399     m_view.guide_zone_box->setVisible(m_view.render_guide->isChecked());
0400     m_view.guide_multi_box->setVisible(m_view.render_multi->isChecked());
0401     m_view.buttonGenerateScript->setVisible(!m_view.render_multi->isChecked());
0402 }
0403 
0404 void RenderWidget::slotUpdateRescaleWidth(int val)
0405 {
0406     KdenliveSettings::setDefaultrescalewidth(val);
0407     if (!m_view.rescale_keep->isChecked()) {
0408         refreshParams();
0409         return;
0410     }
0411     m_view.rescale_height->blockSignals(true);
0412     std::unique_ptr<ProfileModel> &profile = pCore->getCurrentProfile();
0413     m_view.rescale_height->setValue(val * profile->height() / profile->width());
0414     KdenliveSettings::setDefaultrescaleheight(m_view.rescale_height->value());
0415     m_view.rescale_height->blockSignals(false);
0416     refreshParams();
0417 }
0418 
0419 void RenderWidget::slotUpdateRescaleHeight(int val)
0420 {
0421     KdenliveSettings::setDefaultrescaleheight(val);
0422     if (!m_view.rescale_keep->isChecked()) {
0423         refreshParams();
0424         return;
0425     }
0426     m_view.rescale_width->blockSignals(true);
0427     std::unique_ptr<ProfileModel> &profile = pCore->getCurrentProfile();
0428     m_view.rescale_width->setValue(val * profile->width() / profile->height());
0429     KdenliveSettings::setDefaultrescaleheight(m_view.rescale_width->value());
0430     m_view.rescale_width->blockSignals(false);
0431     refreshParams();
0432 }
0433 
0434 void RenderWidget::slotSwitchAspectRatio()
0435 {
0436     KdenliveSettings::setRescalekeepratio(m_view.rescale_keep->isChecked());
0437     if (m_view.rescale_keep->isChecked()) {
0438         slotUpdateRescaleWidth(m_view.rescale_width->value());
0439     }
0440 }
0441 
0442 void RenderWidget::slotCheckStartGuidePosition()
0443 {
0444     if (m_view.guide_start->currentIndex() > m_view.guide_end->currentIndex()) {
0445         m_view.guide_start->setCurrentIndex(m_view.guide_end->currentIndex());
0446     }
0447 }
0448 
0449 void RenderWidget::slotCheckEndGuidePosition()
0450 {
0451     if (m_view.guide_end->currentIndex() < m_view.guide_start->currentIndex()) {
0452         m_view.guide_end->setCurrentIndex(m_view.guide_start->currentIndex());
0453     }
0454 }
0455 
0456 void RenderWidget::setGuides(std::weak_ptr<MarkerListModel> guidesModel)
0457 {
0458     m_guidesModel = std::move(guidesModel);
0459     reloadGuides();
0460     if (auto ptr = m_guidesModel.lock()) {
0461         connect(ptr.get(), &MarkerListModel::modelChanged, this, &RenderWidget::reloadGuides);
0462     }
0463 }
0464 
0465 void RenderWidget::reloadGuides()
0466 {
0467     double projectDuration = GenTime(pCore->projectDuration() - 1, pCore->getCurrentFps()).ms() / 1000;
0468     QVariant startData = m_view.guide_start->currentData();
0469     QVariant endData = m_view.guide_end->currentData();
0470     m_view.guide_start->clear();
0471     m_view.guide_end->clear();
0472 
0473     if (auto ptr = m_guidesModel.lock()) {
0474         m_view.guideCategoryChooser->setMarkerModel(ptr.get());
0475         QList<CommentedTime> markers = ptr->getAllMarkers();
0476         double fps = pCore->getCurrentFps();
0477         m_view.render_guide->setEnabled(!markers.isEmpty());
0478         m_view.render_multi->setEnabled(!markers.isEmpty());
0479         if (!markers.isEmpty()) {
0480             m_view.guide_start->addItem(i18n("Beginning"), 0);
0481             for (const auto &marker : qAsConst(markers)) {
0482                 GenTime pos = marker.time();
0483                 const QString guidePos = Timecode::getStringTimecode(pos.frames(fps), fps);
0484                 m_view.guide_start->addItem(marker.comment() + QLatin1Char('/') + guidePos, pos.seconds());
0485                 m_view.guide_end->addItem(marker.comment() + QLatin1Char('/') + guidePos, pos.seconds());
0486             }
0487             m_view.guide_end->addItem(i18n("End"), projectDuration);
0488             if (!startData.isNull()) {
0489                 int ix = qMax(0, m_view.guide_start->findData(startData));
0490                 m_view.guide_start->setCurrentIndex(ix);
0491             }
0492             if (!endData.isNull()) {
0493                 int ix = qMax(m_view.guide_start->currentIndex() + 1, m_view.guide_end->findData(endData));
0494                 m_view.guide_end->setCurrentIndex(ix);
0495             }
0496         } else {
0497             if (m_view.render_guide->isChecked() || m_view.render_multi->isChecked()) {
0498                 m_view.render_full->setChecked(true);
0499             }
0500         }
0501     } else {
0502         m_view.render_guide->setEnabled(false);
0503         m_view.render_multi->setEnabled(false);
0504         if (m_view.render_guide->isChecked() || m_view.render_multi->isChecked()) {
0505             m_view.render_full->setChecked(true);
0506         }
0507     }
0508     slotRenderModeChanged();
0509 }
0510 
0511 void RenderWidget::slotUpdateButtons(const QUrl &url)
0512 {
0513     if (!RenderPresetRepository::get()->presetExists(m_currentProfile)) {
0514         m_view.buttonSaveAs->setEnabled(false);
0515         m_view.buttonDelete->setEnabled(false);
0516         m_view.buttonEdit->setEnabled(false);
0517         m_view.buttonRender->setEnabled(false);
0518         m_view.buttonGenerateScript->setEnabled(false);
0519     } else {
0520         std::unique_ptr<RenderPresetModel> &profile = RenderPresetRepository::get()->getPreset(m_currentProfile);
0521         m_view.buttonRender->setEnabled(!m_view.out_file->url().isEmpty() && profile->error().isEmpty());
0522         m_view.buttonGenerateScript->setEnabled(!m_view.out_file->url().isEmpty() && profile->error().isEmpty());
0523         m_view.buttonSaveAs->setEnabled(true);
0524         m_view.buttonDelete->setEnabled(profile->editable());
0525         m_view.buttonEdit->setEnabled(profile->editable());
0526     }
0527     if (url.isValid()) {
0528         if (!RenderPresetRepository::get()->presetExists(m_currentProfile)) {
0529             m_view.buttonRender->setEnabled(false);
0530             m_view.buttonGenerateScript->setEnabled(false);
0531             return;
0532         }
0533         std::unique_ptr<RenderPresetModel> &profile = RenderPresetRepository::get()->getPreset(m_currentProfile);
0534         m_view.out_file->setUrl(filenameWithExtension(url, profile->extension()));
0535     }
0536 }
0537 
0538 void RenderWidget::slotUpdateButtons()
0539 {
0540     slotUpdateButtons(QUrl());
0541 }
0542 
0543 void RenderWidget::slotSavePresetAs()
0544 {
0545     if (RenderPresetRepository::get()->presetExists(m_currentProfile)) {
0546         std::unique_ptr<RenderPresetModel> &profile = RenderPresetRepository::get()->getPreset(m_currentProfile);
0547 
0548         QPointer<RenderPresetDialog> dialog = new RenderPresetDialog(this, profile.get(), RenderPresetDialog::SaveAs);
0549         if (dialog->exec() == QDialog::Accepted) {
0550             parseProfiles(dialog->saveName());
0551         }
0552         delete dialog;
0553     } else {
0554         slotNewPreset();
0555     }
0556 }
0557 
0558 void RenderWidget::slotNewPreset()
0559 {
0560     QPointer<RenderPresetDialog> dialog = new RenderPresetDialog(this);
0561     if (dialog->exec() == QDialog::Accepted) {
0562         parseProfiles(dialog->saveName());
0563     }
0564     delete dialog;
0565 }
0566 
0567 void RenderWidget::slotEditPreset()
0568 {
0569     if (!RenderPresetRepository::get()->presetExists(m_currentProfile)) {
0570         return;
0571     }
0572     std::unique_ptr<RenderPresetModel> &profile = RenderPresetRepository::get()->getPreset(m_currentProfile);
0573 
0574     if (!profile->editable()) {
0575         return;
0576     }
0577 
0578     QPointer<RenderPresetDialog> dialog = new RenderPresetDialog(this, profile.get(), RenderPresetDialog::Edit);
0579     if (dialog->exec() == QDialog::Accepted) {
0580         parseProfiles(dialog->saveName());
0581     }
0582     delete dialog;
0583 }
0584 
0585 void RenderWidget::focusItem(const QString &profile)
0586 {
0587     QItemSelectionModel *selection = m_view.profileTree->selectionModel();
0588     QModelIndex index = m_treeModel->findPreset(profile);
0589     if (!index.isValid() && RenderPresetRepository::get()->presetExists(KdenliveSettings::renderProfile())) {
0590         index = m_treeModel->findPreset(KdenliveSettings::renderProfile());
0591     }
0592     if (index.isValid()) {
0593         selection->select(index, QItemSelectionModel::ClearAndSelect);
0594         // expand corresponding category
0595         auto parent = m_treeModel->parent(index);
0596         m_view.profileTree->expand(parent);
0597         m_view.profileTree->scrollTo(index, QAbstractItemView::PositionAtCenter);
0598     }
0599 }
0600 
0601 void RenderWidget::slotPrepareExport(bool delayedRendering, const QString &scriptPath)
0602 {
0603     Q_UNUSED(scriptPath)
0604     if (pCore->projectDuration() < 2) {
0605         // Empty project, don't attempt to render
0606         m_view.infoMessage->setMessageType(KMessageWidget::Warning);
0607         m_view.infoMessage->setText(i18n("Add a clip to timeline before rendering"));
0608         m_view.infoMessage->animatedShow();
0609         return;
0610     }
0611     if (!QFile::exists(KdenliveSettings::rendererpath())) {
0612         m_view.infoMessage->setMessageType(KMessageWidget::Warning);
0613         m_view.infoMessage->setText(i18n("Cannot find the melt program required for rendering (part of Mlt)"));
0614         m_view.infoMessage->animatedShow();
0615         return;
0616     }
0617     m_view.infoMessage->hide();
0618     if (QFile::exists(m_view.out_file->url().toLocalFile())) {
0619         if (KMessageBox::warningTwoActions(this, i18n("Output file already exists. Do you want to overwrite it?"), {}, KStandardGuiItem::overwrite(),
0620                                            KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) {
0621             return;
0622         }
0623     }
0624     // mantisbt 1051
0625     QDir dir(m_view.out_file->url().adjusted(QUrl::RemoveFilename).toLocalFile());
0626     if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
0627         KMessageBox::error(this, i18n("The directory %1, could not be created.\nPlease make sure you have the required permissions.",
0628                                       m_view.out_file->url().adjusted(QUrl::RemoveFilename).toLocalFile()));
0629         return;
0630     }
0631 
0632     prepareRendering(delayedRendering);
0633 }
0634 
0635 void RenderWidget::prepareRendering(bool delayedRendering)
0636 {
0637     saveRenderProfile();
0638 
0639     KdenliveDoc *project = pCore->currentDoc();
0640     QString overlayData = m_view.tc_type->currentData().toString();
0641     QString playlistContent =
0642         pCore->projectManager()->projectSceneList(project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(), overlayData);
0643 
0644     // Set playlist audio volume to 100%
0645     QDomDocument doc;
0646     doc.setContent(playlistContent);
0647 
0648     // Add autoclose to playlists.
0649     QDomNodeList playlists = doc.elementsByTagName(QStringLiteral("playlist"));
0650     for (int i = 0; i < playlists.length(); ++i) {
0651         playlists.item(i).toElement().setAttribute(QStringLiteral("autoclose"), 1);
0652     }
0653 
0654     // Do we want proxy rendering
0655     if (project->useProxy() && !m_view.proxy_render->isChecked()) {
0656         pCore->currentDoc()->useOriginals(doc);
0657     }
0658 
0659     QString outputFile = m_view.out_file->url().toLocalFile();
0660     // in/out points
0661     int in = 0;
0662     // Remove last black frame
0663     int out = pCore->projectDuration() - 1;
0664     Monitor *pMon = pCore->getMonitor(Kdenlive::ProjectMonitor);
0665     double fps = pCore->getCurrentProfile()->fps();
0666     QString subtitleFile;
0667     if (m_view.embed_subtitles->isEnabled() && m_view.embed_subtitles->isChecked() && project->hasSubtitles()) {
0668         QTemporaryFile src(QDir::temp().absoluteFilePath(QString("XXXXXX.srt")));
0669         if (!src.open()) {
0670             // Something went wrong
0671             KMessageBox::error(this, i18n("Could not create temporary subtitle file"));
0672             return;
0673         }
0674         subtitleFile = src.fileName();
0675         src.setAutoRemove(false);
0676         // disable subtitle filter(s) as they will be embeded in a second step of rendering
0677         QDomNodeList filters = doc.elementsByTagName(QStringLiteral("filter"));
0678         for (int i = 0; i < filters.length(); ++i) {
0679             if (Xml::getXmlProperty(filters.item(i).toElement(), QStringLiteral("mlt_service")) == QLatin1String("avfilter.subtitles")) {
0680                 Xml::setXmlProperty(filters.item(i).toElement(), QStringLiteral("disable"), QStringLiteral("1"));
0681             }
0682         }
0683     }
0684     if (m_view.render_zone->isChecked()) {
0685         in = pMon->getZoneStart();
0686         out = pMon->getZoneEnd() - 1;
0687         if (!subtitleFile.isEmpty()) {
0688             project->generateRenderSubtitleFile(in, out, subtitleFile);
0689         }
0690         generateRenderFiles(doc, in, out, outputFile, delayedRendering, subtitleFile);
0691     } else if (m_view.render_guide->isChecked()) {
0692         double guideStart = m_view.guide_start->itemData(m_view.guide_start->currentIndex()).toDouble();
0693         double guideEnd = m_view.guide_end->itemData(m_view.guide_end->currentIndex()).toDouble();
0694         in = int(GenTime(guideStart).frames(fps));
0695         // End rendering at frame before last guide
0696         out = int(GenTime(guideEnd).frames(fps)) - 1;
0697         if (!subtitleFile.isEmpty()) {
0698             project->generateRenderSubtitleFile(in, out, subtitleFile);
0699         }
0700         generateRenderFiles(doc, in, out, outputFile, delayedRendering, subtitleFile);
0701     } else if (m_view.render_multi->isChecked()) {
0702         if (auto ptr = m_guidesModel.lock()) {
0703             int category = m_view.guideCategoryChooser->currentCategory();
0704             QList<CommentedTime> markers = ptr->getAllMarkers(category);
0705             if (!markers.isEmpty()) {
0706                 bool beginParsed = false;
0707                 QStringList names;
0708                 for (int i = 0; i < markers.count(); i++) {
0709                     QString name;
0710                     int absoluteOut = pCore->projectDuration() - 2;
0711                     in = 0;
0712                     out = absoluteOut;
0713                     if (!beginParsed && i == 0 && markers.at(i).time().frames(fps) != 0) {
0714                         i -= 1;
0715                         beginParsed = true;
0716                         name = i18n("begin");
0717                     }
0718                     if (i >= 0) {
0719                         name = markers.at(i).comment();
0720                         in = markers.at(i).time().frames(fps);
0721                     }
0722                     int j = 0;
0723                     QString newName = name;
0724                     // if name alrady exist, add a suffix
0725                     while (names.contains(newName)) {
0726                         newName = QStringLiteral("%1_%2").arg(name).arg(j);
0727                         j++;
0728                     }
0729                     names.append(newName);
0730                     name = newName;
0731                     if (in < absoluteOut) {
0732                         if (i + 1 < markers.count()) {
0733                             out = qMin(markers.at(i + 1).time().frames(fps) - 1, absoluteOut);
0734                         }
0735                         QString filename =
0736                             outputFile.section(QLatin1Char('.'), 0, -2) + QStringLiteral("-%1.").arg(name) + outputFile.section(QLatin1Char('.'), -1);
0737                         QDomDocument docCopy = doc.cloneNode(true).toDocument();
0738                         if (!subtitleFile.isEmpty()) {
0739                             project->generateRenderSubtitleFile(in, out, subtitleFile);
0740                         }
0741                         generateRenderFiles(docCopy, in, out, filename, false, subtitleFile);
0742                         if (!subtitleFile.isEmpty() && i < markers.count() - 1) {
0743                             QTemporaryFile src(QDir::temp().absoluteFilePath(QString("XXXXXX.srt")));
0744                             if (!src.open()) {
0745                                 // Something went wrong
0746                                 KMessageBox::error(this, i18n("Could not create temporary subtitle file"));
0747                                 return;
0748                             }
0749                             subtitleFile = src.fileName();
0750                             src.setAutoRemove(false);
0751                         }
0752                     }
0753                 }
0754             }
0755         }
0756     } else {
0757         if (!subtitleFile.isEmpty()) {
0758             project->generateRenderSubtitleFile(in, out, subtitleFile);
0759         }
0760         generateRenderFiles(doc, in, out, outputFile, delayedRendering, subtitleFile);
0761     }
0762 }
0763 
0764 QString RenderWidget::generatePlaylistFile(bool delayedRendering)
0765 {
0766     if (delayedRendering) {
0767         bool ok;
0768         QString filename = QFileInfo(pCore->currentDoc()->url().toLocalFile()).fileName();
0769         const QString fileExtension = QStringLiteral(".mlt");
0770         if (filename.isEmpty()) {
0771             filename = i18n("export");
0772         } else {
0773             filename = filename.section(QLatin1Char('.'), 0, -2);
0774         }
0775 
0776         QDir projectFolder(pCore->currentDoc()->projectDataFolder());
0777         projectFolder.mkpath(QStringLiteral("kdenlive-renderqueue"));
0778         projectFolder.cd(QStringLiteral("kdenlive-renderqueue"));
0779         int ix = 1;
0780         QString newFilename = filename;
0781         // if name alrady exist, add a suffix
0782         while (projectFolder.exists(newFilename + fileExtension)) {
0783             newFilename = QStringLiteral("%1-%2").arg(filename).arg(ix);
0784             ix++;
0785         }
0786         filename = QInputDialog::getText(this, i18nc("@title:window", "Delayed Rendering"), i18n("Select a name for this rendering."), QLineEdit::Normal,
0787                                          newFilename, &ok);
0788         if (!ok) {
0789             return {};
0790         }
0791         if (!filename.endsWith(fileExtension)) {
0792             filename.append(fileExtension);
0793         }
0794         if (projectFolder.exists(newFilename)) {
0795             if (KMessageBox::questionTwoActions(this, i18n("File %1 already exists.\nDo you want to overwrite it?", filename), {},
0796                                                 KStandardGuiItem::overwrite(), KStandardGuiItem::cancel()) == KMessageBox::PrimaryAction) {
0797                 return {};
0798             }
0799         }
0800         return projectFolder.absoluteFilePath(filename);
0801     }
0802     // No delayed rendering, we can use a temp file
0803     QTemporaryFile tmp(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.mlt")));
0804     if (!tmp.open()) {
0805         // Something went wrong
0806         return {};
0807     }
0808     tmp.close();
0809     return tmp.fileName();
0810 }
0811 
0812 void RenderWidget::generateRenderFiles(QDomDocument doc, int in, int out, QString outputFile, bool delayedRendering, const QString &subtitleFile)
0813 {
0814     QString playlistPath = generatePlaylistFile(delayedRendering);
0815     QString extension = outputFile.section(QLatin1Char('.'), -1);
0816 
0817     if (playlistPath.isEmpty()) {
0818         return;
0819     }
0820     QString renderArgs = m_view.advanced_params->toPlainText().simplified();
0821     QDomElement consumer = doc.createElement(QStringLiteral("consumer"));
0822     consumer.setAttribute(QStringLiteral("in"), in);
0823     consumer.setAttribute(QStringLiteral("out"), out);
0824     consumer.setAttribute(QStringLiteral("mlt_service"), QStringLiteral("avformat"));
0825 
0826     QDomNodeList profiles = doc.elementsByTagName(QStringLiteral("profile"));
0827     if (profiles.isEmpty()) {
0828         doc.documentElement().insertAfter(consumer, doc.documentElement());
0829     } else {
0830         doc.documentElement().insertAfter(consumer, profiles.at(profiles.length() - 1));
0831     }
0832 
0833     // Insert project metadata
0834     if (m_view.export_meta->isChecked()) {
0835         QMap<QString, QString> metadata = pCore->currentDoc()->metadata();
0836         QMap<QString, QString>::const_iterator mi = metadata.constBegin();
0837         while (mi != metadata.constEnd()) {
0838             consumer.setAttribute(mi.key(), mi.value());
0839             ++mi;
0840         }
0841     }
0842 
0843 
0844     // insert params from preset
0845     QStringList args = renderArgs.split(QLatin1Char(' '));
0846     for (auto &param : args) {
0847         if (param.contains(QLatin1Char('='))) {
0848             QString paramName = param.section(QLatin1Char('='), 0, 0);
0849             QString paramValue = param.section(QLatin1Char('='), 1);
0850             consumer.setAttribute(paramName, paramValue);
0851         }
0852     }
0853 
0854     // If we use a pix_fmt with alpha channel (ie. transparent),
0855     // we need to remove the black background track
0856     if (renderArgs.contains(QLatin1String("pix_fmt=argb")) || renderArgs.contains(QLatin1String("pix_fmt=abgr")) ||
0857         renderArgs.contains(QLatin1String("pix_fmt=bgra")) || renderArgs.contains(QLatin1String("pix_fmt=gbra")) ||
0858         renderArgs.contains(QLatin1String("pix_fmt=rgba")) || renderArgs.contains(QLatin1String("pix_fmt=yuva")) ||
0859         renderArgs.contains(QLatin1String("pix_fmt=ya")) || renderArgs.contains(QLatin1String("pix_fmt=ayuv"))) {
0860         auto prods = doc.elementsByTagName(QStringLiteral("producer"));
0861         for (int i = 0; i < prods.count(); ++i) {
0862             auto prod = prods.at(i).toElement();
0863             if (prod.attribute(QStringLiteral("id")) == QStringLiteral("black_track")) {
0864                 Xml::setXmlProperty(prod, QStringLiteral("resource"), QStringLiteral("transparent"));
0865                 break;
0866             }
0867         }
0868     }
0869 
0870     QMap<QString, QString> renderFiles;
0871     if (renderArgs.contains("=stills/")) {
0872         // Image sequence, ensure we have a %0xd at file end.
0873         // Format string for counter
0874         static const QRegularExpression rx(QRegularExpression::anchoredPattern(QStringLiteral(".*%[0-9]*d.*")));
0875         if (!rx.match(outputFile).hasMatch()) {
0876             outputFile = outputFile.section(QLatin1Char('.'), 0, -2) + QStringLiteral("_%05d.") + extension;
0877         }
0878     }
0879 
0880     if (m_view.stemAudioExport->isChecked() && m_view.stemAudioExport->isEnabled()) {
0881         if (delayedRendering) {
0882             if (KMessageBox::warningContinueCancel(this, i18n("Script rendering and multi track audio export can not be used together.\n"
0883                                                               "Script will be saved without multi track export.")) == KMessageBox::Cancel) {
0884                 return;
0885             };
0886         }
0887         int audioCount = 0;
0888         QDomNodeList orginalTractors = doc.elementsByTagName(QStringLiteral("tractor"));
0889         // process in reversed order to make file naming fit to UI
0890         for (int i = orginalTractors.size(); i >= 0; i--) {
0891             bool isAudio = Xml::getXmlProperty(orginalTractors.at(i).toElement(), QStringLiteral("kdenlive:audio_track")).toInt() == 1;
0892             QString trackName = Xml::getXmlProperty(orginalTractors.at(i).toElement(), QStringLiteral("kdenlive:track_name"));
0893             if (isAudio) {
0894                 // setup filenames
0895                 QString appendix = QString("_Audio_%1%2%3.")
0896                                        .arg(audioCount + 1)
0897                                        .arg(trackName.isEmpty() ? QString() : QStringLiteral("-"))
0898                                        .arg(trackName.replace(QStringLiteral(" "), QStringLiteral("_")));
0899                 QString playlistFile = playlistPath.section(QLatin1Char('.'), 0, -2) + appendix + playlistPath.section(QLatin1Char('.'), -1);
0900                 QString targetFile = outputFile.section(QLatin1Char('.'), 0, -2) + appendix + extension;
0901                 renderFiles.insert(playlistFile, targetFile);
0902 
0903                 // init doc copy
0904                 QDomDocument docCopy = doc.cloneNode(true).toDocument();
0905                 QDomElement copyConsumer = docCopy.elementsByTagName(QStringLiteral("consumer")).at(0).toElement();
0906                 copyConsumer.setAttribute(QStringLiteral("target"), targetFile);
0907 
0908                 QDomNodeList tracktors = docCopy.elementsByTagName(QStringLiteral("tractor"));
0909                 // the last tractor is the main tracktor, don't process it (-1)
0910                 for (int j = 0; j < tracktors.size() - 1; j++) {
0911                     QDomNodeList tracks = tracktors.at(j).toElement().elementsByTagName(QStringLiteral("track"));
0912                     for (int l = 0; l < tracks.size(); l++) {
0913                         if (i != j) {
0914                             tracks.at(l).toElement().setAttribute(QStringLiteral("hide"), QStringLiteral("both"));
0915                         }
0916                     }
0917                 }
0918 
0919                 QFile file(playlistFile);
0920                 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0921                     pCore->displayMessage(i18n("Cannot write to file %1", playlistFile), ErrorMessage);
0922                     return;
0923                 }
0924                 file.write(docCopy.toString().toUtf8());
0925                 if (file.error() != QFile::NoError) {
0926                     pCore->displayMessage(i18n("Cannot write to file %1", playlistFile), ErrorMessage);
0927                     file.close();
0928                     return;
0929                 }
0930                 file.close();
0931                 audioCount++;
0932             }
0933         }
0934     }
0935 
0936     QDomDocument clone;
0937     int passes = m_view.checkTwoPass->isChecked() ? 2 : 1;
0938     if (passes == 2) {
0939         // We will generate 2 files, one for each pass.
0940         clone = doc.cloneNode(true).toDocument();
0941     }
0942 
0943     for (int i = 0; i < passes; i++) {
0944         // Append consumer settings
0945         QDomDocument final = i > 0 ? clone : doc;
0946         QDomNodeList consList = final.elementsByTagName(QStringLiteral("consumer"));
0947         QDomElement finalConsumer = consList.at(0).toElement();
0948         finalConsumer.setAttribute(QStringLiteral("target"), outputFile);
0949         int pass = passes == 2 ? i + 1 : 0;
0950         QString playlistName = playlistPath;
0951         if (pass == 2) {
0952             playlistName = playlistName.section(QLatin1Char('.'), 0, -2) + QStringLiteral("-pass%1.").arg(2) + extension;
0953         }
0954         renderFiles.insert(playlistName, outputFile);
0955 
0956         // Prepare rendering args
0957         QString logFile = QStringLiteral("%1_2pass.log").arg(outputFile);
0958         if (renderArgs.contains(QStringLiteral("libx265"))) {
0959             if (pass == 1 || pass == 2) {
0960                 QString x265params = finalConsumer.attribute("x265-params");
0961                 x265params = QString("pass=%1:stats=%2:%3").arg(pass).arg(logFile.replace(":", "\\:"), x265params);
0962                 finalConsumer.setAttribute("x265-params", x265params);
0963             }
0964         } else {
0965             if (pass == 1 || pass == 2) {
0966                 finalConsumer.setAttribute("pass", pass);
0967                 finalConsumer.setAttribute("passlogfile", logFile);
0968             }
0969             if (pass == 1) {
0970                 finalConsumer.setAttribute("fastfirstpass", 1);
0971                 finalConsumer.removeAttribute("acodec");
0972                 finalConsumer.setAttribute("an", 1);
0973             } else {
0974                 finalConsumer.removeAttribute("fastfirstpass");
0975             }
0976         }
0977         QFile file(playlistName);
0978         if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0979             pCore->displayMessage(i18n("Cannot write to file %1", playlistName), ErrorMessage);
0980             return;
0981         }
0982         file.write(final.toString().toUtf8());
0983         if (file.error() != QFile::NoError) {
0984             pCore->displayMessage(i18n("Cannot write to file %1", playlistName), ErrorMessage);
0985             file.close();
0986             return;
0987         }
0988         file.close();
0989     }
0990 
0991     // Create jobs
0992     if (delayedRendering) {
0993         parseScriptFiles();
0994         return;
0995     }
0996     QList<RenderJobItem *> jobList;
0997     QMap<QString, QString>::const_iterator i = renderFiles.constBegin();
0998     while (i != renderFiles.constEnd()) {
0999         RenderJobItem *renderItem = createRenderJob(i.key(), i.value(), in, out, subtitleFile);
1000         if (renderItem != nullptr) {
1001             jobList << renderItem;
1002         }
1003         ++i;
1004     }
1005     if (jobList.count() > 0) {
1006         m_view.running_jobs->setCurrentItem(jobList.at(0));
1007     }
1008     m_view.tabWidget->setCurrentIndex(Tabs::JobsTab);
1009     // check render status
1010     checkRenderStatus();
1011 }
1012 
1013 RenderJobItem *RenderWidget::createRenderJob(const QString &playlist, const QString &outputFile, int in, int out, const QString &subtitleFile)
1014 {
1015     QList<QTreeWidgetItem *> existing = m_view.running_jobs->findItems(outputFile, Qt::MatchExactly, 1);
1016     RenderJobItem *renderItem = nullptr;
1017     if (!existing.isEmpty()) {
1018         renderItem = static_cast<RenderJobItem *>(existing.at(0));
1019         if (renderItem->status() == RUNNINGJOB || renderItem->status() == WAITINGJOB || renderItem->status() == STARTINGJOB) {
1020             // There is an existing job that is still pending
1021             KMessageBox::information(this,
1022                                      i18n("There is already a job writing file:<br /><b>%1</b><br />Abort the job if you want to overwrite it…", outputFile),
1023                                      i18n("Already running"));
1024             // focus the running job
1025             m_view.running_jobs->setCurrentItem(renderItem);
1026             return nullptr;
1027         }
1028         // There is an existing job that already finished
1029         delete renderItem;
1030         renderItem = nullptr;
1031     }
1032     renderItem = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << outputFile);
1033 
1034     QDateTime t = QDateTime::currentDateTime();
1035     renderItem->setData(1, StartTimeRole, t);
1036     renderItem->setData(1, LastTimeRole, t);
1037     renderItem->setData(1, LastFrameRole, in);
1038     QStringList argsJob = {KdenliveSettings::rendererpath(),
1039                            playlist,
1040                            outputFile,
1041                            QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid()),
1042                            QStringLiteral("-out"),
1043                            QString::number(out)};
1044     if (!subtitleFile.isEmpty()) {
1045         argsJob << QStringLiteral("-subtitle") << subtitleFile;
1046     }
1047     renderItem->setData(1, ParametersRole, argsJob);
1048     qDebug() << "* CREATED JOB WITH ARGS: " << argsJob;
1049     renderItem->setData(1, OpenBrowserRole, m_view.open_browser->isChecked());
1050     renderItem->setData(1, PlayAfterRole, m_view.play_after->isChecked());
1051     if (!m_view.audio_box->isChecked()) {
1052         renderItem->setData(1, ExtraInfoRole, i18n("Video without audio track"));
1053     } else if (!m_view.video_box->isChecked()) {
1054         renderItem->setData(1, ExtraInfoRole, i18n("Audio without video track"));
1055     } else {
1056         renderItem->setData(1, ExtraInfoRole, QString());
1057     }
1058     return renderItem;
1059 }
1060 
1061 void RenderWidget::checkRenderStatus()
1062 {
1063     // check if we have a job waiting to render
1064     if (m_blockProcessing) {
1065         return;
1066     }
1067 
1068     // Make sure no other rendering is running
1069     if (runningJobsCount() > 0) {
1070         return;
1071     }
1072 
1073     auto *item = static_cast<RenderJobItem *>(m_view.running_jobs->topLevelItem(0));
1074 
1075     bool waitingJob = false;
1076 
1077     // Find first waiting job
1078     while (item != nullptr) {
1079         if (item->status() == WAITINGJOB) {
1080             QDateTime t = QDateTime::currentDateTime();
1081             item->setData(1, StartTimeRole, t);
1082             item->setData(1, LastTimeRole, t);
1083             waitingJob = true;
1084             startRendering(item);
1085             // Check for 2 pass encoding
1086             QStringList jobData = item->data(1, ParametersRole).toStringList();
1087             if (jobData.size() > 2 && jobData.at(1).endsWith(QStringLiteral("-pass2.mlt"))) {
1088                 // Find and remove 1st pass job
1089                 QTreeWidgetItem *above = m_view.running_jobs->itemAbove(item);
1090                 QString firstPassName = jobData.at(1).section(QLatin1Char('-'), 0, -2) + QStringLiteral(".mlt");
1091                 while (above) {
1092                     QStringList aboveData = above->data(1, ParametersRole).toStringList();
1093                     qDebug() << "// GOT  JOB: " << aboveData.at(1);
1094                     if (aboveData.size() > 2 && aboveData.at(1) == firstPassName) {
1095                         delete above;
1096                         break;
1097                     }
1098                     above = m_view.running_jobs->itemAbove(above);
1099                 }
1100             }
1101             item->setStatus(STARTINGJOB);
1102             break;
1103         }
1104         item = static_cast<RenderJobItem *>(m_view.running_jobs->itemBelow(item));
1105     }
1106     if (!waitingJob && m_view.shutdown->isChecked()) {
1107         emit shutdown();
1108     }
1109 }
1110 
1111 void RenderWidget::startRendering(RenderJobItem *item)
1112 {
1113     auto rendererArgs = item->data(1, ParametersRole).toStringList();
1114     qDebug() << "starting kdenlive_render process using: " << m_renderer;
1115     if (!QProcess::startDetached(m_renderer, rendererArgs)) {
1116         item->setStatus(FAILEDJOB);
1117     } else {
1118         KNotification::event(QStringLiteral("RenderStarted"), i18n("Rendering <i>%1</i> started", item->text(1)), QPixmap(), this);
1119     }
1120 }
1121 
1122 int RenderWidget::waitingJobsCount() const
1123 {
1124     int count = 0;
1125     auto *item = static_cast<RenderJobItem *>(m_view.running_jobs->topLevelItem(0));
1126     while (item != nullptr) {
1127         if (item->status() == WAITINGJOB) {
1128             count++;
1129         }
1130         item = static_cast<RenderJobItem *>(m_view.running_jobs->itemBelow(item));
1131     }
1132     return count;
1133 }
1134 
1135 int RenderWidget::runningJobsCount() const
1136 {
1137     int count = 0;
1138     auto *item = static_cast<RenderJobItem *>(m_view.running_jobs->topLevelItem(0));
1139     while (item != nullptr) {
1140         if (item->status() == RUNNINGJOB || item->status() == STARTINGJOB) {
1141             count++;
1142         }
1143         item = static_cast<RenderJobItem *>(m_view.running_jobs->itemBelow(item));
1144     }
1145     return count;
1146 }
1147 
1148 void RenderWidget::adjustViewToProfile()
1149 {
1150     m_view.rescale_width->setValue(KdenliveSettings::defaultrescalewidth());
1151     if (!m_view.rescale_keep->isChecked()) {
1152         m_view.rescale_height->blockSignals(true);
1153         m_view.rescale_height->setValue(KdenliveSettings::defaultrescaleheight());
1154         m_view.rescale_height->blockSignals(false);
1155     }
1156     refreshView();
1157 }
1158 
1159 void RenderWidget::refreshView()
1160 {
1161     focusItem();
1162     loadProfile();
1163 }
1164 
1165 QUrl RenderWidget::filenameWithExtension(QUrl url, const QString &extension)
1166 {
1167     if (!url.isValid()) {
1168         url = QUrl::fromLocalFile(pCore->currentDoc()->projectDataFolder() + QDir::separator());
1169     }
1170     QString directory = url.adjusted(QUrl::RemoveFilename).toLocalFile();
1171 
1172     QString ext;
1173     if (extension.startsWith(QLatin1Char('.'))) {
1174         ext = extension;
1175     } else {
1176         ext = '.' + extension;
1177     }
1178 
1179     QString filename = url.fileName();
1180     if (filename.isEmpty()) {
1181         filename = pCore->currentDoc()->url().fileName();
1182     }
1183     if (filename.isEmpty()) {
1184         filename = i18n("untitled");
1185     }
1186 
1187     int pos = filename.lastIndexOf('.');
1188     if (pos == 0) {
1189         filename.append(ext);
1190     } else {
1191         if (!filename.endsWith(ext, Qt::CaseInsensitive)) {
1192             filename = filename.left(pos) + ext;
1193         }
1194     }
1195 
1196     return QUrl::fromLocalFile(directory + filename);
1197 }
1198 
1199 void RenderWidget::slotChangeSelection(const QModelIndex &current, const QModelIndex &previous)
1200 {
1201     if (m_treeModel->parent(current) == QModelIndex()) {
1202         // in that case, we have selected a category, which we don't want
1203         QItemSelectionModel *selection = m_view.profileTree->selectionModel();
1204         selection->select(previous, QItemSelectionModel::ClearAndSelect);
1205         // expand corresponding category
1206         auto parent = m_treeModel->parent(previous);
1207         m_view.profileTree->expand(parent);
1208         m_view.profileTree->scrollTo(previous, QAbstractItemView::PositionAtCenter);
1209         return;
1210     }
1211     m_currentProfile = m_treeModel->getPreset(current);
1212     KdenliveSettings::setRenderProfile(m_currentProfile);
1213     loadProfile();
1214 }
1215 
1216 void RenderWidget::loadProfile()
1217 {
1218     slotUpdateButtons();
1219     // Format not available (e.g. codec not installed); Disable start
1220     if (!RenderPresetRepository::get()->presetExists(m_currentProfile)) {
1221         if (m_currentProfile.isEmpty()) {
1222             errorMessage(PresetError, i18n("No preset selected"));
1223         } else {
1224             errorMessage(PresetError, i18n("No matching preset"));
1225         }
1226         m_view.advanced_params->clear();
1227         m_view.buttonRender->setEnabled(false);
1228         m_view.buttonGenerateScript->setEnabled(false);
1229         m_view.optionsGroup->setEnabled(false);
1230         return;
1231     }
1232     std::unique_ptr<RenderPresetModel> &profile = RenderPresetRepository::get()->getPreset(m_currentProfile);
1233     QString params = profile->params();
1234 
1235     if (profile->extension().isEmpty()) {
1236         errorMessage(PresetError, i18n("Invalid preset"));
1237     }
1238     QString error = profile->error();
1239     if (error.isEmpty()) {
1240         error = profile->warning();
1241     }
1242     errorMessage(PresetError, error);
1243 
1244     QUrl url = filenameWithExtension(m_view.out_file->url(), profile->extension());
1245     m_view.out_file->setUrl(url);
1246     m_view.out_file->setFilter("*." + profile->extension());
1247 
1248     m_view.buttonDelete->setEnabled(profile->editable());
1249     m_view.buttonEdit->setEnabled(profile->editable());
1250 
1251     if (!profile->speeds().isEmpty()) {
1252         m_view.speed->setEnabled(true);
1253         m_view.speed->setMaximum(profile->speeds().count() - 1);
1254         m_view.speed->setValue(profile->defaultSpeedIndex());
1255     } else {
1256         m_view.speed->setEnabled(false);
1257     }
1258     adjustSpeed(m_view.speed->value());
1259     bool passes = profile->hasParam(QStringLiteral("passes"));
1260     m_view.checkTwoPass->setEnabled(passes);
1261     m_view.checkTwoPass->setChecked(passes && params.contains(QStringLiteral("passes=2")));
1262 
1263     m_view.encoder_threads->setEnabled(!profile->hasParam(QStringLiteral("threads")));
1264     m_view.embed_subtitles->setEnabled(profile->extension() == QLatin1String("mkv") || profile->extension() == QLatin1String("matroska"));
1265 
1266     m_view.video_box->setChecked(profile->getParam(QStringLiteral("vn")) != QStringLiteral("1"));
1267     m_view.audio_box->setChecked(profile->getParam(QStringLiteral("an")) != QStringLiteral("1"));
1268 
1269     m_view.buttonRender->setEnabled(error.isEmpty());
1270     m_view.buttonGenerateScript->setEnabled(error.isEmpty());
1271     m_view.optionsGroup->setEnabled(true);
1272     refreshParams();
1273 }
1274 
1275 void RenderWidget::refreshParams()
1276 {
1277     if (!RenderPresetRepository::get()->presetExists(m_currentProfile)) {
1278         return;
1279     }
1280     std::unique_ptr<RenderPresetModel> &preset = RenderPresetRepository::get()->getPreset(m_currentProfile);
1281 
1282     if (preset->hasFixedSize()) {
1283         // profile has a fixed size, do not allow resize
1284         m_view.rescale->setEnabled(false);
1285         setRescaleEnabled(false);
1286     } else {
1287         m_view.rescale->setEnabled(m_view.video_box->isChecked());
1288         setRescaleEnabled(m_view.video_box->isChecked() && m_view.rescale->isChecked());
1289     }
1290 
1291     QStringList removeParams = {QStringLiteral("an"), QStringLiteral("audio_off"), QStringLiteral("vn"), QStringLiteral("video_off"),
1292                                 QStringLiteral("real_time")};
1293 
1294     QStringList newParams;
1295 
1296     // Audio Channels: don't override, only set if it is not yet set
1297     if (!preset->hasParam(QStringLiteral("channels"))) {
1298         newParams.append(QStringLiteral("channels=%1").arg(pCore->audioChannels()));
1299     }
1300 
1301     // Check for movit
1302     if (KdenliveSettings::gpu_accel()) {
1303         newParams.append(QStringLiteral("glsl.=1"));
1304     }
1305 
1306     // In case of libx265 add x265-param
1307     if (preset->getParam(QStringLiteral("vcodec")).toLower() == QStringLiteral("libx265")) {
1308         newParams.append(QStringLiteral("x265-param=%1").arg(preset->x265Params()));
1309     }
1310 
1311     // Rescale
1312     bool rescale = m_view.rescale->isEnabled() && m_view.rescale->isChecked();
1313     if (rescale) {
1314         removeParams.append(
1315             {QStringLiteral("s"), QStringLiteral("width"), QStringLiteral("height"), QStringLiteral("sample_aspect_num"), QStringLiteral("sample_aspect_den")});
1316         newParams.append(QStringLiteral("s=%1x%2").arg(m_view.rescale_width->value()).arg(m_view.rescale_height->value()));
1317         if (preset->hasParam(QStringLiteral("sample_aspect_num")) || preset->hasParam(QStringLiteral("sample_aspect_den"))) {
1318             // reset display aspect ratio to 1:1 if we override the resolution to avoid errors
1319             newParams.append(QStringLiteral("sample_aspect_num=1 sample_aspect_den=1"));
1320         }
1321     }
1322 
1323     // Preview rendering
1324     bool previewRes = m_view.render_at_preview_res->isChecked() && pCore->getMonitorProfile().height() != pCore->getCurrentFrameSize().height();
1325     if (previewRes) {
1326         removeParams.append(QStringLiteral("scale"));
1327         newParams.append(QStringLiteral("scale=%1").arg(double(pCore->getMonitorProfile().height()) / pCore->getCurrentFrameSize().height()));
1328     }
1329 
1330     // disable audio if requested
1331     if (!m_view.audio_box->isChecked()) {
1332         newParams.append(QStringLiteral("an=1 audio_off=1"));
1333     }
1334 
1335     if (!m_view.video_box->isChecked()) {
1336         newParams.append(QStringLiteral("vn=1 video_off=1"));
1337     }
1338 
1339     if (!(m_view.video_box->isChecked() || m_view.audio_box->isChecked())) {
1340         errorMessage(OptionsError, i18n("Rendering without audio and video does not work. Please enable at least one of both."));
1341         m_view.buttonRender->setEnabled(false);
1342     } else {
1343         errorMessage(OptionsError, QString());
1344         m_view.buttonRender->setEnabled(preset->error().isEmpty());
1345     }
1346 
1347     // Parallel Processing
1348     int threadCount = KdenliveSettings::processingthreads();
1349     if (!m_view.processing_box->isChecked() || !m_view.processing_box->isEnabled()) {
1350         threadCount = 1;
1351     }
1352     newParams.append(QStringLiteral("real_time=%1").arg(-threadCount));
1353 
1354     // Adjust encoding speed
1355     if (m_view.speed->isEnabled()) {
1356         QStringList speeds = preset->speeds();
1357         if (m_view.speed->value() < speeds.count()) {
1358             const QString &speedValue = speeds.at(m_view.speed->value());
1359             if (speedValue.contains(QLatin1Char('='))) {
1360                 newParams.append(speedValue);
1361             }
1362         }
1363     }
1364 
1365     QString params = preset->params(removeParams);
1366 
1367     // Set the thread counts
1368     if (!params.contains(QStringLiteral("threads="))) {
1369         newParams.append(QStringLiteral("threads=%1").arg(KdenliveSettings::encodethreads()));
1370     }
1371 
1372     if (params.contains(QStringLiteral("%quality")) || params.contains(QStringLiteral("%audioquality"))) {
1373         m_view.qualityGroup->setEnabled(true);
1374     } else {
1375         m_view.qualityGroup->setEnabled(false);
1376     }
1377 
1378     // historically qualities are sorted from best to worse for some reason
1379     int vmin = preset->videoQualities().last().toInt();
1380     int vmax = preset->videoQualities().first().toInt();
1381     int vrange = abs(vmax - vmin);
1382     int amin = preset->audioQualities().last().toInt();
1383     int amax = preset->audioQualities().first().toInt();
1384     int arange = abs(amax - amin);
1385 
1386     double factor = double(m_view.quality->value()) / double(m_view.quality->maximum());
1387     m_view.quality->setMaximum(qMin(100, qMax(vrange, arange)));
1388     m_view.quality->setValue(qRound(m_view.quality->maximum() * factor));
1389     double percent = double(m_view.quality->value()) / double(m_view.quality->maximum());
1390     m_view.qualityPercent->setText(QStringLiteral("%1%").arg(qRound(percent * 100)));
1391 
1392     int val = preset->defaultVQuality().toInt();
1393 
1394     if (m_view.qualityGroup->isChecked()) {
1395         if (vmin < vmax) {
1396             val = vmin + int(vrange * percent);
1397         } else {
1398             val = vmin - int(vrange * percent);
1399         }
1400     }
1401     params.replace(QStringLiteral("%quality"), QString::number(val));
1402     // TODO check if this is finally correct
1403     params.replace(QStringLiteral("%bitrate+'k'"), QStringLiteral("%1k").arg(preset->defaultVBitrate()));
1404     params.replace(QStringLiteral("%bitrate"), QStringLiteral("%1").arg(preset->defaultVBitrate()));
1405 
1406     val = preset->defaultABitrate().toInt() * 1000;
1407     if (m_view.qualityGroup->isChecked()) {
1408         val *= percent;
1409     }
1410     // cvbr = Constrained Variable Bit Rate
1411     params.replace(QStringLiteral("%cvbr"), QString::number(val));
1412 
1413     val = preset->defaultAQuality().toInt();
1414     if (m_view.qualityGroup->isChecked()) {
1415         if (amin < amax) {
1416             val = amin + int(arange * percent);
1417         } else {
1418             val = amin - int(arange * percent);
1419         }
1420     }
1421     params.replace(QStringLiteral("%audioquality"), QString::number(val));
1422     // TODO check if this is finally correct
1423     params.replace(QStringLiteral("%audiobitrate+'k'"), QStringLiteral("%1k").arg(preset->defaultABitrate()));
1424     params.replace(QStringLiteral("%audiobitrate"), QStringLiteral("%1").arg(preset->defaultABitrate()));
1425 
1426     std::unique_ptr<ProfileModel> &projectProfile = pCore->getCurrentProfile();
1427     if (params.contains(QLatin1String("%dv_standard"))) {
1428         QString dvstd;
1429         if (fmod(double(projectProfile->frame_rate_num()) / projectProfile->frame_rate_den(), 30.01) > 27) {
1430             dvstd = QStringLiteral("ntsc");
1431         } else {
1432             dvstd = QStringLiteral("pal");
1433         }
1434         if (double(projectProfile->display_aspect_num()) / projectProfile->display_aspect_den() > 1.5) {
1435             dvstd += QLatin1String("_wide");
1436         }
1437         params.replace(QLatin1String("%dv_standard"), dvstd);
1438     }
1439 
1440     params.replace(
1441         QLatin1String("%dar"),
1442         QStringLiteral("@%1/%2").arg(QString::number(projectProfile->display_aspect_num())).arg(QString::number(projectProfile->display_aspect_den())));
1443     params.replace(QLatin1String("%passes"), QString::number(static_cast<int>(m_view.checkTwoPass->isChecked()) + 1));
1444 
1445     newParams.prepend(params);
1446 
1447     m_view.advanced_params->setPlainText(newParams.join(QStringLiteral(" ")));
1448 }
1449 
1450 void RenderWidget::parseProfiles(const QString &selectedProfile)
1451 {
1452     m_treeModel.reset();
1453     m_treeModel = RenderPresetTreeModel::construct(this);
1454     m_view.profileTree->setModel(m_treeModel.get());
1455     QItemSelectionModel *selectionModel = m_view.profileTree->selectionModel();
1456     connect(selectionModel, &QItemSelectionModel::currentRowChanged, this, &RenderWidget::slotChangeSelection);
1457     connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [&](const QItemSelection &selected, const QItemSelection &deselected) {
1458         QModelIndex current;
1459         QModelIndex old;
1460         if (!selected.indexes().isEmpty()) {
1461             current = selected.indexes().front();
1462         }
1463         if (!deselected.indexes().isEmpty()) {
1464             old = deselected.indexes().front();
1465         }
1466         slotChangeSelection(current, old);
1467     });
1468     focusItem(selectedProfile);
1469 }
1470 
1471 void RenderWidget::setRenderProgress(const QString &dest, int progress, int frame)
1472 {
1473     RenderJobItem *item = nullptr;
1474     QList<QTreeWidgetItem *> existing = m_view.running_jobs->findItems(dest, Qt::MatchExactly, 1);
1475     if (!existing.isEmpty()) {
1476         item = static_cast<RenderJobItem *>(existing.at(0));
1477     } else {
1478         item = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << dest);
1479         if (progress == 0) {
1480             item->setStatus(WAITINGJOB);
1481         }
1482     }
1483     item->setData(1, ProgressRole, progress);
1484     item->setStatus(RUNNINGJOB);
1485     if (progress == 0) {
1486         item->setIcon(0, QIcon::fromTheme(QStringLiteral("media-record")));
1487         slotCheckJob();
1488     } else {
1489         QDateTime startTime = item->data(1, StartTimeRole).toDateTime();
1490         qint64 elapsedTime = startTime.secsTo(QDateTime::currentDateTime());
1491         int dt = elapsedTime - item->data(1, LastTimeRole).toInt();
1492         if (dt == 0) {
1493             return;
1494         }
1495         qint64 remaining = elapsedTime * (100 - progress) / progress;
1496         int days = int(remaining / 86400);
1497         int remainingSecs = int(remaining % 86400);
1498         QTime when = QTime(0, 0, 0, 0).addSecs(remainingSecs);
1499         QString est = i18n("Remaining time ");
1500         if (days > 0) {
1501             est.append(i18np("%1 day ", "%1 days ", days));
1502         }
1503         est.append(when.toString(QStringLiteral("hh:mm:ss")));
1504         int speed = (frame - item->data(1, LastFrameRole).toInt()) / dt;
1505         est.append(i18n(" (frame %1 @ %2 fps)", frame, speed));
1506         item->setData(1, Qt::UserRole, est);
1507         item->setData(1, LastTimeRole, elapsedTime);
1508         item->setData(1, LastFrameRole, frame);
1509     }
1510 }
1511 
1512 void RenderWidget::setRenderStatus(const QString &dest, int status, const QString &error)
1513 {
1514     RenderJobItem *item = nullptr;
1515     QList<QTreeWidgetItem *> existing = m_view.running_jobs->findItems(dest, Qt::MatchExactly, 1);
1516     if (!existing.isEmpty()) {
1517         item = static_cast<RenderJobItem *>(existing.at(0));
1518     } else {
1519         item = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << dest);
1520     }
1521     if (!item) {
1522         return;
1523     }
1524     if (status == -1) {
1525         // Job finished successfully
1526         item->setStatus(FINISHEDJOB);
1527         QDateTime startTime = item->data(1, StartTimeRole).toDateTime();
1528         qint64 elapsedTime = startTime.secsTo(QDateTime::currentDateTime());
1529         int days = static_cast<int>(elapsedTime / 86400);
1530         int secs = static_cast<int>(elapsedTime % 86400);
1531         QTime when = QTime(0, 0, 0, 0);
1532         when = when.addSecs(secs);
1533         QString est = (days > 0) ? i18np("%1 day ", "%1 days ", days) : QString();
1534         est.append(when.toString(QStringLiteral("hh:mm:ss")));
1535         QString t = i18n("Rendering finished in %1", est);
1536         item->setData(1, Qt::UserRole, t);
1537 
1538 #ifdef KF5_USE_PURPOSE
1539         m_shareMenu->model()->setInputData(QJsonObject{{QStringLiteral("mimeType"), QMimeDatabase().mimeTypeForFile(item->text(1)).name()},
1540                                                        {QStringLiteral("urls"), QJsonArray({item->text(1)})}});
1541         m_shareMenu->model()->setPluginType(QStringLiteral("Export"));
1542         m_shareMenu->reload();
1543 #endif
1544         QString notif = i18n("Rendering of %1 finished in %2", item->text(1), est);
1545         KNotification *notify = new KNotification(QStringLiteral("RenderFinished"));
1546         notify->setText(notif);
1547         notify->setUrls({QUrl::fromLocalFile(dest)});
1548         notify->sendEvent();
1549         const QUrl url = QUrl::fromLocalFile(item->text(1));
1550         bool exists = QFile(url.toLocalFile()).exists();
1551         if (exists) {
1552             if (item->data(1, OpenBrowserRole).toBool()) {
1553                 KIO::highlightInFileManager({url});
1554             }
1555             if (item->data(1, PlayAfterRole).toBool()) {
1556                 auto *job = new KIO::OpenUrlJob(url);
1557 #if KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
1558                 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
1559 #else
1560                 job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
1561 #endif
1562                 job->start();
1563             }
1564         }
1565     } else if (status == -2) {
1566         // Rendering crashed
1567         item->setStatus(FAILEDJOB);
1568         m_view.error_log->append(i18n("<strong>Rendering of %1 crashed</strong><br />", dest));
1569         m_view.error_log->append(error);
1570         m_view.error_log->append(QStringLiteral("<hr />"));
1571         m_view.error_box->setVisible(true);
1572     } else if (status == -3) {
1573         // User aborted job
1574         item->setStatus(ABORTEDJOB);
1575     } else {
1576         delete item;
1577     }
1578     slotCheckJob();
1579     checkRenderStatus();
1580 }
1581 
1582 void RenderWidget::slotAbortCurrentJob()
1583 {
1584     auto *current = static_cast<RenderJobItem *>(m_view.running_jobs->currentItem());
1585     if (current) {
1586         if (current->status() == RUNNINGJOB) {
1587             emit abortProcess(current->text(1));
1588         } else {
1589             delete current;
1590             slotCheckJob();
1591             checkRenderStatus();
1592         }
1593     }
1594 }
1595 
1596 void RenderWidget::slotStartCurrentJob()
1597 {
1598     auto *current = static_cast<RenderJobItem *>(m_view.running_jobs->currentItem());
1599     if ((current != nullptr) && current->status() == WAITINGJOB) {
1600         startRendering(current);
1601     }
1602     m_view.start_job->setEnabled(false);
1603 }
1604 
1605 void RenderWidget::slotCheckJob()
1606 {
1607     bool activate = false;
1608     auto *current = static_cast<RenderJobItem *>(m_view.running_jobs->currentItem());
1609     if (current) {
1610         if (current->status() == RUNNINGJOB || current->status() == STARTINGJOB) {
1611             m_view.abort_job->setText(i18n("Abort Job"));
1612             m_view.start_job->setEnabled(false);
1613         } else {
1614             m_view.abort_job->setText(i18n("Remove Job"));
1615             m_view.start_job->setEnabled(current->status() == WAITINGJOB);
1616         }
1617         activate = true;
1618 #ifdef KF5_USE_PURPOSE
1619         if (current->status() == FINISHEDJOB) {
1620             m_shareMenu->model()->setInputData(QJsonObject{{QStringLiteral("mimeType"), QMimeDatabase().mimeTypeForFile(current->text(1)).name()},
1621                                                            {QStringLiteral("urls"), QJsonArray({current->text(1)})}});
1622             m_shareMenu->model()->setPluginType(QStringLiteral("Export"));
1623             m_shareMenu->reload();
1624             m_view.shareButton->setEnabled(true);
1625         } else {
1626             m_view.shareButton->setEnabled(false);
1627         }
1628 #endif
1629     }
1630     m_view.abort_job->setEnabled(activate);
1631 }
1632 
1633 void RenderWidget::slotCleanUpJobs()
1634 {
1635     int ix = 0;
1636     auto *current = static_cast<RenderJobItem *>(m_view.running_jobs->topLevelItem(ix));
1637     while (current != nullptr) {
1638         if (current->status() == FINISHEDJOB || current->status() == ABORTEDJOB) {
1639             delete current;
1640         } else {
1641             ix++;
1642         }
1643         current = static_cast<RenderJobItem *>(m_view.running_jobs->topLevelItem(ix));
1644     }
1645     slotCheckJob();
1646 }
1647 
1648 void RenderWidget::parseScriptFiles()
1649 {
1650     QStringList scriptsFilter;
1651     scriptsFilter << QStringLiteral("*.mlt");
1652     m_view.scripts_list->clear();
1653 
1654     // List the project scripts
1655     QDir projectFolder(pCore->currentDoc()->projectDataFolder());
1656     if (!projectFolder.exists(QStringLiteral("kdenlive-renderqueue"))) {
1657         return;
1658     }
1659     projectFolder.cd(QStringLiteral("kdenlive-renderqueue"));
1660     QStringList scriptFiles = projectFolder.entryList(scriptsFilter, QDir::Files);
1661     if (scriptFiles.isEmpty()) {
1662         // No scripts, delete directory
1663         if (projectFolder.dirName() == QStringLiteral("kdenlive-renderqueue") &&
1664             projectFolder.entryList(scriptsFilter, QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()) {
1665             projectFolder.removeRecursively();
1666             return;
1667         }
1668     }
1669     for (int i = 0; i < scriptFiles.size(); ++i) {
1670         QUrl scriptpath = QUrl::fromLocalFile(projectFolder.absoluteFilePath(scriptFiles.at(i)));
1671         QDomDocument doc;
1672         if (!Xml::docContentFromFile(doc, scriptpath.toLocalFile(), false)) {
1673             continue;
1674         }
1675         QDomElement consumer = doc.documentElement().firstChildElement(QStringLiteral("consumer"));
1676         if (consumer.isNull()) {
1677             continue;
1678         }
1679         QString target = consumer.attribute(QStringLiteral("target"));
1680         int out = consumer.attribute(QStringLiteral("out"), QStringLiteral("0")).toInt();
1681         if (target.isEmpty()) {
1682             continue;
1683         }
1684         QTreeWidgetItem *item = new QTreeWidgetItem(m_view.scripts_list, QStringList() << QString() << scriptpath.fileName());
1685         QFile f(scriptpath.toLocalFile());
1686         auto icon = QFileIconProvider().icon(QFileInfo(f));
1687         item->setIcon(0, icon.isNull() ? QIcon::fromTheme(QStringLiteral("application-x-executable-script")) : icon);
1688         item->setSizeHint(0, QSize(m_view.scripts_list->columnWidth(0), fontMetrics().height() * 2));
1689         item->setData(1, Qt::UserRole, QUrl(QUrl::fromEncoded(target.toUtf8())).url(QUrl::PreferLocalFile));
1690         item->setData(1, Qt::UserRole + 1, scriptpath.toLocalFile());
1691         item->setData(1, Qt::UserRole + 2, out);
1692     }
1693     QTreeWidgetItem *script = m_view.scripts_list->topLevelItem(0);
1694     if (script) {
1695         m_view.scripts_list->setCurrentItem(script);
1696         script->setSelected(true);
1697     }
1698 }
1699 
1700 void RenderWidget::slotCheckScript()
1701 {
1702     QTreeWidgetItem *current = m_view.scripts_list->currentItem();
1703     if (current == nullptr) {
1704         return;
1705     }
1706     m_view.start_script->setEnabled(current->data(0, Qt::UserRole).toString().isEmpty());
1707     m_view.delete_script->setEnabled(true);
1708     for (int i = 0; i < m_view.scripts_list->topLevelItemCount(); ++i) {
1709         current = m_view.scripts_list->topLevelItem(i);
1710         if (current == m_view.scripts_list->currentItem()) {
1711             current->setSizeHint(1, QSize(m_view.scripts_list->columnWidth(1), fontMetrics().height() * 3));
1712         } else {
1713             current->setSizeHint(1, QSize(m_view.scripts_list->columnWidth(1), fontMetrics().height() * 2));
1714         }
1715     }
1716 }
1717 
1718 void RenderWidget::slotStartScript()
1719 {
1720     auto *item = static_cast<RenderJobItem *>(m_view.scripts_list->currentItem());
1721     if (item) {
1722         QString destination = item->data(1, Qt::UserRole).toString();
1723         int out = item->data(1, Qt::UserRole + 2).toInt();
1724         if (QFile::exists(destination)) {
1725             if (KMessageBox::warningTwoActions(this, i18n("Output file already exists. Do you want to overwrite it?"), {}, KStandardGuiItem::overwrite(),
1726                                                KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) {
1727                 return;
1728             }
1729         }
1730         QString path = item->data(1, Qt::UserRole + 1).toString();
1731         // Insert new job in queue
1732         RenderJobItem *renderItem = nullptr;
1733         QList<QTreeWidgetItem *> existing = m_view.running_jobs->findItems(destination, Qt::MatchExactly, 1);
1734         if (!existing.isEmpty()) {
1735             renderItem = static_cast<RenderJobItem *>(existing.at(0));
1736             if (renderItem->status() == RUNNINGJOB || renderItem->status() == WAITINGJOB || renderItem->status() == STARTINGJOB) {
1737                 KMessageBox::information(
1738                     this, i18n("There is already a job writing file:<br /><b>%1</b><br />Abort the job if you want to overwrite it…", destination),
1739                     i18n("Already running"));
1740                 return;
1741             }
1742             delete renderItem;
1743             renderItem = nullptr;
1744         }
1745         if (!renderItem) {
1746             renderItem = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << destination);
1747         }
1748         renderItem->setData(1, ProgressRole, 0);
1749         renderItem->setStatus(WAITINGJOB);
1750         renderItem->setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause")));
1751         renderItem->setData(1, Qt::UserRole, i18n("Waiting…"));
1752         QDateTime t = QDateTime::currentDateTime();
1753         renderItem->setData(1, StartTimeRole, t);
1754         renderItem->setData(1, LastTimeRole, t);
1755         QStringList argsJob = {KdenliveSettings::rendererpath(),
1756                                path,
1757                                destination,
1758                                QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid()),
1759                                QStringLiteral("-out"),
1760                                QString::number(out)};
1761         renderItem->setData(1, ParametersRole, argsJob);
1762         checkRenderStatus();
1763         m_view.tabWidget->setCurrentIndex(Tabs::JobsTab);
1764     }
1765 }
1766 
1767 void RenderWidget::slotDeleteScript()
1768 {
1769     QTreeWidgetItem *item = m_view.scripts_list->currentItem();
1770     if (item) {
1771         QString path = item->data(1, Qt::UserRole + 1).toString();
1772         bool success = true;
1773         success &= static_cast<int>(QFile::remove(path));
1774         if (!success) {
1775             qCWarning(KDENLIVE_LOG) << "// Error removing script or playlist: " << path << ", " << path << ".mlt";
1776         }
1777         parseScriptFiles();
1778     }
1779 }
1780 
1781 void RenderWidget::slotHideLog()
1782 {
1783     m_view.error_box->setVisible(false);
1784 }
1785 
1786 void RenderWidget::setRenderProfile(const QMap<QString, QString> &props)
1787 {
1788     int exportAudio = props.value(QStringLiteral("renderexportaudio")).toInt();
1789     m_view.audio_box->setChecked(exportAudio != 1);
1790     if (props.contains(QStringLiteral("renderrescale"))) {
1791         m_view.rescale->setChecked(props.value(QStringLiteral("renderrescale")).toInt() != 0);
1792     }
1793     if (props.contains(QStringLiteral("renderrescalewidth"))) {
1794         m_view.rescale_width->setValue(props.value(QStringLiteral("renderrescalewidth")).toInt());
1795     } else {
1796         std::unique_ptr<ProfileModel> &profile = pCore->getCurrentProfile();
1797         m_view.rescale_width->setValue(profile->width() / 2);
1798         slotUpdateRescaleWidth(m_view.rescale_width->value());
1799     }
1800     if (props.contains(QStringLiteral("renderrescaleheight"))) {
1801         m_view.rescale_height->setValue(props.value(QStringLiteral("renderrescaleheight")).toInt());
1802     }
1803     if (props.contains(QStringLiteral("rendertctype"))) {
1804         m_view.tc_type->setCurrentIndex(props.value(QStringLiteral("rendertctype")).toInt() + 1);
1805     }
1806     if (props.contains(QStringLiteral("rendertcoverlay"))) {
1807         // for backward compatibility
1808         if (props.value(QStringLiteral("rendertcoverlay")).toInt() == 0) {
1809             m_view.tc_type->setCurrentIndex(0);
1810         }
1811     }
1812     if (props.contains(QStringLiteral("renderratio"))) {
1813         m_view.rescale_keep->setChecked(props.value(QStringLiteral("renderratio")).toInt() != 0);
1814     }
1815     if (props.contains(QStringLiteral("rendercustomquality")) && props.value(QStringLiteral("rendercustomquality")).toInt() >= 0) {
1816         m_view.qualityGroup->setChecked(true);
1817         m_view.quality->setValue(props.value(QStringLiteral("rendercustomquality")).toInt());
1818     } else {
1819         m_view.qualityGroup->setChecked(false);
1820     }
1821     if (props.contains(QStringLiteral("renderplay"))) {
1822         m_view.play_after->setChecked(props.value(QStringLiteral("renderplay")).toInt() != 0);
1823     }
1824     if (props.contains(QStringLiteral("rendertwopass"))) {
1825         m_view.checkTwoPass->setChecked(props.value(QStringLiteral("rendertwopass")).toInt() != 0);
1826     }
1827 
1828     if (props.contains(QStringLiteral("renderpreview"))) {
1829         m_view.render_at_preview_res->setChecked(props.value(QStringLiteral("renderpreview")).toInt() != 0);
1830     } else {
1831         m_view.render_at_preview_res->setChecked(false);
1832     }
1833 
1834     int mode = props.value(QStringLiteral("renderguide")).toInt();
1835     if (mode == 1) {
1836         m_view.render_zone->setChecked(true);
1837     } else if (mode == 2) {
1838         m_view.render_guide->setChecked(true);
1839         m_view.guide_start->setCurrentIndex(props.value(QStringLiteral("renderstartguide")).toInt());
1840         m_view.guide_end->setCurrentIndex(props.value(QStringLiteral("renderendguide")).toInt());
1841     } else if (mode == 3) {
1842         m_view.render_multi->setChecked(true);
1843     } else {
1844         m_view.render_full->setChecked(true);
1845     }
1846     slotRenderModeChanged();
1847 
1848     QString url = props.value(QStringLiteral("renderurl"));
1849     if (url.isEmpty()) {
1850         if (RenderPresetRepository::get()->presetExists(m_currentProfile)) {
1851             std::unique_ptr<RenderPresetModel> &profile = RenderPresetRepository::get()->getPreset(m_currentProfile);
1852             url = filenameWithExtension(QUrl::fromLocalFile(pCore->currentDoc()->projectDataFolder() + QDir::separator()), profile->extension()).toLocalFile();
1853         }
1854     } else if (QFileInfo(url).isRelative()) {
1855         url.prepend(pCore->currentDoc()->documentRoot());
1856     }
1857     m_view.out_file->setUrl(QUrl::fromLocalFile(url));
1858 
1859     if (props.contains(QStringLiteral("renderprofile")) || props.contains(QStringLiteral("rendercategory"))) {
1860         focusItem(props.value(QStringLiteral("renderprofile")));
1861     }
1862 
1863     if (props.contains(QStringLiteral("renderspeed"))) {
1864         m_view.speed->setValue(props.value(QStringLiteral("renderspeed")).toInt());
1865     }
1866 }
1867 
1868 void RenderWidget::saveRenderProfile()
1869 {
1870     // Save rendering profile to document
1871     QMap<QString, QString> renderProps;
1872     std::unique_ptr<RenderPresetModel> &preset = RenderPresetRepository::get()->getPreset(m_currentProfile);
1873     renderProps.insert(QStringLiteral("rendercategory"), preset->groupName());
1874     renderProps.insert(QStringLiteral("renderprofile"), preset->name());
1875     renderProps.insert(QStringLiteral("renderurl"), m_view.out_file->url().toLocalFile());
1876     int mode = 0; // 0 = full project
1877     if (m_view.render_zone->isChecked()) {
1878         mode = 1;
1879     } else if (m_view.render_guide->isChecked()) {
1880         mode = 2;
1881     } else if (m_view.render_multi->isChecked()) {
1882         mode = 3;
1883     }
1884     renderProps.insert(QStringLiteral("rendermode"), QString::number(mode));
1885     renderProps.insert(QStringLiteral("renderstartguide"), QString::number(m_view.guide_start->currentIndex()));
1886     renderProps.insert(QStringLiteral("renderendguide"), QString::number(m_view.guide_end->currentIndex()));
1887     int export_audio = 0;
1888 
1889     renderProps.insert(QStringLiteral("renderexportaudio"), QString::number(export_audio));
1890     renderProps.insert(QStringLiteral("renderrescale"), QString::number(static_cast<int>(m_view.rescale->isChecked())));
1891     renderProps.insert(QStringLiteral("renderrescalewidth"), QString::number(m_view.rescale_width->value()));
1892     renderProps.insert(QStringLiteral("renderrescaleheight"), QString::number(m_view.rescale_height->value()));
1893 
1894     // for backward compatibility, remove ones we have a doc version > 1.04
1895     renderProps.insert(QStringLiteral("rendertcoverlay"), QString::number(static_cast<int>(!m_view.tc_type->currentData().toString().isEmpty())));
1896 
1897     renderProps.insert(QStringLiteral("rendertctype"), QString::number(m_view.tc_type->currentIndex() - 1));
1898     renderProps.insert(QStringLiteral("renderratio"), QString::number(static_cast<int>(m_view.rescale_keep->isChecked())));
1899     renderProps.insert(QStringLiteral("renderplay"), QString::number(static_cast<int>(m_view.play_after->isChecked())));
1900     renderProps.insert(QStringLiteral("rendertwopass"), QString::number(static_cast<int>(m_view.checkTwoPass->isChecked())));
1901     renderProps.insert(QStringLiteral("rendercustomquality"), QString::number(m_view.qualityGroup->isChecked() ? m_view.quality->value() : -1));
1902     renderProps.insert(QStringLiteral("renderspeed"), QString::number(m_view.speed->value()));
1903     renderProps.insert(QStringLiteral("renderpreview"), QString::number(static_cast<int>(m_view.render_at_preview_res->isChecked())));
1904 
1905     emit selectedRenderProfile(renderProps);
1906 }
1907 
1908 bool RenderWidget::startWaitingRenderJobs()
1909 {
1910     m_blockProcessing = true;
1911 #ifdef Q_OS_WIN
1912     const QLatin1String ScriptFormat(".bat");
1913 #else
1914     const QLatin1String ScriptFormat(".sh");
1915 #endif
1916     QTemporaryFile tmp(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX") + ScriptFormat));
1917     if (!tmp.open()) {
1918         // Something went wrong
1919         return false;
1920     }
1921     tmp.close();
1922     QString autoscriptFile = tmp.fileName();
1923     QFile file(autoscriptFile);
1924     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
1925         qCWarning(KDENLIVE_LOG) << "//////  ERROR writing to file: " << autoscriptFile;
1926         KMessageBox::error(nullptr, i18n("Cannot write to file %1", autoscriptFile));
1927         return false;
1928     }
1929 
1930     QTextStream outStream(&file);
1931 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1932     outStream.setCodec("UTF-8");
1933 #endif
1934 #ifndef Q_OS_WIN
1935     outStream << "#!/bin/sh\n\n";
1936 #endif
1937     auto *item = static_cast<RenderJobItem *>(m_view.running_jobs->topLevelItem(0));
1938     while (item != nullptr) {
1939         if (item->status() == WAITINGJOB) {
1940             // Add render process for item
1941             const QString params = item->data(1, ParametersRole).toStringList().join(QLatin1Char(' '));
1942             outStream << '\"' << m_renderer << "\" " << params << '\n';
1943         }
1944         item = static_cast<RenderJobItem *>(m_view.running_jobs->itemBelow(item));
1945     }
1946 // erase itself when rendering is finished
1947 #ifndef Q_OS_WIN
1948     outStream << "rm \"" << autoscriptFile << "\"\n";
1949 #else
1950     outStream << "del \"" << autoscriptFile << "\"\n";
1951 #endif
1952     if (file.error() != QFile::NoError) {
1953         KMessageBox::error(nullptr, i18n("Cannot write to file %1", autoscriptFile));
1954         file.close();
1955         m_blockProcessing = false;
1956         return false;
1957     }
1958     file.close();
1959     QFile::setPermissions(autoscriptFile, file.permissions() | QFile::ExeUser);
1960     QProcess::startDetached(autoscriptFile, QStringList());
1961     return true;
1962 }
1963 
1964 void RenderWidget::slotPlayRendering(QTreeWidgetItem *item, int)
1965 {
1966     auto *renderItem = static_cast<RenderJobItem *>(item);
1967     if (renderItem->status() != FINISHEDJOB) {
1968         return;
1969     }
1970     auto *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(item->text(1)));
1971 #if KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
1972     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
1973 #else
1974     job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
1975 #endif
1976     job->start();
1977 }
1978 
1979 void RenderWidget::errorMessage(RenderError type, const QString &message)
1980 {
1981     QString fullMessage;
1982     m_errorMessages.insert(type, message);
1983     QMapIterator<int, QString> i(m_errorMessages);
1984     while (i.hasNext()) {
1985         i.next();
1986         if (!i.value().isEmpty()) {
1987             if (!fullMessage.isEmpty()) {
1988                 fullMessage.append(QLatin1Char('\n'));
1989             }
1990             fullMessage.append(i.value());
1991         }
1992     }
1993     if (!fullMessage.isEmpty()) {
1994         m_view.infoMessage->setMessageType(KMessageWidget::Warning);
1995         m_view.infoMessage->setText(fullMessage);
1996         m_view.infoMessage->show();
1997     } else {
1998         m_view.infoMessage->hide();
1999     }
2000 }
2001 
2002 void RenderWidget::updateProxyConfig(bool enable)
2003 {
2004     m_view.proxy_render->setVisible(enable);
2005 }
2006 
2007 void RenderWidget::setRescaleEnabled(bool enable)
2008 {
2009     for (int i = 0; i < m_view.rescale_box->layout()->count(); ++i) {
2010         if (m_view.rescale_box->itemAt(i)->widget()) {
2011             m_view.rescale_box->itemAt(i)->widget()->setEnabled(enable);
2012         }
2013     }
2014 }
2015 
2016 void RenderWidget::keyPressEvent(QKeyEvent *e)
2017 {
2018     if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
2019         switch (m_view.tabWidget->currentIndex()) {
2020         case Tabs::RenderTab:
2021             if (m_view.start_job->isEnabled()) {
2022                 slotStartCurrentJob();
2023             }
2024             break;
2025         case Tabs::ScriptsTab:
2026             if (m_view.start_script->isEnabled()) {
2027                 slotStartScript();
2028             }
2029             break;
2030         default:
2031             if (m_view.buttonRender->isEnabled()) {
2032                 slotPrepareExport();
2033             }
2034             break;
2035         }
2036     } else if (e->key() == Qt::Key_Delete) {
2037         // If in Scripts tab, let Del key invoke DeleteScript
2038         if (m_view.tabWidget->currentIndex() == Tabs::ScriptsTab) {
2039             if (m_view.delete_script->isEnabled()) {
2040                 slotDeleteScript();
2041             }
2042         } else {
2043             QDialog::keyPressEvent(e);
2044         }
2045     } else {
2046         QDialog::keyPressEvent(e);
2047     }
2048 }
2049 
2050 void RenderWidget::adjustSpeed(int speedIndex)
2051 {
2052     std::unique_ptr<RenderPresetModel> &profile = RenderPresetRepository::get()->getPreset(m_currentProfile);
2053     if (profile) {
2054         QStringList speeds = profile->speeds();
2055         if (speedIndex < speeds.count()) {
2056             m_view.speed->setToolTip(i18n("Codec speed parameters:\n%1", speeds.at(speedIndex)));
2057         }
2058     }
2059     refreshParams();
2060 }
2061 
2062 void RenderWidget::prepareJobContextMenu(const QPoint &pos)
2063 {
2064     QTreeWidgetItem *nd = m_view.running_jobs->itemAt(pos);
2065     RenderJobItem *renderItem = nullptr;
2066     if (nd) {
2067         renderItem = static_cast<RenderJobItem *>(nd);
2068     }
2069     if (!renderItem) {
2070         return;
2071     }
2072     if (renderItem->status() != FINISHEDJOB) {
2073         return;
2074     }
2075     QMenu menu(this);
2076     QAction *newAct = new QAction(i18n("Add to current project"), this);
2077     connect(newAct, &QAction::triggered, [&, renderItem]() { pCore->bin()->slotAddClipToProject(QUrl::fromLocalFile(renderItem->text(1))); });
2078     menu.addAction(newAct);
2079 
2080     menu.exec(m_view.running_jobs->mapToGlobal(pos));
2081 }
2082 
2083 void RenderWidget::resetRenderPath(const QString &path)
2084 {
2085     QString extension;
2086     if (RenderPresetRepository::get()->presetExists(m_currentProfile)) {
2087         std::unique_ptr<RenderPresetModel> &profile = RenderPresetRepository::get()->getPreset(m_currentProfile);
2088         extension = profile->extension();
2089     } else {
2090         extension = m_view.out_file->url().toLocalFile().section(QLatin1Char('.'), -1);
2091     }
2092     QFileInfo updatedPath(path);
2093     QString fileName = QDir(pCore->currentDoc()->projectDataFolder(updatedPath.absolutePath())).absoluteFilePath(updatedPath.fileName());
2094     QString url = filenameWithExtension(QUrl::fromLocalFile(fileName), extension).toLocalFile();
2095     if (QFileInfo(url).isRelative()) {
2096         url.prepend(pCore->currentDoc()->documentRoot());
2097     }
2098     m_view.out_file->setUrl(QUrl::fromLocalFile(url));
2099     QMap<QString, QString> renderProps;
2100     renderProps.insert(QStringLiteral("renderurl"), url);
2101     emit selectedRenderProfile(renderProps);
2102 }
2103 
2104 void RenderWidget::updateMetadataToolTip()
2105 {
2106     QString tipText;
2107     QMapIterator<QString, QString> i(pCore->currentDoc()->metadata());
2108     while (i.hasNext()) {
2109         i.next();
2110         QString metaName = i.key().section(QLatin1Char('.'), 2, 2);
2111         metaName[0] = metaName[0].toUpper();
2112         tipText.append(QString("%1: <b>%2</b><br/>").arg(metaName, i.value()));
2113     }
2114     m_view.edit_metadata->setToolTip(tipText);
2115 }