trackmodel.cpp 113 KB
Newer Older
1
/*
2
    SPDX-FileCopyrightText: 2017 Nicolas Carion
Camille Moulin's avatar
Camille Moulin committed
3
    SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4
*/
5
6

#include "trackmodel.hpp"
7
#include "clipmodel.hpp"
8
#include "compositionmodel.hpp"
9
#include "core.h"
Nicolas Carion's avatar
Nicolas Carion committed
10
#include "effects/effectstack/model/effectstackmodel.hpp"
11
#include "kdenlivesettings.h"
12
#include "transitions/transitionsrepository.hpp"
Vincent Pinon's avatar
Vincent Pinon committed
13
#ifdef CRASH_AUTO_TEST
14
#include "logger.hpp"
Vincent Pinon's avatar
Vincent Pinon committed
15
16
17
#else
#define TRACE_CONSTR(...)
#endif
18
#include "snapmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
19
#include "timelinemodel.hpp"
20
#include <QDebug>
21
#include <QModelIndex>
22
#include <memory>
Nicolas Carion's avatar
Nicolas Carion committed
23
#include <mlt++/MltTransition.h>
24

25
TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, int id, const QString &trackName, bool audioTrack)
26
27
    : m_parent(parent)
    , m_id(id == -1 ? TimelineModel::getNextId() : id)
Nicolas Carion's avatar
Nicolas Carion committed
28
    , m_lock(QReadWriteLock::Recursive)
29
{
30
    if (auto ptr = parent.lock()) {
Nicolas Carion's avatar
Nicolas Carion committed
31
        m_track = std::make_shared<Mlt::Tractor>(*ptr->getProfile());
32
33
        m_playlists[0].set_profile(*ptr->getProfile());
        m_playlists[1].set_profile(*ptr->getProfile());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
34
35
        m_track->insert_track(m_playlists[0], 0);
        m_track->insert_track(m_playlists[1], 1);
36
        m_mainPlaylist = std::make_shared<Mlt::Producer>(&m_playlists[0]);
37
38
39
40
41
        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
42
43
            for (auto &m_playlist : m_playlists) {
                m_playlist.set("hide", 1);
44
45
            }
        }
46
        // For now we never use the second playlist, only planned for same track transitions
47
        m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
48
        m_track->set("kdenlive:timeline_active", 1);
49
        m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
50
51
52
53
        // TODO
        // When we use the second playlist, register it's stask as child of main playlist effectstack
        // m_subPlaylist = std::make_shared<Mlt::Producer>(&m_playlists[1]);
        // m_effectStack->addService(m_subPlaylist);
54
        QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, const QVector<int> &roles) {
55
56
            if (auto ptr2 = m_parent.lock()) {
                QModelIndex ix = ptr2->makeTrackIndexFromID(m_id);
57
                qDebug() << "==== TRACK ZONES CHANGED";
Vincent Pinon's avatar
Vincent Pinon committed
58
                emit ptr2->dataChanged(ix, ix, roles);
59
60
            }
        });
61
62
63
64
    } else {
        qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
        Q_ASSERT(false);
    }
65
66
}

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

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

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

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

121
bool TrackModel::switchPlaylist(int clipId, int position, int sourcePlaylist, int destPlaylist)
122
123
{
    QWriteLocker locker(&m_lock);
124
125
126
127
128
129
    if (sourcePlaylist == destPlaylist) {
        return true;
    }
    Q_ASSERT(!m_playlists[sourcePlaylist].is_blank_at(position) && m_playlists[destPlaylist].is_blank_at(position));
    int target_clip = m_playlists[sourcePlaylist].get_clip_index_at(position);
    std::unique_ptr<Mlt::Producer> prod(m_playlists[sourcePlaylist].replace_with_blank(target_clip));
130
    m_playlists[sourcePlaylist].consolidate_blanks();
131
132
    if (auto ptr = m_parent.lock()) {
        std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
133
        clip->setSubPlaylistIndex(destPlaylist, m_id);
134
        int index = m_playlists[destPlaylist].insert_at(position, *clip, 1);
135
        m_playlists[destPlaylist].consolidate_blanks();
136
137
138
139
140
        return index != -1;
    }
    return false;
}

