trackmodel.cpp 98.8 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 "core.h"
25
#include "compositionmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
26
#include "effects/effectstack/model/effectstackmodel.hpp"
27
#include "transitions/transitionsrepository.hpp"
28
#include "kdenlivesettings.h"
Vincent Pinon's avatar
Vincent Pinon committed
29
#ifdef CRASH_AUTO_TEST
30
#include "logger.hpp"
Vincent Pinon's avatar
Vincent Pinon committed
31
32
33
#else
#define TRACE_CONSTR(...)
#endif
34
#include "snapmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
35
#include "timelinemodel.hpp"
36
#include <QDebug>
37
#include <QModelIndex>
38
#include <memory>
Nicolas Carion's avatar
Nicolas Carion committed
39
#include <mlt++/MltTransition.h>
40

41
TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, int id, const QString &trackName, bool audioTrack)
42
43
    : m_parent(parent)
    , m_id(id == -1 ? TimelineModel::getNextId() : id)
Nicolas Carion's avatar
Nicolas Carion committed
44
    , m_lock(QReadWriteLock::Recursive)
45
{
46
    if (auto ptr = parent.lock()) {
Nicolas Carion's avatar
Nicolas Carion committed
47
        m_track = std::make_shared<Mlt::Tractor>(*ptr->getProfile());
48
49
        m_playlists[0].set_profile(*ptr->getProfile());
        m_playlists[1].set_profile(*ptr->getProfile());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
50
51
        m_track->insert_track(m_playlists[0], 0);
        m_track->insert_track(m_playlists[1], 1);
52
        m_mainPlaylist = std::make_shared<Mlt::Producer>(&m_playlists[0]);
53
54
55
56
57
        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
58
59
            for (auto &m_playlist : m_playlists) {
                m_playlist.set("hide", 1);
60
61
            }
        }
62
        // For now we never use the second playlist, only planned for same track transitions
63
        m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
64
        m_track->set("kdenlive:timeline_active", 1);
65
        m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
66
67
68
69
        // 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);
70
        QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector<int> roles) {
71
72
            if (auto ptr2 = m_parent.lock()) {
                QModelIndex ix = ptr2->makeTrackIndexFromID(m_id);
73
                qDebug()<<"==== TRACK ZONES CHANGED";
Vincent Pinon's avatar
Vincent Pinon committed
74
                emit ptr2->dataChanged(ix, ix, roles);
75
76
            }
        });
77
78
79
80
    } else {
        qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
        Q_ASSERT(false);
    }
81
82
}

Nicolas Carion's avatar
Nicolas Carion committed
83
TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, Mlt::Tractor mltTrack, int id)
84
85
86
87
    : m_parent(parent)
    , m_id(id == -1 ? TimelineModel::getNextId() : id)
{
    if (auto ptr = parent.lock()) {
Nicolas Carion's avatar
Nicolas Carion committed
88
        m_track = std::make_shared<Mlt::Tractor>(mltTrack);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
89
90
        m_playlists[0] = *m_track->track(0);
        m_playlists[1] = *m_track->track(1);
91
        m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
92
93
94
95
96
97
    } else {
        qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
        Q_ASSERT(false);
    }
}

98
99
TrackModel::~TrackModel()
{
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
100
101
    m_track->remove_track(1);
    m_track->remove_track(0);
102
103
}

104
int TrackModel::construct(const std::weak_ptr<TimelineModel> &parent, int id, int pos, const QString &trackName, bool audioTrack)
105
{
106
    std::shared_ptr<TrackModel> track(new TrackModel(parent, id, trackName, audioTrack));
107
    TRACE_CONSTR(track.get(), parent, id, pos, trackName, audioTrack);
108
    id = track->m_id;
109
    if (auto ptr = parent.lock()) {
110
        ptr->registerTrack(std::move(track), pos);
111
112
113
114
    } else {
        qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
        Q_ASSERT(false);
    }
115
    return id;
116
}
117

