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 / 10; // to limit ms to 2 digits (for .ass)
985
986
987
988
989
            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'));
990
991
992
993
994
995
996

            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++;
997
            if (assFormat) {
998
999
            	//Format: Layer, Start, End, Style, Actor, MarginL, MarginR, MarginV, Effect, Text
            	out <<"Dialogue: 0,"<<startTimeString<<","<<endTimeString<<","<<styleName<<",,0000,0000,0000,,"<<dialogue<<endl;
1000
            } else {