141
Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove, bool groupMove, const QList<int> &allowedClipMixes)
142
{
Nicolas Carion's avatar
Nicolas Carion committed
143
    QWriteLocker locker(&m_lock);
144
    // By default, insertion occurs in topmost track
145
    int target_playlist = 0;
146
    int length = -1;
147
148
    if (auto ptr = m_parent.lock()) {
        Q_ASSERT(ptr->getClipPtr(clipId)->getCurrentTrackId() == -1);
149
        target_playlist = ptr->getClipPtr(clipId)->getSubPlaylistIndex();
150
        length = ptr->getClipPtr(clipId)->getPlaytime() - 1;
151
        /*if (target_playlist == 1 && ptr->getClipPtr(clipId)->getMixDuration() == 0) {
152
            target_playlist = 0;
153
        }*/
154
        // qDebug()<<"==== GOT TRARGET PLAYLIST: "<<target_playlist;
155
156
157
158
    } else {
        qDebug() << "impossible to get parent timeline";
        Q_ASSERT(false);
    }
159
160
161
    // Find out the clip id at position
    int target_clip = m_playlists[target_playlist].get_clip_index_at(position);
    int count = m_playlists[target_playlist].count();
162

Nicolas Carion's avatar
Nicolas Carion committed
163
    // we create the function that has to be executed after the melt order. This is essentially book-keeping
Vincent Pinon's avatar
Vincent Pinon committed
164
    auto end_function = [clipId, this, position, updateView, finalMove](int subPlaylist) {
165
        if (auto ptr = m_parent.lock()) {
166
            std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
167
168
            m_allClips[clip->getId()] = clip; // store clip
            // update clip position and track
169
            clip->setPosition(position);
170
            if (finalMove) {
171
                clip->setSubPlaylistIndex(subPlaylist, m_id);
172
            }
173
            int new_in = clip->getPosition();
174
            int new_out = new_in + clip->getPlaytime();
175
176
            ptr->m_snaps->addPoint(new_in);
            ptr->m_snaps->addPoint(new_out);
177
            if (updateView) {
178
                int clip_index = getRowfromClip(clipId);
179
                ptr->_beginInsertRows(ptr->makeTrackIndexFromID(m_id), clip_index, clip_index);
180
                ptr->_endInsertRows();
181
                bool audioOnly = clip->isAudioOnly();
182
                if (!audioOnly && !isHidden() && !isAudioTrack()) {
183
184
185
                    // only refresh monitor if not an audio track and not hidden
                    ptr->checkRefresh(new_in, new_out);
                }
186
                if (!audioOnly && finalMove && !isAudioTrack()) {
Vincent Pinon's avatar
Vincent Pinon committed
187
                    emit ptr->invalidateZone(new_in, new_out);
188
                }
189
            }
190
            return true;
Nicolas Carion's avatar
Nicolas Carion committed
191
192
193
        }
        qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
        return false;
194
    };
195
196
197
198
    if (!finalMove && !hasMix(clipId)) {
        if (allowedClipMixes.isEmpty()) {
            if (!m_playlists[0].is_blank_at(position) || !m_playlists[1].is_blank_at(position)) {
                // Track is not empty
199
                qWarning() << "clip insert failed - non blank 1";
200
201
202
203
204
205
                return []() { return false; };
            }
        } else {
            // This is a group move with a mix, some clips are allowed
            if (!m_playlists[target_playlist].is_blank_at(position)) {
                // Track is not empty
206
                qWarning() << "clip insert failed - non blank 2";
207
208
209
210
                return []() { return false; };
            }
            // Check if there are clips on the other playlist, and if they are in the allowed list
            std::unordered_set<int> collisions = getClipsInRange(position, position + length);
211
            qDebug() << "==== DETECTING COLLISIONS AT: " << position << " to " << (position + length) << " COUNT: " << collisions.size();
212
213
214
            for (int c : collisions) {
                if (!allowedClipMixes.contains(c)) {
                    // Track is not empty
215
                    qWarning() << "clip insert failed - non blank 3";
216
217
218
219
                    return []() { return false; };
                }
            }
        }
220
    }
221
    if (target_clip >= count && m_playlists[target_playlist].is_blank_at(position)) {
Nicolas Carion's avatar
Nicolas Carion committed
222
        // In that case, we append after, in the first playlist
223
        return [this, position, clipId, end_function, finalMove, groupMove, target_playlist]() {
224
225
226
227
            if (isLocked()) {
                qWarning() << "clip insert failed - locked track";
                return false;
            }
228
            if (auto ptr = m_parent.lock()) {
229
                // Lock MLT playlist so that we don't end up with an invalid frame being displayed
230
                m_playlists[target_playlist].lock();
231
                std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
232
                clip->setCurrentTrackId(m_id, finalMove);
233
234
235
                int index = m_playlists[target_playlist].insert_at(position, *clip, 1);
                m_playlists[target_playlist].consolidate_blanks();
                m_playlists[target_playlist].unlock();
236
                if (finalMove && !groupMove) {
237
238
                    ptr->updateDuration();
                }
239
                return index != -1 && end_function(target_playlist);
Nicolas Carion's avatar
Nicolas Carion committed
240
241
242
243
244
            }
            qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
            return false;
        };
    }
245
246
    if (m_playlists[target_playlist].is_blank_at(position)) {
        int blank_end = getBlankEnd(position, target_playlist);
Nicolas Carion's avatar
Nicolas Carion committed
247
        if (blank_end >= position + length) {
248
            return [this, position, clipId, end_function, target_playlist]() {
249
                if (isLocked()) return false;
Nicolas Carion's avatar
Nicolas Carion committed
250
                if (auto ptr = m_parent.lock()) {
251
                    // Lock MLT playlist so that we don't end up with an invalid frame being displayed
252
                    m_playlists[target_playlist].lock();
Nicolas Carion's avatar
Nicolas Carion committed
253
                    std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
254
                    clip->setCurrentTrackId(m_id);
255
256
257
258
                    int index = m_playlists[target_playlist].insert_at(position, *clip, 1);
                    m_playlists[target_playlist].consolidate_blanks();
                    m_playlists[target_playlist].unlock();
                    return index != -1 && end_function(target_playlist);
Nicolas Carion's avatar
Nicolas Carion committed
259
                }
260
261
                qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
                return false;
Nicolas Carion's avatar
Nicolas Carion committed
262
            };
263
        }
Nicolas Carion's avatar
Nicolas Carion committed
264
265
    }
    return []() { return false; };
266
267
}

