File indexing completed on 2024-04-21 04:51:06
0001 /* 0002 SPDX-FileCopyrightText: 2020 Sashmita Raghav 0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 #include "subtitlemodel.hpp" 0007 #include "bin/bin.h" 0008 #include "core.h" 0009 #include "doc/kdenlivedoc.h" 0010 #include "kdenlivesettings.h" 0011 #include "macros.hpp" 0012 #include "profiles/profilemodel.hpp" 0013 #include "project/projectmanager.h" 0014 #include "timeline2/model/snapmodel.hpp" 0015 #include "timeline2/model/timelineitemmodel.hpp" 0016 #include "undohelper.hpp" 0017 0018 #include <mlt++/Mlt.h> 0019 #include <mlt++/MltProperties.h> 0020 0021 #include "utils/KMessageBox_KdenliveCompat.h" 0022 #include <KEncodingProber> 0023 #include <KLocalizedString> 0024 #include <KMessageBox> 0025 #include <QApplication> 0026 #include <QJsonArray> 0027 #include <QJsonDocument> 0028 #include <QJsonObject> 0029 #include <QRegularExpression> 0030 #include <utility> 0031 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0032 #include <QStringConverter> 0033 #else 0034 #include <QTextCodec> 0035 #endif 0036 0037 SubtitleModel::SubtitleModel(std::shared_ptr<TimelineItemModel> timeline, const std::weak_ptr<SnapInterface> &snapModel, QObject *parent) 0038 : QAbstractListModel(parent) 0039 , m_timeline(timeline) 0040 , m_lock(QReadWriteLock::Recursive) 0041 , m_subtitleFilter(new Mlt::Filter(pCore->getProjectProfile(), "avfilter.subtitles")) 0042 { 0043 qDebug() << "subtitle constructor"; 0044 // Ensure the subtitle also covers transparent zones (useful for timeline sequences) 0045 m_subtitleFilter->set("av.alpha", 1); 0046 if (m_timeline->tractor() != nullptr) { 0047 qDebug() << "Tractor!"; 0048 m_subtitleFilter->set("internal_added", 237); 0049 } 0050 setup(); 0051 QSize frameSize = pCore->getCurrentFrameDisplaySize(); 0052 int fontSize = frameSize.height() / 15; 0053 int fontMargin = frameSize.height() - (2 * fontSize); 0054 scriptInfoSection = 0055 QString( 0056 "[Script Info]\n; This is a Sub Station Alpha v4 script.\n;\nScriptType: v4.00\nCollisions: Normal\nPlayResX: %1\nPlayResY: %2\nTimer: 100.0000\n") 0057 .arg(frameSize.width()) 0058 .arg(frameSize.height()); 0059 styleSection = 0060 QString( 0061 "[V4 Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, " 0062 "Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding\nStyle: Default,Consolas,%1,16777215,65535,255,0,-1,0,1,2,2,6,40,40,%2,0,1\n") 0063 .arg(fontSize) 0064 .arg(fontMargin); 0065 eventSection = QStringLiteral("[Events]\n"); 0066 styleName = QStringLiteral("Default"); 0067 connect(this, &SubtitleModel::modelChanged, [this]() { jsontoSubtitle(toJson()); }); 0068 int id = pCore->currentDoc()->getSequenceProperty(timeline->uuid(), QStringLiteral("kdenlive:activeSubtitleIndex"), QStringLiteral("0")).toInt(); 0069 const QString subPath = pCore->currentDoc()->subTitlePath(timeline->uuid(), id, true); 0070 const QString workPath = pCore->currentDoc()->subTitlePath(timeline->uuid(), id, false); 0071 registerSnap(snapModel); 0072 QFile subFile(subPath); 0073 if (subFile.exists()) { 0074 subFile.copy(workPath); 0075 parseSubtitle(workPath); 0076 } 0077 QMap<std::pair<int, QString>, QString> multiSubs = pCore->currentDoc()->multiSubtitlePath(timeline->uuid()); 0078 m_subtitlesList = multiSubs; 0079 if (m_subtitlesList.isEmpty()) { 0080 m_subtitlesList.insert({0, i18n("Subtitles")}, subPath); 0081 } 0082 } 0083 0084 void SubtitleModel::setStyle(const QString &style) 0085 { 0086 QString oldStyle = m_subtitleFilter->get("av.force_style"); 0087 Fun redo = [this, style]() { 0088 m_subtitleFilter->set("av.force_style", style.toUtf8().constData()); 0089 // Force refresh to show the new style 0090 pCore->refreshProjectMonitorOnce(); 0091 return true; 0092 }; 0093 Fun undo = [this, oldStyle]() { 0094 m_subtitleFilter->set("av.force_style", oldStyle.toUtf8().constData()); 0095 // Force refresh to show the new style 0096 pCore->refreshProjectMonitorOnce(); 0097 return true; 0098 }; 0099 redo(); 0100 pCore->pushUndo(undo, redo, i18n("Edit subtitle style")); 0101 } 0102 0103 const QString SubtitleModel::getStyle() const 0104 { 0105 const QString style = m_subtitleFilter->get("av.force_style"); 0106 return style; 0107 } 0108 0109 void SubtitleModel::setup() 0110 { 0111 // We connect the signals of the abstractitemmodel to a more generic one. 0112 connect(this, &SubtitleModel::columnsMoved, this, &SubtitleModel::modelChanged); 0113 connect(this, &SubtitleModel::columnsRemoved, this, &SubtitleModel::modelChanged); 0114 connect(this, &SubtitleModel::columnsInserted, this, &SubtitleModel::modelChanged); 0115 connect(this, &SubtitleModel::rowsMoved, this, &SubtitleModel::modelChanged); 0116 connect(this, &SubtitleModel::modelReset, this, &SubtitleModel::modelChanged); 0117 } 0118 0119 void SubtitleModel::unsetModel() 0120 { 0121 m_timeline.reset(); 0122 } 0123 0124 QByteArray SubtitleModel::guessFileEncoding(const QString &file, bool *confidence) 0125 { 0126 QFile textFile{file}; 0127 if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 0128 qWarning() << "Could not open" << file; 0129 return ""; 0130 } 0131 KEncodingProber prober; 0132 QByteArray sample = textFile.read(1024); 0133 if (sample.isEmpty()) { 0134 qWarning() << "Tried to guess the encoding of an empty file"; 0135 return ""; 0136 } 0137 auto state = prober.feed(sample); 0138 *confidence = false; 0139 switch (state) { 0140 case KEncodingProber::ProberState::FoundIt: 0141 qDebug() << "Guessed subtitle file encoding to be " << prober.encoding() << ", confidence: " << prober.confidence(); 0142 if (prober.confidence() < 0.6) { 0143 return QByteArray("UTF-8"); 0144 } 0145 *confidence = true; 0146 break; 0147 case KEncodingProber::ProberState::NotMe: 0148 qWarning() << "Subtitle file encoding not recognized"; 0149 return QByteArray("UTF-8"); 0150 case KEncodingProber::ProberState::Probing: 0151 qWarning() << "Subtitle file encoding indeterminate, confidence is" << prober.confidence() << ", ENCODING: " << prober.encoding(); 0152 if (prober.confidence() < 0.5) { 0153 // Encoding cannot be guessed, default to UTF-8 0154 return QByteArray(QByteArray("UTF-8")); 0155 } 0156 break; 0157 } 0158 return prober.encoding(); 0159 } 0160 0161 void SubtitleModel::importSubtitle(const QString &filePath, int offset, bool externalImport, float startFramerate, float targetFramerate, const QByteArray &encoding) 0162 { 0163 QString start, end, comment; 0164 QString timeLine; 0165 QStringList srtTime; 0166 GenTime startPos, endPos; 0167 int turn = 0, r = 0, endIndex = 1, defaultTurn = 0; 0168 double transformMult = targetFramerate/startFramerate; 0169 /* 0170 * turn = 0 -> Parse next subtitle line [srt] (or) [vtt] (or) [sbv] (or) Parse next section [ssa] 0171 * turn = 1 -> Add string to timeLine 0172 * turn > 1 -> Add string to completeLine 0173 */ 0174 if (filePath.isEmpty() || isLocked()) return; 0175 Fun redo = []() { return true; }; 0176 Fun undo = [this]() { 0177 Q_EMIT modelChanged(); 0178 return true; 0179 }; 0180 GenTime subtitleOffset(offset, pCore->getCurrentFps()); 0181 if (filePath.endsWith(".srt") || filePath.endsWith(".vtt") || filePath.endsWith(".sbv")) { 0182 // if (!filePath.endsWith(".vtt") || !filePath.endsWith(".sbv")) {defaultTurn = -10;} 0183 if (filePath.endsWith(".vtt") || filePath.endsWith(".sbv")) { 0184 defaultTurn = -10; 0185 turn = defaultTurn; 0186 } 0187 endIndex = filePath.endsWith(".sbv") ? 1 : 2; 0188 QFile srtFile(filePath); 0189 if (!srtFile.exists() || !srtFile.open(QIODevice::ReadOnly)) { 0190 qDebug() << " File not found " << filePath; 0191 return; 0192 } 0193 0194 qDebug() << "srt/vtt/sbv File"; 0195 //parsing srt file 0196 QTextStream stream(&srtFile); 0197 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0198 QTextCodec *inputEncoding = QTextCodec::codecForName(encoding); 0199 if (inputEncoding) { 0200 stream.setCodec(inputEncoding); 0201 } else { 0202 qWarning() << "No QTextCodec named" << encoding; 0203 stream.setCodec("UTF-8"); 0204 } 0205 #else 0206 std::optional<QStringConverter::Encoding> inputEncoding = QStringConverter::encodingForName(encoding.data()); 0207 if (inputEncoding) { 0208 stream.setEncoding(inputEncoding.value()); 0209 } 0210 // else: UTF8 is the default 0211 #endif 0212 QString line; 0213 QStringList srtTime; 0214 static const QRegularExpression rx("([0-9]{1,2}):([0-9]{2})"); 0215 QLatin1Char separator = filePath.endsWith(".sbv") ? QLatin1Char(',') : QLatin1Char(' '); 0216 while (stream.readLineInto(&line)) { 0217 line = line.trimmed(); 0218 // qDebug()<<"Turn: "<<turn; 0219 // qDebug()<<"Line: "<<line; 0220 if (!line.isEmpty()) { 0221 if (!turn) { 0222 // index=atoi(line.toStdString().c_str()); 0223 turn++; 0224 continue; 0225 } 0226 // Check if position has already been read 0227 if (endPos == startPos && (line.contains(QLatin1String("-->")) || line.contains(rx))) { 0228 timeLine += line; 0229 srtTime = timeLine.split(separator); 0230 if (srtTime.count() > endIndex) { 0231 start = srtTime.at(0); 0232 startPos = stringtoTime(start, transformMult); 0233 end = srtTime.at(endIndex); 0234 endPos = stringtoTime(end, transformMult); 0235 } else { 0236 continue; 0237 } 0238 } else { 0239 r++; 0240 if (!comment.isEmpty()) comment += " "; 0241 if (r == 1) 0242 comment += line; 0243 else 0244 comment = comment + "\n" + line; 0245 } 0246 turn++; 0247 } else { 0248 if (endPos > startPos) { 0249 addSubtitle(startPos + subtitleOffset, endPos + subtitleOffset, comment, undo, redo, false); 0250 // qDebug() << "Adding Subtitle: \n Start time: " << start << "\n End time: " << end << "\n Text: " << comment; 0251 } else { 0252 qDebug() << "===== INVALID SUBTITLE FOUND: " << start << "-" << end << ", " << comment; 0253 } 0254 // reinitialize 0255 comment.clear(); 0256 timeLine.clear(); 0257 startPos = endPos; 0258 r = 0; 0259 turn = defaultTurn; 0260 } 0261 } 0262 // Ensure last subtitle is read 0263 if (endPos > startPos && !comment.isEmpty()) { 0264 addSubtitle(startPos + subtitleOffset, endPos + subtitleOffset, comment, undo, redo, false); 0265 } 0266 srtFile.close(); 0267 } else if (filePath.endsWith(QLatin1String(".ass"))) { 0268 qDebug() << "ass File"; 0269 QString startTime, endTime; 0270 QString eventFormat, section; 0271 turn = 0; 0272 int numEventFields = 0; 0273 QFile assFile(filePath); 0274 if (!assFile.exists() || !assFile.open(QIODevice::ReadOnly)) { 0275 qDebug() << " Failed attempt on opening " << filePath; 0276 return; 0277 } 0278 QTextStream stream(&assFile); 0279 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0280 stream.setCodec(QTextCodec::codecForName(encoding)); 0281 #else 0282 stream.setEncoding(QStringConverter::encodingForName(encoding.data()).value()); 0283 #endif 0284 QString line; 0285 qDebug() << " correct ass file " << filePath; 0286 scriptInfoSection.clear(); 0287 styleSection.clear(); 0288 eventSection.clear(); 0289 int textIndex = 9; 0290 while (stream.readLineInto(&line)) { 0291 line = line.simplified(); 0292 if (!line.isEmpty()) { 0293 if (!turn) { 0294 // qDebug() << " turn = 0 " << line; 0295 // check if it is script info, event,or v4+ style 0296 QString linespace = line; 0297 if (linespace.replace(" ", "").contains("ScriptInfo")) { 0298 // qDebug()<< "Script Info"; 0299 section = "Script Info"; 0300 scriptInfoSection += line + "\n"; 0301 turn++; 0302 // qDebug()<< "turn" << turn; 0303 continue; 0304 } else if (line.contains("Styles")) { 0305 // qDebug()<< "V4 Styles"; 0306 section = "V4 Styles"; 0307 styleSection += line + "\n"; 0308 turn++; 0309 // qDebug()<< "turn" << turn; 0310 continue; 0311 } else if (line.contains("Events")) { 0312 turn++; 0313 section = "Events"; 0314 eventSection += line + "\n"; 0315 // qDebug()<< "turn" << turn; 0316 continue; 0317 } else { 0318 // unknown section 0319 } 0320 } 0321 if (section.contains("Script Info")) { 0322 scriptInfoSection += line + "\n"; 0323 } 0324 if (section.contains("V4 Styles")) { 0325 QStringList styleFormat; 0326 styleSection += line + "\n"; 0327 // Style: 0328 // Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,Encoding 0329 styleFormat = (line.split(": ")[1].replace(" ", "")).split(','); 0330 if (!styleFormat.isEmpty()) { 0331 styleName = styleFormat.first(); 0332 } 0333 } 0334 // qDebug() << "\n turn != 0 " << turn<< line; 0335 if (section.contains("Events")) { 0336 // if it is event 0337 QStringList format; 0338 if (line.contains("Format:")) { 0339 eventSection += line + "\n"; 0340 eventFormat += line.toLower().simplified().remove(QLatin1Char(' ')); 0341 format = eventFormat.split(":")[1].split(QLatin1Char(',')); 0342 // qDebug() << format << format.count(); 0343 numEventFields = format.count(); 0344 // TIME 0345 if (numEventFields > 2) startTime = format.at(1); 0346 if (numEventFields > 3) endTime = format.at(2); 0347 } else { 0348 start.clear(); 0349 end.clear(); 0350 comment.clear(); 0351 QStringList dialogue = line.section(":", 1).split(QLatin1Char(',')); 0352 if (dialogue.count() > textIndex) { 0353 // TIME 0354 start = dialogue.at(1); 0355 startPos = stringtoTime(start, transformMult); 0356 end = dialogue.at(2); 0357 endPos = stringtoTime(end, transformMult); 0358 // Text field is always the last field, since it can have commas 0359 comment = line.section(",", numEventFields - 1); 0360 // qDebug()<<"Start: "<< start << "End: "<<end << comment; 0361 if (endPos > startPos) { 0362 addSubtitle(startPos + subtitleOffset, endPos + subtitleOffset, comment, undo, redo, false); 0363 } else { 0364 qDebug() << "==== FOUND INVALID SUBTITLE ITEM: " << start << "-" << end << ", " << comment; 0365 } 0366 } 0367 } 0368 } 0369 turn++; 0370 } else { 0371 turn = 0; 0372 startTime = endTime = QString(); 0373 } 0374 } 0375 assFile.close(); 0376 } else { 0377 if (endPos > startPos) { 0378 addSubtitle(startPos + subtitleOffset, endPos + subtitleOffset, comment, undo, redo, false); 0379 } else { 0380 qDebug() << "===== INVALID VTT SUBTITLE FOUND: " << start << "-" << end << ", " << comment; 0381 } 0382 // reinitialize for next comment: 0383 comment.clear(); 0384 timeLine.clear(); 0385 turn = 0; 0386 r = 0; 0387 } 0388 Fun update_model = [this]() { 0389 Q_EMIT modelChanged(); 0390 return true; 0391 }; 0392 PUSH_LAMBDA(update_model, redo); 0393 update_model(); 0394 if (externalImport) { 0395 pCore->pushUndo(undo, redo, i18n("Edit subtitle")); 0396 } 0397 } 0398 0399 void SubtitleModel::parseSubtitle(const QString &workPath) 0400 { 0401 qDebug() << "Parsing started"; 0402 if (!workPath.isEmpty()) { 0403 m_subtitleFilter->set("av.filename", workPath.toUtf8().constData()); 0404 } 0405 QString filePath = m_subtitleFilter->get("av.filename"); 0406 importSubtitle(filePath, 0, false); 0407 // jsontoSubtitle(toJson()); 0408 } 0409 0410 const QString SubtitleModel::getUrl() 0411 { 0412 return m_subtitleFilter->get("av.filename"); 0413 } 0414 0415 GenTime SubtitleModel::stringtoTime(QString &str, const double factor) 0416 { 0417 QStringList total, secs; 0418 double hours = 0, mins = 0, seconds = 0, ms = 0; 0419 double total_sec = 0; 0420 GenTime pos; 0421 total = str.split(QLatin1Char(':')); 0422 if (total.count() == 3) { 0423 // There is an hour timestamp 0424 hours = atoi(total.takeFirst().toStdString().c_str()); 0425 } 0426 if (total.count() == 2) { 0427 mins = atoi(total.at(0).toStdString().c_str()); 0428 if (total.at(1).contains(QLatin1Char('.'))) { 0429 secs = total.at(1).split(QLatin1Char('.')); // ssa file 0430 } else { 0431 secs = total.at(1).split(QLatin1Char(',')); // srt file 0432 } 0433 if (secs.count() < 2) { 0434 seconds = atoi(total.at(1).toStdString().c_str()); 0435 } else { 0436 seconds = atoi(secs.at(0).toStdString().c_str()); 0437 ms = atoi(secs.at(1).toStdString().c_str()); 0438 } 0439 } else { 0440 // invalid time found 0441 return GenTime(); 0442 } 0443 0444 total_sec = hours * 3600 + mins * 60 + seconds + ms * 0.001; 0445 pos = GenTime(total_sec) / factor; 0446 // Ensure times are aligned with our project's frames 0447 int frames = pos.frames(pCore->getCurrentFps()); 0448 return GenTime(frames, pCore->getCurrentFps()); 0449 } 0450 0451 bool SubtitleModel::addSubtitle(GenTime start, GenTime end, const QString &str, Fun &undo, Fun &redo, bool updateFilter) 0452 { 0453 if (isLocked()) { 0454 return false; 0455 } 0456 int id = TimelineModel::getNextId(); 0457 Fun local_redo = [this, id, start, end, str, updateFilter]() { 0458 addSubtitle(id, start, end, str, false, updateFilter); 0459 QPair<int, int> range = {start.frames(pCore->getCurrentFps()), end.frames(pCore->getCurrentFps())}; 0460 pCore->invalidateRange(range); 0461 pCore->refreshProjectRange(range); 0462 return true; 0463 }; 0464 Fun local_undo = [this, id, start, end, updateFilter]() { 0465 removeSubtitle(id, false, updateFilter); 0466 QPair<int, int> range = {start.frames(pCore->getCurrentFps()), end.frames(pCore->getCurrentFps())}; 0467 pCore->invalidateRange(range); 0468 pCore->refreshProjectRange(range); 0469 return true; 0470 }; 0471 local_redo(); 0472 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 0473 return true; 0474 } 0475 0476 bool SubtitleModel::addSubtitle(int id, GenTime start, GenTime end, const QString &str, bool temporary, bool updateFilter) 0477 { 0478 if (start.frames(pCore->getCurrentFps()) < 0 || end.frames(pCore->getCurrentFps()) < 0 || isLocked()) { 0479 qDebug() << "Time error: is negative"; 0480 return false; 0481 } 0482 if (start.frames(pCore->getCurrentFps()) > end.frames(pCore->getCurrentFps())) { 0483 qDebug() << "Time error: start should be less than end"; 0484 return false; 0485 } 0486 // Don't allow 2 subtitles at same start pos 0487 if (m_subtitleList.count(start) > 0) { 0488 qDebug() << "already present in model" 0489 << "string :" << m_subtitleList[start].first << " start time " << start.frames(pCore->getCurrentFps()) 0490 << "end time : " << m_subtitleList[start].second.frames(pCore->getCurrentFps()); 0491 return false; 0492 } 0493 m_timeline->registerSubtitle(id, start, temporary); 0494 int row = m_timeline->getSubtitleIndex(id); 0495 beginInsertRows(QModelIndex(), row, row); 0496 m_subtitleList[start] = {str, end}; 0497 endInsertRows(); 0498 addSnapPoint(start); 0499 addSnapPoint(end); 0500 if (!temporary && end.frames(pCore->getCurrentFps()) > m_timeline->duration()) { 0501 m_timeline->updateDuration(); 0502 } 0503 // qDebug() << "Added to model"; 0504 if (updateFilter) { 0505 Q_EMIT modelChanged(); 0506 } 0507 return true; 0508 } 0509 0510 QHash<int, QByteArray> SubtitleModel::roleNames() const 0511 { 0512 QHash<int, QByteArray> roles; 0513 roles[SubtitleRole] = "subtitle"; 0514 roles[StartPosRole] = "startposition"; 0515 roles[EndPosRole] = "endposition"; 0516 roles[StartFrameRole] = "startframe"; 0517 roles[EndFrameRole] = "endframe"; 0518 roles[GrabRole] = "grabbed"; 0519 roles[IdRole] = "id"; 0520 roles[SelectedRole] = "selected"; 0521 return roles; 0522 } 0523 0524 QVariant SubtitleModel::data(const QModelIndex &index, int role) const 0525 { 0526 if (index.row() < 0 || index.row() >= static_cast<int>(m_subtitleList.size()) || !index.isValid()) { 0527 return QVariant(); 0528 } 0529 auto subInfo = m_timeline->getSubtitleIdFromIndex(index.row()); 0530 switch (role) { 0531 case Qt::DisplayRole: 0532 case Qt::EditRole: 0533 case SubtitleRole: 0534 return m_subtitleList.at(subInfo.second).first; 0535 case IdRole: 0536 return subInfo.first; 0537 case StartPosRole: 0538 return subInfo.second.seconds(); 0539 case EndPosRole: 0540 return m_subtitleList.at(subInfo.second).second.seconds(); 0541 case StartFrameRole: 0542 return subInfo.second.frames(pCore->getCurrentFps()); 0543 case EndFrameRole: 0544 return m_subtitleList.at(subInfo.second).second.frames(pCore->getCurrentFps()); 0545 case SelectedRole: 0546 return m_selected.contains(subInfo.first); 0547 case GrabRole: 0548 return m_grabbedIds.contains(subInfo.first); 0549 } 0550 return QVariant(); 0551 } 0552 0553 int SubtitleModel::rowCount(const QModelIndex &parent) const 0554 { 0555 if (parent.isValid()) return 0; 0556 return static_cast<int>(m_subtitleList.size()); 0557 } 0558 0559 QList<SubtitledTime> SubtitleModel::getAllSubtitles() const 0560 { 0561 QList<SubtitledTime> subtitle; 0562 for (const auto &subtitles : m_subtitleList) { 0563 SubtitledTime s(subtitles.first, subtitles.second.first, subtitles.second.second); 0564 subtitle << s; 0565 } 0566 return subtitle; 0567 } 0568 0569 SubtitledTime SubtitleModel::getSubtitle(GenTime startFrame) const 0570 { 0571 for (const auto &subtitles : m_subtitleList) { 0572 if (subtitles.first == startFrame) { 0573 return SubtitledTime(subtitles.first, subtitles.second.first, subtitles.second.second); 0574 } 0575 } 0576 return SubtitledTime(GenTime(), QString(), GenTime()); 0577 } 0578 0579 QString SubtitleModel::getText(int id) const 0580 { 0581 if (m_timeline->m_allSubtitles.find(id) == m_timeline->m_allSubtitles.end()) { 0582 return QString(); 0583 } 0584 GenTime start = m_timeline->m_allSubtitles.at(id); 0585 return m_subtitleList.at(start).first; 0586 } 0587 0588 bool SubtitleModel::setText(int id, const QString &text) 0589 { 0590 if (m_timeline->m_allSubtitles.find(id) == m_timeline->m_allSubtitles.end() || isLocked()) { 0591 return false; 0592 } 0593 GenTime start = m_timeline->m_allSubtitles.at(id); 0594 GenTime end = m_subtitleList.at(start).second; 0595 QString oldText = m_subtitleList.at(start).first; 0596 m_subtitleList[start].first = text; 0597 Fun local_redo = [this, start, id, end, text]() { 0598 editSubtitle(id, text); 0599 QPair<int, int> range = {start.frames(pCore->getCurrentFps()), end.frames(pCore->getCurrentFps())}; 0600 pCore->invalidateRange(range); 0601 pCore->refreshProjectRange(range); 0602 return true; 0603 }; 0604 Fun local_undo = [this, start, id, end, oldText]() { 0605 editSubtitle(id, oldText); 0606 QPair<int, int> range = {start.frames(pCore->getCurrentFps()), end.frames(pCore->getCurrentFps())}; 0607 pCore->invalidateRange(range); 0608 pCore->refreshProjectRange(range); 0609 return true; 0610 }; 0611 local_redo(); 0612 pCore->pushUndo(local_undo, local_redo, i18n("Edit subtitle")); 0613 return true; 0614 } 0615 0616 std::unordered_set<int> SubtitleModel::getItemsInRange(int startFrame, int endFrame) const 0617 { 0618 if (isLocked()) { 0619 return {}; 0620 } 0621 GenTime startTime(startFrame, pCore->getCurrentFps()); 0622 GenTime endTime(endFrame, pCore->getCurrentFps()); 0623 std::unordered_set<int> matching; 0624 for (const auto &subtitles : m_subtitleList) { 0625 if (endFrame > -1 && subtitles.first > endTime) { 0626 // Outside range 0627 continue; 0628 } 0629 if (subtitles.first >= startTime || subtitles.second.second > startTime) { 0630 int sid = getIdForStartPos(subtitles.first); 0631 if (sid > -1) { 0632 matching.emplace(sid); 0633 } else { 0634 qDebug() << "==== FOUND INVALID SUBTILE AT: " << subtitles.first.frames(pCore->getCurrentFps()); 0635 } 0636 } 0637 } 0638 return matching; 0639 } 0640 0641 bool SubtitleModel::cutSubtitle(int position) 0642 { 0643 Fun redo = []() { return true; }; 0644 Fun undo = []() { return true; }; 0645 if (cutSubtitle(position, undo, redo) > -1) { 0646 pCore->pushUndo(undo, redo, i18n("Cut clip")); 0647 return true; 0648 } 0649 return false; 0650 } 0651 0652 int SubtitleModel::cutSubtitle(int position, Fun &undo, Fun &redo) 0653 { 0654 if (isLocked()) { 0655 return -1; 0656 } 0657 GenTime pos(position, pCore->getCurrentFps()); 0658 GenTime start = GenTime(-1); 0659 for (const auto &subtitles : m_subtitleList) { 0660 if (subtitles.first <= pos && subtitles.second.second > pos) { 0661 start = subtitles.first; 0662 break; 0663 } 0664 } 0665 if (start >= GenTime()) { 0666 GenTime end = m_subtitleList.at(start).second; 0667 QString originalText = m_subtitleList.at(start).first; 0668 QString leftText, rightText; 0669 0670 if (KdenliveSettings::subtitle_razor_mode() == RAZOR_MODE_DUPLICATE) { 0671 leftText = originalText; 0672 rightText = originalText; 0673 } else if (KdenliveSettings::subtitle_razor_mode() == RAZOR_MODE_AFTER_FIRST_LINE) { 0674 static const QRegularExpression newlineRe("\\r?\\n\\s*\\S"); 0675 QRegularExpressionMatch newlineMatch = newlineRe.match(originalText); 0676 if (!newlineMatch.hasMatch()) { 0677 undo(); 0678 return -1; 0679 } else { 0680 leftText = originalText; 0681 leftText.truncate(newlineMatch.capturedStart()); 0682 0683 // Add 1 because the regex matches the non-whitespace character at the end. 0684 rightText = originalText.right(originalText.length() - newlineMatch.capturedEnd() + 1); 0685 } 0686 } else { 0687 undo(); 0688 return -1; 0689 } 0690 0691 int subId = getIdForStartPos(start); 0692 int duration = position - start.frames(pCore->getCurrentFps()); 0693 bool res = requestResize(subId, duration, true, undo, redo, false); 0694 if (res) { 0695 int id = TimelineModel::getNextId(); 0696 Fun local_redo = [this, id, pos, end, subId, leftText, rightText]() { 0697 editSubtitle(subId, leftText); 0698 return addSubtitle(id, pos, end, rightText); 0699 }; 0700 Fun local_undo = [this, id, subId, originalText]() { 0701 editSubtitle(subId, originalText); 0702 removeSubtitle(id); 0703 return true; 0704 }; 0705 if (local_redo()) { 0706 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 0707 return id; 0708 } 0709 } 0710 } 0711 undo(); 0712 return -1; 0713 } 0714 0715 void SubtitleModel::registerSnap(const std::weak_ptr<SnapInterface> &snapModel) 0716 { 0717 // make sure ptr is valid 0718 if (auto ptr = snapModel.lock()) { 0719 // ptr is valid, we store it 0720 m_regSnaps.push_back(snapModel); 0721 // we now add the already existing subtitles to the snap 0722 for (const auto &subtitle : m_subtitleList) { 0723 ptr->addPoint(subtitle.first.frames(pCore->getCurrentFps())); 0724 } 0725 } else { 0726 qDebug() << "Error: added snapmodel for subtitle is null"; 0727 Q_ASSERT(false); 0728 } 0729 } 0730 0731 void SubtitleModel::addSnapPoint(GenTime startpos) 0732 { 0733 std::vector<std::weak_ptr<SnapInterface>> validSnapModels; 0734 Q_ASSERT(m_regSnaps.size() > 0); 0735 for (const auto &snapModel : m_regSnaps) { 0736 if (auto ptr = snapModel.lock()) { 0737 validSnapModels.push_back(snapModel); 0738 ptr->addPoint(startpos.frames(pCore->getCurrentFps())); 0739 } 0740 } 0741 // Update the list of snapModel known to be valid 0742 std::swap(m_regSnaps, validSnapModels); 0743 } 0744 0745 void SubtitleModel::removeSnapPoint(GenTime startpos) 0746 { 0747 std::vector<std::weak_ptr<SnapInterface>> validSnapModels; 0748 for (const auto &snapModel : m_regSnaps) { 0749 if (auto ptr = snapModel.lock()) { 0750 validSnapModels.push_back(snapModel); 0751 ptr->removePoint(startpos.frames(pCore->getCurrentFps())); 0752 } 0753 } 0754 // Update the list of snapModel known to be valid 0755 std::swap(m_regSnaps, validSnapModels); 0756 } 0757 0758 void SubtitleModel::editEndPos(GenTime startPos, GenTime newEndPos, bool refreshModel) 0759 { 0760 qDebug() << "Changing the sub end timings in model"; 0761 if (m_subtitleList.count(startPos) <= 0) { 0762 // is not present in model only 0763 return; 0764 } 0765 m_subtitleList[startPos].second = newEndPos; 0766 // Trigger update of the qml view 0767 int id = getIdForStartPos(startPos); 0768 int row = m_timeline->getSubtitleIndex(id); 0769 Q_EMIT dataChanged(index(row), index(row), {EndFrameRole}); 0770 if (refreshModel) { 0771 Q_EMIT modelChanged(); 0772 } 0773 qDebug() << startPos.frames(pCore->getCurrentFps()) << m_subtitleList[startPos].second.frames(pCore->getCurrentFps()); 0774 } 0775 0776 void SubtitleModel::switchGrab(int sid) 0777 { 0778 if (m_grabbedIds.contains(sid)) { 0779 m_grabbedIds.removeAll(sid); 0780 } else { 0781 m_grabbedIds << sid; 0782 } 0783 int row = m_timeline->getSubtitleIndex(sid); 0784 Q_EMIT dataChanged(index(row), index(row), {GrabRole}); 0785 } 0786 0787 void SubtitleModel::clearGrab() 0788 { 0789 QVector<int> grabbed = m_grabbedIds; 0790 m_grabbedIds.clear(); 0791 for (int sid : grabbed) { 0792 int row = m_timeline->getSubtitleIndex(sid); 0793 Q_EMIT dataChanged(index(row), index(row), {GrabRole}); 0794 } 0795 } 0796 0797 bool SubtitleModel::requestResize(int id, int size, bool right) 0798 { 0799 Fun undo = []() { return true; }; 0800 Fun redo = []() { return true; }; 0801 bool res = requestResize(id, size, right, undo, redo, true); 0802 if (res) { 0803 pCore->pushUndo(undo, redo, i18n("Resize subtitle")); 0804 return true; 0805 } else { 0806 undo(); 0807 return false; 0808 } 0809 } 0810 0811 bool SubtitleModel::requestResize(int id, int size, bool right, Fun &undo, Fun &redo, bool logUndo) 0812 { 0813 if (isLocked()) { 0814 return false; 0815 } 0816 Q_ASSERT(m_timeline->m_allSubtitles.find(id) != m_timeline->m_allSubtitles.end()); 0817 GenTime startPos = m_timeline->m_allSubtitles.at(id); 0818 GenTime endPos = m_subtitleList.at(startPos).second; 0819 Fun operation = []() { return true; }; 0820 Fun reverse = []() { return true; }; 0821 if (right) { 0822 GenTime newEndPos = startPos + GenTime(size, pCore->getCurrentFps()); 0823 operation = [this, id, startPos, endPos, newEndPos, logUndo]() { 0824 m_subtitleList[startPos].second = newEndPos; 0825 removeSnapPoint(endPos); 0826 addSnapPoint(newEndPos); 0827 // Trigger update of the qml view 0828 int row = m_timeline->getSubtitleIndex(id); 0829 Q_EMIT dataChanged(index(row), index(row), {EndFrameRole}); 0830 if (logUndo) { 0831 Q_EMIT modelChanged(); 0832 QPair<int, int> range; 0833 if (endPos > newEndPos) { 0834 range = {newEndPos.frames(pCore->getCurrentFps()), endPos.frames(pCore->getCurrentFps())}; 0835 } else { 0836 range = {endPos.frames(pCore->getCurrentFps()), newEndPos.frames(pCore->getCurrentFps())}; 0837 } 0838 pCore->invalidateRange(range); 0839 pCore->refreshProjectRange(range); 0840 } 0841 return true; 0842 }; 0843 reverse = [this, id, startPos, endPos, newEndPos, logUndo]() { 0844 m_subtitleList[startPos].second = endPos; 0845 removeSnapPoint(newEndPos); 0846 addSnapPoint(endPos); 0847 // Trigger update of the qml view 0848 int row = m_timeline->getSubtitleIndex(id); 0849 Q_EMIT dataChanged(index(row), index(row), {EndFrameRole}); 0850 if (logUndo) { 0851 Q_EMIT modelChanged(); 0852 QPair<int, int> range; 0853 if (endPos > newEndPos) { 0854 range = {newEndPos.frames(pCore->getCurrentFps()), endPos.frames(pCore->getCurrentFps())}; 0855 } else { 0856 range = {endPos.frames(pCore->getCurrentFps()), newEndPos.frames(pCore->getCurrentFps())}; 0857 } 0858 pCore->invalidateRange(range); 0859 pCore->refreshProjectRange(range); 0860 } 0861 return true; 0862 }; 0863 } else { 0864 GenTime newStartPos = endPos - GenTime(size, pCore->getCurrentFps()); 0865 if (m_subtitleList.count(newStartPos) > 0) { 0866 // There already is another subtitle at this position, abort 0867 return false; 0868 } 0869 const QString text = m_subtitleList.at(startPos).first; 0870 operation = [this, id, startPos, newStartPos, endPos, text, logUndo]() { 0871 m_timeline->m_allSubtitles[id] = newStartPos; 0872 m_subtitleList.erase(startPos); 0873 m_subtitleList[newStartPos] = {text, endPos}; 0874 // Trigger update of the qml view 0875 removeSnapPoint(startPos); 0876 addSnapPoint(newStartPos); 0877 int row = m_timeline->getSubtitleIndex(id); 0878 Q_EMIT dataChanged(index(row), index(row), {StartFrameRole}); 0879 if (logUndo) { 0880 Q_EMIT modelChanged(); 0881 QPair<int, int> range; 0882 if (startPos > newStartPos) { 0883 range = {newStartPos.frames(pCore->getCurrentFps()), startPos.frames(pCore->getCurrentFps())}; 0884 } else { 0885 range = {startPos.frames(pCore->getCurrentFps()), newStartPos.frames(pCore->getCurrentFps())}; 0886 } 0887 pCore->invalidateRange(range); 0888 pCore->refreshProjectRange(range); 0889 } 0890 return true; 0891 }; 0892 reverse = [this, id, startPos, newStartPos, endPos, text, logUndo]() { 0893 m_timeline->m_allSubtitles[id] = startPos; 0894 m_subtitleList.erase(newStartPos); 0895 m_subtitleList[startPos] = {text, endPos}; 0896 removeSnapPoint(newStartPos); 0897 addSnapPoint(startPos); 0898 // Trigger update of the qml view 0899 int row = m_timeline->getSubtitleIndex(id); 0900 Q_EMIT dataChanged(index(row), index(row), {StartFrameRole}); 0901 if (logUndo) { 0902 Q_EMIT modelChanged(); 0903 QPair<int, int> range; 0904 if (startPos > newStartPos) { 0905 range = {newStartPos.frames(pCore->getCurrentFps()), startPos.frames(pCore->getCurrentFps())}; 0906 } else { 0907 range = {startPos.frames(pCore->getCurrentFps()), newStartPos.frames(pCore->getCurrentFps())}; 0908 } 0909 pCore->invalidateRange(range); 0910 pCore->refreshProjectRange(range); 0911 } 0912 return true; 0913 }; 0914 } 0915 operation(); 0916 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 0917 return true; 0918 } 0919 0920 bool SubtitleModel::editSubtitle(int id, const QString &newSubtitleText) 0921 { 0922 if (isLocked()) { 0923 return false; 0924 } 0925 if (m_timeline->m_allSubtitles.find(id) == m_timeline->m_allSubtitles.end()) { 0926 qDebug() << "No Subtitle at pos in model"; 0927 return false; 0928 } 0929 GenTime start = m_timeline->m_allSubtitles.at(id); 0930 if (m_subtitleList.count(start) <= 0) { 0931 qDebug() << "No Subtitle at pos in model"; 0932 return false; 0933 } 0934 0935 qDebug() << "Editing existing subtitle in model"; 0936 m_subtitleList[start].first = newSubtitleText; 0937 int row = m_timeline->getSubtitleIndex(id); 0938 Q_EMIT dataChanged(index(row), index(row), QVector<int>() << SubtitleRole); 0939 Q_EMIT modelChanged(); 0940 return true; 0941 } 0942 0943 bool SubtitleModel::removeSubtitle(int id, bool temporary, bool updateFilter) 0944 { 0945 qDebug() << "Deleting subtitle in model"; 0946 if (isLocked()) { 0947 return false; 0948 } 0949 if (m_timeline->m_allSubtitles.find(id) == m_timeline->m_allSubtitles.end()) { 0950 qDebug() << "No Subtitle at pos in model"; 0951 return false; 0952 } 0953 GenTime start = m_timeline->m_allSubtitles.at(id); 0954 if (m_subtitleList.count(start) <= 0) { 0955 qDebug() << "No Subtitle at pos in model"; 0956 return false; 0957 } 0958 GenTime end = m_subtitleList.at(start).second; 0959 int row = m_timeline->getSubtitleIndex(id); 0960 m_timeline->deregisterSubtitle(id, temporary); 0961 beginRemoveRows(QModelIndex(), row, row); 0962 bool lastSub = false; 0963 if (start == m_subtitleList.rbegin()->first) { 0964 // Check if this is the last subtitle 0965 lastSub = true; 0966 } 0967 m_subtitleList.erase(start); 0968 endRemoveRows(); 0969 removeSnapPoint(start); 0970 removeSnapPoint(end); 0971 if (lastSub) { 0972 m_timeline->updateDuration(); 0973 } 0974 if (updateFilter) { 0975 Q_EMIT modelChanged(); 0976 } 0977 return true; 0978 } 0979 0980 void SubtitleModel::removeAllSubtitles() 0981 { 0982 if (isLocked()) { 0983 return; 0984 } 0985 auto ids = m_timeline->m_allSubtitles; 0986 for (const auto &p : ids) { 0987 removeSubtitle(p.first); 0988 } 0989 } 0990 0991 void SubtitleModel::requestSubtitleMove(int clipId, GenTime position) 0992 { 0993 0994 GenTime oldPos = getStartPosForId(clipId); 0995 Fun local_redo = [this, clipId, position]() { return moveSubtitle(clipId, position, true, true); }; 0996 Fun local_undo = [this, clipId, oldPos]() { return moveSubtitle(clipId, oldPos, true, true); }; 0997 bool res = local_redo(); 0998 if (res) { 0999 pCore->pushUndo(local_undo, local_redo, i18n("Move subtitle")); 1000 } 1001 } 1002 1003 bool SubtitleModel::moveSubtitle(int subId, GenTime newPos, bool updateModel, bool updateView) 1004 { 1005 if (m_timeline->m_allSubtitles.count(subId) == 0 || isLocked()) { 1006 return false; 1007 } 1008 GenTime oldPos = m_timeline->m_allSubtitles.at(subId); 1009 if (m_subtitleList.count(oldPos) <= 0 || m_subtitleList.count(newPos) > 0) { 1010 // is not present in model, or already another one at new position 1011 qDebug() << "==== MOVE FAILED"; 1012 return false; 1013 } 1014 QString subtitleText = m_subtitleList[oldPos].first; 1015 removeSnapPoint(oldPos); 1016 removeSnapPoint(m_subtitleList[oldPos].second); 1017 GenTime duration = m_subtitleList[oldPos].second - oldPos; 1018 GenTime endPos = newPos + duration; 1019 int id = getIdForStartPos(oldPos); 1020 m_timeline->m_allSubtitles[id] = newPos; 1021 m_subtitleList.erase(oldPos); 1022 m_subtitleList[newPos] = {subtitleText, endPos}; 1023 addSnapPoint(newPos); 1024 addSnapPoint(endPos); 1025 if (updateView) { 1026 updateSub(id, {StartFrameRole, EndFrameRole}); 1027 QPair<int, int> range; 1028 if (oldPos < newPos) { 1029 range = {oldPos.frames(pCore->getCurrentFps()), endPos.frames(pCore->getCurrentFps())}; 1030 } else { 1031 range = {newPos.frames(pCore->getCurrentFps()), (oldPos + duration).frames(pCore->getCurrentFps())}; 1032 } 1033 pCore->invalidateRange(range); 1034 pCore->refreshProjectRange(range); 1035 } 1036 if (updateModel) { 1037 // Trigger update of the subtitle file 1038 Q_EMIT modelChanged(); 1039 if (newPos == m_subtitleList.rbegin()->first) { 1040 // Check if this is the last subtitle 1041 m_timeline->updateDuration(); 1042 } 1043 } 1044 return true; 1045 } 1046 1047 int SubtitleModel::getIdForStartPos(GenTime startTime) const 1048 { 1049 auto findResult = std::find_if(std::begin(m_timeline->m_allSubtitles), std::end(m_timeline->m_allSubtitles), 1050 [&](const std::pair<int, GenTime> &pair) { return pair.second == startTime; }); 1051 if (findResult != std::end(m_timeline->m_allSubtitles)) { 1052 return findResult->first; 1053 } 1054 return -1; 1055 } 1056 1057 GenTime SubtitleModel::getStartPosForId(int id) const 1058 { 1059 if (m_timeline->m_allSubtitles.count(id) == 0) { 1060 return GenTime(); 1061 }; 1062 return m_timeline->m_allSubtitles.at(id); 1063 } 1064 1065 int SubtitleModel::getPreviousSub(int id) const 1066 { 1067 GenTime start = getStartPosForId(id); 1068 int row = static_cast<int>(std::distance(m_subtitleList.begin(), m_subtitleList.find(start))); 1069 if (row > 0) { 1070 row--; 1071 auto it = m_subtitleList.begin(); 1072 std::advance(it, row); 1073 const GenTime res = it->first; 1074 return getIdForStartPos(res); 1075 } 1076 return -1; 1077 } 1078 1079 int SubtitleModel::getNextSub(int id) const 1080 { 1081 GenTime start = getStartPosForId(id); 1082 int row = static_cast<int>(std::distance(m_subtitleList.begin(), m_subtitleList.find(start))); 1083 if (row < static_cast<int>(m_subtitleList.size()) - 1) { 1084 row++; 1085 auto it = m_subtitleList.begin(); 1086 std::advance(it, row); 1087 const GenTime res = it->first; 1088 return getIdForStartPos(res); 1089 } 1090 return -1; 1091 } 1092 1093 void SubtitleModel::subtitleFileFromZone(int in, int out, const QString &outFile) 1094 { 1095 QJsonArray list; 1096 double fps = pCore->getCurrentFps(); 1097 GenTime zoneIn(in, fps); 1098 GenTime zoneOut(out, fps); 1099 for (auto subtitle : m_subtitleList) { 1100 GenTime inTime = subtitle.first; 1101 GenTime outTime = subtitle.second.second; 1102 if (outTime < zoneIn) { 1103 // Outside zone 1104 continue; 1105 } 1106 if (zoneOut > GenTime() && inTime > zoneOut) { 1107 // Outside zone 1108 continue; 1109 } 1110 if (inTime < zoneIn) { 1111 inTime = zoneIn; 1112 } 1113 if (zoneOut > GenTime() && outTime > zoneOut) { 1114 outTime = zoneOut; 1115 } 1116 inTime -= zoneIn; 1117 outTime -= zoneIn; 1118 1119 QJsonObject currentSubtitle; 1120 currentSubtitle.insert(QLatin1String("startPos"), QJsonValue(inTime.seconds())); 1121 currentSubtitle.insert(QLatin1String("dialogue"), QJsonValue(subtitle.second.first)); 1122 currentSubtitle.insert(QLatin1String("endPos"), QJsonValue(outTime.seconds())); 1123 list.push_back(currentSubtitle); 1124 // qDebug()<<subtitle.first.seconds(); 1125 } 1126 QJsonDocument jsonDoc(list); 1127 QString subData = QString(jsonDoc.toJson()); 1128 saveSubtitleData(subData, outFile); 1129 } 1130 1131 QString SubtitleModel::toJson() 1132 { 1133 // qDebug()<< "to JSON"; 1134 QJsonArray list; 1135 for (const auto &subtitle : m_subtitleList) { 1136 QJsonObject currentSubtitle; 1137 currentSubtitle.insert(QLatin1String("startPos"), QJsonValue(subtitle.first.seconds())); 1138 currentSubtitle.insert(QLatin1String("dialogue"), QJsonValue(subtitle.second.first)); 1139 currentSubtitle.insert(QLatin1String("endPos"), QJsonValue(subtitle.second.second.seconds())); 1140 list.push_back(currentSubtitle); 1141 // qDebug()<<subtitle.first.seconds(); 1142 } 1143 QJsonDocument jsonDoc(list); 1144 // qDebug()<<QString(jsonDoc.toJson()); 1145 return QString(jsonDoc.toJson()); 1146 } 1147 1148 void SubtitleModel::copySubtitle(const QString &path, int ix, bool checkOverwrite, bool updateFilter) 1149 { 1150 QFile srcFile(pCore->currentDoc()->subTitlePath(m_timeline->uuid(), ix, false)); 1151 if (srcFile.exists()) { 1152 QFile prev(path); 1153 if (prev.exists()) { 1154 if (checkOverwrite || !path.endsWith(QStringLiteral(".srt"))) { 1155 if (KMessageBox::questionTwoActions(QApplication::activeWindow(), i18n("File %1 already exists.\nDo you want to overwrite it?", path), {}, 1156 KStandardGuiItem::overwrite(), KStandardGuiItem::cancel()) == KMessageBox::SecondaryAction) { 1157 return; 1158 } 1159 } 1160 prev.remove(); 1161 } 1162 srcFile.copy(path); 1163 if (updateFilter) { 1164 m_subtitleFilter->set("av.filename", path.toUtf8().constData()); 1165 } 1166 } 1167 } 1168 1169 void SubtitleModel::restoreTmpFile(int ix) 1170 { 1171 1172 QString outFile = pCore->currentDoc()->subTitlePath(m_timeline->uuid(), ix, false); 1173 m_subtitleFilter->set("av.filename", outFile.toUtf8().constData()); 1174 } 1175 1176 void SubtitleModel::jsontoSubtitle(const QString &data) 1177 { 1178 int ix = pCore->currentDoc()->getSequenceProperty(m_timeline->uuid(), QStringLiteral("kdenlive:activeSubtitleIndex"), QStringLiteral("0")).toInt(); 1179 QString outFile = pCore->currentDoc()->subTitlePath(m_timeline->uuid(), ix, false); 1180 QString masterFile = m_subtitleFilter->get("av.filename"); 1181 if (masterFile.isEmpty()) { 1182 m_subtitleFilter->set("av.filename", outFile.toUtf8().constData()); 1183 } 1184 int line = saveSubtitleData(data, outFile); 1185 qDebug() << "Saving subtitle filter: " << outFile; 1186 if (line > 0) { 1187 m_subtitleFilter->set("av.filename", outFile.toUtf8().constData()); 1188 m_timeline->tractor()->attach(*m_subtitleFilter.get()); 1189 } else { 1190 m_timeline->tractor()->detach(*m_subtitleFilter.get()); 1191 } 1192 } 1193 1194 int SubtitleModel::saveSubtitleData(const QString &data, const QString &outFile) 1195 { 1196 bool assFormat = outFile.endsWith(".ass"); 1197 if (!assFormat) { 1198 qDebug() << "srt/vtt/sbv file import"; // if imported file isn't .ass, it is .srt format 1199 } 1200 QFile outF(outFile); 1201 1202 // qDebug()<< "Import from JSON"; 1203 QWriteLocker locker(&m_lock); 1204 auto json = QJsonDocument::fromJson(data.toUtf8()); 1205 if (!json.isArray()) { 1206 qDebug() << "Error : Json file should be an array"; 1207 return 0; 1208 } 1209 int line = 0; 1210 auto list = json.array(); 1211 if (outF.open(QIODevice::WriteOnly)) { 1212 QTextStream out(&outF); 1213 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 1214 out.setCodec("UTF-8"); 1215 #endif 1216 if (assFormat) { 1217 out << scriptInfoSection << '\n'; 1218 out << styleSection << '\n'; 1219 out << eventSection; 1220 } 1221 for (const auto &entry : qAsConst(list)) { 1222 if (!entry.isObject()) { 1223 qDebug() << "Warning : Skipping invalid subtitle data"; 1224 continue; 1225 } 1226 auto entryObj = entry.toObject(); 1227 if (!entryObj.contains(QLatin1String("startPos"))) { 1228 qDebug() << "Warning : Skipping invalid subtitle data (does not contain position)"; 1229 continue; 1230 } 1231 double startPos = entryObj[QLatin1String("startPos")].toDouble(); 1232 // convert seconds to FORMAT= hh:mm:ss.SS (in .ass) and hh:mm:ss,SSS (in .srt) 1233 int millisec = int(startPos * 1000); 1234 int seconds = millisec / 1000; 1235 millisec %= 1000; 1236 int minutes = seconds / 60; 1237 seconds %= 60; 1238 int hours = minutes / 60; 1239 minutes %= 60; 1240 int milli_2 = millisec / 10; 1241 QString startTimeString = QString("%1:%2:%3.%4") 1242 .arg(hours, 2, 10, QChar('0')) 1243 .arg(minutes, 2, 10, QChar('0')) 1244 .arg(seconds, 2, 10, QChar('0')) 1245 .arg(milli_2, 2, 10, QChar('0')); 1246 QString startTimeStringSRT = QString("%1:%2:%3,%4") 1247 .arg(hours, 2, 10, QChar('0')) 1248 .arg(minutes, 2, 10, QChar('0')) 1249 .arg(seconds, 2, 10, QChar('0')) 1250 .arg(millisec, 3, 10, QChar('0')); 1251 QString dialogue = entryObj[QLatin1String("dialogue")].toString(); 1252 double endPos = entryObj[QLatin1String("endPos")].toDouble(); 1253 millisec = int(endPos * 1000); 1254 seconds = millisec / 1000; 1255 millisec %= 1000; 1256 minutes = seconds / 60; 1257 seconds %= 60; 1258 hours = minutes / 60; 1259 minutes %= 60; 1260 1261 milli_2 = millisec / 10; // to limit ms to 2 digits (for .ass) 1262 QString endTimeString = QString("%1:%2:%3.%4") 1263 .arg(hours, 2, 10, QChar('0')) 1264 .arg(minutes, 2, 10, QChar('0')) 1265 .arg(seconds, 2, 10, QChar('0')) 1266 .arg(milli_2, 2, 10, QChar('0')); 1267 1268 QString endTimeStringSRT = QString("%1:%2:%3,%4") 1269 .arg(hours, 2, 10, QChar('0')) 1270 .arg(minutes, 2, 10, QChar('0')) 1271 .arg(seconds, 2, 10, QChar('0')) 1272 .arg(millisec, 3, 10, QChar('0')); 1273 line++; 1274 if (assFormat) { 1275 // Format: Layer, Start, End, Style, Actor, MarginL, MarginR, MarginV, Effect, Text 1276 out << "Dialogue: 0," << startTimeString << "," << endTimeString << "," << styleName << ",,0000,0000,0000,," << dialogue << '\n'; 1277 } else { 1278 out << line << "\n" << startTimeStringSRT << " --> " << endTimeStringSRT << "\n" << dialogue << "\n" << '\n'; 1279 } 1280 1281 // qDebug() << "ADDING SUBTITLE to FILE AT START POS: " << startPos <<" END POS: "<<endPos;//<< ", FPS: " << pCore->getCurrentFps(); 1282 } 1283 outF.close(); 1284 } 1285 return line; 1286 } 1287 1288 void SubtitleModel::updateSub(int id, const QVector<int> &roles) 1289 { 1290 int row = m_timeline->getSubtitleIndex(id); 1291 Q_EMIT dataChanged(index(row), index(row), roles); 1292 } 1293 1294 int SubtitleModel::getRowForId(int id) const 1295 { 1296 return m_timeline->getSubtitleIndex(id); 1297 } 1298 1299 int SubtitleModel::getSubtitlePlaytime(int id) const 1300 { 1301 GenTime startPos = m_timeline->m_allSubtitles.at(id); 1302 return m_subtitleList.at(startPos).second.frames(pCore->getCurrentFps()) - startPos.frames(pCore->getCurrentFps()); 1303 } 1304 1305 int SubtitleModel::getSubtitleEnd(int id) const 1306 { 1307 GenTime startPos = m_timeline->m_allSubtitles.at(id); 1308 return m_subtitleList.at(startPos).second.frames(pCore->getCurrentFps()); 1309 } 1310 1311 QPair<int, int> SubtitleModel::getInOut(int sid) const 1312 { 1313 GenTime startPos = m_timeline->m_allSubtitles.at(sid); 1314 return {startPos.frames(pCore->getCurrentFps()), m_subtitleList.at(startPos).second.frames(pCore->getCurrentFps())}; 1315 } 1316 1317 void SubtitleModel::setSelected(int id, bool select) 1318 { 1319 if (isLocked()) { 1320 return; 1321 } 1322 if (select) { 1323 m_selected << id; 1324 } else { 1325 m_selected.removeAll(id); 1326 } 1327 updateSub(id, {SelectedRole}); 1328 } 1329 1330 bool SubtitleModel::isSelected(int id) const 1331 { 1332 return m_selected.contains(id); 1333 } 1334 1335 int SubtitleModel::trackDuration() const 1336 { 1337 if (m_subtitleList.empty()) { 1338 return 0; 1339 } 1340 return m_subtitleList.rbegin()->second.second.frames(pCore->getCurrentFps()); 1341 } 1342 1343 void SubtitleModel::switchDisabled() 1344 { 1345 m_subtitleFilter->set("disable", 1 - m_subtitleFilter->get_int("disable")); 1346 } 1347 1348 void SubtitleModel::switchLocked() 1349 { 1350 bool isLocked = m_subtitleFilter->get_int("kdenlive:locked") == 1; 1351 m_subtitleFilter->set("kdenlive:locked", isLocked ? 0 : 1); 1352 1353 // En/disable snapping on lock 1354 /*std::vector<std::weak_ptr<SnapInterface>> validSnapModels; 1355 for (const auto &snapModel : m_regSnaps) { 1356 if (auto ptr = snapModel.lock()) { 1357 validSnapModels.push_back(snapModel); 1358 if (isLocked) { 1359 for (const auto &subtitle : m_subtitleList) { 1360 ptr->addPoint(subtitle.first.frames(pCore->getCurrentFps())); 1361 ptr->addPoint(subtitle.second.second.frames(pCore->getCurrentFps())); 1362 } 1363 } else { 1364 for (const auto &subtitle : m_subtitleList) { 1365 ptr->removePoint(subtitle.first.frames(pCore->getCurrentFps())); 1366 ptr->removePoint(subtitle.second.second.frames(pCore->getCurrentFps())); 1367 } 1368 } 1369 } 1370 } 1371 // Update the list of snapModel known to be valid 1372 std::swap(m_regSnaps, validSnapModels); 1373 if (!isLocked) { 1374 // Clear selection 1375 while (!m_selected.isEmpty()) { 1376 int id = m_selected.takeFirst(); 1377 updateSub(id, {SelectedRole}); 1378 } 1379 }*/ 1380 } 1381 1382 bool SubtitleModel::isDisabled() const 1383 { 1384 return m_subtitleFilter->get_int("disable") == 1; 1385 } 1386 1387 bool SubtitleModel::isLocked() const 1388 { 1389 return m_subtitleFilter->get_int("kdenlive:locked") == 1; 1390 } 1391 1392 void SubtitleModel::loadProperties(const QMap<QString, QString> &subProperties) 1393 { 1394 if (subProperties.isEmpty()) { 1395 if (m_subtitleFilter->property_exists("av.force_style")) { 1396 const QString style = m_subtitleFilter->get("av.force_style"); 1397 Q_EMIT updateSubtitleStyle(style); 1398 } else { 1399 Q_EMIT updateSubtitleStyle(QString()); 1400 } 1401 return; 1402 } 1403 QMap<QString, QString>::const_iterator i = subProperties.constBegin(); 1404 while (i != subProperties.constEnd()) { 1405 if (!i.value().isEmpty()) { 1406 m_subtitleFilter->set(i.key().toUtf8().constData(), i.value().toUtf8().constData()); 1407 } 1408 ++i; 1409 } 1410 if (subProperties.contains(QLatin1String("av.force_style"))) { 1411 Q_EMIT updateSubtitleStyle(subProperties.value(QLatin1String("av.force_style"))); 1412 } else { 1413 Q_EMIT updateSubtitleStyle(QString()); 1414 } 1415 qDebug() << "::::: LOADED SUB PROPS " << subProperties; 1416 } 1417 1418 void SubtitleModel::allSnaps(std::vector<int> &snaps) 1419 { 1420 for (const auto &subtitle : m_subtitleList) { 1421 snaps.push_back(subtitle.first.frames(pCore->getCurrentFps())); 1422 snaps.push_back(subtitle.second.second.frames(pCore->getCurrentFps())); 1423 } 1424 } 1425 1426 QDomElement SubtitleModel::toXml(int sid, QDomDocument &document) 1427 { 1428 GenTime startPos = m_timeline->m_allSubtitles.at(sid); 1429 int endPos = m_subtitleList.at(startPos).second.frames(pCore->getCurrentFps()); 1430 QDomElement container = document.createElement(QStringLiteral("subtitle")); 1431 container.setAttribute(QStringLiteral("in"), startPos.frames(pCore->getCurrentFps())); 1432 container.setAttribute(QStringLiteral("out"), endPos); 1433 container.setAttribute(QStringLiteral("text"), m_subtitleList.at(startPos).first); 1434 return container; 1435 } 1436 1437 bool SubtitleModel::isBlankAt(int pos) const 1438 { 1439 GenTime matchPos(pos, pCore->getCurrentFps()); 1440 for (const auto &subtitles : m_subtitleList) { 1441 if (subtitles.first > matchPos) { 1442 continue; 1443 } 1444 if (subtitles.second.second > matchPos) { 1445 return false; 1446 } 1447 } 1448 return true; 1449 ; 1450 } 1451 1452 int SubtitleModel::getBlankEnd(int pos) const 1453 { 1454 GenTime matchPos(pos, pCore->getCurrentFps()); 1455 bool found = false; 1456 GenTime min; 1457 for (const auto &subtitles : m_subtitleList) { 1458 if (subtitles.first > matchPos && (min == GenTime() || subtitles.first < min)) { 1459 min = subtitles.first; 1460 found = true; 1461 } 1462 } 1463 return found ? min.frames(pCore->getCurrentFps()) : 0; 1464 } 1465 1466 int SubtitleModel::getBlankSizeAtPos(int frame) const 1467 { 1468 int bkStart = getBlankStart(frame); 1469 int bkEnd = getBlankEnd(frame); 1470 return bkEnd - bkStart; 1471 } 1472 1473 int SubtitleModel::getBlankStart(int pos) const 1474 { 1475 GenTime matchPos(pos, pCore->getCurrentFps()); 1476 bool found = false; 1477 GenTime min; 1478 for (const auto &subtitles : m_subtitleList) { 1479 if (subtitles.second.second <= matchPos && (min == GenTime() || subtitles.second.second > min)) { 1480 min = subtitles.second.second; 1481 found = true; 1482 } 1483 } 1484 return found ? min.frames(pCore->getCurrentFps()) : 0; 1485 } 1486 1487 int SubtitleModel::getNextBlankStart(int pos) const 1488 { 1489 while (!isBlankAt(pos)) { 1490 std::unordered_set<int> matches = getItemsInRange(pos, pos); 1491 if (matches.size() == 0) { 1492 if (isBlankAt(pos)) { 1493 break; 1494 } else { 1495 // We are at the end of the track, abort 1496 return -1; 1497 } 1498 } else { 1499 for (int id : matches) { 1500 pos = qMax(pos, getSubtitleEnd(id)); 1501 } 1502 } 1503 } 1504 return getBlankStart(pos); 1505 } 1506 1507 void SubtitleModel::editSubtitle(int id, const QString &newText, const QString &oldText) 1508 { 1509 qDebug() << "Editing existing subtitle :" << id; 1510 if (oldText == newText) { 1511 return; 1512 } 1513 Fun local_redo = [this, id, newText]() { 1514 editSubtitle(id, newText); 1515 QPair<int, int> range = getInOut(id); 1516 pCore->invalidateRange(range); 1517 pCore->refreshProjectRange(range); 1518 return true; 1519 }; 1520 Fun local_undo = [this, id, oldText]() { 1521 editSubtitle(id, oldText); 1522 QPair<int, int> range = getInOut(id); 1523 pCore->invalidateRange(range); 1524 pCore->refreshProjectRange(range); 1525 return true; 1526 }; 1527 local_redo(); 1528 pCore->pushUndo(local_undo, local_redo, i18n("Edit subtitle")); 1529 } 1530 1531 void SubtitleModel::resizeSubtitle(int startFrame, int endFrame, int oldEndFrame, bool refreshModel) 1532 { 1533 qDebug() << "Editing existing subtitle in controller at:" << startFrame; 1534 int max = qMax(endFrame, oldEndFrame); 1535 Fun local_redo = [this, startFrame, endFrame, max, refreshModel]() { 1536 editEndPos(GenTime(startFrame, pCore->getCurrentFps()), GenTime(endFrame, pCore->getCurrentFps()), refreshModel); 1537 pCore->refreshProjectRange({startFrame, max}); 1538 return true; 1539 }; 1540 Fun local_undo = [this, startFrame, oldEndFrame, max, refreshModel]() { 1541 editEndPos(GenTime(startFrame, pCore->getCurrentFps()), GenTime(oldEndFrame, pCore->getCurrentFps()), refreshModel); 1542 pCore->refreshProjectRange({startFrame, max}); 1543 return true; 1544 }; 1545 local_redo(); 1546 if (refreshModel) { 1547 pCore->pushUndo(local_undo, local_redo, i18n("Resize subtitle")); 1548 } 1549 } 1550 1551 void SubtitleModel::addSubtitle(int startframe, QString text) 1552 { 1553 if (startframe == -1) { 1554 startframe = pCore->getMonitorPosition(); 1555 } 1556 int endframe = startframe + pCore->getDurationFromString(KdenliveSettings::subtitle_duration()); 1557 int id = TimelineModel::getNextId(); 1558 if (text.isEmpty()) { 1559 text = i18n("Add text"); 1560 } 1561 Fun local_undo = [this, id, startframe, endframe]() { 1562 removeSubtitle(id); 1563 QPair<int, int> range = {startframe, endframe}; 1564 pCore->invalidateRange(range); 1565 pCore->refreshProjectRange(range); 1566 return true; 1567 }; 1568 Fun local_redo = [this, id, startframe, endframe, text]() { 1569 if (addSubtitle(id, GenTime(startframe, pCore->getCurrentFps()), GenTime(endframe, pCore->getCurrentFps()), text)) { 1570 QPair<int, int> range = {startframe, endframe}; 1571 pCore->invalidateRange(range); 1572 pCore->refreshProjectRange(range); 1573 return true; 1574 } 1575 return false; 1576 }; 1577 if (local_redo()) { 1578 m_timeline->requestAddToSelection(id, true); 1579 pCore->pushUndo(local_undo, local_redo, i18n("Add subtitle")); 1580 int index = m_timeline->positionForIndex(id); 1581 if (index > -1) { 1582 Q_EMIT m_timeline->highlightSub(index); 1583 } 1584 } 1585 } 1586 1587 void SubtitleModel::doCutSubtitle(int id, int cursorPos) 1588 { 1589 Q_ASSERT(m_timeline->isSubTitle(id)); 1590 // Cut subtitle at edit position 1591 int timelinePos = pCore->getMonitorPosition(); 1592 GenTime position(timelinePos, pCore->getCurrentFps()); 1593 GenTime start = m_timeline->m_allSubtitles.at(id); 1594 SubtitledTime subData = getSubtitle(start); 1595 if (position > start && position < subData.end()) { 1596 QString originalText = subData.subtitle(); 1597 QString firstText = originalText; 1598 QString secondText = originalText.right(originalText.length() - cursorPos); 1599 firstText.truncate(cursorPos); 1600 Fun undo = []() { return true; }; 1601 Fun redo = []() { return true; }; 1602 int newId = cutSubtitle(timelinePos, undo, redo); 1603 if (newId > -1) { 1604 Fun local_redo = [this, id, newId, firstText, secondText]() { 1605 editSubtitle(id, firstText); 1606 editSubtitle(newId, secondText); 1607 return true; 1608 }; 1609 Fun local_undo = [this, id, originalText]() { 1610 editSubtitle(id, originalText); 1611 return true; 1612 }; 1613 local_redo(); 1614 UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); 1615 pCore->pushUndo(undo, redo, i18n("Cut clip")); 1616 } 1617 } 1618 } 1619 1620 void SubtitleModel::deleteSubtitle(int startframe, int endframe, const QString &text) 1621 { 1622 int id = getIdForStartPos(GenTime(startframe, pCore->getCurrentFps())); 1623 Fun local_redo = [this, id, startframe, endframe]() { 1624 removeSubtitle(id); 1625 pCore->refreshProjectRange({startframe, endframe}); 1626 return true; 1627 }; 1628 Fun local_undo = [this, id, startframe, endframe, text]() { 1629 addSubtitle(id, GenTime(startframe, pCore->getCurrentFps()), GenTime(endframe, pCore->getCurrentFps()), text); 1630 pCore->refreshProjectRange({startframe, endframe}); 1631 return true; 1632 }; 1633 local_redo(); 1634 pCore->pushUndo(local_undo, local_redo, i18n("Delete subtitle")); 1635 } 1636 1637 QMap<std::pair<int, QString>, QString> SubtitleModel::getSubtitlesList() const 1638 { 1639 return m_subtitlesList; 1640 } 1641 1642 void SubtitleModel::setSubtitlesList(QMap<std::pair<int, QString>, QString> list) 1643 { 1644 m_subtitlesList = list; 1645 } 1646 1647 void SubtitleModel::updateModelName(int ix, const QString &name) 1648 { 1649 QMapIterator<std::pair<int, QString>, QString> i(m_subtitlesList); 1650 std::pair<int, QString> oldSub = {-1, QStringLiteral()}; 1651 QString path; 1652 while (i.hasNext()) { 1653 i.next(); 1654 if (i.key().first == ix) { 1655 // match 1656 oldSub = i.key(); 1657 path = i.value(); 1658 break; 1659 } 1660 } 1661 if (oldSub.first > -1) { 1662 int ix = oldSub.first; 1663 m_subtitlesList.remove(oldSub); 1664 m_subtitlesList.insert({ix, name}, path); 1665 } else { 1666 qDebug() << "COULD NOT FIND SUBTITLE TO EDIT, CNT:" << m_subtitlesList.size(); 1667 } 1668 } 1669 1670 int SubtitleModel::createNewSubtitle(const QString subtitleName, int id) 1671 { 1672 // Create new subtitle file 1673 QList<std::pair<int, QString>> keys = m_subtitlesList.keys(); 1674 QStringList existingNames; 1675 int maxIx = 0; 1676 for (auto &l : keys) { 1677 existingNames << l.second; 1678 if (l.first > maxIx) { 1679 maxIx = l.first; 1680 } 1681 } 1682 maxIx++; 1683 int ix = m_subtitlesList.size() + 1; 1684 QString newName = subtitleName; 1685 if (newName.isEmpty()) { 1686 newName = i18nc("@item:inlistbox subtitle track name", "Subtitle %1", ix); 1687 while (existingNames.contains(newName)) { 1688 ix++; 1689 newName = i18nc("@item:inlistbox subtitle track name", "Subtitle %1", ix); 1690 } 1691 } 1692 const QString newPath = pCore->currentDoc()->subTitlePath(m_timeline->uuid(), maxIx, true); 1693 m_subtitlesList.insert({maxIx, newName}, newPath); 1694 if (id >= 0) { 1695 // Duplicate existing subtitle 1696 QString source = pCore->currentDoc()->subTitlePath(m_timeline->uuid(), id, false); 1697 if (!QFile::exists(source)) { 1698 source = pCore->currentDoc()->subTitlePath(m_timeline->uuid(), id, true); 1699 } 1700 QFile::copy(source, newPath); 1701 } 1702 return m_subtitlesList.size(); 1703 } 1704 1705 bool SubtitleModel::deleteSubtitle(int ix) 1706 { 1707 QMapIterator<std::pair<int, QString>, QString> i(m_subtitlesList); 1708 bool success = false; 1709 std::pair<int, QString> matchingItem = {-1, QString()}; 1710 while (i.hasNext()) { 1711 i.next(); 1712 if (i.key().first == ix) { 1713 // Found a match 1714 matchingItem = i.key(); 1715 success = true; 1716 break; 1717 } 1718 } 1719 if (success && matchingItem.first > -1) { 1720 // Delete subtitle files 1721 const QString workPath = pCore->currentDoc()->subTitlePath(m_timeline->uuid(), matchingItem.first, false); 1722 const QString finalPath = pCore->currentDoc()->subTitlePath(m_timeline->uuid(), matchingItem.first, true); 1723 QFile::remove(workPath); 1724 QFile::remove(finalPath); 1725 // Remove entry from our subtitles list 1726 m_subtitlesList.remove(matchingItem); 1727 } 1728 return success; 1729 } 1730 1731 const QString SubtitleModel::subtitlesFilesToJson() 1732 { 1733 QJsonArray list; 1734 QMapIterator<std::pair<int, QString>, QString> i(m_subtitlesList); 1735 std::pair<int, QString> oldSub = {-1, QStringLiteral()}; 1736 QString path; 1737 while (i.hasNext()) { 1738 i.next(); 1739 QJsonObject currentSubtitle; 1740 currentSubtitle.insert(QLatin1String("name"), QJsonValue(i.key().second)); 1741 currentSubtitle.insert(QLatin1String("id"), QJsonValue(i.key().first)); 1742 currentSubtitle.insert(QLatin1String("file"), QJsonValue(i.value())); 1743 list.push_back(currentSubtitle); 1744 } 1745 QJsonDocument json(list); 1746 return QString::fromUtf8(json.toJson()); 1747 } 1748 1749 void SubtitleModel::activateSubtitle(int ix) 1750 { 1751 int currentIx = pCore->currentDoc()->getSequenceProperty(m_timeline->uuid(), QStringLiteral("kdenlive:activeSubtitleIndex"), QStringLiteral("0")).toInt(); 1752 if (currentIx == ix) { 1753 return; 1754 } 1755 const QString workPath = pCore->currentDoc()->subTitlePath(m_timeline->uuid(), ix, false); 1756 const QString finalPath = pCore->currentDoc()->subTitlePath(m_timeline->uuid(), ix, true); 1757 if (!QFile::exists(workPath) && QFile::exists(finalPath)) { 1758 QFile::copy(finalPath, workPath); 1759 } 1760 QFile file(workPath); 1761 if (!file.exists()) { 1762 // Create work file 1763 file.open(QIODevice::WriteOnly); 1764 file.close(); 1765 } 1766 beginRemoveRows(QModelIndex(), 0, m_timeline->m_allSubtitles.size()); 1767 m_timeline->m_allSubtitles.clear(); 1768 m_subtitleList.clear(); 1769 endRemoveRows(); 1770 pCore->currentDoc()->setSequenceProperty(m_timeline->uuid(), QStringLiteral("kdenlive:activeSubtitleIndex"), ix); 1771 parseSubtitle(workPath); 1772 }