trackmodel.cpp 48 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/***************************************************************************
 *   Copyright (C) 2017 by Nicolas Carion                                  *
 *   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/>. *
 ***************************************************************************/

#include "trackmodel.hpp"
23
#include "clipmodel.hpp"
24
#include "compositionmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
25
#include "effects/effectstack/model/effectstackmodel.hpp"
26
#include "kdenlivesettings.h"
27
#include "logger.hpp"
28
#include "snapmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
29
#include "timelinemodel.hpp"
30
#include <QDebug>
31
#include <QModelIndex>
Nicolas Carion's avatar
Nicolas Carion committed
32
#include <mlt++/MltTransition.h>
33

34
TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, int id, const QString &trackName, bool audioTrack)
35
36
    : m_parent(parent)
    , m_id(id == -1 ? TimelineModel::getNextId() : id)
Nicolas Carion's avatar
Nicolas Carion committed
37
    , m_lock(QReadWriteLock::Recursive)
38
{
39
    if (auto ptr = parent.lock()) {
Nicolas Carion's avatar
Nicolas Carion committed
40
        m_track = std::make_shared<Mlt::Tractor>(*ptr->getProfile());
41
42
        m_playlists[0].set_profile(*ptr->getProfile());
        m_playlists[1].set_profile(*ptr->getProfile());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
43
44
        m_track->insert_track(m_playlists[0], 0);
        m_track->insert_track(m_playlists[1], 1);
45
46
47
48
49
        if (!trackName.isEmpty()) {
            m_track->set("kdenlive:track_name", trackName.toUtf8().constData());
        }
        if (audioTrack) {
            m_track->set("kdenlive:audio_track", 1);
Nicolas Carion's avatar
Nicolas Carion committed
50
51
            for (auto &m_playlist : m_playlists) {
                m_playlist.set("hide", 1);
52
53
54
            }
        }
        m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight());
55
        m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
56
        QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector<int> roles) {
57
58
            if (auto ptr2 = m_parent.lock()) {
                QModelIndex ix = ptr2->makeTrackIndexFromID(m_id);
59
                ptr2->dataChanged(ix, ix, roles);
60
61
            }
        });
62
63
64
65
    } else {
        qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
        Q_ASSERT(false);
    }
66
67
}

Nicolas Carion's avatar
Nicolas Carion committed
68
TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, Mlt::Tractor mltTrack, int id)
69
70
71
72
    : m_parent(parent)
    , m_id(id == -1 ? TimelineModel::getNextId() : id)
{
    if (auto ptr = parent.lock()) {
Nicolas Carion's avatar
Nicolas Carion committed
73
        m_track = std::make_shared<Mlt::Tractor>(mltTrack);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
74
75
        m_playlists[0] = *m_track->track(0);
        m_playlists[1] = *m_track->track(1);
76
        m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
77
78
79
80
81
82
    } else {
        qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
        Q_ASSERT(false);
    }
}

83
84
TrackModel::~TrackModel()
{
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
85
86
    m_track->remove_track(1);
    m_track->remove_track(0);
87
88
}

89
int TrackModel::construct(const std::weak_ptr<TimelineModel> &parent, int id, int pos, const QString &trackName, bool audioTrack)
90
{
91
    std::shared_ptr<TrackModel> track(new TrackModel(parent, id, trackName, audioTrack));
92
    TRACE_CONSTR(track.get(), parent, id, pos, trackName, audioTrack);
93
    id = track->m_id;
94
    if (auto ptr = parent.lock()) {
95
        ptr->registerTrack(std::move(track), pos);
96
97
98
99
    } else {
        qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
        Q_ASSERT(false);
    }
100
    return id;
101
}
102

103
int TrackModel::getClipsCount()
104
{
Nicolas Carion's avatar
Nicolas Carion committed
105
    READ_LOCK();
106
#ifdef QT_DEBUG
107
    int count = 0;
Nicolas Carion's avatar
Nicolas Carion committed
108
109
110
    for (auto &m_playlist : m_playlists) {
        for (int i = 0; i < m_playlist.count(); i++) {
            if (!m_playlist.is_blank(i)) {
111
112
                count++;
            }
113
114
115
        }
    }
    Q_ASSERT(count == static_cast<int>(m_allClips.size()));
116
#else
117
    int count = (int)m_allClips.size();
118
#endif
119
    return count;
120
121
}