268
269
bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, bool newInsertion,
                                      const QList<int> &allowedClipMixes)
270
{
Nicolas Carion's avatar
Nicolas Carion committed
271
    QWriteLocker locker(&m_lock);
272
    if (isLocked()) {
273
        qDebug() << "==== ERROR INSERT OK LOCKED TK";
274
275
        return false;
    }
276
    if (position < 0) {
277
        qDebug() << "==== ERROR INSERT ON NEGATIVE POS: " << position;
278
279
        return false;
    }
280
    if (auto ptr = m_parent.lock()) {
281
282
        std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
        if (isAudioTrack() && !clip->canBeAudio()) {
283
            qDebug() << "// ATTEMPTING TO INSERT NON AUDIO CLIP ON AUDIO TRACK";
284
285
            return false;
        }
286
        if (!isAudioTrack() && !clip->canBeVideo()) {
287
            qDebug() << "// ATTEMPTING TO INSERT NON VIDEO CLIP ON VIDEO TRACK";
288
289
290
291
292
            return false;
        }
        Fun local_undo = []() { return true; };
        Fun local_redo = []() { return true; };
        bool res = true;
293
294
        if (clip->clipState() != PlaylistState::Disabled) {
            res = clip->setClipState(isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo);
295
        }
296
        int duration = trackDuration();
297
        auto operation = requestClipInsertion_lambda(clipId, position, updateView, finalMove, groupMove, allowedClipMixes);
298
299
        res = res && operation();
        if (res) {
300
301
302
303
            if (finalMove && duration != trackDuration()) {
                // A clip move changed the track duration, update track effects
                m_effectStack->adjustStackLength(true, 0, duration, 0, trackDuration(), 0, undo, redo, true);
            }
304
            auto reverse = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove, newInsertion && finalMove);
305
306
307
308
309
310
311
            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;
312
    }
313
314
315
    return false;
}

