timelinemodel.cpp 114 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 "timelinemodel.hpp"
23
#include "assets/model/assetparametermodel.hpp"
24
#include "bin/projectclip.h"
25
#include "bin/projectitemmodel.h"
26
#include "clipmodel.hpp"
27
#include "compositionmodel.hpp"
Nicolas Carion's avatar
style    
Nicolas Carion committed
28
#include "core.h"
Nicolas Carion's avatar
Nicolas Carion committed
29
#include "doc/docundostack.hpp"
30
#include "effects/effectsrepository.hpp"
31
#include "effects/effectstack/model/effectstackmodel.hpp"
32
#include "groupsmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
33
#include "kdenlivesettings.h"
34
#include "logger.hpp"
35
#include "snapmodel.hpp"
36
#include "timelinefunctions.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
37
#include "trackmodel.hpp"
38

39
#include <QDebug>
40
#include <QModelIndex>
Nicolas Carion's avatar
Nicolas Carion committed
41
#include <klocalizedstring.h>
Nicolas Carion's avatar
Nicolas Carion committed
42
#include <mlt++/MltConsumer.h>
Nicolas Carion's avatar
Nicolas Carion committed
43
#include <mlt++/MltField.h>
44
#include <mlt++/MltProfile.h>
Nicolas Carion's avatar
Nicolas Carion committed
45
#include <mlt++/MltTractor.h>
46
#include <mlt++/MltTransition.h>
47
#include <queue>
48

49
50
#include "macros.hpp"

