subtitlemodel.cpp 41.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/***************************************************************************
 *   Copyright (C) 2020 by Sashmita Raghav                                 *
 *   This file is part of Kdenlive. See www.kdenlive.org.                  *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) version 3 or any later version accepted by the       *
 *   membership of KDE e.V. (or its successor approved  by the membership  *
 *   of KDE e.V.), which shall act as a proxy defined in Section 14 of     *
 *   version 3 of the license.                                             *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 ***************************************************************************/

Sashmita Raghav's avatar
Sashmita Raghav committed
22
#include "subtitlemodel.hpp"
Sashmita Raghav's avatar
Sashmita Raghav committed
23
#include "bin/bin.h"
Sashmita Raghav's avatar
Sashmita Raghav committed
24 25
#include "core.h"
#include "project/projectmanager.h"
26
#include "doc/kdenlivedoc.h"
Sashmita Raghav's avatar
Sashmita Raghav committed
27
#include "timeline2/model/snapmodel.hpp"
28 29
#include "timeline2/model/timelineitemmodel.hpp"
#include "macros.hpp"
30
#include "profiles/profilemodel.hpp"
31 32
#include "undohelper.hpp"

33 34
#include <mlt++/MltProperties.h>
#include <mlt++/Mlt.h>
Sashmita Raghav's avatar
Sashmita Raghav committed
35

36
#include <KLocalizedString>
37
#include <KMessageBox>
38 39 40
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
41
#include <QApplication>
42

43
SubtitleModel::SubtitleModel(Mlt::Tractor *tractor, std::shared_ptr<TimelineItemModel> timeline, QObject *parent)
Sashmita Raghav's avatar
Sashmita Raghav committed
44
    : QAbstractListModel(parent)
45
    , m_timeline(timeline)
46
    , m_lock(QReadWriteLock::Recursive)
47 48
    , m_subtitleFilter(new Mlt::Filter(pCore->getCurrentProfile()->profile(), "avfilter.subtitles"))
    , m_tractor(tractor)
Sashmita Raghav's avatar
Sashmita Raghav committed
49
{
50 51
    qDebug()<< "subtitle constructor";
    qDebug()<<"Filter!";
Sashmita Raghav's avatar
Sashmita Raghav committed
52
    if (tractor != nullptr) {
53
        qDebug()<<"Tractor!";
54
        m_subtitleFilter->set("internal_added", 237);
55
    }
56
    setup();
57 58 59 60 61 62 63
    QSize frameSize = pCore->getCurrentFrameDisplaySize();
    int fontSize = frameSize.height() / 15;
    int fontMargin = frameSize.height() - (2 *fontSize);
    scriptInfoSection = QString("[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").arg(frameSize.width()).arg(frameSize.height());
    styleSection = QString("[V4 Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, 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").arg(fontSize).arg(fontMargin);
    eventSection = QStringLiteral("[Events]\n");
    styleName = QStringLiteral("Default");
64 65 66
    connect(this, &SubtitleModel::modelChanged, [this]() {
        jsontoSubtitle(toJson()); 
    });
67
    
68 69 70 71 72 73 74 75 76 77
}

void SubtitleModel::setup()
{
    // We connect the signals of the abstractitemmodel to a more generic one.
    connect(this, &SubtitleModel::columnsMoved, this, &SubtitleModel::modelChanged);
    connect(this, &SubtitleModel::columnsRemoved, this, &SubtitleModel::modelChanged);
    connect(this, &SubtitleModel::columnsInserted, this, &SubtitleModel::modelChanged);
    connect(this, &SubtitleModel::rowsMoved, this, &SubtitleModel::modelChanged);
    connect(this, &SubtitleModel::modelReset, this, &SubtitleModel::modelChanged);
Sashmita Raghav's avatar
Sashmita Raghav committed
78 79
}