122
Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove, bool groupMove)
123
{
Nicolas Carion's avatar
Nicolas Carion committed
124
    QWriteLocker locker(&m_lock);
125
    // By default, insertion occurs in topmost track
126
    // Find out the clip id at position
127
128
    int target_clip = m_playlists[0].get_clip_index_at(position);
    int count = m_playlists[0].count();
129
130
131
132
133
134
    if (auto ptr = m_parent.lock()) {
        Q_ASSERT(ptr->getClipPtr(clipId)->getCurrentTrackId() == -1);
    } else {
        qDebug() << "impossible to get parent timeline";
        Q_ASSERT(false);
    }
135

Nicolas Carion's avatar
Nicolas Carion committed
136
    // we create the function that has to be executed after the melt order. This is essentially book-keeping
Nicolas Carion's avatar
Nicolas Carion committed
137
    auto end_function = [clipId, this, position, updateView, finalMove](int subPlaylist) {
138
        if (auto ptr = m_parent.lock()) {
139
            std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
140
141
            m_allClips[clip->getId()] = clip; // store clip
            // update clip position and track
142
            clip->setPosition(position);
Nicolas Carion's avatar
Nicolas Carion committed
143
            clip->setSubPlaylistIndex(subPlaylist);
144
            int new_in = clip->getPosition();
145
            int new_out = new_in + clip->getPlaytime();
146
147
            ptr->m_snaps->addPoint(new_in);
            ptr->m_snaps->addPoint(new_out);
148
            if (updateView) {
149
                int clip_index = getRowfromClip(clipId);
150
                ptr->_beginInsertRows(ptr->makeTrackIndexFromID(m_id), clip_index, clip_index);
151
                ptr->_endInsertRows();
152
                bool audioOnly = clip->isAudioOnly();
153
                if (!audioOnly && !isHidden() && !isAudioTrack()) {
154
155
156
                    // only refresh monitor if not an audio track and not hidden
                    ptr->checkRefresh(new_in, new_out);
                }
157
                if (!audioOnly && finalMove && !isAudioTrack()) {
158
                    ptr->invalidateZone(new_in, new_out);
159
                }
160
            }
161
            return true;
Nicolas Carion's avatar
Nicolas Carion committed
162
163
164
        }
        qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
        return false;
165
    };
166
    if (target_clip >= count && isBlankAt(position)) {
Nicolas Carion's avatar
Nicolas Carion committed
167
        // In that case, we append after, in the first playlist
168
        return [this, position, clipId, end_function, finalMove, groupMove]() {
169
            if (isLocked()) return false;
170
            if (auto ptr = m_parent.lock()) {
171
172
                // Lock MLT playlist so that we don't end up with an invalid frame being displayed
                m_playlists[0].lock();
173
                std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
174
                clip->setCurrentTrackId(m_id, finalMove);
175
                int index = m_playlists[0].insert_at(position, *clip, 1);
176
                m_playlists[0].consolidate_blanks();
177
                m_playlists[0].unlock();
178
                if (finalMove && !groupMove) {
179
180
                    ptr->updateDuration();
                }
Nicolas Carion's avatar
Nicolas Carion committed
181
                return index != -1 && end_function(0);
Nicolas Carion's avatar
Nicolas Carion committed
182
183
184
185
186
187
188
189
190
191
192
193
194
            }
            qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
            return false;
        };
    }
    if (isBlankAt(position)) {
        int blank_end = getBlankEnd(position);
        int length = -1;
        if (auto ptr = m_parent.lock()) {
            std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
            length = clip->getPlaytime();
        }
        if (blank_end >= position + length) {
195
            return [this, position, clipId, end_function]() {
196
                if (isLocked()) return false;
Nicolas Carion's avatar
Nicolas Carion committed
197
                if (auto ptr = m_parent.lock()) {
198
199
                    // Lock MLT playlist so that we don't end up with an invalid frame being displayed
                    m_playlists[0].lock();
Nicolas Carion's avatar
Nicolas Carion committed
200
                    std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
201
                    clip->setCurrentTrackId(m_id);
Nicolas Carion's avatar
Nicolas Carion committed
202
                    int index = m_playlists[0].insert_at(position, *clip, 1);
203
                    m_playlists[0].consolidate_blanks();
204
                    m_playlists[0].unlock();
Nicolas Carion's avatar
Nicolas Carion committed
205
                    return index != -1 && end_function(0);
Nicolas Carion's avatar
Nicolas Carion committed
206
                }
207
208
                qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
                return false;
Nicolas Carion's avatar
Nicolas Carion committed
209
            };
210
        }
Nicolas Carion's avatar
Nicolas Carion committed
211
212
    }
    return []() { return false; };
213
214
}

215
bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
216
{
Nicolas Carion's avatar
Nicolas Carion committed
217
    QWriteLocker locker(&m_lock);
218
219
220
    if (isLocked()) {
        return false;
    }
221
222
223
    if (position < 0) {
        return false;
    }
224
225
    if (auto ptr = m_parent.lock()) {
        if (isAudioTrack() && !ptr->getClipPtr(clipId)->canBeAudio()) {
226
            qDebug() << "// ATTEMPTING TO INSERT NON AUDIO CLIP ON AUDIO TRACK";
227
228
229
            return false;
        }
        if (!isAudioTrack() && !ptr->getClipPtr(clipId)->canBeVideo()) {
230
            qDebug() << "// ATTEMPTING TO INSERT NON VIDEO CLIP ON VIDEO TRACK";
231
232
233
234
235
236
            return false;
        }
        Fun local_undo = []() { return true; };
        Fun local_redo = []() { return true; };
        bool res = true;
        if (ptr->getClipPtr(clipId)->clipState() != PlaylistState::Disabled) {
237
            res = res && ptr->getClipPtr(clipId)->setClipState(isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo);
238
        }
239
        auto operation = requestClipInsertion_lambda(clipId, position, updateView, finalMove, groupMove);
240
241
        res = res && operation();
        if (res) {
242
            auto reverse = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove);
243
244
245
246
247
248
249
            UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
            UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
            return true;
        }
        bool undone = local_undo();
        Q_ASSERT(undone);
        return false;
250
    }
251
252
253
    return false;
}