316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
void TrackModel::temporaryUnplugClip(int clipId)
{
    QWriteLocker locker(&m_lock);
    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));
    std::unique_ptr<Mlt::Producer> prod(m_playlists[target_track].replace_with_blank(target_clip));
    m_playlists[target_track].unlock();
}

void TrackModel::temporaryReplugClip(int cid)
{
    QWriteLocker locker(&m_lock);
    int clip_position = m_allClips[cid]->getPosition();
    int target_track = m_allClips[cid]->getSubPlaylistIndex();
    m_playlists[target_track].lock();
    if (auto ptr = m_parent.lock()) {
        std::shared_ptr<ClipModel> clip = ptr->getClipPtr(cid);
        m_playlists[target_track].insert_at(clip_position, *clip, 1);
    }
    m_playlists[target_track].unlock();
}

344
345
void TrackModel::replugClip(int clipId)
{
346
    QWriteLocker locker(&m_lock);
347
    int clip_position = m_allClips[clipId]->getPosition();
348
    auto clip_loc = getClipIndexAt(clip_position, m_allClips[clipId]->getSubPlaylistIndex());
349
350
351
352
353
354
    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));
355
    std::unique_ptr<Mlt::Producer> prod(m_playlists[target_track].replace_with_blank(target_clip));
356
357
    if (auto ptr = m_parent.lock()) {
        std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
358
        m_playlists[target_track].insert_at(clip_position, *clip, 1);
359
        if (!clip->isAudioOnly() && !isAudioTrack()) {
Vincent Pinon's avatar
Vincent Pinon committed
360
            emit ptr->invalidateZone(clip->getIn(), clip->getOut());
361
362
363
364
365
        }
        if (!clip->isAudioOnly() && !isHidden() && !isAudioTrack()) {
            // only refresh monitor if not an audio track and not hidden
            ptr->checkRefresh(clip->getIn(), clip->getOut());
        }
366
    }
367
    m_playlists[target_track].consolidate_blanks();
368
369
370
    m_playlists[target_track].unlock();
}

371
Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove, bool groupMove, bool finalDeletion)
372
{
Nicolas Carion's avatar
Nicolas Carion committed
373
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
374
    // Find index of clip
375
    int clip_position = m_allClips[clipId]->getPosition();
376
    bool audioOnly = m_allClips[clipId]->isAudioOnly();
377
    int old_in = clip_position;
378
    int old_out = old_in + m_allClips[clipId]->getPlaytime();
379
    return [clip_position, clipId, old_in, old_out, updateView, audioOnly, finalMove, groupMove, finalDeletion, this]() {
380
        if (isLocked()) return false;
381
382
383
384
385
386
387
        if (finalDeletion && m_allClips[clipId]->selected) {
            m_allClips[clipId]->selected = false;
            if (auto ptr = m_parent.lock()) {
                // item was selected, unselect
                ptr->requestClearSelection(true);
            }
        }
388
389
        int target_track = m_allClips[clipId]->getSubPlaylistIndex();
        auto clip_loc = getClipIndexAt(clip_position, target_track);
390
391
392
393
394
        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();
395
        }
396
        int target_clip = clip_loc.second;
397
398
        // lock MLT playlist so that we don't end up with invalid frames in monitor
        m_playlists[target_track].lock();
399
400
401
        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);