80
void SubtitleModel::importSubtitle(const QString filePath, int offset, bool externalImport)
81
{
Sashmita Raghav's avatar
Sashmita Raghav committed
82 83
    QString start,end,comment;
    QString timeLine;
84
    GenTime startPos, endPos;
Sashmita Raghav's avatar
Sashmita Raghav committed
85
    int turn = 0,r = 0;
Sashmita Raghav's avatar
Sashmita Raghav committed
86
    /*
Sashmita Raghav's avatar
Sashmita Raghav committed
87
     * turn = 0 -> Parse next subtitle line [srt] (or) Parse next section [ssa]
Sashmita Raghav's avatar
Sashmita Raghav committed
88 89 90
     * turn = 1 -> Add string to timeLine
     * turn > 1 -> Add string to completeLine
     */
91
    if (filePath.isEmpty() || isLocked())
Sashmita Raghav's avatar
Sashmita Raghav committed
92
        return;
93 94 95 96 97
    Fun redo = []() { return true; };
    Fun undo = [this]() {
        emit modelChanged();
        return true;
    };
98
    GenTime subtitleOffset(offset, pCore->getCurrentFps());
99
    if (filePath.endsWith(".srt")) {
Sashmita Raghav's avatar
Sashmita Raghav committed
100
        QFile srtFile(filePath);
101
        if (!srtFile.exists() || !srtFile.open(QIODevice::ReadOnly)) {
Sashmita Raghav's avatar
Sashmita Raghav committed
102 103 104
            qDebug() << " File not found " << filePath;
            return;
        }
Sashmita Raghav's avatar
Sashmita Raghav committed
105
        qDebug()<< "srt File";
Sashmita Raghav's avatar
Sashmita Raghav committed
106
        //parsing srt file
Sashmita Raghav's avatar
Sashmita Raghav committed
107 108 109 110
        QTextStream stream(&srtFile);
        QString line;
        while (stream.readLineInto(&line)) {
            line = line.simplified();
111
            if (!line.isEmpty()) {
Sashmita Raghav's avatar
Sashmita Raghav committed
112
                if (!turn) {
Sashmita Raghav's avatar
Sashmita Raghav committed
113
                    // index=atoi(line.toStdString().c_str());
Sashmita Raghav's avatar
Sashmita Raghav committed
114 115 116
                    turn++;
                    continue;
                }
117
                if (line.contains(QLatin1String("-->"))) {
Sashmita Raghav's avatar
Sashmita Raghav committed
118
                    timeLine += line;
119
                    QStringList srtTime = timeLine.split(QLatin1Char(' '));
120 121 122 123
                    if (srtTime.count() < 3) {
                        // invalid time
                        continue;
                    }
124
                    start = srtTime.at(0);
Sashmita Raghav's avatar
Sashmita Raghav committed
125
                    startPos= stringtoTime(start);
126
                    end = srtTime.at(2);
127
                    endPos = stringtoTime(end);
Sashmita Raghav's avatar
Sashmita Raghav committed
128 129
                } else {
                    r++;
130
                    if (!comment.isEmpty())
Sashmita Raghav's avatar
Sashmita Raghav committed
131 132 133 134 135 136 137 138
                        comment += " ";
                    if (r == 1)
                        comment += line;
                    else
                        comment = comment + "\r" +line;
                }
                turn++;
            } else {
139 140 141
                if (endPos > startPos) {
                    addSubtitle(startPos + subtitleOffset, endPos + subtitleOffset, comment, undo, redo, false);
                }
Sashmita Raghav's avatar
Sashmita Raghav committed
142
                //reinitialize
143 144
                comment.clear();
                timeLine.clear();
Sashmita Raghav's avatar
Sashmita Raghav committed
145
                turn = 0; r = 0;
Sashmita Raghav's avatar
Sashmita Raghav committed
146
            }            
Sashmita Raghav's avatar
Sashmita Raghav committed
147 148
        }  
        srtFile.close();
149
    } else if (filePath.endsWith(QLatin1String(".ass"))) {
Sashmita Raghav's avatar
Sashmita Raghav committed
150 151 152 153 154 155 156 157 158 159 160 161 162
        qDebug()<< "ass File";
        QString startTime,endTime,text;
        QString EventFormat, section;
        turn = 0;
        int maxSplit =0;
        QFile assFile(filePath);
        if (!assFile.exists() || !assFile.open(QIODevice::ReadOnly)) {
            qDebug() << " Failed attempt on opening " << filePath;
            return;
        }
        QTextStream stream(&assFile);
        QString line;
        qDebug() << " correct ass file  " << filePath;
163 164 165
        scriptInfoSection.clear();
        styleSection.clear();
        eventSection.clear();
Sashmita Raghav's avatar
Sashmita Raghav committed
166 167
        while (stream.readLineInto(&line)) {
            line = line.simplified();
168
            if (!line.isEmpty()) {
Sashmita Raghav's avatar
Sashmita Raghav committed
169
                if (!turn) {
Sashmita Raghav's avatar
Sashmita Raghav committed
170
                    //qDebug() << " turn = 0  " << line;
Sashmita Raghav's avatar
Sashmita Raghav committed
171
                    //check if it is script info, event,or v4+ style
172 173
                    QString linespace = line;
                    if (linespace.replace(" ","").contains("ScriptInfo")) {
Sashmita Raghav's avatar
Sashmita Raghav committed
174
                        //qDebug()<< "Script Info";
Sashmita Raghav's avatar
Sashmita Raghav committed
175
                        section = "Script Info";
176
                        scriptInfoSection += line+"\n";
Sashmita Raghav's avatar
Sashmita Raghav committed
177 178 179 180
                        turn++;
                        //qDebug()<< "turn" << turn;
                        continue;
                    } else if (line.contains("Styles")) {
Sashmita Raghav's avatar
Sashmita Raghav committed
181
                        //qDebug()<< "V4 Styles";
Sashmita Raghav's avatar
Sashmita Raghav committed
182
                        section = "V4 Styles";
183
                        styleSection += line + "\n";
Sashmita Raghav's avatar
Sashmita Raghav committed
184 185 186
                        turn++;
                        //qDebug()<< "turn" << turn;
                        continue;
187
                    } else if (line.contains("Events")) {
Sashmita Raghav's avatar
Sashmita Raghav committed
188 189
                        turn++;
                        section = "Events";
190
                        eventSection += line +"\n";
Sashmita Raghav's avatar
Sashmita Raghav committed
191 192
                        //qDebug()<< "turn" << turn;
                        continue;
193 194 195
                    } else {
                        //unknown section
                        
Sashmita Raghav's avatar
Sashmita Raghav committed
196 197
                    }
                }
Sashmita Raghav's avatar
Sashmita Raghav committed
198
                if (section.contains("Script Info")) {
199 200
                    scriptInfoSection += line + "\n";
                }
Sashmita Raghav's avatar
Sashmita Raghav committed
201
                if (section.contains("V4 Styles")) {
202 203 204 205
                    QStringList styleFormat;
                    styleSection +=line + "\n";
                    //Style: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,Encoding
                    styleFormat = (line.split(": ")[1].replace(" ","")).split(',');
206 207 208
                    if (!styleFormat.isEmpty()) {
                        styleName = styleFormat.first();
                    }
209 210

                }
Sashmita Raghav's avatar
Sashmita Raghav committed
211
                //qDebug() << "\n turn != 0  " << turn<< line;
Sashmita Raghav's avatar
Sashmita Raghav committed
212 213 214 215
                if (section.contains("Events")) {
                    //if it is event
                    QStringList format;
                    if (line.contains("Format:")) {
216
                    	eventSection += line +"\n";
Sashmita Raghav's avatar
Sashmita Raghav committed
217 218 219 220 221
                        EventFormat += line;
                        format = (EventFormat.split(": ")[1].replace(" ","")).split(',');
                        //qDebug() << format << format.count();
                        maxSplit = format.count();
                        //TIME
222 223 224 225
                        if (maxSplit > 2)
                            startTime = format.at(1);
                        if (maxSplit > 3)
                            endTime = format.at(2);
Sashmita Raghav's avatar
Sashmita Raghav committed
226
                        // Text
227 228
                        if (maxSplit > 9)
                            text = format.at(9);
Sashmita Raghav's avatar
Sashmita Raghav committed
229 230 231 232
                        //qDebug()<< startTime << endTime << text;
                    } else {
                        QString EventDialogue;
                        QStringList dialogue;
233
                        start = "";end = "";comment = "";
Sashmita Raghav's avatar
Sashmita Raghav committed
234 235
                        EventDialogue += line;
                        dialogue = EventDialogue.split(": ")[1].split(',');
236 237 238 239 240 241 242 243 244 245 246 247 248
                        if (dialogue.count() > 9) {
                            QString remainingStr = "," + EventDialogue.split(": ")[1].section(',', maxSplit);
                            //qDebug()<< dialogue;
                            //TIME
                            start = dialogue.at(1);
                            startPos= stringtoTime(start);
                            end = dialogue.at(2);
                            endPos= stringtoTime(end);
                            // Text
                            comment = dialogue.at(9)+ remainingStr;
                            //qDebug()<<"Start: "<< start << "End: "<<end << comment;
                            addSubtitle(startPos + subtitleOffset, endPos + subtitleOffset, comment, undo, redo, false);
                        }
Sashmita Raghav's avatar
Sashmita Raghav committed
249 250 251 252 253 254
                    }
                }
                turn++;
            } else {
                turn = 0;
                text = startTime = endTime = "";
Sashmita Raghav's avatar
Sashmita Raghav committed
255
            }
Sashmita Raghav's avatar
Sashmita Raghav committed
256
        }
Sashmita Raghav's avatar
Sashmita Raghav committed
257
        assFile.close();
Sashmita Raghav's avatar
Sashmita Raghav committed
258
    }
259 260 261 262 263 264
    Fun update_model= [this]() {
        emit modelChanged();
        return true;
    };
    PUSH_LAMBDA(update_model, redo);
    redo();
265
    if (externalImport) {
266
        pCore->pushUndo(undo, redo, i18n("Edit subtitle"));
267
    }
268 269 270 271 272 273 274 275 276 277
}