254
255
void TrackModel::replugClip(int clipId)
{
256
    QWriteLocker locker(&m_lock);
257
258
259
260
261
262
263
264
    int clip_position = m_allClips[clipId]->getPosition();
    auto clip_loc = getClipIndexAt(clip_position);
    int target_track = clip_loc.first;
    int target_clip = clip_loc.second;
    // lock MLT playlist so that we don't end up with invalid frames in monitor
    m_playlists[target_track].lock();
    Q_ASSERT(target_clip < m_playlists[target_track].count());
    Q_ASSERT(!m_playlists[target_track].is_blank(target_clip));
265
    std::unique_ptr<Mlt::Producer> prod(m_playlists[target_track].replace_with_blank(target_clip));
266
267
    if (auto ptr = m_parent.lock()) {
        std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
268
        m_playlists[target_track].insert_at(clip_position, *clip, 1);
269
270
271
272
273
274
275
        if (!clip->isAudioOnly() && !isAudioTrack()) {
            ptr->invalidateZone(clip->getIn(), clip->getOut());
        }
        if (!clip->isAudioOnly() && !isHidden() && !isAudioTrack()) {
            // only refresh monitor if not an audio track and not hidden
            ptr->checkRefresh(clip->getIn(), clip->getOut());
        }
276
    }
277
    m_playlists[target_track].consolidate_blanks();
278
279
280
    m_playlists[target_track].unlock();
}

281
Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove, bool groupMove)
282
{
Nicolas Carion's avatar
Nicolas Carion committed
283
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
284
    // Find index of clip
285
    int clip_position = m_allClips[clipId]->getPosition();
286
    bool audioOnly = m_allClips[clipId]->isAudioOnly();
287
    int old_in = clip_position;
288
    int old_out = old_in + m_allClips[clipId]->getPlaytime();
289
    return [clip_position, clipId, old_in, old_out, updateView, audioOnly, finalMove, groupMove, this]() {
290
        if (isLocked()) return false;
291
        auto clip_loc = getClipIndexAt(clip_position);
292
293
294
295
296
        if (updateView) {
            int old_clip_index = getRowfromClip(clipId);
            auto ptr = m_parent.lock();
            ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index);
            ptr->_endRemoveRows();
297
        }
Nicolas Carion's avatar
Nicolas Carion committed
298
        int target_track = m_allClips[clipId]->getSubPlaylistIndex();
299
        int target_clip = clip_loc.second;
300
301
        // lock MLT playlist so that we don't end up with invalid frames in monitor
        m_playlists[target_track].lock();
302
303
304
        Q_ASSERT(target_clip < m_playlists[target_track].count());
        Q_ASSERT(!m_playlists[target_track].is_blank(target_clip));
        auto prod = m_playlists[target_track].replace_with_blank(target_clip);
305
        if (prod != nullptr) {
306
            m_playlists[target_track].consolidate_blanks();
307
            m_allClips[clipId]->setCurrentTrackId(-1);
Nicolas Carion's avatar
Nicolas Carion committed
308
            m_allClips[clipId]->setSubPlaylistIndex(-1);
309
            m_allClips.erase(clipId);
310
            delete prod;
311
            m_playlists[target_track].unlock();
312
            if (auto ptr = m_parent.lock()) {
313
                ptr->m_snaps->removePoint(old_in);
314
                ptr->m_snaps->removePoint(old_out);
315
316
317
318
                if (finalMove) {
                    if (!audioOnly && !isAudioTrack()) {
                        ptr->invalidateZone(old_in, old_out);
                    }
319
                    if (!groupMove && target_clip >= m_playlists[target_track].count()) {
320
321
322
                        // deleted last clip in playlist
                        ptr->updateDuration();
                    }
323
                }
324
                if (!audioOnly && !isHidden() && !isAudioTrack()) {
325
326
327
                    // only refresh monitor if not an audio track and not hidden
                    ptr->checkRefresh(old_in, old_out);
                }
328
            }
329
330
            return true;
        }
331
        m_playlists[target_track].unlock();
332
333
334
335
        return false;
    };
}

336
bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, bool finalDeletion)
337
{
Nicolas Carion's avatar
Nicolas Carion committed
338
    QWriteLocker locker(&m_lock);
339
    Q_ASSERT(m_allClips.count(clipId) > 0);
340
341
342
    if (isLocked()) {
        return false;
    }
343
    auto old_clip = m_allClips[clipId];
344
    int old_position = old_clip->getPosition();
345
    // qDebug() << "/// REQUESTOING CLIP DELETION_: " << updateView;
346
347
348
    if (finalDeletion) {
        m_allClips[clipId]->selected = false;
    }
349
    auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove);
350
    if (operation()) {
351
        auto reverse = requestClipInsertion_lambda(clipId, old_position, updateView, finalMove, groupMove);
352
        UPDATE_UNDO_REDO(operation, reverse, undo, redo);
353
354
355
356
357
        return true;
    }
    return false;
}

358
359
360
361
int TrackModel::getBlankSizeAtPos(int frame)
{
    READ_LOCK();
    int min_length = 0;
Nicolas Carion's avatar
Nicolas Carion committed
362
363
364
365
    for (auto &m_playlist : m_playlists) {
        int ix = m_playlist.get_clip_index_at(frame);
        if (m_playlist.is_blank(ix)) {
            int blank_length = m_playlist.clip_length(ix);
366
367
368
369
370
371
372
373
            if (min_length == 0 || (blank_length > 0 && blank_length < min_length)) {
                min_length = blank_length;
            }
        }
    }
    return min_length;
}