Nicolas Carion's avatar
Nicolas Carion committed
51
52
53
54
55
56
57
58
59
60
61
62
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wfloat-equal"
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wpedantic"
#include <rttr/registration>
#pragma GCC diagnostic pop
RTTR_REGISTRATION
{
    using namespace rttr;
    registration::class_<TimelineModel>("TimelineModel")
Nicolas Carion's avatar
Nicolas Carion committed
63
64
65
66
67
68
69
70
        .method("requestClipMove", select_overload<bool(int, int, int, bool, bool, bool)>(&TimelineModel::requestClipMove))(
            parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
        .method("requestCompositionMove", select_overload<bool(int, int, int, bool, bool)>(&TimelineModel::requestCompositionMove))(
            parameter_names("compoId", "trackId", "position", "updateView", "logUndo"))
        .method("requestClipInsertion", select_overload<bool(const QString &, int, int, int &, bool, bool, bool)>(&TimelineModel::requestClipInsertion))(
            parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets"))
        .method("requestItemDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo"))
        .method("requestGroupMove", select_overload<bool(int, int, int, int, bool, bool)>(&TimelineModel::requestGroupMove))(
71
            parameter_names("itemId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
Nicolas Carion's avatar
Nicolas Carion committed
72
73
74
75
        .method("requestGroupDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo"))
        .method("requestItemResize", select_overload<int(int, int, bool, bool, int, bool)>(&TimelineModel::requestItemResize))(
            parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize"))
        .method("requestClipsGroup", select_overload<int(const std::unordered_set<int> &, bool, GroupType)>(&TimelineModel::requestClipsGroup))(
76
77
78
            parameter_names("itemIds", "logUndo", "type"))
        .method("requestClipUngroup", select_overload<bool(int, bool)>(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo"))
        .method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo"))
Nicolas Carion's avatar
Nicolas Carion committed
79
80
        .method("requestTrackInsertion", select_overload<bool(int, int &, const QString &, bool)>(&TimelineModel::requestTrackInsertion))(
            parameter_names("pos", "id", "trackName", "audioTrack"))
81
        .method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
82
        .method("requestClearSelection", select_overload<bool(bool)>(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
83
84
        .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear"))
        .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
85
86
87
        .method("requestSetSelection", select_overload<bool(const std::unordered_set<int> &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds"))
        .method("requestFakeClipMove", select_overload<bool(int, int, int, bool, bool, bool)>(&TimelineModel::requestFakeClipMove))(
            parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
88
89
        .method("requestFakeGroupMove", select_overload<bool(int, int, int, int, bool, bool)>(&TimelineModel::requestFakeGroupMove))(
            parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
90
91
92
93
        // (parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView" "logUndo"))
        .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance"))
        .method("suggestCompositionMove",
                &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance"))
94
95
        // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos"))
        // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos"))
96
97
98
99
        // .method("requestCompositionInsertion", select_overload<bool(const QString &, int, int, int, std::unique_ptr<Mlt::Properties>, int &, bool)>(
        //                                            &TimelineModel::requestCompositionInsertion))(
        //     parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo"))
        .method("requestClipTimeWarp", select_overload<bool(int, double)>(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed"));
Nicolas Carion's avatar
Nicolas Carion committed
100
101
}

102
int TimelineModel::next_id = 0;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
103
int TimelineModel::seekDuration = 30000;
104

105
TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr<DocUndoStack> undo_stack)
106
    : QAbstractItemModel_shared_from_this()
107
    , m_tractor(new Mlt::Tractor(*profile))
108
    , m_snaps(new SnapModel())
Nicolas Carion's avatar
Nicolas Carion committed
109
    , m_undoStack(std::move(undo_stack))
110
    , m_profile(profile)
111
    , m_blackClip(new Mlt::Producer(*profile, "color:black"))
112
113
    , m_lock(QReadWriteLock::Recursive)
    , m_timelineEffectsEnabled(true)
Nicolas Carion's avatar
Nicolas Carion committed
114
    , m_id(getNextId())
115
    , m_overlayTrackCount(-1)
116
117
    , m_audioTarget(-1)
    , m_videoTarget(-1)
118
    , m_editMode(TimelineMode::NormalEdit)
119
    , m_blockRefresh(false)
120
{
121
122
123
    // Create black background track
    m_blackClip->set("id", "black_track");
    m_blackClip->set("mlt_type", "producer");
124
    m_blackClip->set("aspect_ratio", 1);
125
    m_blackClip->set("length", INT_MAX);
126
    m_blackClip->set("set.test_audio", 0);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
127
    m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
128
    m_tractor->insert_track(*m_blackClip, 0);
129

130
    TRACE_CONSTR(this);
131
132
}

133
134
TimelineModel::~TimelineModel()
{
135
    std::vector<int> all_ids;
Nicolas Carion's avatar
Nicolas Carion committed
136
    for (auto tracks : m_iteratorTable) {
137
138
        all_ids.push_back(tracks.first);
    }
Nicolas Carion's avatar
Nicolas Carion committed
139
    for (auto tracks : all_ids) {
140
        deregisterTrack_lambda(tracks, false)();
141
    }
142
    for (const auto &clip : m_allClips) {
143
144
        clip.second->deregisterClipToBin();
    }
145
146
}

147
148
int TimelineModel::getTracksCount() const
{
149
    READ_LOCK();
150
    int count = m_tractor->count();
151
152
    if (m_overlayTrackCount > -1) {
        count -= m_overlayTrackCount;
153
    }
154
    Q_ASSERT(count >= 0);
155
    // don't count the black background track
Nicolas Carion's avatar
Nicolas Carion committed
156
    Q_ASSERT(count - 1 == static_cast<int>(m_allTracks.size()));
157
    return count - 1;
158
}
159

160
161
int TimelineModel::getTrackIndexFromPosition(int pos) const
{
162
    Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size());
163
164
165
166
167
168
169
170
171
    READ_LOCK();
    auto it = m_allTracks.begin();
    while (pos > 0) {
        it++;
        pos--;
    }
    return (*it)->getId();
}

172
int TimelineModel::getClipsCount() const
173
{
174
175
176
    READ_LOCK();
    int size = int(m_allClips.size());
    return size;
177
}
178

179
180
181
182
183
184
int TimelineModel::getCompositionsCount() const
{
    READ_LOCK();
    int size = int(m_allCompositions.size());
    return size;
}
185

186
int TimelineModel::getClipTrackId(int clipId) const
187
{
188
    READ_LOCK();
189
190
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
191
    return clip->getCurrentTrackId();
192
193
}

194
195
196
197
198
199
200
201
202
203
int TimelineModel::getCompositionTrackId(int compoId) const
{
    Q_ASSERT(m_allCompositions.count(compoId) > 0);
    const auto trans = m_allCompositions.at(compoId);
    return trans->getCurrentTrackId();
}

int TimelineModel::getItemTrackId(int itemId) const
{
    READ_LOCK();
204
    Q_ASSERT(isItem(itemId));
205
206
207
208
209
210
    if (isComposition(itemId)) {
        return getCompositionTrackId(itemId);
    }
    return getClipTrackId(itemId);
}

211
int TimelineModel::getClipPosition(int clipId) const
212
{
213
    READ_LOCK();
214
215
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
216
217
    int pos = clip->getPosition();
    return pos;
218
219
}

220
221
222
223
224
225
226
double TimelineModel::getClipSpeed(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_allClips.at(clipId)->getSpeed();
}

227
228
229
230
231
232
233
int TimelineModel::getClipSplitPartner(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_groups->getSplitPartner(clipId);
}

234
235
236
237
238
int TimelineModel::getClipIn(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
239
240
241
242
243
244
245
246
247
    return clip->getIn();
}

PlaylistState::ClipState TimelineModel::getClipState(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
    return clip->clipState();
248
249
250
251
252
253
254
255
256
257
258
}

const QString TimelineModel::getClipBinId(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
    QString id = clip->binId();
    return id;
}

259
int TimelineModel::getClipPlaytime(int clipId) const
260
{
261
    READ_LOCK();
262
    Q_ASSERT(isClip(clipId));
263
    const auto clip = m_allClips.at(clipId);
264
265
    int playtime = clip->getPlaytime();
    return playtime;
266
267
}

268
269
270
271
272
273
274
275
QSize TimelineModel::getClipFrameSize(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(isClip(clipId));
    const auto clip = m_allClips.at(clipId);
    return clip->getFrameSize();
}

276
int TimelineModel::getTrackClipsCount(int trackId) const
277
{
278
    READ_LOCK();
279
    Q_ASSERT(isTrack(trackId));
280
    int count = getTrackById_const(trackId)->getClipsCount();
281
    return count;
282
283
}

284
285
286
287
288
289
290
int TimelineModel::getClipByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getClipByPosition(position);
}

291
292
293
294
295
296
297
int TimelineModel::getCompositionByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getCompositionByPosition(position);
}

298
int TimelineModel::getTrackPosition(int trackId) const
299
{
300
    READ_LOCK();
301
    Q_ASSERT(isTrack(trackId));
302
    auto it = m_allTracks.begin();
303
    int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId));
304
    return pos;
305
306
}

307
308
309
310
311
312
313
int TimelineModel::getTrackMltIndex(int trackId) const
{
    READ_LOCK();
    // Because of the black track that we insert in first position, the mlt index is the position + 1
    return getTrackPosition(trackId) + 1;
}

314
315
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
344
345
346
347
348
int TimelineModel::getTrackSortValue(int trackId, bool separated) const
{
    if (separated) {
        return getTrackPosition(trackId) + 1;
    }
    auto it = m_allTracks.end();
    int aCount = 0;
    int vCount = 0;
    bool isAudio = false;
    int trackPos = 0;
    while (it != m_allTracks.begin()) {
        --it;
        bool audioTrack = (*it)->isAudioTrack();
        if (audioTrack) {
            aCount++;
        } else {
            vCount++;
        }
        if (trackId == (*it)->getId()) {
            isAudio = audioTrack;
            trackPos = audioTrack ? aCount : vCount;
        }
    }
    int trackDiff = aCount - vCount;
    if (trackDiff > 0) {
        // more audio tracks
        if (!isAudio) {
            trackPos -= trackDiff;
        } else if (trackPos > vCount) {
            return -trackPos;
        }
    }
    return isAudio ? ((aCount * trackPos) - 1) : (vCount + 1 - trackPos) * 2;
}

Nicolas Carion's avatar
Nicolas Carion committed
349
QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const
350
351
352
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
Nicolas Carion's avatar
Nicolas Carion committed
353
    QList<int> results;
354
    auto it = m_iteratorTable.at(trackId);
355
356
357
    while (it != m_allTracks.begin()) {
        --it;
        if (type == TrackType::AnyTrack) {
358
359
            results << (*it)->getId();
            continue;
360
        }
361
362
        bool audioTrack = (*it)->isAudioTrack();
        if (type == TrackType::AudioTrack && audioTrack) {
363
            results << (*it)->getId();
364
        } else if (type == TrackType::VideoTrack && !audioTrack) {
365
            results << (*it)->getId();
366
367
        }
    }
368
    return results;
369
370
}

371
372
373
374
375
376
377
378
379
380
381
382
383
384
int TimelineModel::getPreviousVideoTrackIndex(int trackId) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
    while (it != m_allTracks.begin()) {
        --it;
        if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) {
            break;
        }
    }
    return it == m_allTracks.begin() ? 0 : (*it)->getId();
}

385
int TimelineModel::getPreviousVideoTrackPos(int trackId) const
386
387
388
389
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
390
391
    while (it != m_allTracks.begin()) {
        --it;
392
        if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) {
393
394
            break;
        }
395
    }
396
    return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId());
397
398
}

399
400
401
402
403
404
405
406
407
408
int TimelineModel::getMirrorVideoTrackId(int trackId) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
    if (!(*it)->isAudioTrack()) {
        // we expected an audio track...
        return -1;
    }
    int count = 0;
409
410
411
    if (it != m_allTracks.end()) {
        ++it;
    }
412
413
414
415
416
417
418
419
420
421
422
    while (it != m_allTracks.end()) {
        if ((*it)->isAudioTrack()) {
            count++;
        } else {
            if (count == 0) {
                return (*it)->getId();
            }
            count--;
        }
        ++it;
    }
423
    if (it != m_allTracks.end() && !(*it)->isAudioTrack() && count == 0) {
424
425
426
427
428
        return (*it)->getId();
    }
    return -1;
}

429
430
431
432
433
434
435
436
int TimelineModel::getMirrorTrackId(int trackId) const
{
    if (isAudioTrack(trackId)) {
        return getMirrorVideoTrackId(trackId);
    }
    return getMirrorAudioTrackId(trackId);
}

437
438
439
440
441
442
443
444
445
446
int TimelineModel::getMirrorAudioTrackId(int trackId) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
    if ((*it)->isAudioTrack()) {
        // we expected a video track...
        return -1;
    }
    int count = 0;
447
448
449
    if (it != m_allTracks.begin()) {
        --it;
    }
450
451
452
453
454
455
456
457
458
459
460
    while (it != m_allTracks.begin()) {
        if (!(*it)->isAudioTrack()) {
            count++;
        } else {
            if (count == 0) {
                return (*it)->getId();
            }
            count--;
        }
        --it;
    }
461
    if ((*it)->isAudioTrack() && count == 0) {
462
463
        return (*it)->getId();
    }
464
465
466
    return -1;
}