402
        if (prod != nullptr) {
403
            m_playlists[target_track].consolidate_blanks();
404
            m_allClips[clipId]->setCurrentTrackId(-1);
405
            // m_allClips[clipId]->setSubPlaylistIndex(-1);
406
            m_allClips.erase(clipId);
407
            delete prod;
408
            m_playlists[target_track].unlock();
409
            if (auto ptr = m_parent.lock()) {
410
                ptr->m_snaps->removePoint(old_in);
411
                ptr->m_snaps->removePoint(old_out);
412
413
                if (finalMove) {
                    if (!audioOnly && !isAudioTrack()) {
Vincent Pinon's avatar
Vincent Pinon committed
414
                        emit ptr->invalidateZone(old_in, old_out);
415
                    }
416
                    if (!groupMove && target_clip >= m_playlists[target_track].count()) {
417
418
419
                        // deleted last clip in playlist
                        ptr->updateDuration();
                    }
420
                }
421
                if (!audioOnly && !isHidden() && !isAudioTrack()) {
422
423
424
                    // only refresh monitor if not an audio track and not hidden
                    ptr->checkRefresh(old_in, old_out);
                }
425
            }
426
427
            return true;
        }
428
        m_playlists[target_track].unlock();
429
430
431
432
        return false;
    };
}

433
434
bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, bool finalDeletion,
                                     const QList<int> &allowedClipMixes)
435
{
Nicolas Carion's avatar
Nicolas Carion committed
436
    QWriteLocker locker(&m_lock);
437
    Q_ASSERT(m_allClips.count(clipId) > 0);
438
439
440
    if (isLocked()) {
        return false;
    }
441
    auto old_clip = m_allClips[clipId];
442
    int old_position = old_clip->getPosition();
443
    // qDebug() << "/// REQUESTOING CLIP DELETION_: " << updateView;
444
    int duration = trackDuration();
445
    if (finalDeletion) {
446
        pCore->taskManager.discardJobs({ObjectType::TimelineClip, clipId});
447
    }
448
    auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove, finalDeletion);
449
    if (operation()) {
450
451
452
453
        if (finalMove && duration != trackDuration()) {
            // A clip move changed the track duration, update track effects
            m_effectStack->adjustStackLength(true, 0, duration, 0, trackDuration(), 0, undo, redo, true);
        }
454
        auto reverse = requestClipInsertion_lambda(clipId, old_position, updateView, finalMove, groupMove, allowedClipMixes);
455
        UPDATE_UNDO_REDO(operation, reverse, undo, redo);
456
457
458
459
460
        return true;
    }
    return false;
}

461
462
463
464
int TrackModel::getBlankSizeAtPos(int frame)
{
    READ_LOCK();
    int min_length = 0;
465
    int blank_length = 0;
Nicolas Carion's avatar
Nicolas Carion committed
466
    for (auto &m_playlist : m_playlists) {
467
468
469
        int playlistLength = m_playlist.get_length();
        if (frame >= playlistLength) {
            continue;
470
471
472
473
474
475
476
        } else {
            int ix = m_playlist.get_clip_index_at(frame);
            if (m_playlist.is_blank(ix)) {
                blank_length = m_playlist.clip_length(ix);
            } else {
                // There is a clip at that position, abort
                return 0;
477
478
            }
        }
479
480
481
        if (min_length == 0 || blank_length < min_length) {
            min_length = blank_length;
        }
482
    }
483
484
485
486
    if (blank_length == 0) {
        // playlists are shorter than frame
        return -1;
    }
487
488
489
    return min_length;
}

490
491
492
493
494
495
496
497
498
499
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
500
    int other_track = 1 - track;
501
502
503
504
505
    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));
    }
506
507
508
    return end_pos - position;
}