374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
int TrackModel::suggestCompositionLength(int position)
{
    READ_LOCK();
    if (m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position)) {
        return -1;
    }
    auto clip_loc = getClipIndexAt(position);
    int track = clip_loc.first;
    int index = clip_loc.second;
    int other_index; // index in the other track
    int other_track = (track + 1) % 2;
    int end_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index);
    other_index = m_playlists[other_track].get_clip_index_at(end_pos);
    if (other_index < m_playlists[other_track].count()) {
        end_pos = std::min(end_pos, m_playlists[other_track].clip_start(other_index) + m_playlists[other_track].clip_length(other_index));
    }
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
    int min = -1;
    std::unordered_set<int> existing = getCompositionsInRange(position, end_pos);
    if (existing.size() > 0) {
        for (int id : existing) {
            if (min < 0) {
                min = m_allCompositions[id]->getPosition();
            } else {
                min = qMin(min, m_allCompositions[id]->getPosition());
            }
        }
    }
    if (min >= 0) {
        // An existing composition is limiting the space
        end_pos = min;
    }
405
406
407
    return end_pos - position;
}

408
int TrackModel::getBlankSizeNearClip(int clipId, bool after)
409
{
Nicolas Carion's avatar
Nicolas Carion committed
410
    READ_LOCK();
411
412
    Q_ASSERT(m_allClips.count(clipId) > 0);
    int clip_position = m_allClips[clipId]->getPosition();
413
414
415
    auto clip_loc = getClipIndexAt(clip_position);
    int track = clip_loc.first;
    int index = clip_loc.second;
Nicolas Carion's avatar
Nicolas Carion committed
416
417
    int other_index; // index in the other track
    int other_track = (track + 1) % 2;
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
    if (after) {
        int first_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index);
        other_index = m_playlists[other_track].get_clip_index_at(first_pos);
        index++;
    } else {
        int last_pos = m_playlists[track].clip_start(index) - 1;
        other_index = m_playlists[other_track].get_clip_index_at(last_pos);
        index--;
    }
    if (index < 0) return 0;
    int length = INT_MAX;
    if (index < m_playlists[track].count()) {
        if (!m_playlists[track].is_blank(index)) {
            return 0;
        }
        length = std::min(length, m_playlists[track].clip_length(index));
    }
    if (other_index < m_playlists[other_track].count()) {
        if (!m_playlists[other_track].is_blank(other_index)) {
            return 0;
        }
        length = std::min(length, m_playlists[other_track].clip_length(other_index));
    }
    return length;
}

444
int TrackModel::getBlankSizeNearComposition(int compoId, bool after)
445
{
Nicolas Carion's avatar
Nicolas Carion committed
446
    READ_LOCK();
447
448
    Q_ASSERT(m_allCompositions.count(compoId) > 0);
    int clip_position = m_allCompositions[compoId]->getPosition();
449
450
451
452
    Q_ASSERT(m_compoPos.count(clip_position) > 0);
    Q_ASSERT(m_compoPos[clip_position] == compoId);
    auto it = m_compoPos.find(clip_position);
    int clip_length = m_allCompositions[compoId]->getPlaytime();
453
    int length = INT_MAX;
454
455
456
457
458
459
460
461
    if (after) {
        ++it;
        if (it != m_compoPos.end()) {
            return it->first - clip_position - clip_length;
        }
    } else {
        if (it != m_compoPos.begin()) {
            --it;
Nicolas Carion's avatar
Nicolas Carion committed
462
463
464
            return clip_position - it->first - m_allCompositions[it->second]->getPlaytime();
        }
        return clip_position;
465
    }
466
467
468
    return length;
}