467
468
469
470
471
472
473
474
475
476
void TimelineModel::setEditMode(TimelineMode::EditMode mode)
{
    m_editMode = mode;
}

bool TimelineModel::normalEdit() const
{
    return m_editMode == TimelineMode::NormalEdit;
}

477
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
478
{
479
480
481
482
    Q_UNUSED(updateView);
    Q_UNUSED(invalidateTimeline);
    Q_UNUSED(undo);
    Q_UNUSED(redo);
483
484
485
486
487
    Q_ASSERT(isClip(clipId));
    m_allClips[clipId]->setFakePosition(position);
    bool trackChanged = false;
    if (trackId > -1) {
        if (trackId != m_allClips[clipId]->getFakeTrackId()) {
488
            if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
489
490
491
492
493
494
495
                m_allClips[clipId]->setFakeTrackId(trackId);
                trackChanged = true;
            }
        }
    }
    QModelIndex modelIndex = makeClipIndexFromID(clipId);
    if (modelIndex.isValid()) {
496
        QVector<int> roles{FakePositionRole};
497
498
499
500
501
502
503
504
505
        if (trackChanged) {
            roles << FakeTrackIdRole;
        }
        notifyChange(modelIndex, modelIndex, roles);
        return true;
    }
    return false;
}

506
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
507
{
508
    // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView;
509
510
511
    if (trackId == -1) {
        return false;
    }
512
    Q_ASSERT(isClip(clipId));
513
514
515
516
517
518
519
520
    if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) {
        if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) {
            return false;
        }
        if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) {
            return false;
        }
    } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) {
521
        // Move not allowed (audio / video mismatch)
522
        qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState();
523
524
        return false;
    }