void SubtitleModel::parseSubtitle(const QString subPath)
{   
	qDebug()<<"Parsing started";
    if (!subPath.isEmpty()) {
        m_subtitleFilter->set("av.filename", subPath.toUtf8().constData());
    }
    QString filePath = m_subtitleFilter->get("av.filename");
    m_subFilePath = filePath;
278
    importSubtitle(filePath, 0, false);
279
    //jsontoSubtitle(toJson());
Sashmita Raghav's avatar
Sashmita Raghav committed
280 281
}

282 283 284 285 286
const QString SubtitleModel::getUrl()
{
    return m_subtitleFilter->get("av.filename");
}

Sashmita Raghav's avatar
Sashmita Raghav committed
287
GenTime SubtitleModel::stringtoTime(QString &str)
Sashmita Raghav's avatar
Sashmita Raghav committed
288 289
{
    QStringList total,secs;
290 291
    double hours = 0, mins = 0, seconds = 0, ms = 0;
    double total_sec = 0;
Sashmita Raghav's avatar
Sashmita Raghav committed
292
    GenTime pos;
293 294 295 296 297 298 299 300 301
    total = str.split(QLatin1Char(':'));
    if (total.count() != 3) {
        // invalid time found
        return GenTime();
    }
    hours = atoi(total.at(0).toStdString().c_str());
    mins = atoi(total.at(1).toStdString().c_str());
    if (total.at(2).contains(QLatin1Char('.')))
        secs = total.at(2).split(QLatin1Char('.')); //ssa file
Sashmita Raghav's avatar
Sashmita Raghav committed
302
    else
303 304 305 306 307 308 309
        secs = total.at(2).split(QLatin1Char(',')); //srt file
    if (secs.count() < 2) {
        seconds = atoi(total.at(2).toStdString().c_str());
    } else {
        seconds = atoi(secs.at(0).toStdString().c_str());
        ms = atoi(secs.at(1).toStdString().c_str());
    }
310
    total_sec = hours *3600 + mins *60 + seconds + ms * 0.001 ;
Sashmita Raghav's avatar
Sashmita Raghav committed
311
    pos= GenTime(total_sec);
Sashmita Raghav's avatar
Sashmita Raghav committed
312
    return pos;
313 314
}

315 316
bool SubtitleModel::addSubtitle(GenTime start, GenTime end, const QString str, Fun &undo, Fun &redo, bool updateFilter)
{
317 318 319
    if (isLocked()) {
        return false;
    }
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
    int id = TimelineModel::getNextId();
    Fun local_redo = [this, id, start, end, str, updateFilter]() {
        addSubtitle(id, start, end, str, false, updateFilter);
        pCore->refreshProjectRange({start.frames(pCore->getCurrentFps()), end.frames(pCore->getCurrentFps())});
        return true;
    };
    Fun local_undo = [this, id, start, end, updateFilter]() {
        removeSubtitle(id, false, updateFilter);
        pCore->refreshProjectRange({start.frames(pCore->getCurrentFps()), end.frames(pCore->getCurrentFps())});
        return true;
    };
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
}

