subtitlemodel.cpp 41.9 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>
Johnny Jazeix's avatar
Johnny Jazeix committed
41
#include <QTextCodec>
42
#include <QApplication>
43

44
SubtitleModel::SubtitleModel(Mlt::Tractor *tractor, std::shared_ptr<TimelineItemModel> timeline, QObject *parent)
Sashmita Raghav's avatar
Sashmita Raghav committed
45
    : QAbstractListModel(parent)
46
    , m_timeline(timeline)
47
    , m_lock(QReadWriteLock::Recursive)
48
49
    , m_subtitleFilter(new Mlt::Filter(pCore->getCurrentProfile()->profile(), "avfilter.subtitles"))
    , m_tractor(tractor)
Sashmita Raghav's avatar
Sashmita Raghav committed
50
{
51
52
    qDebug()<< "subtitle constructor";
    qDebug()<<"Filter!";
Sashmita Raghav's avatar
Sashmita Raghav committed
53
    if (tractor != nullptr) {
54
        qDebug()<<"Tractor!";
55
        m_subtitleFilter->set("internal_added", 237);
56
    }
57
    setup();
58
59
60
61
62
63
64
    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");
65
66
67
    connect(this, &SubtitleModel::modelChanged, [this]() {
        jsontoSubtitle(toJson()); 
    });
68
    
69
70
71
72
73
74
75
76
77
78
}

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
79
80
}

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

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

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;
281
    importSubtitle(filePath, 0, false);
282
    //jsontoSubtitle(toJson());
Sashmita Raghav's avatar
Sashmita Raghav committed
283
284
}

285
286
287
288
289
const QString SubtitleModel::getUrl()
{
    return m_subtitleFilter->get("av.filename");
}

Sashmita Raghav's avatar
Sashmita Raghav committed
290
GenTime SubtitleModel::stringtoTime(QString &str)
Sashmita Raghav's avatar
Sashmita Raghav committed
291
292
{
    QStringList total,secs;
293
294
    double hours = 0, mins = 0, seconds = 0, ms = 0;
    double total_sec = 0;
Sashmita Raghav's avatar
Sashmita Raghav committed
295
    GenTime pos;
296
297
298
299
300
301
302
303
304
    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
305
    else
306
307
308
309
310
311
312
        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());
    }
313
    total_sec = hours *3600 + mins *60 + seconds + ms * 0.001 ;
Sashmita Raghav's avatar
Sashmita Raghav committed
314
    pos= GenTime(total_sec);
Sashmita Raghav's avatar
Sashmita Raghav committed
315
    return pos;
316
317
}

318
319
bool SubtitleModel::addSubtitle(GenTime start, GenTime end, const QString str, Fun &undo, Fun &redo, bool updateFilter)
{
320
321
322
    if (isLocked()) {
        return false;
    }
323
324
325
326
327
328
329
330
331
332
333
    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;
    };
334
    local_redo();
335
336
337
338
339
    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)
340
{
341
	if (start.frames(pCore->getCurrentFps()) < 0 || end.frames(pCore->getCurrentFps()) < 0 || isLocked()) {
342
        qDebug()<<"Time error: is negative";
343
        return false;
344
345
346
    }
    if (start.frames(pCore->getCurrentFps()) > end.frames(pCore->getCurrentFps())) {
        qDebug()<<"Time error: start should be less than end";
347
        return false;
348
    }
349
350
    // Don't allow 2 subtitles at same start pos
    if (m_subtitleList.count(start) > 0) {
Sashmita Raghav's avatar
Sashmita Raghav committed
351
        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());
352
        return false;
Sashmita Raghav's avatar
Sashmita Raghav committed
353
    }
354
355
    int row = m_timeline->m_allSubtitles.size();
    beginInsertRows(QModelIndex(), row, row);
356
357
358
359
360
    m_subtitleList[start] = {str, end};
    m_timeline->registerSubtitle(id, start, temporary);
    endInsertRows();
    addSnapPoint(start);
    addSnapPoint(end);
361
362
363
    if (!temporary && end.frames(pCore->getCurrentFps()) > m_timeline->duration()) {
        m_timeline->updateDuration();
    }
Sashmita Raghav's avatar
Sashmita Raghav committed
364
    qDebug()<<"Added to model";
365
366
367
    if (updateFilter) {
        emit modelChanged();
    }
368
    return true;
Sashmita Raghav's avatar
Sashmita Raghav committed
369
370
371
372
373
374
375
376
377
378
}

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";
379
380
    roles[IdRole] = "id";
    roles[SelectedRole] = "selected";
Sashmita Raghav's avatar
Sashmita Raghav committed
381
382
383
384
385
386
387
388
    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();
    }
389
    auto subInfo = m_timeline->getSubtitleIdFromIndex(index.row());
Sashmita Raghav's avatar
Sashmita Raghav committed
390
    switch (role) {
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
        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
407
408
    }
    return QVariant();
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
}

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;
426
427
}