118
int TrackModel::getClipsCount()
119
{
Nicolas Carion's avatar
Nicolas Carion committed
120
    READ_LOCK();
121
#ifdef QT_DEBUG
122
    int count = 0;
Nicolas Carion's avatar
Nicolas Carion committed
123
124
125
    for (auto &m_playlist : m_playlists) {
        for (int i = 0; i < m_playlist.count(); i++) {
            if (!m_playlist.is_blank(i)) {
126
127
                count++;
            }
128
129
130
        }
    }
    Q_ASSERT(count == static_cast<int>(m_allClips.size()));
131
#else
Vincent Pinon's avatar
Vincent Pinon committed
132
    int count = int(m_allClips.size());
133
#endif
134
    return count;
135
136
}

137
bool TrackModel::switchPlaylist(int clipId, int position, int sourcePlaylist, int destPlaylist)
138
139
{
    QWriteLocker locker(&m_lock);
140
141
142
143
144
145
    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));
146
    m_playlists[sourcePlaylist].consolidate_blanks();
147
148
    if (auto ptr = m_parent.lock()) {
        std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
149
        clip->setSubPlaylistIndex(destPlaylist, m_id);
150
        int index = m_playlists[destPlaylist].insert_at(position, *clip, 1);
151
        m_playlists[destPlaylist].consolidate_blanks();
152
153
154
155
156
        return index != -1;
    }
    return false;
}

157
Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove, bool groupMove, QList<int> allowedClipMixes)
158
{
Nicolas Carion's avatar
Nicolas Carion committed
159
    QWriteLocker locker(&m_lock);
160
    // By default, insertion occurs in topmost track
161
    int target_playlist = 0;
162
    int length = -1;
163
164
    if (auto ptr = m_parent.lock()) {
        Q_ASSERT(ptr->getClipPtr(clipId)->getCurrentTrackId() == -1);
165
        target_playlist = ptr->getClipPtr(clipId)->getSubPlaylistIndex();
166
        length = ptr->getClipPtr(clipId)->getPlaytime();
167
        /*if (target_playlist == 1 && ptr->getClipPtr(clipId)->getMixDuration() == 0) {
168
            target_playlist = 0;
169
        }*/
170
        //qDebug()<<"==== GOT TRARGET PLAYLIST: "<<target_playlist;
171
172
173
174
    } else {
        qDebug() << "impossible to get parent timeline";
        Q_ASSERT(false);
    }
175
176
177
    // 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();
178

Nicolas Carion's avatar
Nicolas Carion committed
179
    // 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
180
    auto end_function = [clipId, this, position, updateView, finalMove](int subPlaylist) {
181
        if (auto ptr = m_parent.lock()) {
182
            std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
183
184
            m_allClips[clip->getId()] = clip; // store clip
            // update clip position and track
185
            clip->setPosition(position);
186
            if (finalMove) {
187
                clip->setSubPlaylistIndex(subPlaylist, m_id);
188
            }
189
            int new_in = clip->getPosition();
190
            int new_out = new_in + clip->getPlaytime();
191
192
            ptr->m_snaps->addPoint(new_in);
            ptr->m_snaps->addPoint(new_out);
193
            if (updateView) {
194
                int clip_index = getRowfromClip(clipId);
195
                ptr->_beginInsertRows(ptr->makeTrackIndexFromID(m_id), clip_index, clip_index);
196
                ptr->_endInsertRows();
197
                bool audioOnly = clip->isAudioOnly();
198
                if (!audioOnly && !isHidden() && !isAudioTrack()) {
199
200
201
                    // only refresh monitor if not an audio track and not hidden
                    ptr->checkRefresh(new_in, new_out);
                }
202
                if (!audioOnly && finalMove && !isAudioTrack()) {
Vincent Pinon's avatar
Vincent Pinon committed
203
                    emit ptr->invalidateZone(new_in, new_out);
204
                }
205
            }
206
            return true;
Nicolas Carion's avatar
Nicolas Carion committed
207
208
209
        }
        qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
        return false;
210
    };
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
    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
                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
                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);
            qDebug()<<"==== DETECTING COLLISIONS AT: "<<position<<" to "<<(position+length)<<" COUNT: "<<collisions.size();
            for (int c : collisions) {
                if (!allowedClipMixes.contains(c)) {
                    // Track is not empty
                    return []() { return false; };
                }
            }
        }