469
Fun TrackModel::requestClipResize_lambda(int clipId, int in, int out, bool right)
470
{
Nicolas Carion's avatar
Nicolas Carion committed
471
    QWriteLocker locker(&m_lock);
472
    int clip_position = m_allClips[clipId]->getPosition();
473
    int old_in = clip_position;
474
    int old_out = old_in + m_allClips[clipId]->getPlaytime();
475
476
477
478
    auto clip_loc = getClipIndexAt(clip_position);
    int target_track = clip_loc.first;
    int target_clip = clip_loc.second;
    Q_ASSERT(target_clip < m_playlists[target_track].count());
479
    int size = out - in + 1;
480
    bool checkRefresh = false;
481
    if (!isHidden() && !isAudioTrack()) {
482
483
        checkRefresh = true;
    }
484

485
    auto update_snaps = [old_in, old_out, checkRefresh, right, this](int new_in, int new_out) {
486
487
        if (auto ptr = m_parent.lock()) {
            ptr->m_snaps->removePoint(old_in);
488
            ptr->m_snaps->removePoint(old_out);
489
            ptr->m_snaps->addPoint(new_in);
490
            ptr->m_snaps->addPoint(new_out);
491
            if (checkRefresh) {
492
493
494
495
496
497
498
499
500
501
502
                if (right) {
                    if (old_out < new_out) {
                        ptr->checkRefresh(old_out, new_out);
                    } else {
                        ptr->checkRefresh(old_in, old_out);
                    }
                } else if (old_in < new_in) {
                    ptr->checkRefresh(old_in, new_in);
                } else {
                    ptr->checkRefresh(new_in, old_in);
                }
503
            }
504
505
506
507
508
509
        } else {
            qDebug() << "Error : clip resize failed because parent timeline is not available anymore";
            Q_ASSERT(false);
        }
    };

510
    int delta = m_allClips[clipId]->getPlaytime() - size;
511
    if (delta == 0) {
Nicolas Carion's avatar
Nicolas Carion committed
512
        return []() { return true; };
513
    }
514
    // qDebug() << "RESIZING CLIP: " << clipId << " FROM: " << delta;
Nicolas Carion's avatar
Nicolas Carion committed
515
516
    if (delta > 0) { // we shrink clip
        return [right, target_clip, target_track, clip_position, delta, in, out, clipId, update_snaps, this]() {
517
            if (isLocked()) return false;
518
519
520
521
            int target_clip_mutable = target_clip;
            int blank_index = right ? (target_clip_mutable + 1) : target_clip_mutable;
            // insert blank to space that is going to be empty
            // The second is parameter is delta - 1 because this function expects an out time, which is basically size - 1
522
            m_playlists[target_track].insert_blank(blank_index, delta - 1);
523
            if (!right) {
524
                m_allClips[clipId]->setPosition(clip_position + delta);
Nicolas Carion's avatar
Nicolas Carion committed
525
                // Because we inserted blank before, the index of our clip has increased
526
527
                target_clip_mutable++;
            }
528
            int err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out);
Nicolas Carion's avatar
Nicolas Carion committed
529
            // make sure to do this after, to avoid messing the indexes
530
            m_playlists[target_track].consolidate_blanks();
531
            if (err == 0) {
532
                update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
533
534
535
536
537
538
                if (right && m_playlists[target_track].count() - 1 == target_clip_mutable) {
                    // deleted last clip in playlist
                    if (auto ptr = m_parent.lock()) {
                        ptr->updateDuration();
                    }
                }
539
            }
540
541
            return err == 0;
        };
Nicolas Carion's avatar
Nicolas Carion committed
542
543
544
545
546
547
548
    }
    int blank = -1;
    int other_blank_end = getBlankEnd(clip_position, (target_track + 1) % 2);
    if (right) {
        if (target_clip == m_playlists[target_track].count() - 1 && other_blank_end >= out) {
            // clip is last, it can always be extended
            return [this, target_clip, target_track, in, out, update_snaps, clipId]() {
549
                if (isLocked()) return false;
Nicolas Carion's avatar
Nicolas Carion committed
550
551
552
553
                // color, image and title clips can have unlimited resize
                QScopedPointer<Mlt::Producer> clip(m_playlists[target_track].get_clip(target_clip));
                if (out >= clip->get_length()) {
                    clip->parent().set("length", out + 1);
554
                    clip->parent().set("out", out);
Nicolas Carion's avatar
Nicolas Carion committed
555
556
557
558
                    clip->set("length", out + 1);
                }
                int err = m_playlists[target_track].resize_clip(target_clip, in, out);
                if (err == 0) {
559
                    update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
Nicolas Carion's avatar
Nicolas Carion committed
560
                }
561
                m_playlists[target_track].consolidate_blanks();
562
563
564
565
566
567
                if (m_playlists[target_track].count() - 1 == target_clip) {
                    // deleted last clip in playlist
                    if (auto ptr = m_parent.lock()) {
                        ptr->updateDuration();
                    }
                }
Nicolas Carion's avatar
Nicolas Carion committed
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
                return err == 0;
            };
        }

        blank = target_clip + 1;
    } else {
        if (target_clip == 0) {
            // clip is first, it can never be extended on the left
            return []() { return false; };
        }
        blank = target_clip - 1;
    }
    if (m_playlists[target_track].is_blank(blank)) {
        int blank_length = m_playlists[target_track].clip_length(blank);
        if (blank_length + delta >= 0 && other_blank_end >= out) {
            return [blank_length, blank, right, clipId, delta, update_snaps, this, in, out, target_clip, target_track]() {
584
                if (isLocked()) return false;
Nicolas Carion's avatar
Nicolas Carion committed
585
586
587
588
589
590
591
592
593
594
595
                int target_clip_mutable = target_clip;
                int err = 0;
                if (blank_length + delta == 0) {
                    err = m_playlists[target_track].remove(blank);
                    if (!right) {
                        target_clip_mutable--;
                    }
                } else {
                    err = m_playlists[target_track].resize_clip(blank, 0, blank_length + delta - 1);
                }
                if (err == 0) {
596
                    QScopedPointer<Mlt::Producer> clip(m_playlists[target_track].get_clip(target_clip_mutable));
597
                    if (out >= clip->get_length()) {
598
                        clip->parent().set("length", out + 1);
599
                        clip->parent().set("out", out);
600
601
                        clip->set("length", out + 1);
                    }
Nicolas Carion's avatar
Nicolas Carion committed
602
603
604
605
606
607
                    err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out);
                }
                if (!right && err == 0) {
                    m_allClips[clipId]->setPosition(m_playlists[target_track].clip_start(target_clip_mutable));
                }
                if (err == 0) {
608
                    update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
Nicolas Carion's avatar
Nicolas Carion committed
609
                }
610
                m_playlists[target_track].consolidate_blanks();
Nicolas Carion's avatar
Nicolas Carion committed
611
612
                return err == 0;
            };
613
        }
Nicolas Carion's avatar
Nicolas Carion committed
614
615
616
    }

    return []() { return false; };
617
618
}

619
620
621
622
int TrackModel::getId() const
{
    return m_id;
}
623