509
QPair<int, int> TrackModel::validateCompositionLength(int pos, int offset, int duration, int endPos)
510
511
512
513
514
515
516
517
518
519
{
    int startPos = pos;
    bool startingFromOffset = false;
    if (duration < offset) {
        startPos += offset;
        startingFromOffset = true;
        if (startPos + duration > endPos) {
            startPos = endPos - duration;
        }
    }
520

521
522
523
524
    int compsitionEnd = startPos + duration;
    std::unordered_set<int> existing;
    if (startingFromOffset) {
        existing = getCompositionsInRange(startPos, compsitionEnd);
525
        for (int id : existing) {
526
527
528
            if (m_allCompositions[id]->getPosition() < startPos) {
                int end = m_allCompositions[id]->getPosition() + m_allCompositions[id]->getPlaytime();
                startPos = qMax(startPos, end);
529
530
            }
        }
531
532
533
534
535
536
    } else if (offset > 0) {
        existing = getCompositionsInRange(startPos, startPos + offset);
        for (int id : existing) {
            int end = m_allCompositions[id]->getPosition() + m_allCompositions[id]->getPlaytime();
            startPos = qMax(startPos, end);
        }
537
    }
538
539
540
541
    existing = getCompositionsInRange(startPos, compsitionEnd);
    for (int id : existing) {
        int start = m_allCompositions[id]->getPosition();
        compsitionEnd = qMin(compsitionEnd, start);
542
    }
543
    return {startPos, compsitionEnd - startPos};
544
545
}

546
int TrackModel::getBlankSizeNearClip(int clipId, bool after)
547
{
Nicolas Carion's avatar
Nicolas Carion committed
548
    READ_LOCK();
549
550
    Q_ASSERT(m_allClips.count(clipId) > 0);
    int clip_position = m_allClips[clipId]->getPosition();
551
552
553
    auto clip_loc = getClipIndexAt(clip_position);
    int track = clip_loc.first;
    int index = clip_loc.second;
Nicolas Carion's avatar
Nicolas Carion committed
554
    int other_index; // index in the other track
555
    int other_track = 1 - track;
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
    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));
572
573
    } else if (!after) {
        length = std::min(length, m_playlists[track].clip_start(clip_loc.second) - m_playlists[track].get_length());
574
575
576
577
578
579
    }
    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));
580
581
    } else if (!after) {
        length = std::min(length, m_playlists[track].clip_start(clip_loc.second) - m_playlists[other_track].get_length());
582
583
584
585
    }
    return length;
}

586
int TrackModel::getBlankSizeNearComposition(int compoId, bool after)
587
{
Nicolas Carion's avatar
Nicolas Carion committed
588
    READ_LOCK();
589
590
    Q_ASSERT(m_allCompositions.count(compoId) > 0);
    int clip_position = m_allCompositions[compoId]->getPosition();
591
592
593
594
    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();
595
    int length = INT_MAX;
596
597
598
599
600
601
602
603
    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
604
605
606
            return clip_position - it->first - m_allCompositions[it->second]->getPlaytime();
        }
        return clip_position;
607
    }
608
609
610
    return length;
}