233
    }
234
    if (target_clip >= count && m_playlists[target_playlist].is_blank_at(position)) {
Nicolas Carion's avatar
Nicolas Carion committed
235
        // In that case, we append after, in the first playlist
236
        return [this, position, clipId, end_function, finalMove, groupMove, target_playlist]() {
237
            if (isLocked()) return false;
238
            if (auto ptr = m_parent.lock()) {
239
                // Lock MLT playlist so that we don't end up with an invalid frame being displayed
240
                m_playlists[target_playlist].lock();
241
                std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
242
                clip->setCurrentTrackId(m_id, finalMove);
243
244
245
                int index = m_playlists[target_playlist].insert_at(position, *clip, 1);
                m_playlists[target_playlist].consolidate_blanks();
                m_playlists[target_playlist].unlock();
246
                if (finalMove && !groupMove) {
247
248
                    ptr->updateDuration();
                }
249
                return index != -1 && end_function(target_playlist);
Nicolas Carion's avatar
Nicolas Carion committed
250
251
252
253
254
            }
            qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
            return false;
        };
    }
255
256
    if (m_playlists[target_playlist].is_blank_at(position)) {
        int blank_end = getBlankEnd(position, target_playlist);
Nicolas Carion's avatar
Nicolas Carion committed
257
        if (blank_end >= position + length) {
258
            return [this, position, clipId, end_function, target_playlist]() {
259
                if (isLocked()) return false;
Nicolas Carion's avatar
Nicolas Carion committed
260
                if (auto ptr = m_parent.lock()) {
261
                    // Lock MLT playlist so that we don't end up with an invalid frame being displayed
262
                    m_playlists[target_playlist].lock();
Nicolas Carion's avatar
Nicolas Carion committed
263
                    std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
264
                    clip->setCurrentTrackId(m_id);
265
266
267
268
                    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
269
                }
270
271
                qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
                return false;
Nicolas Carion's avatar
Nicolas Carion committed
272
            };
273
        }
Nicolas Carion's avatar
Nicolas Carion committed
274
275
    }
    return []() { return false; };
276
277
}

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

325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
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();
}


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

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

443
bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, bool finalDeletion, QList<int> allowedClipMixes)
444
{
Nicolas Carion's avatar
Nicolas Carion committed
445
    QWriteLocker locker(&m_lock);
446
    Q_ASSERT(m_allClips.count(clipId) > 0);
447
448
449
    if (isLocked()) {
        return false;
    }
450
    auto old_clip = m_allClips[clipId];
451
    int old_position = old_clip->getPosition();
452
    // qDebug() << "/// REQUESTOING CLIP DELETION_: " << updateView;
453
    int duration = trackDuration();
454
    if (finalDeletion) {
455
        pCore->taskManager.discardJobs({ObjectType::TimelineClip,clipId});
456
    }
457
    auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove, finalDeletion);
458
    if (operation()) {
459
460
461
462
        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);
        }
463
        auto reverse = requestClipInsertion_lambda(clipId, old_position, updateView, finalMove, groupMove, allowedClipMixes);
464
        UPDATE_UNDO_REDO(operation, reverse, undo, redo);
465
466
467
468
469
        return true;
    }
    return false;
}

470
471
472
473
int TrackModel::getBlankSizeAtPos(int frame)
{
    READ_LOCK();
    int min_length = 0;
474
    int blank_length = 0;
Nicolas Carion's avatar
Nicolas Carion committed
475
    for (auto &m_playlist : m_playlists) {
476
477
478
        int playlistLength = m_playlist.get_length();
        if (frame >= playlistLength) {
            continue;
479
480
481
482
483
484
485
        } 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;
486
487
            }
        }
488
489
490
        if (min_length == 0 || blank_length < min_length) {
            min_length = blank_length;
        }
491
    }
492
493
494
495
    if (blank_length == 0) {
        // playlists are shorter than frame
        return -1;
    }
496
497
498
    return min_length;
}

499
500
501
502
503
504
505
506
507
508
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
509
    int other_track = 1 - track;
510
511
512
513
514
    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));
    }
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
    return end_pos - position;
}