Nicolas Carion's avatar
Nicolas Carion committed
525
526
    std::function<bool(void)> local_undo = []() { return true; };
    std::function<bool(void)> local_redo = []() { return true; };
527
    bool ok = true;
528
    int old_trackId = getClipTrackId(clipId);
529
    bool notifyViewOnly = false;
530
    // qDebug()<<"MOVING CLIP FROM: "<<old_trackId<<" == "<<trackId;
531
532
533
    Fun update_model = []() { return true; };
    if (old_trackId == trackId) {
        // Move on same track, simply inform the view
Nicolas Carion's avatar
Nicolas Carion committed
534
        updateView = false;
535
        notifyViewOnly = true;
536
        update_model = [clipId, this, invalidateTimeline]() {
537
            QModelIndex modelIndex = makeClipIndexFromID(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
538
            notifyChange(modelIndex, modelIndex, StartRole);
539
540
541
542
            if (invalidateTimeline) {
                int in = getClipPosition(clipId);
                emit invalidateZone(in, in + getClipPlaytime(clipId));
            }
543
544
545
            return true;
        };
    }
546
    if (old_trackId != -1) {
547
548
549
        if (notifyViewOnly) {
            PUSH_LAMBDA(update_model, local_undo);
        }
Nicolas Carion's avatar
Nicolas Carion committed
550
        ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, invalidateTimeline, local_undo, local_redo);
551
        if (!ok) {
552
553
            bool undone = local_undo();
            Q_ASSERT(undone);
554
555
556
            return false;
        }
    }