624
int TrackModel::getClipByPosition(int position)
625
{
Nicolas Carion's avatar
Nicolas Carion committed
626
    READ_LOCK();
627
628
629
    QSharedPointer<Mlt::Producer> prod(nullptr);
    if (m_playlists[0].count() > 0) {
        prod = QSharedPointer<Mlt::Producer>(m_playlists[0].get_clip_at(position));
Nicolas Carion's avatar
Nicolas Carion committed
630
    }
631
632
633
634
    if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) {
        prod = QSharedPointer<Mlt::Producer>(m_playlists[1].get_clip_at(position));
    }
    if (!prod || prod->is_blank()) {
635
        return -1;
636
    }
637
    return prod->get_int("_kdenlive_cid");
638
639
}

640
641
642
643
644
645
646
647
648
649
650
651
652
QSharedPointer<Mlt::Producer> TrackModel::getClipProducer(int clipId)
{
    READ_LOCK();
    QSharedPointer<Mlt::Producer> prod(nullptr);
    if (m_playlists[0].count() > 0) {
        prod = QSharedPointer<Mlt::Producer>(m_playlists[0].get_clip(clipId));
    }
    if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) {
        prod = QSharedPointer<Mlt::Producer>(m_playlists[1].get_clip(clipId));
    }
    return prod;
}

653
654
655
656
int TrackModel::getCompositionByPosition(int position)
{
    READ_LOCK();
    for (const auto &comp : m_compoPos) {
657
658
659
660
661
662
        if (comp.first == position) {
            return comp.second;
        } else if (comp.first < position) {
            if (comp.first + m_allCompositions[comp.second]->getPlaytime() >= position) {
                return comp.second;
            }
663
664
665
666
667
        }
    }
    return -1;
}

668
669
int TrackModel::getClipByRow(int row) const
{
Nicolas Carion's avatar
Nicolas Carion committed
670
    READ_LOCK();
671
672
673
    if (row >= static_cast<int>(m_allClips.size())) {
        return -1;
    }
674
    auto it = m_allClips.cbegin();
675
    std::advance(it, row);
676
    return (*it).first;
677
678
}

Nicolas Carion's avatar
Nicolas Carion committed
679
std::unordered_set<int> TrackModel::getClipsInRange(int position, int end)
680
{
Nicolas Carion's avatar
Nicolas Carion committed
681
    READ_LOCK();
682
    std::unordered_set<int> ids;
Nicolas Carion's avatar
Nicolas Carion committed
683
    for (const auto &clp : m_allClips) {
684
        int pos = clp.second->getPosition();
Nicolas Carion's avatar
Nicolas Carion committed
685
        int length = clp.second->getPlaytime();
686
        if (end > -1 && pos >= end) {
687
688
            continue;
        }
Nicolas Carion's avatar
Nicolas Carion committed
689
        if (pos >= position || pos + length - 1 >= position) {
690
691
            ids.insert(clp.first);
        }
692
693
694
695
    }
    return ids;
}

696
int TrackModel::getRowfromClip(int clipId) const
697
{
Nicolas Carion's avatar
Nicolas Carion committed
698
    READ_LOCK();
699
700
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return (int)std::distance(m_allClips.begin(), m_allClips.find(clipId));
701
702
}

Nicolas Carion's avatar
Nicolas Carion committed
703
std::unordered_set<int> TrackModel::getCompositionsInRange(int position, int end)
704
705
706
707
708
709
{
    READ_LOCK();
    // TODO: this function doesn't take into accounts the fact that there are two tracks
    std::unordered_set<int> ids;
    for (const auto &compo : m_allCompositions) {
        int pos = compo.second->getPosition();
Nicolas Carion's avatar
Nicolas Carion committed
710
711
712
713
714
715
        int length = compo.second->getPlaytime();
        if (end > -1 && pos >= end) {
            continue;
        }
        if (pos >= position || pos + length - 1 >= position) {
            ids.insert(compo.first);
716
717
718
719
720
        }
    }
    return ids;
}

721
722
723
724
725
726
727
int TrackModel::getRowfromComposition(int tid) const
{
    READ_LOCK();
    Q_ASSERT(m_allCompositions.count(tid) > 0);
    return (int)m_allClips.size() + (int)std::distance(m_allCompositions.begin(), m_allCompositions.find(tid));
}

728
QVariant TrackModel::getProperty(const QString &name) const
729
{
Nicolas Carion's avatar
Nicolas Carion committed
730
    READ_LOCK();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
731
    return QVariant(m_track->get(name.toUtf8().constData()));
732
733
734
735
}

void TrackModel::setProperty(const QString &name, const QString &value)
{
Nicolas Carion's avatar
Nicolas Carion committed
736
    QWriteLocker locker(&m_lock);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
737
    m_track->set(name.toUtf8().constData(), value.toUtf8().constData());
738
739
    // Hide property mus be defined at playlist level or it won't be saved
    if (name == QLatin1String("kdenlive:audio_track") || name == QLatin1String("hide")) {
Nicolas Carion's avatar
Nicolas Carion committed
740
741
        for (auto &m_playlist : m_playlists) {
            m_playlist.set(name.toUtf8().constData(), value.toInt());
742
743
        }
    }
744
745
}

