File indexing completed on 2024-04-14 04:46:22

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