Nicolas Carion's avatar
Nicolas Carion committed
557
    ok = ok & getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, local_undo, local_redo);
558
    if (!ok) {
559
        qDebug() << "-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------";
560
561
        bool undone = local_undo();
        Q_ASSERT(undone);
562
        return false;
563
    }
564
565
566
567
    update_model();
    if (notifyViewOnly) {
        PUSH_LAMBDA(update_model, local_redo);
    }
Nicolas Carion's avatar
Nicolas Carion committed
568
569
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
570
571
}

572
573
574
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
    QWriteLocker locker(&m_lock);
575
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
576
577
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
578
        TRACE_RES(true);
579
580
581
582
583
584
585
586
587
588
        return true;
    }
    if (m_groups->isInGroup(clipId)) {
        // element is in a group.
        int groupId = m_groups->getRootId(clipId);
        int current_trackId = getClipTrackId(clipId);
        int track_pos1 = getTrackPosition(trackId);
        int track_pos2 = getTrackPosition(current_trackId);
        int delta_track = track_pos1 - track_pos2;
        int delta_pos = position - m_allClips[clipId]->getPosition();
589
590
591
        bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
        TRACE_RES(res);
        return res;
592
593
594
    }
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
595
    bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
596
597
598
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
    }
599
    TRACE_RES(res);
600
601
602
    return res;
}

603
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
604
{
605
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
606
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline);
607
608
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
Nicolas Carion's avatar
Nicolas Carion committed
609
        TRACE_RES(true);
610
611
        return true;
    }