746
747
bool TrackModel::checkConsistency()
{
748
749
750
751
    auto ptr = m_parent.lock();
    if (!ptr) {
        return false;
    }
Nicolas Carion's avatar
Nicolas Carion committed
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
    auto check_blank_zone = [&](int playlist, int in, int out) {
        if (in >= m_playlists[playlist].get_playtime()) {
            return true;
        }
        int index = m_playlists[playlist].get_clip_index_at(in);
        if (!m_playlists[playlist].is_blank(index)) {
            return false;
        }
        int cin = m_playlists[playlist].clip_start(index);
        if (cin > in) {
            return false;
        }
        if (cin + m_playlists[playlist].clip_length(index) - 1 < out) {
            return false;
        }
        return true;
    };
Nicolas Carion's avatar
Nicolas Carion committed
769
770
    std::vector<std::pair<int, int>> clips; // clips stored by (position, id)
    for (const auto &c : m_allClips) {
771
772
        Q_ASSERT(c.second);
        Q_ASSERT(c.second.get() == ptr->getClipPtr(c.first).get());
Nicolas Carion's avatar
Nicolas Carion committed
773
        clips.emplace_back(c.second->getPosition(), c.first);
774
775
    }
    std::sort(clips.begin(), clips.end());
Nicolas Carion's avatar
Nicolas Carion committed
776
777
778
779
780
781
782
783
784
785
    int last_out = 0;
    for (size_t i = 0; i < clips.size(); ++i) {
        auto cur_clip = m_allClips[clips[i].second];
        if (last_out < clips[i].first) {
            // we have some blank space before this clip, check it
            for (int pl = 0; pl <= 1; ++pl) {
                if (!check_blank_zone(pl, last_out, clips[i].first - 1)) {
                    qDebug() << "ERROR: Some blank was required on playlist " << pl << " between " << last_out << " and " << clips[i].first - 1;
                    return false;
                }
Nicolas Carion's avatar
linting    
Nicolas Carion committed
786
            }
Nicolas Carion's avatar
Nicolas Carion committed
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
        }
        int cur_playlist = cur_clip->getSubPlaylistIndex();
        int clip_index = m_playlists[cur_playlist].get_clip_index_at(clips[i].first);
        if (m_playlists[cur_playlist].is_blank(clip_index)) {
            qDebug() << "ERROR: Found blank when clip was required at position " << clips[i].first;
            return false;
        }
        if (m_playlists[cur_playlist].clip_start(clip_index) != clips[i].first) {
            qDebug() << "ERROR: Inconsistent start position for clip at position " << clips[i].first;
            return false;
        }
        if (m_playlists[cur_playlist].clip_start(clip_index) != clips[i].first) {
            qDebug() << "ERROR: Inconsistent start position for clip at position " << clips[i].first;
            return false;
        }
        if (m_playlists[cur_playlist].clip_length(clip_index) != cur_clip->getPlaytime()) {
            qDebug() << "ERROR: Inconsistent length for clip at position " << clips[i].first;
            return false;
        }
        auto pr = m_playlists[cur_playlist].get_clip(clip_index);
        Mlt::Producer prod(pr);
        if (!prod.same_clip(*cur_clip)) {
            qDebug() << "ERROR: Wrong clip at position " << clips[i].first;
            delete pr;
            return false;
        }
        delete pr;

        // the current playlist is valid, we check that the other is essentially blank
        int other_playlist = (cur_playlist + 1) % 2;
        int in_blank = clips[i].first;
        int out_blank = clips[i].first + cur_clip->getPlaytime() - 1;

        // the previous clip on the same playlist must not intersect
        int prev_clip_id_same_playlist = -1;
        for (int j = (int)i - 1; j >= 0; --j) {
            if (cur_playlist == m_allClips[clips[(size_t)j].second]->getSubPlaylistIndex()) {
                prev_clip_id_same_playlist = j;
                break;
Nicolas Carion's avatar
linting    
Nicolas Carion committed
826
            }
Nicolas Carion's avatar
Nicolas Carion committed
827
828
829
830
831
832
833
834
835
836
837
838
839
        }
        if (prev_clip_id_same_playlist >= 0 &&
            clips[(size_t)prev_clip_id_same_playlist].first + m_allClips[clips[(size_t)prev_clip_id_same_playlist].second]->getPlaytime() > clips[i].first) {
            qDebug() << "ERROR: found overlapping clips at position " << clips[i].first;
            return false;
        }

        // the previous clip on the other playlist might restrict the blank in/out
        int prev_clip_id_other_playlist = -1;
        for (int j = (int)i - 1; j >= 0; --j) {
            if (other_playlist == m_allClips[clips[(size_t)j].second]->getSubPlaylistIndex()) {
                prev_clip_id_other_playlist = j;
                break;
840
            }
Nicolas Carion's avatar
Nicolas Carion committed
841
842
843
844
845
        }
        if (prev_clip_id_other_playlist >= 0) {
            in_blank = std::max(in_blank, clips[(size_t)prev_clip_id_other_playlist].first +
                                              m_allClips[clips[(size_t)prev_clip_id_other_playlist].second]->getPlaytime());
        }
Nicolas Carion's avatar
linting    
Nicolas Carion committed
846

Nicolas Carion's avatar
Nicolas Carion committed
847
848
849
850
851
852
        // the next clip on the other playlist might restrict the blank in/out
        int next_clip_id_other_playlist = -1;
        for (int j = (int)i + 1; j < (int)clips.size(); ++j) {
            if (other_playlist == m_allClips[clips[(size_t)j].second]->getSubPlaylistIndex()) {
                next_clip_id_other_playlist = j;
                break;
853
854
            }
        }
Nicolas Carion's avatar
Nicolas Carion committed
855
856
857
858
859
860
861
862
863
        if (next_clip_id_other_playlist >= 0) {
            out_blank = std::min(out_blank, clips[(size_t)next_clip_id_other_playlist].first - 1);
        }
        if (in_blank <= out_blank && !check_blank_zone(other_playlist, in_blank, out_blank)) {
            qDebug() << "ERROR: we expected blank on playlist " << other_playlist << " between " << in_blank << " and " << out_blank;
            return false;
        }

        last_out = clips[i].first + cur_clip->getPlaytime();
864
    }
Nicolas Carion's avatar
Nicolas Carion committed
865
866
867
868
869
870
    int playtime = std::max(m_playlists[0].get_playtime(), m_playlists[1].get_playtime());
    if (!clips.empty() && playtime != clips.back().first + m_allClips[clips.back().second]->getPlaytime()) {
        qDebug() << "Error: playtime is " << playtime << " but was expected to be" << clips.back().first + m_allClips[clips.back().second]->getPlaytime();
        return false;
    }

871
872
873
874
875
    // We now check compositions positions
    if (m_allCompositions.size() != m_compoPos.size()) {
        qDebug() << "Error: the number of compositions position doesn't match number of compositions";
        return false;
    }
Nicolas Carion's avatar
Nicolas Carion committed
876
    for (const auto &compo : m_allCompositions) {
877
878
        int pos = compo.second->getPosition();
        if (m_compoPos.count(pos) == 0) {
Nicolas Carion's avatar
Nicolas Carion committed
879
            qDebug() << "Error: the position of composition " << compo.first << " is not properly stored";
880
881
882
            return false;
        }
        if (m_compoPos[pos] != compo.first) {
Nicolas Carion's avatar
Nicolas Carion committed
883
            qDebug() << "Error: found composition" << m_compoPos[pos] << "instead of " << compo.first << "at position" << pos;
884
885
886
887
888
889
890
891
892
893
894
895
896
897
            return false;
        }
    }
    for (auto it = m_compoPos.begin(); it != m_compoPos.end(); ++it) {
        int compoId = it->second;
        int cur_in = m_allCompositions[compoId]->getPosition();
        Q_ASSERT(cur_in == it->first);
        int cur_out = cur_in + m_allCompositions[compoId]->getPlaytime() - 1;
        ++it;
        if (it != m_compoPos.end()) {
            int next_compoId = it->second;
            int next_in = m_allCompositions[next_compoId]->getPosition();
            int next_out = next_in + m_allCompositions[next_compoId]->getPlaytime() - 1;
            if (next_in <= cur_out) {
Nicolas Carion's avatar
Nicolas Carion committed
898
899
                qDebug() << "Error: found collision between composition " << compoId << "[ " << cur_in << ", " << cur_out << "] and " << next_compoId << "[ "
                         << next_in << ", " << next_out << "]";
900
901
902
903
904
                return false;
            }
        }
        --it;
    }
905
906
    return true;
}
907
908
909

