File indexing completed on 2024-04-21 04:51:19
0001 /* 0002 SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com> 0003 0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "exportguidesdialog.h" 0008 0009 #include "bin/model/markerlistmodel.hpp" 0010 #include "core.h" 0011 #include "doc/kdenlivedoc.h" 0012 #include "kdenlivesettings.h" 0013 #include "profiles/profilemodel.hpp" 0014 #include "project/projectmanager.h" 0015 0016 #include "kdenlive_debug.h" 0017 #include <KMessageWidget> 0018 #include <QAction> 0019 #include <QButtonGroup> 0020 #include <QClipboard> 0021 #include <QDateTimeEdit> 0022 #include <QFileDialog> 0023 #include <QFontDatabase> 0024 #include <QPushButton> 0025 #include <QTime> 0026 0027 #include "klocalizedstring.h" 0028 0029 #define YT_FORMAT "{{timecode}} {{comment}}" 0030 0031 ExportGuidesDialog::ExportGuidesDialog(const MarkerListModel *model, const GenTime duration, QWidget *parent) 0032 : QDialog(parent) 0033 , m_markerListModel(model) 0034 , m_projectDuration(duration) 0035 { 0036 // setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); 0037 setupUi(this); 0038 setWindowTitle(i18n("Export guides")); 0039 0040 // We should setup TimecodeDisplay since it requires a proper Timecode 0041 offsetTimeSpinbox->setTimecode(Timecode(Timecode::HH_MM_SS_FF, pCore->getCurrentFps())); 0042 0043 const QString defaultFormat(YT_FORMAT); 0044 formatEdit->setText(KdenliveSettings::exportGuidesFormat().isEmpty() ? defaultFormat : KdenliveSettings::exportGuidesFormat()); 0045 categoryChooser->setMarkerModel(m_markerListModel); 0046 messageWidget->setText(i18n("If you are using the exported text for YouTube, you might want to check:\n" 0047 "1. The start time of 00:00 must have a chapter.\n" 0048 "2. There must be at least three timestamps in ascending order.\n" 0049 "3. The minimum length for video chapters is 10 seconds.")); 0050 messageWidget->setVisible(false); 0051 0052 updateContentByModel(); 0053 0054 QPushButton *btn = buttonBox->addButton(i18n("Copy to Clipboard"), QDialogButtonBox::ActionRole); 0055 btn->setIcon(QIcon::fromTheme("edit-copy")); 0056 QPushButton *btn2 = buttonBox->addButton(i18n("Save"), QDialogButtonBox::ActionRole); 0057 btn2->setIcon(QIcon::fromTheme("document-save")); 0058 0059 connect(categoryChooser, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this]() { updateContentByModel(); }); 0060 0061 connect(offsetTimeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int newIndex) { 0062 offsetTimeSpinbox->setEnabled(newIndex != 0); 0063 updateContentByModel(); 0064 }); 0065 0066 connect(offsetTimeSpinbox, &TimecodeDisplay::timeCodeUpdated, this, [this]() { updateContentByModel(); }); 0067 0068 connect(formatEdit, &QLineEdit::textEdited, this, [this]() { updateContentByModel(); }); 0069 0070 connect(btn, &QAbstractButton::clicked, this, [this]() { 0071 QClipboard *clipboard = QGuiApplication::clipboard(); 0072 clipboard->setText(this->generatedContent->toPlainText()); 0073 }); 0074 0075 connect(btn2, &QAbstractButton::clicked, this, [this]() { 0076 QString filter = format_text->isChecked() ? QString("%1 (*.txt)").arg(i18n("Text File")) : QString("%1 (*.json)").arg(i18n("JSON File")); 0077 const QString startFolder = pCore->projectManager()->current()->projectDataFolder(); 0078 QString filename = QFileDialog::getSaveFileName(this, i18nc("@title:window", "Export Guides Data"), startFolder, filter); 0079 QFile file(filename); 0080 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { 0081 messageWidget->setText(i18n("Cannot write to file %1", QUrl::fromLocalFile(filename).fileName())); 0082 messageWidget->setMessageType(KMessageWidget::Warning); 0083 messageWidget->animatedShow(); 0084 return; 0085 } 0086 file.write(generatedContent->toPlainText().toUtf8()); 0087 file.close(); 0088 messageWidget->setText(i18n("Guides saved to %1", QUrl::fromLocalFile(filename).fileName())); 0089 messageWidget->setMessageType(KMessageWidget::Positive); 0090 messageWidget->animatedShow(); 0091 }); 0092 QButtonGroup *exportType = new QButtonGroup(this); 0093 exportType->addButton(format_text, 1); 0094 exportType->addButton(format_ffmpeg, 2); 0095 exportType->addButton(format_json, 3); 0096 0097 connect(exportType, QOverload<QAbstractButton *, bool>::of(&QButtonGroup::buttonToggled), this, [this, exportType](QAbstractButton *button, bool checked) { 0098 if (checked) { 0099 textOptions->setEnabled(exportType->id(button) == 1); 0100 updateContentByModel(); 0101 } 0102 }); 0103 0104 connect(buttonReset, &QAbstractButton::clicked, [this, defaultFormat]() { 0105 formatEdit->setText(defaultFormat); 0106 updateContentByModel(); 0107 }); 0108 0109 // fill info button menu 0110 QMap<QString, QString> infoMenu; 0111 infoMenu.insert(QStringLiteral("{{category}}"), i18n("Category name")); 0112 infoMenu.insert(QStringLiteral("{{index}}"), i18n("Guide number")); 0113 infoMenu.insert(QStringLiteral("{{realtimecode}}"), i18n("Guide position in HH:MM:SS:FF")); 0114 infoMenu.insert(QStringLiteral("{{timecode}}"), i18n("Guide position in (HH:)MM.SS")); 0115 infoMenu.insert(QStringLiteral("{{nexttimecode}}"), i18n("Next guide position in (HH:)MM.SS")); 0116 infoMenu.insert(QStringLiteral("{{frame}}"), i18n("Guide position in frames")); 0117 infoMenu.insert(QStringLiteral("{{nextframe}}"), i18n("Next guide position in frames")); 0118 infoMenu.insert(QStringLiteral("{{comment}}"), i18n("Guide comment")); 0119 QMapIterator<QString, QString> i(infoMenu); 0120 QAction *a; 0121 while (i.hasNext()) { 0122 i.next(); 0123 a = new QAction(this); 0124 a->setText(QString("%1 - %2").arg(i.value(), i.key())); 0125 a->setData(i.key()); 0126 infoButton->addAction(a); 0127 } 0128 connect(infoButton, &QToolButton::triggered, [this](QAction *a) { 0129 formatEdit->insert(a->data().toString()); 0130 updateContentByModel(); 0131 }); 0132 adjustSize(); 0133 } 0134 0135 ExportGuidesDialog::~ExportGuidesDialog() 0136 { 0137 KdenliveSettings::setExportGuidesFormat(formatEdit->text()); 0138 } 0139 0140 GenTime ExportGuidesDialog::offsetTime() const 0141 { 0142 switch (offsetTimeComboBox->currentIndex()) { 0143 case 1: // Add 0144 return offsetTimeSpinbox->gentime(); 0145 case 2: // Subtract 0146 return -offsetTimeSpinbox->gentime(); 0147 case 0: // Disabled 0148 default: 0149 return GenTime(0); 0150 } 0151 } 0152 0153 QString chapterTimeStringFromMs(double timeMs) 0154 { 0155 int totalSec = qAbs(timeMs / 1000); 0156 bool negative = timeMs < 0 && totalSec > 0; // since our minimal unit is second. 0157 int hour = totalSec / 3600; 0158 int min = totalSec % 3600 / 60; 0159 int sec = totalSec % 3600 % 60; 0160 if (hour == 0) { 0161 return QString::asprintf("%s%d:%02d", negative ? "-" : "", min, sec); 0162 } else { 0163 return QString::asprintf("%s%d:%02d:%02d", negative ? "-" : "", hour, min, sec); 0164 } 0165 } 0166 0167 void ExportGuidesDialog::updateContentByModel() const 0168 { 0169 const int markerIndex = categoryChooser->currentCategory(); 0170 if (format_json->isChecked()) { 0171 messageWidget->setVisible(false); 0172 generatedContent->setPlainText(m_markerListModel->toJson({markerIndex})); 0173 return; 0174 } 0175 if (format_ffmpeg->isChecked()) { 0176 messageWidget->setVisible(false); 0177 generatedContent->setPlainText(getFFmpegChaptersData()); 0178 return; 0179 } 0180 const QString format(formatEdit->text()); 0181 const GenTime offset(offsetTime()); 0182 0183 QStringList chapterTexts; 0184 QList<CommentedTime> markers(m_markerListModel->getAllMarkers(markerIndex)); 0185 bool needCheck = format == YT_FORMAT; 0186 bool needShowInfoMsg = false; 0187 0188 const int markerCount = markers.length(); 0189 const double currentFps = pCore->getCurrentFps(); 0190 for (int i = 0; i < markers.length(); i++) { 0191 const CommentedTime ¤tMarker = markers.at(i); 0192 const GenTime &nextGenTime = markerCount - 1 == i ? m_projectDuration : markers.at(i + 1).time(); 0193 0194 QString line(format); 0195 GenTime currentTime = currentMarker.time() + offset; 0196 GenTime nextTime = nextGenTime + offset; 0197 0198 if (i == 0 && needCheck && !qFuzzyCompare(currentTime.seconds(), 0)) { 0199 needShowInfoMsg = true; 0200 } 0201 0202 if (needCheck && std::abs(nextTime.seconds() - currentTime.seconds()) < 10) { 0203 needShowInfoMsg = true; 0204 } 0205 0206 line.replace("{{index}}", QString::number(i + 1)); 0207 line.replace("{{realtimecode}}", pCore->timecode().getDisplayTimecode(currentTime, false)); 0208 line.replace("{{timecode}}", chapterTimeStringFromMs(currentTime.ms())); 0209 line.replace("{{nexttimecode}}", chapterTimeStringFromMs(nextTime.ms())); 0210 line.replace("{{frame}}", QString::number(currentTime.frames(currentFps))); 0211 line.replace("{{nextframe}}", QString::number(nextTime.frames(currentFps))); 0212 line.replace("{{comment}}", currentMarker.comment()); 0213 line.replace("{{category}}", pCore->markerTypes[currentMarker.markerType()].displayName); 0214 chapterTexts.append(line); 0215 } 0216 0217 generatedContent->setPlainText(chapterTexts.join('\n')); 0218 0219 if (needCheck && markerCount < 3) { 0220 needShowInfoMsg = true; 0221 } 0222 0223 messageWidget->setVisible(needShowInfoMsg); 0224 } 0225 0226 const QString ExportGuidesDialog::getFFmpegChaptersData() const 0227 { 0228 QString result = QStringLiteral(";FFMETADATA1\n\n"); 0229 int frame_rate_num = pCore->getCurrentProfile()->frame_rate_num(); 0230 int frame_rate_den = pCore->getCurrentProfile()->frame_rate_den(); 0231 const double currentFps = pCore->getCurrentFps(); 0232 const GenTime offset(offsetTime()); 0233 const QString frameRate = QStringLiteral("[CHAPTER]\nTIMEBASE=%1/%2\n").arg(frame_rate_num).arg(frame_rate_den); 0234 const int markerIndex = categoryChooser->currentCategory(); 0235 QList<CommentedTime> markers(m_markerListModel->getAllMarkers(markerIndex)); 0236 const int markerCount = markers.length(); 0237 for (int i = 0; i < markers.length(); i++) { 0238 const CommentedTime ¤tMarker = markers.at(i); 0239 const GenTime &nextGenTime = markerCount - 1 == i ? m_projectDuration : markers.at(i + 1).time(); 0240 GenTime currentTime = currentMarker.time() + offset; 0241 GenTime nextTime = nextGenTime + offset; 0242 result.append(frameRate); 0243 result.append(QStringLiteral("START=%1\n").arg(currentTime.frames(currentFps))); 0244 result.append(QStringLiteral("END=%1\n").arg(nextTime.frames(currentFps))); 0245 result.append(QStringLiteral("title=%1\n\n").arg(currentMarker.comment())); 0246 } 0247 return result; 0248 } 0249 0250 #undef YT_FORMAT