612
    if (m_groups->isInGroup(clipId)) {
Nicolas Carion's avatar
Nicolas Carion committed
613
        // element is in a group.
614
615
616
617
        int groupId = m_groups->getRootId(clipId);
        int current_trackId = getClipTrackId(clipId);
        int track_pos1 = getTrackPosition(trackId);
        int track_pos2 = getTrackPosition(current_trackId);
618
        int delta_track = track_pos1 - track_pos2;
619
620
        int delta_pos = position - m_allClips[clipId]->getPosition();
        return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
621
    }
Nicolas Carion's avatar
Nicolas Carion committed
622
623
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
624
625
626
    bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
627
    }
Nicolas Carion's avatar
Nicolas Carion committed
628
    TRACE_RES(res);
629
630
631
    return res;
}

632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position)
{
    QWriteLocker locker(&m_lock);
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
        return true;
    }
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    bool res = true;
    if (m_groups->isInGroup(clipId)) {
        // element is in a group.
        int groupId = m_groups->getRootId(clipId);
        int current_trackId = getClipTrackId(clipId);
        int track_pos1 = getTrackPosition(trackId);
        int track_pos2 = getTrackPosition(current_trackId);
        int delta_track = track_pos1 - track_pos2;
        int delta_pos = position - m_allClips[clipId]->getPosition();
650
        res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false);
651
    } else {
652
        res = requestClipMove(clipId, trackId, position, false, false, undo, redo);
653
654
655
656
657
658
659
    }
    if (res) {
        undo();
    }
    return res;
}

660
int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
661
662
{
    if (isClip(itemId)) {
663
        return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
664
    }
665
    return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
666
667
}

668
int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance)
669
{
670
    QWriteLocker locker(&m_lock);
671
    TRACE(clipId, trackId, position, cursorPosition, snapDistance);
672
673
674
    Q_ASSERT(isClip(clipId));
    Q_ASSERT(isTrack(trackId));
    int currentPos = getClipPosition(clipId);
675
    int sourceTrackId = getClipTrackId(clipId);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
676
    if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) {
677
678
679
        // Trying move on incompatible track type, stay on same track
        trackId = sourceTrackId;
    }
680
    if (currentPos == position && sourceTrackId == trackId) {
681
        TRACE_RES(position);
682
683
        return position;
    }
684
    bool after = position > currentPos;
685
686
687
    if (snapDistance > 0) {
        // For snapping, we must ignore all in/outs of the clips of the group being moved
        std::vector<int> ignored_pts;
688
        std::unordered_set<int> all_items = {clipId};
689
690
        if (m_groups->isInGroup(clipId)) {
            int groupId = m_groups->getRootId(clipId);
691
            all_items = m_groups->getLeaves(groupId);
692
        }
693
        for (int current_clipId : all_items) {
694
            if (getItemTrackId(current_clipId) != -1) {
695
696
697
698
699
                int in = getItemPosition(current_clipId);
                int out = in + getItemPlaytime(current_clipId);
                ignored_pts.push_back(in);
                ignored_pts.push_back(out);
            }
700
701
        }

702
703
        int snapped = getBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector<int>(),
                                     cursorPosition, snapDistance);
Nicolas Carion's avatar
Nicolas Carion committed
704
        // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped;
705
706
707
        if (snapped >= 0) {
            position = snapped;
        }
708
    }
Nicolas Carion's avatar
Nicolas Carion committed
709
    // we check if move is possible
710
711
    bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false)
                                                           : requestFakeClipMove(clipId, trackId, position, true, false, false);
712
    /*} else {
713
        possible = requestClipMoveAttempt(clipId, trackId, position);
714
    }*/
715
    if (possible) {
716
        TRACE_RES(position);
717
718
        return position;
    }
719
720
721
722
723
    if (sourceTrackId == -1) {
        // not clear what to do hear, if the current move doesn't work. We could try to find empty space, but it might end up being far away...
        TRACE_RES(currentPos);
        return currentPos;
    }