std::pair<int, int> TrackModel::getClipIndexAt(int position)
{
Nicolas Carion's avatar
Nicolas Carion committed
910
    READ_LOCK();
911
    for (int j = 0; j < 2; j++) {
Nicolas Carion's avatar
Nicolas Carion committed
912
        if (!m_playlists[j].is_blank_at(position)) {
913
914
915
916
            return {j, m_playlists[j].get_clip_index_at(position)};
        }
    }
    Q_ASSERT(false);
Nicolas Carion's avatar
Nicolas Carion committed
917
    return {-1, -1};
918
919
920
921
}

bool TrackModel::isBlankAt(int position)
{
Nicolas Carion's avatar
Nicolas Carion committed
922
    READ_LOCK();
923
924
925
    return m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position);
}

926
927
928
929
int TrackModel::getBlankStart(int position)
{
    READ_LOCK();
    int result = 0;
Nicolas Carion's avatar
Nicolas Carion committed
930
931
    for (auto &m_playlist : m_playlists) {
        if (m_playlist.count() == 0) {
932
933
            break;
        }
Nicolas Carion's avatar
Nicolas Carion committed
934
        if (!m_playlist.is_blank_at(position)) {
935
936
937
            result = position;
            break;
        }
Nicolas Carion's avatar
Nicolas Carion committed
938
939
        int clip_index = m_playlist.get_clip_index_at(position);
        int start = m_playlist.clip_start(clip_index);
940
941
942
943
944
945
946
        if (start > result) {
            result = start;
        }
    }
    return result;
}

947
948
int TrackModel::getBlankEnd(int position, int track)
{
Nicolas Carion's avatar
Nicolas Carion committed
949
    READ_LOCK();
Nicolas Carion's avatar
Nicolas Carion committed
950
    // Q_ASSERT(m_playlists[track].is_blank_at(position));
951
952
953
    if (!m_playlists[track].is_blank_at(position)) {
        return position;
    }
954
955
956
957
958
959
960
961
962
    int clip_index = m_playlists[track].get_clip_index_at(position);
    int count = m_playlists[track].count();
    if (clip_index < count) {
        int blank_start = m_playlists[track].clip_start(clip_index);
        int blank_length = m_playlists[track].clip_length(clip_index);
        return blank_start + blank_length;
    }
    return INT_MAX;
}
963

964
965
int TrackModel::getBlankEnd(int position)
{
Nicolas Carion's avatar
Nicolas Carion committed
966
    READ_LOCK();
967
968