bool SubtitleModel::addSubtitle(int id, GenTime start, GenTime end, const QString str, bool temporary, bool updateFilter)
336
{
337
	if (start.frames(pCore->getCurrentFps()) < 0 || end.frames(pCore->getCurrentFps()) < 0 || isLocked()) {
338
        qDebug()<<"Time error: is negative";
339
        return false;
340 341 342
    }
    if (start.frames(pCore->getCurrentFps()) > end.frames(pCore->getCurrentFps())) {
        qDebug()<<"Time error: start should be less than end";
343
        return false;
344
    }
345 346
    // Don't allow 2 subtitles at same start pos
    if (m_subtitleList.count(start) > 0) {
Sashmita Raghav's avatar
Sashmita Raghav committed
347
        qDebug()<<"already present in model"<<"string :"<<m_subtitleList[start].first<<" start time "<<start.frames(pCore->getCurrentFps())<<"end time : "<< m_subtitleList[start].second.frames(pCore->getCurrentFps());
348
        return false;
Sashmita Raghav's avatar
Sashmita Raghav committed
349
    }
350 351
    int row = m_timeline->m_allSubtitles.size();
    beginInsertRows(QModelIndex(), row, row);
352 353 354 355 356
    m_subtitleList[start] = {str, end};
    m_timeline->registerSubtitle(id, start, temporary);
    endInsertRows();
    addSnapPoint(start);
    addSnapPoint(end);
357 358 359
    if (!temporary && end.frames(pCore->getCurrentFps()) > m_timeline->duration()) {
        m_timeline->updateDuration();
    }
Sashmita Raghav's avatar
Sashmita Raghav committed
360
    qDebug()<<"Added to model";
361 362 363
    if (updateFilter) {
        emit modelChanged();
    }
364
    return true;
Sashmita Raghav's avatar
Sashmita Raghav committed
365 366 367 368 369 370 371 372 373 374
}

QHash<int, QByteArray> SubtitleModel::roleNames() const 
{
    QHash<int, QByteArray> roles;
    roles[SubtitleRole] = "subtitle";
    roles[StartPosRole] = "startposition";
    roles[EndPosRole] = "endposition";
    roles[StartFrameRole] = "startframe";
    roles[EndFrameRole] = "endframe";
375 376
    roles[IdRole] = "id";
    roles[SelectedRole] = "selected";
Sashmita Raghav's avatar
Sashmita Raghav committed
377 378 379 380 381 382 383 384
    return roles;
}

QVariant SubtitleModel::data(const QModelIndex& index, int role) const
{   
    if (index.row() < 0 || index.row() >= static_cast<int>(m_subtitleList.size()) || !index.isValid()) {
        return QVariant();
    }
385
    auto subInfo = m_timeline->getSubtitleIdFromIndex(index.row());
Sashmita Raghav's avatar
Sashmita Raghav committed
386
    switch (role) {
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
        case Qt::DisplayRole:
        case Qt::EditRole:
        case SubtitleRole:
            return m_subtitleList.at(subInfo.second).first;
        case IdRole:
            return subInfo.first;
        case StartPosRole:
            return subInfo.second.seconds();
        case EndPosRole:
            return m_subtitleList.at(subInfo.second).second.seconds();
        case StartFrameRole:
            return subInfo.second.frames(pCore->getCurrentFps());
        case EndFrameRole:
            return m_subtitleList.at(subInfo.second).second.frames(pCore->getCurrentFps());
        case SelectedRole:
            return m_selected.contains(subInfo.first);
Sashmita Raghav's avatar
Sashmita Raghav committed
403 404
    }
    return QVariant();
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
}

int SubtitleModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    return static_cast<int>(m_subtitleList.size());
}

QList<SubtitledTime> SubtitleModel::getAllSubtitles() const
{
    QList<SubtitledTime> subtitle;
    for (const auto &subtitles : m_subtitleList) {
        SubtitledTime s(subtitles.first, subtitles.second.first, subtitles.second.second);
        subtitle << s;
    }
    return subtitle;
422 423
}

424 425 426 427 428 429 430 431 432 433
SubtitledTime SubtitleModel::getSubtitle(GenTime startFrame) const
{
    for (const auto &subtitles : m_subtitleList) {
        if (subtitles.first == startFrame) {
            return SubtitledTime(subtitles.first, subtitles.second.first, subtitles.second.second);
        }
    }
    return SubtitledTime(GenTime(), QString(), GenTime());
}

434 435 436 437 438 439 440 441 442 443 444
QString SubtitleModel::getText(int id) const
{
    if (m_timeline->m_allSubtitles.find( id ) == m_timeline->m_allSubtitles.end()) {
        return QString();
    }
    GenTime start = m_timeline->m_allSubtitles.at(id);
    return m_subtitleList.at(start).first;
}