724
725
    // Find best possible move
    if (!m_groups->isInGroup(clipId)) {
726
        // Try same track move
727
        if (trackId != sourceTrackId && sourceTrackId != -1) {
728
729
730
731
732
733
            qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId;
            trackId = sourceTrackId;
            possible = requestClipMove(clipId, trackId, position, true, false, false);
            if (!possible) {
                qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position;
            } else {
734
                TRACE_RES(position);
735
736
                return position;
            }
737
738
        }

739
740
741
742
        int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after);
        qDebug() << "Found blank" << blank_length;
        if (blank_length < INT_MAX) {
            if (after) {
743
744
745
746
747
                position = currentPos + blank_length;
            } else {
                position = currentPos - blank_length;
            }
        } else {
748
            TRACE_RES(currentPos);
749
            return currentPos;
750
        }
751
        possible = requestClipMove(clipId, trackId, position, true, false, false);
752
        TRACE_RES(possible ? position : currentPos);
753
754
755
756
        return possible ? position : currentPos;
    }
    // find best pos for groups
    int groupId = m_groups->getRootId(clipId);
757
    std::unordered_set<int> all_items = m_groups->getLeaves(groupId);
Nicolas Carion's avatar
Nicolas Carion committed
758
    QMap<int, int> trackPosition;
759
760

    // First pass, sort clips by track and keep only the first / last depending on move direction
761
    for (int current_clipId : all_items) {
762
        int clipTrack = getItemTrackId(current_clipId);
763
764
765
        if (clipTrack == -1) {
            continue;
        }
766
        int in = getItemPosition(current_clipId);
767
768
769
        if (trackPosition.contains(clipTrack)) {
            if (after) {
                // keep only last clip position for track
770
                int out = in + getItemPlaytime(current_clipId);
771
772
773
774
775
776
777
778
                if (trackPosition.value(clipTrack) < out) {
                    trackPosition.insert(clipTrack, out);
                }
            } else {
                // keep only first clip position for track
                if (trackPosition.value(clipTrack) > in) {
                    trackPosition.insert(clipTrack, in);
                }
779
            }
Nicolas Carion's avatar
Nicolas Carion committed
780
        } else {
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
            trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) : in);
        }
    }
    // Now check space on each track
    QMapIterator<int, int> i(trackPosition);
    int blank_length = -1;
    while (i.hasNext()) {
        i.next();
        int track_space;
        if (!after) {
            // Check space before the position
            track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1);
            if (blank_length == -1 || blank_length > track_space) {
                blank_length = track_space;
            }
        } else {
797
798
            // Check space after the position
            track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1;
799
800
801
802
803
804
805
            if (blank_length == -1 || blank_length > track_space) {
                blank_length = track_space;
            }
        }
    }
    if (blank_length != 0) {
        int updatedPos = currentPos + (after ? blank_length : -blank_length);
806
        possible = requestClipMove(clipId, trackId, updatedPos, true, false, false);
807
        if (possible) {
808
            TRACE_RES(updatedPos);
809
            return updatedPos;
Nicolas Carion's avatar
Nicolas Carion committed
810
        }
811
    }
812
    TRACE_RES(currentPos);
813
    return currentPos;
814
815
}

816
int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance)
817
818
{
    QWriteLocker locker(&m_lock);
819
    TRACE(compoId, trackId, position, cursorPosition, snapDistance);
820
    Q_ASSERT(isComposition(compoId));
821
    Q_ASSERT(isTrack(trackId));
822
823
    int currentPos = getCompositionPosition(compoId);
    int currentTrack = getCompositionTrackId(compoId);
824
    if (getTrackById_const(trackId)->isAudioTrack()) {
825
826
827
        // Trying move on incompatible track type, stay on same track
        trackId = currentTrack;
    }
828
    if (currentPos == position && currentTrack == trackId) {
829
        TRACE_RES(position);
830
831
832
        return position;
    }

833
834
835
836
837
    if (snapDistance > 0) {
        // For snapping, we must ignore all in/outs of the clips of the group being moved
        std::vector<int> ignored_pts;
        if (m_groups->isInGroup(compoId)) {
            int groupId = m_groups->getRootId(compoId);
838
839
            auto all_items = m_groups->getLeaves(groupId);
            for (int current_compoId : all_items) {
840
841
842
843
844
845
846
847
848
849
                // TODO: fix for composition
                int in = getItemPosition(current_compoId);
                int out = in + getItemPlaytime(current_compoId);
                ignored_pts.push_back(in);
                ignored_pts.push_back(out);
            }
        } else {
            int in = currentPos;
            int out = in + getCompositionPlaytime(compoId);
            qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out;
850
851
852
853
            ignored_pts.push_back(in);
            ignored_pts.push_back(out);
        }

854
        int snapped = getBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance);
855
856
857
858
        qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped;
        if (snapped >= 0) {
            position = snapped;
        }
859
    }