428
429
430
431
432
433
434
435
436
437
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());
}

438
439
440
441
442
443
444
445
446
447
448
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)
{
449
    if (m_timeline->m_allSubtitles.find( id ) == m_timeline->m_allSubtitles.end() || isLocked()) {
450
451
452
453
454
455
456
        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]() {
457
        editSubtitle(start, text);
458
459
460
461
        pCore->refreshProjectRange({start.frames(pCore->getCurrentFps()), end.frames(pCore->getCurrentFps())});
        return true;
    };
    Fun local_undo = [this, start, end, oldText]() {
462
        editSubtitle(start, oldText);
463
464
465
466
467
468
469
470
        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;
}

471
472
std::unordered_set<int> SubtitleModel::getItemsInRange(int startFrame, int endFrame) const
{
473
474
475
    if (isLocked()) {
        return {};
    }
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
    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)
{
503
504
505
    if (isLocked()) {
        return false;
    }
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
    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]() {
524
                return addSubtitle(id, pos, end, text);
525
526
527
528
529
            };
            Fun local_undo = [this, id]() {
                removeSubtitle(id);
                return true;
            };
530
531
532
533
            if (local_redo()) {
                UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
                return true;
            }
534
535
        }
    }
536
    undo();
537
538
539
    return false;
}

540
541
542
543
544
545
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
546
        // we now add the already existing subtitles to the snap
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
        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
567
}
568

569
570
571
572
573
574
575
576
577
578
579
580
581
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);
}

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

600
601
602
603
604
605
606
607
608
609
610
611
612
613
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;
    }
}

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

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

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

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

778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
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"));
    }
}

794
bool SubtitleModel::moveSubtitle(int subId, GenTime newPos, bool updateModel, bool updateView)
795
796
{
    qDebug()<<"Moving Subtitle";
797
    if (m_timeline->m_allSubtitles.count(subId) == 0 || isLocked()) {
798
799
800
801
802
803
804
        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;
805
    }
806
807
808
    QString subtitleText = m_subtitleList[oldPos].first ;
    removeSnapPoint(oldPos);
    removeSnapPoint(m_subtitleList[oldPos].second);
809
810
    GenTime duration = m_subtitleList[oldPos].second - oldPos;
    GenTime endPos = newPos + duration;
811
812
813
814
815
816
817
818
    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});
819
820
821
822
823
        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())});
        }
824
825
826
827
    }
    if (updateModel) {
        // Trigger update of the subtitle file
        emit modelChanged();
828
829
830
831
        if (newPos == m_subtitleList.rbegin()->first) {
            // Check if this is the last subtitle
            m_timeline->updateDuration();
        }
832
    }
833
    return true;
834
835
836
837
838
839
840
841
842
843
844
}

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;
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
879
880
881
882
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;
}

883
884
QString SubtitleModel::toJson()
{
885
    //qDebug()<< "to JSON";
886
887
888
889
890
891
892
    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);
893
        //qDebug()<<subtitle.first.seconds();
894
895
    }
    QJsonDocument jsonDoc(list);
896
    //qDebug()<<QString(jsonDoc.toJson());
897
    return QString(jsonDoc.toJson());
898
899
}

900
void SubtitleModel::copySubtitle(const QString &path, bool checkOverwrite)
901
{
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
    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());
924
    }
925
926
    bool assFormat = outFile.endsWith(".ass");
    if (!assFormat) {
927
        qDebug()<< "srt file import"; // if imported file isn't .ass, it is .srt format
928
    }
929
930
    QFile outF(outFile);

931
    //qDebug()<< "Import from JSON";
932
    QWriteLocker locker(&m_lock);
933
934
935
936
937
    auto json = QJsonDocument::fromJson(data.toUtf8());
    if (!json.isArray()) {
        qDebug() << "Error : Json file should be an array";
        return;
    }
938
    int line=0;
939
    auto list = json.array();
Sashmita Raghav's avatar
Sashmita Raghav committed
940
    if (outF.open(QIODevice::WriteOnly)) {
941
        QTextStream out(&outF);
942
        out.setCodec("UTF-8");
943
        if (assFormat) {
944
945
946
947
        	out<<scriptInfoSection<<endl;
        	out<<styleSection<<endl;
        	out<<eventSection;
        }
948
949
950
951
952
953
954
955
956
957
958
        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();
959
            //convert seconds to FORMAT= hh:mm:ss.SS (in .ass) and hh:mm:ss,SSS (in .srt)
960
961
962
963
964
965
966
967
968
969
970
971
972
            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'));
973
974
975
976
977
            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'));
978
979
980
981
982
983
984
985
986
987
            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;

988
            milli_2 = millisec / 10; // to limit ms to 2 digits (for .ass)
989
990
991
992
993
            QString endTimeString = 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'));
994
995
996
997
998
999
1000

            QString endTimeStringSRT = 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'));
            line++;