bool SubtitleModel::setText(int id, const QString text)
{
445
    if (m_timeline->m_allSubtitles.find( id ) == m_timeline->m_allSubtitles.end() || isLocked()) {
446 447 448 449 450 451 452
        return false;
    }
    GenTime start = m_timeline->m_allSubtitles.at(id);
    GenTime end = m_subtitleList.at(start).second;
    QString oldText = m_subtitleList.at(start).first;
    m_subtitleList[start].first = text;
    Fun local_redo = [this, start, end, text]() {
453
        editSubtitle(start, text);
454 455 456 457
        pCore->refreshProjectRange({start.frames(pCore->getCurrentFps()), end.frames(pCore->getCurrentFps())});
        return true;
    };
    Fun local_undo = [this, start, end, oldText]() {
458
        editSubtitle(start, oldText);
459 460 461 462 463 464 465 466
        pCore->refreshProjectRange({start.frames(pCore->getCurrentFps()), end.frames(pCore->getCurrentFps())});
        return true;
    };
    local_redo();
    pCore->pushUndo(local_undo, local_redo, i18n("Edit subtitle"));
    return true;
}

467 468
std::unordered_set<int> SubtitleModel::getItemsInRange(int startFrame, int endFrame) const
{
469 470 471
    if (isLocked()) {
        return {};
    }
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498
    GenTime startTime(startFrame, pCore->getCurrentFps());
    GenTime endTime(endFrame, pCore->getCurrentFps());
    std::unordered_set<int> matching;
    for (const auto &subtitles : m_subtitleList) {
        if (endFrame > -1 && subtitles.first > endTime) {
            // Outside range
            continue;
        }
        if (subtitles.first >= startTime || subtitles.second.second >= startTime) {
            matching.emplace(getIdForStartPos(subtitles.first));
        }
    }
    return matching;
}

void SubtitleModel::cutSubtitle(int position)
{
    Fun redo = []() { return true; };
    Fun undo = []() { return true; };
    if (cutSubtitle(position, undo, redo)) {
        pCore->pushUndo(undo, redo, i18n("Cut clip"));
    }
}


bool SubtitleModel::cutSubtitle(int position, Fun &undo, Fun &redo)
{
499 500 501
    if (isLocked()) {
        return false;
    }
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
    GenTime pos(position, pCore->getCurrentFps());
    GenTime start = GenTime(-1);
    for (const auto &subtitles : m_subtitleList) {
        if (subtitles.first <= pos && subtitles.second.second > pos) {
            start = subtitles.first;
            break;
        }
    }
    if (start >= GenTime()) {
        GenTime end = m_subtitleList.at(start).second;
        QString text = m_subtitleList.at(start).first;
        
        int subId = getIdForStartPos(start);
        int duration = position - start.frames(pCore->getCurrentFps());
        bool res = requestResize(subId, duration, true, undo, redo, false);
        if (res) {
            int id = TimelineModel::getNextId();
            Fun local_redo = [this, id, pos, end, text]() {
520
                return addSubtitle(id, pos, end, text);
521 522 523 524 525
            };
            Fun local_undo = [this, id]() {
                removeSubtitle(id);
                return true;
            };
526 527 528 529
            if (local_redo()) {
                UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
                return true;
            }
530 531
        }
    }
532
    undo();
533 534 535
    return false;
}

536 537 538 539 540 541
void SubtitleModel::registerSnap(const std::weak_ptr<SnapInterface> &snapModel)
{
    // make sure ptr is valid
    if (auto ptr = snapModel.lock()) {
        // ptr is valid, we store it
        m_regSnaps.push_back(snapModel);
Sashmita Raghav's avatar
Sashmita Raghav committed
542
        // we now add the already existing subtitles to the snap
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562
        for (const auto &subtitle : m_subtitleList) {
            ptr->addPoint(subtitle.first.frames(pCore->getCurrentFps()));
        }
    } else {
        qDebug() << "Error: added snapmodel for subtitle is null";
        Q_ASSERT(false);
    }
}

void SubtitleModel::addSnapPoint(GenTime startpos)
{
    std::vector<std::weak_ptr<SnapInterface>> validSnapModels;
    for (const auto &snapModel : m_regSnaps) {
        if (auto ptr = snapModel.lock()) {
            validSnapModels.push_back(snapModel);
            ptr->addPoint(startpos.frames(pCore->getCurrentFps()));
        }
    }
    // Update the list of snapModel known to be valid
    std::swap(m_regSnaps, validSnapModels);
Sashmita Raghav's avatar
Sashmita Raghav committed
563
}
564

565 566 567 568 569 570 571 572 573 574 575 576 577
void SubtitleModel::removeSnapPoint(GenTime startpos)
{
    std::vector<std::weak_ptr<SnapInterface>> validSnapModels;
    for (const auto &snapModel : m_regSnaps) {
        if (auto ptr = snapModel.lock()) {
            validSnapModels.push_back(snapModel);
            ptr->removePoint(startpos.frames(pCore->getCurrentFps()));
        }
    }
    // Update the list of snapModel known to be valid
    std::swap(m_regSnaps, validSnapModels);
}

578
void SubtitleModel::editEndPos(GenTime startPos, GenTime newEndPos, bool refreshModel)
579
{
580
    qDebug()<<"Changing the sub end timings in model";
581
    if (m_subtitleList.count(startPos) <= 0) {
582 583 584
        //is not present in model only
        return;
    }
585
    m_subtitleList[startPos].second = newEndPos;
586
    // Trigger update of the qml view
587 588
    int id = getIdForStartPos(startPos);
    int row = m_timeline->getSubtitleIndex(id);
589
    emit dataChanged(index(row), index(row), {EndFrameRole});
590 591 592
    if (refreshModel) {
        emit modelChanged();
    }
593
    qDebug()<<startPos.frames(pCore->getCurrentFps())<<m_subtitleList[startPos].second.frames(pCore->getCurrentFps());
594
}
595