QPair <int, int> TrackModel::validateCompositionLength(int pos, int offset, int duration, int endPos)
{
    int startPos = pos;
    bool startingFromOffset = false;
    if (duration < offset) {
        startPos += offset;
        startingFromOffset = true;
        if (startPos + duration > endPos) {
            startPos = endPos - duration;
        }
    }
    
    int compsitionEnd = startPos + duration;
    std::unordered_set<int> existing;
    if (startingFromOffset) {
        existing = getCompositionsInRange(startPos, compsitionEnd);
534
        for (int id : existing) {
535
536
537
            if (m_allCompositions[id]->getPosition() < startPos) {
                int end = m_allCompositions[id]->getPosition() + m_allCompositions[id]->getPlaytime();
                startPos = qMax(startPos, end);
538
539
            }
        }
540
541
542
543
544
545
    } 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);
        }
546
    }
547
548
549
550
    existing = getCompositionsInRange(startPos, compsitionEnd);
    for (int id : existing) {
        int start = m_allCompositions[id]->getPosition();
        compsitionEnd = qMin(compsitionEnd, start);
551
    }
552
    return {startPos, compsitionEnd - startPos};
553
554
}

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

595
int TrackModel::getBlankSizeNearComposition(int compoId, bool after)
596
{
Nicolas Carion's avatar
Nicolas Carion committed
597
    READ_LOCK();
598
599
    Q_ASSERT(m_allCompositions.count(compoId) > 0);
    int clip_position = m_allCompositions[compoId]->getPosition();
600
601
602
603
    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();
604
    int length = INT_MAX;
605
606
607
608
609
610
611
612
    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
613
614
615
            return clip_position - it->first - m_allCompositions[it->second]->getPlaytime();
        }
        return clip_position;
616
    }
617
618
619
    return length;
}

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

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

776
777
778
779
int TrackModel::getId() const
{
    return m_id;
}
780

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

792
int TrackModel::getClipByPosition(int position, int playlist)
793
{
Nicolas Carion's avatar
Nicolas Carion committed
794
    READ_LOCK();
795
    QSharedPointer<Mlt::Producer> prod(nullptr);
796
    if ((playlist == 0 || playlist == -1) && m_playlists[0].count() > 0) {
797
        prod = QSharedPointer<Mlt::Producer>(m_playlists[0].get_clip_at(position));
Nicolas Carion's avatar
Nicolas Carion committed
798
    }
799
    if (playlist != 0 && (!prod || prod->is_blank()) && m_playlists[1].count() > 0) {
800
801
802
        prod = QSharedPointer<Mlt::Producer>(m_playlists[1].get_clip_at(position));
    }
    if (!prod || prod->is_blank()) {
803
        return -1;
804
    }
805
    return prod->get_int("_kdenlive_cid");
806
807
}

808
809
810
811
812
813
814
815
816
817
818
819
820
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;
}

821
822
823
824
int TrackModel::getCompositionByPosition(int position)
{
    READ_LOCK();
    for (const auto &comp : m_compoPos) {
825
826
827
828
829
830
        if (comp.first == position) {
            return comp.second;
        } else if (comp.first < position) {
            if (comp.first + m_allCompositions[comp.second]->getPlaytime() >= position) {
                return comp.second;
            }
831
832
833
834
835
        }
    }
    return -1;
}

836
837
int TrackModel::getClipByRow(int row) const
{
Nicolas Carion's avatar
Nicolas Carion committed
838
    READ_LOCK();
839
840
841
    if (row >= static_cast<int>(m_allClips.size())) {
        return -1;
    }
842
    auto it = m_allClips.cbegin();
843
    std::advance(it, row);
844
    return (*it).first;
845
846
}

Nicolas Carion's avatar
Nicolas Carion committed
847
std::unordered_set<int> TrackModel::getClipsInRange(int position, int end)
848
{
Nicolas Carion's avatar
Nicolas Carion committed
849
    READ_LOCK();
850
    std::unordered_set<int> ids;
Nicolas Carion's avatar
Nicolas Carion committed
851
    for (const auto &clp : m_allClips) {
852
        int pos =