Nicolas Carion's avatar
Nicolas Carion committed
860
    // we check if move is possible
861
    bool possible = requestCompositionMove(compoId, trackId, position, true, false);
862
863
    qDebug() << "Original move success" << possible;
    if (possible) {
864
        TRACE_RES(position);
865
866
        return position;
    }
867
    /*bool after = position > currentPos;
868
    int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after);
869
870
871
872
    qDebug() << "Found blank" << blank_length;
    if (blank_length < INT_MAX) {
        if (after) {
            return currentPos + blank_length;
Nicolas Carion's avatar
Nicolas Carion committed
873
874
        }
        return currentPos - blank_length;
875
    }
876
    return position;*/
877
    TRACE_RES(currentPos);
878
    return currentPos;
879
880
}

881
bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo)
882
{
883
    qDebug() << "requestClipCreation " << binClipId;
Nicolas Carion's avatar
Nicolas Carion committed
884
    QString bid = binClipId;
885
886
    if (binClipId.contains(QLatin1Char('/'))) {
        bid = binClipId.section(QLatin1Char('/'), 0, 0);
Nicolas Carion's avatar
Nicolas Carion committed
887
    }
888
    if (!pCore->projectItemModel()->hasClip(bid)) {
889
        qDebug() << " / / / /MASTER CLIP NOT FOUND";
890
891
        return false;
    }
892
    std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid);
893
    if (!master->isReady() || !master->isCompatible(state)) {
Nicolas Carion's avatar
Nicolas Carion committed
894
        qDebug() << "// CLIP NOT READY OR NOT COMPATIBLE: " << state;
895
896
897
898
899
        return false;
    }
    int clipId = TimelineModel::getNextId();
    id = clipId;
    Fun local_undo = deregisterClip_lambda(clipId);
900
    ClipModel::construct(shared_from_this(), bid, clipId, state, speed);
901
    auto clip = m_allClips[clipId];
902
    Fun local_redo = [clip, this, state]() {
903
904
        // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is
        // sufficient to register it.
905
        registerClip(clip, true);
906
        clip->refreshProducerFromBin(state);
907
908
        return true;
    };
Nicolas Carion's avatar
Nicolas Carion committed
909

910
911
912
    if (binClipId.contains(QLatin1Char('/'))) {
        int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt();
        int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt();
Nicolas Carion's avatar
Nicolas Carion committed
913
        int initLength = m_allClips[clipId]->getPlaytime();
914
915
        bool res = true;
        if (in != 0) {
916
            res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo);
917
        }
918
        res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo);
Nicolas Carion's avatar
Nicolas Carion committed
919
920
921
922
923
        if (!res) {
            bool undone = local_undo();
            Q_ASSERT(undone);
            return false;
        }
924
925
926
927
928
    }
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
}

929
930
931
bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets)
{
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
932
    TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets);
933
934
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
935
936
937
    bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo);
    if (result && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Insert Clip"));
938
    }
Nicolas Carion's avatar
Nicolas Carion committed
939
    TRACE_RES(result);
940
941
942
943
    return result;
}

bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets,
944
                                         Fun &undo, Fun &redo)
945
{
946
947
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
948
949
    qDebug() << "requestClipInsertion " << binClipId << " "
             << " " << trackId << " " << position;
950
    bool res = false;
951
    ClipType::ProducerType type = ClipType::Unknown;
952
    QString bid = binClipId.section(QLatin1Char('/'), 0, 0);
953
954