596 597 598 599 600 601 602 603 604 605 606 607 608 609
bool SubtitleModel::requestResize(int id, int size, bool right)
{
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
    bool res = requestResize(id, size, right, undo, redo, true);
    if (res) {
        pCore->pushUndo(undo, redo, i18n("Resize subtitle"));
        return true;
    } else {
        undo();
        return false;
    }
}

610 611
bool SubtitleModel::requestResize(int id, int size, bool right, Fun &undo, Fun &redo, bool logUndo)
{
612 613 614
    if (isLocked()) {
        return false;
    }
615 616 617 618 619 620 621
    Q_ASSERT(m_timeline->m_allSubtitles.find( id ) != m_timeline->m_allSubtitles.end());
    GenTime startPos = m_timeline->m_allSubtitles.at(id);
    GenTime endPos = m_subtitleList.at(startPos).second;
    Fun operation = []() { return true; };
    Fun reverse = []() { return true; };
    if (right) {
        GenTime newEndPos = startPos + GenTime(size, pCore->getCurrentFps());
622
        operation = [this, id, startPos, endPos, newEndPos, logUndo]() {
623 624 625 626
            m_subtitleList[startPos].second = newEndPos;
            removeSnapPoint(endPos);
            addSnapPoint(newEndPos);
            // Trigger update of the qml view
627
            int row = m_timeline->getSubtitleIndex(id);
628 629 630
            emit dataChanged(index(row), index(row), {EndFrameRole});
            if (logUndo) {
                emit modelChanged();
631 632 633 634 635
                if (endPos > newEndPos) {
                    pCore->refreshProjectRange({newEndPos.frames(pCore->getCurrentFps()), endPos.frames(pCore->getCurrentFps())});
                } else {
                    pCore->refreshProjectRange({endPos.frames(pCore->getCurrentFps()), newEndPos.frames(pCore->getCurrentFps())});
                }
636 637 638
            }
            return true;
        };
639
        reverse = [this, id, startPos, endPos, newEndPos, logUndo]() {
640 641 642 643
            m_subtitleList[startPos].second = endPos;
            removeSnapPoint(newEndPos);
            addSnapPoint(endPos);
            // Trigger update of the qml view
644
            int row = m_timeline->getSubtitleIndex(id);
645 646 647
            emit dataChanged(index(row), index(row), {EndFrameRole});
            if (logUndo) {
                emit modelChanged();
648 649 650 651 652
                if (endPos > newEndPos) {
                    pCore->refreshProjectRange({newEndPos.frames(pCore->getCurrentFps()), endPos.frames(pCore->getCurrentFps())});
                } else {
                    pCore->refreshProjectRange({endPos.frames(pCore->getCurrentFps()), newEndPos.frames(pCore->getCurrentFps())});
                }
653 654 655 656 657
            }
            return true;
        };
    } else {
        GenTime newStartPos = endPos - GenTime(size, pCore->getCurrentFps());
658 659 660 661
        if (m_subtitleList.count(newStartPos) > 0) {
            // There already is another subtitle at this position, abort
            return false;
        }
662 663 664 665 666 667 668 669
        const QString text = m_subtitleList.at(startPos).first;
        operation = [this, id, startPos, newStartPos, endPos, text, logUndo]() {
            m_timeline->m_allSubtitles[id] = newStartPos;
            m_subtitleList.erase(startPos);
            m_subtitleList[newStartPos] = {text, endPos};
            // Trigger update of the qml view
            removeSnapPoint(startPos);
            addSnapPoint(newStartPos);
670
            int row = m_timeline->getSubtitleIndex(id);
671 672 673
            emit dataChanged(index(row), index(row), {StartFrameRole});
            if (logUndo) {
                emit modelChanged();
674 675 676 677 678
                if (startPos > newStartPos) {
                    pCore->refreshProjectRange({newStartPos.frames(pCore->getCurrentFps()), startPos.frames(pCore->getCurrentFps())});
                } else {
                    pCore->refreshProjectRange({startPos.frames(pCore->getCurrentFps()), newStartPos.frames(pCore->getCurrentFps())});
                }
679 680 681 682 683 684 685 686 687 688
            }
            return true;
        };
        reverse = [this, id, startPos, newStartPos, endPos, text, logUndo]() {
            m_timeline->m_allSubtitles[id] = startPos;
            m_subtitleList.erase(newStartPos);
            m_subtitleList[startPos] = {text, endPos};
            removeSnapPoint(newStartPos);
            addSnapPoint(startPos);
            // Trigger update of the qml view
689
            int row = m_timeline->getSubtitleIndex(id);
690 691 692
            emit dataChanged(index(row), index(row), {StartFrameRole});
            if (logUndo) {
                emit modelChanged();
693 694 695 696 697
                if (startPos > newStartPos) {
                    pCore->refreshProjectRange({newStartPos.frames(pCore->getCurrentFps()), startPos.frames(pCore->getCurrentFps())});
                } else {
                    pCore->refreshProjectRange({startPos.frames(pCore->getCurrentFps()), newStartPos.frames(pCore->getCurrentFps())});
                }
698 699 700 701 702 703 704 705 706
            }
            return true;
        };
    }
    operation();
    UPDATE_UNDO_REDO(operation, reverse, undo, redo);
    return true;
}

