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 ¤t, 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 }