611
Fun TrackModel::requestClipResize_lambda(int clipId, int in, int out, bool right, bool hasMix)
612
{
Nicolas Carion's avatar
Nicolas Carion committed
613
    QWriteLocker locker(&m_lock);
614
    int clip_position = m_allClips[clipId]->getPosition();
615
    int old_in = clip_position;
616
    int old_out = old_in + m_allClips[clipId]->getPlaytime();
617
    auto clip_loc = getClipIndexAt(clip_position, m_allClips[clipId]->getSubPlaylistIndex());
618
619
620
    int target_track = clip_loc.first;
    int target_clip = clip_loc.second;
    Q_ASSERT(target_clip < m_playlists[target_track].count());
621
    int size = out - in + 1;
622
    bool checkRefresh = false;
623
    if (!isHidden() && !isAudioTrack()) {
624
625
        checkRefresh = true;
    }
Vincent Pinon's avatar
Vincent Pinon committed
626
    auto update_snaps = [old_in, old_out, checkRefresh, right, this](int new_in, int new_out) {
627
        if (auto ptr = m_parent.lock()) {
628
629
630
631
632
633
634
            if (right) {
                ptr->m_snaps->removePoint(old_out);
                ptr->m_snaps->addPoint(new_out);
            } else {
                ptr->m_snaps->removePoint(old_in);
                ptr->m_snaps->addPoint(new_in);
            }
635
            if (checkRefresh) {
636
637
638
639
                if (right) {
                    if (old_out < new_out) {
                        ptr->checkRefresh(old_out, new_out);
                    } else {
640
                        ptr->checkRefresh(new_out, old_out);
641
642
643
644
645
646
                    }
                } else if (old_in < new_in) {
                    ptr->checkRefresh(old_in, new_in);
                } else {
                    ptr->checkRefresh(new_in, old_in);
                }
647
            }
648
649
650
651
652
653
        } else {
            qDebug() << "Error : clip resize failed because parent timeline is not available anymore";
            Q_ASSERT(false);
        }
    };

654
    int delta = m_allClips[clipId]->getPlaytime() - size;
655
    if (delta == 0) {
Nicolas Carion's avatar
Nicolas Carion committed
656
        return []() { return true; };
657
    }
Nicolas Carion's avatar
Nicolas Carion committed
658
659
    if (delta > 0) { // we shrink clip
        return [right, target_clip, target_track, clip_position, delta, in, out, clipId, update_snaps, this]() {
660
            if (isLocked()) return false;
661
662
663
            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
664
            m_playlists[target_track].lock();
665
            // The second is parameter is delta - 1 because this function expects an out time, which is basically size - 1
666
            m_playlists[target_track].insert_blank(blank_index, delta - 1);
667
            if (!right) {
668
                m_allClips[clipId]->setPosition(clip_position + delta);
Nicolas Carion's avatar
Nicolas Carion committed
669
                // Because we inserted blank before, the index of our clip has increased
670
671
                target_clip_mutable++;
            }
672
            int err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out);
Nicolas Carion's avatar
Nicolas Carion committed
673
            // make sure to do this after, to avoid messing the indexes
674
            m_playlists[target_track].consolidate_blanks();
675
            m_playlists[target_track].unlock();
676
            if (err == 0) {
677
                update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
678
679
680
681
682
683
                if (right && m_playlists[target_track].count() - 1 == target_clip_mutable) {
                    // deleted last clip in playlist
                    if (auto ptr = m_parent.lock()) {
                        ptr->updateDuration();
                    }
                }
684
            }
685
686
            return err == 0;
        };
Nicolas Carion's avatar
Nicolas Carion committed
687
688
    }
    int blank = -1;
689
690
    int startPos = clip_position;
    if (hasMix) {
691
        startPos += m_allClips[clipId]->getMixDuration();
692
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
693
    int endPos = m_allClips[clipId]->getPosition() + (out - in);
694
    int other_blank_end = getBlankEnd(startPos, 1 - target_track);
Nicolas Carion's avatar
Nicolas Carion committed
695
    if (right) {
696
        if (target_clip == m_playlists[target_track].count() - 1 && (hasMix || other_blank_end >= endPos)) {
697
            // clip is last, it can always be extended
698
699
700
701
            if (hasMix && other_blank_end < endPos && !hasEndMix(clipId)) {
                // If clip has a start mix only, limit to next clip on other track
                return []() { return false; };
            }
Nicolas Carion's avatar
Nicolas Carion committed
702
            return [this, target_clip, target_track, in, out, update_snaps, clipId]() {
703
                if (isLocked()) return false;
Nicolas Carion's avatar
Nicolas Carion committed
704
705
706
707
                // 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);
708
                    clip->parent().set("out", out);
Nicolas Carion's avatar
Nicolas Carion committed
709
710
711
712
                    clip->set("length", out + 1);
                }
                int err = m_playlists[target_track].resize_clip(target_clip, in, out);
                if (err == 0) {
713
                    update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
Nicolas Carion's avatar
Nicolas Carion committed
714
                }
715
                m_playlists[target_track].consolidate_blanks();
716
                if (m_playlists[target_track].count() - 1 == target_clip) {
717
                    // Resized last clip in playlist
718
719
720
721
                    if (auto ptr = m_parent.lock()) {
                        ptr->updateDuration();
                    }
                }
Nicolas Carion's avatar
Nicolas Carion committed
722
723
                return err == 0;
            };
724
        } else {
Nicolas Carion's avatar
Nicolas Carion committed
725
726
727
728
729
730
731
732
733
734
735
        }
        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);