707
void SubtitleModel::editSubtitle(GenTime startPos, QString newSubtitleText)
708
{
709 710 711
    if (isLocked()) {
        return;
    }
712
    if(startPos.frames(pCore->getCurrentFps()) < 0) {
713 714 715
        qDebug()<<"Time error: is negative";
        return;
    }
716
    qDebug()<<"Editing existing subtitle in model";
717
    m_subtitleList[startPos].first = newSubtitleText ;
718
    int id = getIdForStartPos(startPos);
719
    qDebug()<<startPos.frames(pCore->getCurrentFps())<<m_subtitleList[startPos].first<<m_subtitleList[startPos].second.frames(pCore->getCurrentFps());
720
    int row = m_timeline->getSubtitleIndex(id);
721
    emit dataChanged(index(row), index(row), QVector<int>() << SubtitleRole);
722
    emit modelChanged();
723 724
    return;
}
725

726
bool SubtitleModel::removeSubtitle(int id, bool temporary, bool updateFilter)
727 728
{
    qDebug()<<"Deleting subtitle in model";
729 730 731
    if (isLocked()) {
        return false;
    }
732
    if (m_timeline->m_allSubtitles.find( id ) == m_timeline->m_allSubtitles.end()) {
733
        qDebug()<<"No Subtitle at pos in model";
734 735 736 737 738 739
        return false;
    }
    GenTime start = m_timeline->m_allSubtitles.at(id);
    if (m_subtitleList.count(start) <= 0) {
        qDebug()<<"No Subtitle at pos in model";
        return false;
740
    }
741
    GenTime end = m_subtitleList.at(start).second;
742
    int row = m_timeline->getSubtitleIndex(id);
743 744
    m_timeline->deregisterSubtitle(id, temporary);
    beginRemoveRows(QModelIndex(), row, row);
745 746 747 748 749
    bool lastSub = false;
    if (start == m_subtitleList.rbegin()->first) {
        // Check if this is the last subtitle
        lastSub = true;
    }
750 751 752 753
    m_subtitleList.erase(start);
    endRemoveRows();
    removeSnapPoint(start);
    removeSnapPoint(end);
754 755 756
    if (lastSub) {
        m_timeline->updateDuration();
    }
757 758 759
    if (updateFilter) {
        emit modelChanged();
    }
760
    return true;
761
}
762

763 764
void SubtitleModel::removeAllSubtitles()
{
765 766 767
    if (isLocked()) {
        return;
    }
768 769 770
    auto ids = m_timeline->m_allSubtitles;
    for (const auto &p : ids) {
        removeSubtitle(p.first);
771 772 773
    }
}

774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
void SubtitleModel::requestSubtitleMove(int clipId, GenTime position)
{
    
    GenTime oldPos = getStartPosForId(clipId);
    Fun local_redo = [this, clipId, position]() {
        return moveSubtitle(clipId, position, true, true);
    };
    Fun local_undo = [this, clipId, oldPos]() {
        return moveSubtitle(clipId, oldPos, true, true);
    };
    bool res = local_redo();
    if (res) {
        pCore->pushUndo(local_undo, local_redo, i18n("Move subtitle"));
    }
}

790
bool SubtitleModel::moveSubtitle(int subId, GenTime newPos, bool updateModel, bool updateView)
791 792
{
    qDebug()<<"Moving Subtitle";
793
    if (m_timeline->m_allSubtitles.count(subId) == 0 || isLocked()) {
794 795 796 797 798 799 800
        return false;
    }
    GenTime oldPos = m_timeline->m_allSubtitles.at(subId);
    if (m_subtitleList.count(oldPos) <= 0 || m_subtitleList.count(newPos) > 0) {
        //is not present in model, or already another one at new position
        qDebug()<<"==== MOVE FAILED";
        return false;
801
    }
802 803 804
    QString subtitleText = m_subtitleList[oldPos].first ;
    removeSnapPoint(oldPos);
    removeSnapPoint(m_subtitleList[oldPos].second);
805 806
    GenTime duration = m_subtitleList[oldPos].second - oldPos;
    GenTime endPos = newPos + duration;
807 808 809 810 811 812 813 814
    int id = getIdForStartPos(oldPos);
    m_timeline->m_allSubtitles[id] = newPos;
    m_subtitleList.erase(oldPos);
    m_subtitleList[newPos] = {subtitleText, endPos};
    addSnapPoint(newPos);
    addSnapPoint(endPos);
    if (updateView) {
        updateSub(id, {StartFrameRole, EndFrameRole});
815 816 817 818 819
        if (oldPos < newPos) {
            pCore->refreshProjectRange({oldPos.frames(pCore->getCurrentFps()), endPos.frames(pCore->getCurrentFps())});
        } else {
            pCore->refreshProjectRange({newPos.frames(pCore->getCurrentFps()), (oldPos + duration).frames(pCore->getCurrentFps())});
        }
820 821 822 823
    }
    if (updateModel) {
        // Trigger update of the subtitle file
        emit modelChanged();
824 825 826 827
        if (newPos == m_subtitleList.rbegin()->first) {
            // Check if this is the last subtitle
            m_timeline->updateDuration();
        }
828
    }
829
    return true;
830 831 832 833 834 835 836 837 838 839 840
}

int SubtitleModel::getIdForStartPos(GenTime startTime) const
{
    auto findResult = std::find_if(std::begin(m_timeline->m_allSubtitles), std::end(m_timeline->m_allSubtitles), [&](const std::pair<int, GenTime> &pair) {
        return pair.second == startTime;
    });
    if (findResult != std::end(m_timeline->m_allSubtitles)) {
        return findResult->first;
    }
    return -1;
841
}
842

843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878
GenTime SubtitleModel::getStartPosForId(int id) const
{
    if (m_timeline->m_allSubtitles.count(id) == 0) {
        return GenTime();
    };
    return m_timeline->m_allSubtitles.at(id);
}

