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 }