736
        if (blank_length + delta >= 0 && (hasMix || other_blank_end >= out - in)) {
Nicolas Carion's avatar
Nicolas Carion committed
737
            return [blank_length, blank, right, clipId, delta, update_snaps, this, in, out, target_clip, target_track]() {
738
                if (isLocked()) return false;
Nicolas Carion's avatar
Nicolas Carion committed
739
740
                int target_clip_mutable = target_clip;
                int err = 0;
741
                m_playlists[target_track].lock();
Nicolas Carion's avatar
Nicolas Carion committed
742
743
744
745
746
747
748
749
750
                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) {
751
                    QScopedPointer<Mlt::Producer> clip(m_playlists[target_track].get_clip(target_clip_mutable));
752
                    if (out >= clip->get_length()) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
753
754
                        clip->parent().set("length", out + 1);
                        clip->parent().set("out", out);
755
                        clip->set("length", out + 1);
756
                        clip->set("out", out);
757
                    }
Nicolas Carion's avatar
Nicolas Carion committed
758
759
760
761
762
763
                    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) {
764
                    update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
Nicolas Carion's avatar
Nicolas Carion committed
765
                }
766
                m_playlists[target_track].consolidate_blanks();
767
                m_playlists[target_track].unlock();
Nicolas Carion's avatar
Nicolas Carion committed
768
769
                return err == 0;
            };
770
        } else {
771
        }
Nicolas Carion's avatar
Nicolas Carion committed
772
    }
773
774
775
776
    return []() {
        qDebug() << "=====FULL FAILURE ";
        return false;
    };
777
778
}

779
780
781
782
int TrackModel::getId() const
{
    return m_id;
}
783

784
785
786
787
788
789
790
791
792
793
794
int TrackModel::getClipByStartPosition(int position) const
{
    READ_LOCK();
    for (auto &clip : m_allClips) {
        if (clip.second->getPosition() == position) {
            return clip.second->getId();
        }
    }
    return -1;
}

795
int TrackModel::getClipByPosition(int position, int playlist)
796
{
Nicolas Carion's avatar
Nicolas Carion committed
797
    READ_LOCK();
798
    QSharedPointer<Mlt::Producer> prod(nullptr);
799
    if ((playlist == 0 || playlist == -1) && m_playlists[0].count() > 0) {
800
        prod = QSharedPointer<Mlt::Producer>(m_playlists[0].get_clip_at(position));
Nicolas Carion's avatar
Nicolas Carion committed
801
    }
802
    if (playlist != 0 && (!prod || prod->is_blank()) && m_playlists[1].count() > 0) {
803
804
805
        prod = QSharedPointer<Mlt::Producer>(m_playlists[1].get_clip_at(position));
    }
    if (!prod || prod->is_blank()) {
806
        return -1;
807
    }
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
    int cid = prod->get_int("_kdenlive_cid");
    if (playlist == -1) {
        if (hasStartMix(cid)) {
            if (position < m_allClips[cid]->getPosition() + m_allClips[cid]->getMixCutPosition()) {
                return m_mixList.key(cid, -1);
            }
        }
        if (m_mixList.contains(cid)) {
            // Clip has end mix
            int otherId