int SubtitleModel::getPreviousSub(int id) const
{
    GenTime start = getStartPosForId(id);
    int row = static_cast<int>(std::distance(m_subtitleList.begin(), m_subtitleList.find(start)));
    if (row > 0) {
        row--;
        auto it = m_subtitleList.begin();
        std::advance(it, row);
        const GenTime res = it->first;
        return getIdForStartPos(res);
    }
    return -1;
}

int SubtitleModel::getNextSub(int id) const
{
    GenTime start = getStartPosForId(id);
    int row = static_cast<int>(std::distance(m_subtitleList.begin(), m_subtitleList.find(start)));
    if (row < static_cast<int>(m_subtitleList.size()) - 1) {
        row++;
        auto it = m_subtitleList.begin();
        std::advance(it, row);
        const GenTime res = it->first;
        return getIdForStartPos(res);
    }
    return -1;
}

879 880
QString SubtitleModel::toJson()
{
881
    //qDebug()<< "to JSON";
882 883 884 885 886 887 888
    QJsonArray list;
    for (const auto &subtitle : m_subtitleList) {
        QJsonObject currentSubtitle;
        currentSubtitle.insert(QLatin1String("startPos"), QJsonValue(subtitle.first.seconds()));
        currentSubtitle.insert(QLatin1String("dialogue"), QJsonValue(subtitle.second.first));
        currentSubtitle.insert(QLatin1String("endPos"), QJsonValue(subtitle.second.second.seconds()));
        list.push_back(currentSubtitle);
889
        //qDebug()<<subtitle.first.seconds();
890 891
    }
    QJsonDocument jsonDoc(list);
892
    //qDebug()<<QString(jsonDoc.toJson());
893
    return QString(jsonDoc.toJson());
894 895
}

896
void SubtitleModel::copySubtitle(const QString &path, bool checkOverwrite)
897
{
898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919
    QFile srcFile(pCore->currentDoc()->subTitlePath(false));
    if (srcFile.exists()) {
        QFile prev(path);
        if (prev.exists()) {
            if (checkOverwrite || !path.endsWith(QStringLiteral(".srt"))) {
                if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("File %1 already exists.\nDo you want to overwrite it?", path)) == KMessageBox::No) {
                    return;
                }
            }
            prev.remove();
        }
        srcFile.copy(path);
    }
}


void SubtitleModel::jsontoSubtitle(const QString &data)
{
    QString outFile = pCore->currentDoc()->subTitlePath(false);
    QString masterFile = m_subtitleFilter->get("av.filename");
    if (masterFile.isEmpty()) {
        m_subtitleFilter->set("av.filename", outFile.toUtf8().constData());
920
    }
921 922
    bool assFormat = outFile.endsWith(".ass");
    if (!assFormat) {
923
        qDebug()<< "srt file import"; // if imported file isn't .ass, it is .srt format
924
    }
925 926
    QFile outF(outFile);

927
    //qDebug()<< "Import from JSON";
928
    QWriteLocker locker(&m_lock);
929 930 931 932 933
    auto json = QJsonDocument::fromJson(data.toUtf8());
    if (!json.isArray()) {
        qDebug() << "Error : Json file should be an array";
        return;
    }
934
    int line=0;
935
    auto list = json.array();
Sashmita Raghav's avatar
Sashmita Raghav committed
936
    if (outF.open(QIODevice::WriteOnly)) {
937
        QTextStream out(&outF);
938
        out.setCodec("UTF-8");
939
        if (assFormat) {
940 941 942 943
        	out<<scriptInfoSection<<endl;
        	out<<styleSection<<endl;
        	out<<eventSection;
        }
944 945 946 947 948 949 950 951 952 953 954
        for (const auto &entry : list) {
            if (!entry.isObject()) {
                qDebug() << "Warning : Skipping invalid subtitle data";
                continue;
            }
            auto entryObj = entry.toObject();
            if (!entryObj.contains(QLatin1String("startPos"))) {
                qDebug() << "Warning : Skipping invalid subtitle data (does not contain position)";
                continue;
            }
            double startPos = entryObj[QLatin1String("startPos")].toDouble();
955
            //convert seconds to FORMAT= hh:mm:ss.SS (in .ass) and hh:mm:ss,SSS (in .srt)
956 957 958 959 960 961 962 963 964 965 966 967 968
            int millisec = startPos * 1000;
            int seconds = millisec / 1000;
            millisec %=1000;
            int minutes = seconds / 60;
            seconds %= 60;
            int hours = minutes /60;
            minutes %= 60;
            int milli_2 = millisec / 10;
            QString startTimeString = QString("%1:%2:%3.%4")
              .arg(hours, 1, 10, QChar('0'))
              .arg(minutes, 2, 10, QChar('0'))
              .arg(seconds, 2, 10, QChar('0'))
              .arg(milli_2,2,10,QChar('0'));
969 970 971 972 973
            QString startTimeStringSRT = QString("%1:%2:%3,%4")
              .arg(hours, 1, 10, QChar('0'))
              .arg(minutes, 2, 10, QChar('0'))
              .arg(seconds, 2, 10, QChar('0'))
              .arg(millisec,3,10,QChar('0'));
974 975 976 977 978 979 980 981 982 983
            QString dialogue = entryObj[QLatin1String("dialogue")].toString();
            double endPos = entryObj[QLatin1String("endPos")].toDouble();
            millisec = endPos * 1000;
            seconds = millisec / 1000;
            millisec %=1000;
            minutes = seconds / 60;
            seconds %= 60;
            hours = minutes /60;
            minutes %= 60;

984
            milli_2 = millisec