timelinemodel.cpp 148 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 <QThread>
41
#include <QModelIndex>
Nicolas Carion's avatar
Nicolas Carion committed
42
#include <klocalizedstring.h>
Nicolas Carion's avatar
Nicolas Carion committed
43
#include <mlt++/MltConsumer.h>
Nicolas Carion's avatar
Nicolas Carion committed
44
#include <mlt++/MltField.h>
45
#include <mlt++/MltProfile.h>
Nicolas Carion's avatar
Nicolas Carion committed
46
#include <mlt++/MltTractor.h>
47
#include <mlt++/MltTransition.h>
48
#include <queue>
49

50
51
#include "macros.hpp"

Nicolas Carion's avatar
Nicolas Carion committed
52
53
54
55
56
57
58
59
60
61
62
63
#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")
64
        .method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock"))
65
66
        .method("requestClipMove", select_overload<bool(int, int, int, bool, bool, bool, bool)>(&TimelineModel::requestClipMove))(
            parameter_names("clipId", "trackId", "position", "moveMirrorTracks", "updateView", "logUndo", "invalidateTimeline"))
Nicolas Carion's avatar
Nicolas Carion committed
67
68
69
70
71
        .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"))
72
73
        .method("requestGroupMove", select_overload<bool(int, int, int, int, bool, bool, bool)>(&TimelineModel::requestGroupMove))(
            parameter_names("itemId", "groupId", "delta_track", "delta_pos", "moveMirrorTracks", "updateView", "logUndo"))
Nicolas Carion's avatar
Nicolas Carion committed
74
75
76
77
        .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))(
78
79
80
            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
81
82
        .method("requestTrackInsertion", select_overload<bool(int, int &, const QString &, bool)>(&TimelineModel::requestTrackInsertion))(
            parameter_names("pos", "id", "trackName", "audioTrack"))
83
        .method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
84
        .method("requestClearSelection", select_overload<bool(bool)>(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
85
86
        .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear"))
        .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
87
88
89
        .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"))
90
91
        .method("requestFakeGroupMove", select_overload<bool(int, int, int, int, bool, bool)>(&TimelineModel::requestFakeGroupMove))(
            parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
92
        .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance", "moveMirrorTracks"))
93
94
        .method("suggestCompositionMove",
                &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance"))
95
96
        // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos"))
        // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos"))
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"))
100
        .method("requestClipTimeWarp", select_overload<bool(int, double,bool,bool)>(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed","pitchCompensate","changeDuration"));
Nicolas Carion's avatar
Nicolas Carion committed
101
102
}

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

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

134
    TRACE_CONSTR(this);
135
136
}

137
138
void TimelineModel::prepareClose()
{
139
    requestClearSelection(true);
140
    QWriteLocker locker(&m_lock);
141
    // Unlock all tracks to allow deleting clip from tracks
142
143
144
    m_closing = true;
    auto it = m_allTracks.begin();
    while (it != m_allTracks.end()) {
145
        (*it)->unlock();
146
147
148
149
        ++it;
    }
}

150
151
TimelineModel::~TimelineModel()
{
152
    std::vector<int> all_ids;
Nicolas Carion's avatar
Nicolas Carion committed
153
    for (auto tracks : m_iteratorTable) {
154
155
        all_ids.push_back(tracks.first);
    }
Nicolas Carion's avatar
Nicolas Carion committed
156
    for (auto tracks : all_ids) {
157
        deregisterTrack_lambda(tracks)();
158
    }
159
    for (const auto &clip : m_allClips) {
160
161
        clip.second->deregisterClipToBin();
    }
162
163
}

164
165
int TimelineModel::getTracksCount() const
{
166
    READ_LOCK();
167
    int count = m_tractor->count();
168
169
    if (m_overlayTrackCount > -1) {
        count -= m_overlayTrackCount;
170
    }
171
    Q_ASSERT(count >= 0);
172
    // don't count the black background track
Nicolas Carion's avatar
Nicolas Carion committed
173
    Q_ASSERT(count - 1 == static_cast<int>(m_allTracks.size()));
174
    return count - 1;
175
}
176

177
178
int TimelineModel::getTrackIndexFromPosition(int pos) const
{
179
    Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size());
180
    READ_LOCK();
181
    auto it = m_allTracks.cbegin();
182
183
184
185
186
187
188
    while (pos > 0) {
        it++;
        pos--;
    }
    return (*it)->getId();
}

189
int TimelineModel::getClipsCount() const
190
{
191
192
193
    READ_LOCK();
    int size = int(m_allClips.size());
    return size;
194
}
195

196
197
198
199
200
201
int TimelineModel::getCompositionsCount() const
{
    READ_LOCK();
    int size = int(m_allCompositions.size());
    return size;
}
202

203
int TimelineModel::getClipTrackId(int clipId) const
204
{
205
    READ_LOCK();
206
207
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
208
    return clip->getCurrentTrackId();
209
210
}

211
212
213
214
215
216
217
218
219
220
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();
221
    Q_ASSERT(isItem(itemId));
222
223
224
225
226
227
    if (isComposition(itemId)) {
        return getCompositionTrackId(itemId);
    }
    return getClipTrackId(itemId);
}

228
int TimelineModel::getClipPosition(int clipId) const
229
{
230
    READ_LOCK();
231
232
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
233
234
    int pos = clip->getPosition();
    return pos;
235
236
}

237
238
239
240
241
242
243
double TimelineModel::getClipSpeed(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_allClips.at(clipId)->getSpeed();
}

244
245
246
247
248
249
250
int TimelineModel::getClipSplitPartner(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_groups->getSplitPartner(clipId);
}

251
252
253
254
255
int TimelineModel::getClipIn(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
256
257
258
259
260
261
262
263
264
    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();
265
266
267
268
269
270
271
272
273
274
275
}

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;
}

276
int TimelineModel::getClipPlaytime(int clipId) const
277
{
278
    READ_LOCK();
279
    Q_ASSERT(isClip(clipId));
280
    const auto clip = m_allClips.at(clipId);
281
282
    int playtime = clip->getPlaytime();
    return playtime;
283
284
}

285
286
287
288
289
290
291
292
QSize TimelineModel::getClipFrameSize(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(isClip(clipId));
    const auto clip = m_allClips.at(clipId);
    return clip->getFrameSize();
}

293
int TimelineModel::getTrackClipsCount(int trackId) const
294
{
295
    READ_LOCK();
296
    Q_ASSERT(isTrack(trackId));
297
    int count = getTrackById_const(trackId)->getClipsCount();
298
    return count;
299
300
}

301
302
303
304
305
306
307
int TimelineModel::getClipByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getClipByPosition(position);
}

308
309
310
311
312
313
314
int TimelineModel::getCompositionByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getCompositionByPosition(position);
}

315
int TimelineModel::getTrackPosition(int trackId) const
316
{
317
    READ_LOCK();
318
    Q_ASSERT(isTrack(trackId));
319
    auto it = m_allTracks.cbegin();
320
    int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId));
321
    return pos;
322
323
}

324
325
326
327
328
329
330
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;
}

331
int TimelineModel::getTrackSortValue(int trackId, int separated) const
332
{
333
    if (separated == 1) {
334
        // This will be A2, A1, V1, V2
335
336
        return getTrackPosition(trackId) + 1;
    }
337
    if (separated == 2) {
338
        // This will be A1, A2, V1, V2
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
        // Count audio/video tracks
        auto it = m_allTracks.cbegin();
        int aCount = 0;
        int vCount = 0;
        int refPos = 0;
        bool isVideo = true;
        while (it != m_allTracks.cend()) {
            if ((*it)->isAudioTrack()) {
                if ((*it)->getId() == trackId) {
                    refPos = aCount;
                    isVideo = false;
                }
                aCount++;
            } else {
                // video track
                if ((*it)->getId() == trackId) {
                    refPos = vCount;
                }
                vCount++;
            }
            ++it;
        }
        return isVideo ? aCount + refPos + 1 : aCount - refPos;
    }
363
    // This will be A1, V1, A2, V2
364
    auto it = m_allTracks.cend();
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
    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;
        }
    }
382
    int trackDiff = qMax(0, aCount - vCount);
383
    if (trackDiff > 0) {
384
385
        // more audio tracks, keep them below
        if (isAudio && trackPos > vCount) {
386
387
388
            return -trackPos;
        }
    }
389
    return isAudio ? 2 * trackPos : 2 * (vCount + 1 - trackPos) + 1;
390
391
}

Nicolas Carion's avatar
Nicolas Carion committed
392
QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const
393
394
395
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
Nicolas Carion's avatar
Nicolas Carion committed
396
    QList<int> results;
397
    auto it = m_iteratorTable.at(trackId);
398
    while (it != m_allTracks.cbegin()) {
399
400
        --it;
        if (type == TrackType::AnyTrack) {
401
402
            results << (*it)->getId();
            continue;
403
        }
404
405
        bool audioTrack = (*it)->isAudioTrack();
        if (type == TrackType::AudioTrack && audioTrack) {
406
            results << (*it)->getId();
407
        } else if (type == TrackType::VideoTrack && !audioTrack) {
408
            results << (*it)->getId();
409
410
        }
    }
411
    return results;
412
413
}

414
415
416
417
418
int TimelineModel::getPreviousVideoTrackIndex(int trackId) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
419
    while (it != m_allTracks.cbegin()) {
420
        --it;
421
422
        if (!(*it)->isAudioTrack()) {
            return (*it)->getId();
423
424
        }
    }
425
    return 0;
426
427
}

428
int TimelineModel::getPreviousVideoTrackPos(int trackId) const
429
430
431
432
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
433
    while (it != m_allTracks.cbegin()) {
434
        --it;
435
436
        if (!(*it)->isAudioTrack()) {
            return getTrackMltIndex((*it)->getId());
437
        }
438
    }
439
    return 0;
440
441
}

442
443
444
445
446
447
448
449
450
451
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;
452
    while (it != m_allTracks.cend()) {
453
454
455
        if ((*it)->isAudioTrack()) {
            count++;
        } else {
456
            count--;
457
458
459
460
461
462
463
464
465
            if (count == 0) {
                return (*it)->getId();
            }
        }
        ++it;
    }
    return -1;
}

466
467
468
469
470
471
472
473
int TimelineModel::getMirrorTrackId(int trackId) const
{
    if (isAudioTrack(trackId)) {
        return getMirrorVideoTrackId(trackId);
    }
    return getMirrorAudioTrackId(trackId);
}

474
475
476
477
478
479
480
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...
481
        qDebug()<<"++++++++\n+++++++ ERROR RQSTNG AUDIO MIRROR FOR AUDIO";
482
483
484
        return -1;
    }
    int count = 0;
485
    while (it != m_allTracks.cbegin()) {
486
487
488
        if (!(*it)->isAudioTrack()) {
            count++;
        } else {
489
            count--;
490
491
492
493
            if (count == 0) {
                return (*it)->getId();
            }
        }
494
        --it;
495
    }
496
    if ((*it)->isAudioTrack() && count == 1) {
497
498
        return (*it)->getId();
    }
499
500
501
    return -1;
}

502
503
504
505
506
507
508
509
510
511
void TimelineModel::setEditMode(TimelineMode::EditMode mode)
{
    m_editMode = mode;
}

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

512
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
513
{
514
515
516
517
    Q_UNUSED(updateView);
    Q_UNUSED(invalidateTimeline);
    Q_UNUSED(undo);
    Q_UNUSED(redo);
518
519
520
521
522
    Q_ASSERT(isClip(clipId));
    m_allClips[clipId]->setFakePosition(position);
    bool trackChanged = false;
    if (trackId > -1) {
        if (trackId != m_allClips[clipId]->getFakeTrackId()) {
523
            if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
524
525
526
527
528
529
530
                m_allClips[clipId]->setFakeTrackId(trackId);
                trackChanged = true;
            }
        }
    }
    QModelIndex modelIndex = makeClipIndexFromID(clipId);
    if (modelIndex.isValid()) {
531
        QVector<int> roles{FakePositionRole};
532
533
534
535
536
537
538
539
540
        if (trackChanged) {
            roles << FakeTrackIdRole;
        }
        notifyChange(modelIndex, modelIndex, roles);
        return true;
    }
    return false;
}

541
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
542
{
Vincent Pinon's avatar
Vincent Pinon committed
543
    Q_UNUSED(moveMirrorTracks)
544
    // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<<finalMove;
545
546
547
    if (trackId == -1) {
        return false;
    }
548
    Q_ASSERT(isClip(clipId));
549
550
551
552
553
554
555
556
    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()) {
557
        // Move not allowed (audio / video mismatch)
558
        //qDebug() << "// CLIP MISMATC FOR TK: "<<trackId<< ", " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState();
559
560
        return false;
    }
Nicolas Carion's avatar
Nicolas Carion committed
561
562
    std::function<bool(void)> local_undo = []() { return true; };
    std::function<bool(void)> local_redo = []() { return true; };
563
    bool ok = true;
564
    int old_trackId = getClipTrackId(clipId);
565
    bool notifyViewOnly = false;
566
    // qDebug()<<"MOVING CLIP FROM: "<<old_trackId<<" == "<<trackId;
567
568
569
    Fun update_model = []() { return true; };
    if (old_trackId == trackId) {
        // Move on same track, simply inform the view
Nicolas Carion's avatar
Nicolas Carion committed
570
        updateView = false;
571
        notifyViewOnly = true;
572
        update_model = [clipId, this, trackId, invalidateTimeline]() {
573
            QModelIndex modelIndex = makeClipIndexFromID(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
574
            notifyChange(modelIndex, modelIndex, StartRole);
575
            if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
576
577
578
                int in = getClipPosition(clipId);
                emit invalidateZone(in, in + getClipPlaytime(clipId));
            }
579
580
581
            return true;
        };
    }
582
    if (old_trackId != -1) {
583
584
585
        if (notifyViewOnly) {
            PUSH_LAMBDA(update_model, local_undo);
        }
586
        ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove, false);
587
        if (!ok) {
588
589
            bool undone = local_undo();
            Q_ASSERT(undone);
590
591
592
            return false;
        }
    }
593
    ok = getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove);
594
    if (!ok) {
595
        qDebug() << "-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------";
596
597
        bool undone = local_undo();
        Q_ASSERT(undone);
598
        return false;
599
    }
600
601
602
603
    update_model();
    if (notifyViewOnly) {
        PUSH_LAMBDA(update_model, local_redo);
    }
Nicolas Carion's avatar
Nicolas Carion committed
604
605
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
606
607
}

608
609
610
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
    QWriteLocker locker(&m_lock);
611
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
612
    Q_ASSERT(m_allClips.count(clipId) > 0);
613
    if (m_allClips[clipId]->getPosition() == position && m_allClips[clipId]->getFakeTrackId() == trackId) {
614
        TRACE_RES(true);
615
        qDebug()<<"........\nABORTING MOVE; SAME POS/TRACK\n..........";
616
617
618
619
620
621
622
623
624
625
        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();
626
627
628
        bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
        TRACE_RES(res);
        return res;
629
630
631
    }
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
632
    bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
633
634
635
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
    }
636
    TRACE_RES(res);
637
638
639
    return res;
}

640
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool logUndo, bool invalidateTimeline)
641
{
642
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
643
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline);
644
645
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
Nicolas Carion's avatar
Nicolas Carion committed
646
        TRACE_RES(true);
647
648
        return true;
    }
649
    if (m_groups->isInGroup(clipId)) {
Nicolas Carion's avatar
Nicolas Carion committed
650
        // element is in a group.
651
652
653
654
        int groupId = m_groups->getRootId(clipId);
        int current_trackId = getClipTrackId(clipId);
        int track_pos1 = getTrackPosition(trackId);
        int track_pos2 = getTrackPosition(current_trackId);
655
        int delta_track = track_pos1 - track_pos2;
656
        int delta_pos = position - m_allClips[clipId]->getPosition();
657
        return requestGroupMove(clipId, groupId, delta_track, delta_pos, moveMirrorTracks, updateView, logUndo);
658
    }
Nicolas Carion's avatar
Nicolas Carion committed
659
660
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
661
    bool res = requestClipMove(clipId, trackId, position, moveMirrorTracks, updateView, invalidateTimeline, logUndo, undo, redo);
662
663
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
664
    }
Nicolas Carion's avatar
Nicolas Carion committed
665
    TRACE_RES(res);
666
667
668
    return res;
}

669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
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();
687
        res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false);
688
    } else {
689
        res = requestClipMove(clipId, trackId, position, true, false, false, false, undo, redo);
690
691
692
693
694
695
696
    }
    if (res) {
        undo();
    }
    return res;
}

697
int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
698
699
{
    if (isClip(itemId)) {
700
        return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
701
    }
702
    return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
703
704
}

705
int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool moveMirrorTracks)
706
{
707
    QWriteLocker locker(&m_lock);
708
    TRACE(clipId, trackId, position, cursorPosition, snapDistance);
709
710
711
    Q_ASSERT(isClip(clipId));
    Q_ASSERT(isTrack(trackId));
    int currentPos = getClipPosition(clipId);
712
    int sourceTrackId = getClipTrackId(clipId);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
713
    if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) {
714
715
716
        // Trying move on incompatible track type, stay on same track
        trackId = sourceTrackId;
    }
717
    if (currentPos == position && m_editMode == TimelineMode::NormalEdit && sourceTrackId == trackId) {
718
        TRACE_RES(position);
719
720
        return position;
    }
721
    bool after = position > currentPos;
722
723
724
    if (snapDistance > 0) {
        // For snapping, we must ignore all in/outs of the clips of the group being moved
        std::vector<int> ignored_pts;
725
        std::unordered_set<int> all_items = {clipId};
726
727
        if (m_groups->isInGroup(clipId)) {
            int groupId = m_groups->getRootId(clipId);
728
            all_items = m_groups->getLeaves(groupId);
729
        }
730
        for (int current_clipId : all_items) {
731
            if (getItemTrackId(current_clipId) != -1) {
732
733
734
735
736
737
738
739
                if (isClip(current_clipId)) {
                    m_allClips[current_clipId]->allSnaps(ignored_pts);
                } else {
                    // Composition
                    int in = getItemPosition(current_clipId);
                    ignored_pts.push_back(in);
                    ignored_pts.push_back(in + getItemPlaytime(current_clipId));
                }
740
            }
741
        }
742
        int snapped = getBestSnapPos(currentPos, position - currentPos, m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector<int>(),
743
                                     cursorPosition, snapDistance);
Nicolas Carion's avatar
Nicolas Carion committed
744
        // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped;
745
746
747
        if (snapped >= 0) {
            position = snapped;
        }
748
    }
Nicolas Carion's avatar
Nicolas Carion committed
749
    // we check if move is possible
750
    bool possible = (m_editMode == TimelineMode::NormalEdit) ? requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false)
751
                                                           : requestFakeClipMove(clipId, trackId, position, true, false, false);
752
    /*} else {
753
        possible = requestClipMoveAttempt(clipId, trackId, position);
754
    }*/
755
    if (possible) {
756
        TRACE_RES(position);
757
758
        return position;
    }
759
760
761
762
763
    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;
    }
764
765
    // Find best possible move
    if (!m_groups->isInGroup(clipId)) {
766
        // Try same track move
767
        if (trackId != sourceTrackId && sourceTrackId != -1) {
768
769
            qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId;
            trackId = sourceTrackId;
770
            possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
771
772
773
            if (!possible) {
                qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position;
            } else {
774
                TRACE_RES(position);
775
776
                return position;
            }
777
778
        }

779
        int blank_length = getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after);
780
781
782
        qDebug() << "Found blank" << blank_length;
        if (blank_length < INT_MAX) {
            if (after) {
783
784
785
786
787
                position = currentPos + blank_length;
            } else {
                position = currentPos - blank_length;
            }
        } else {
788
            TRACE_RES(currentPos);
789
            return currentPos;
790
        }
791
        possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
792
        TRACE_RES(possible ? position : currentPos);
793
794
        return possible ? position : currentPos;
    }
795
796
797
798
799
    if (trackId != sourceTrackId) {
        // Try same track move
        possible = requestClipMove(clipId, sourceTrackId, position, moveMirrorTracks, true, false, false);
        return possible ? position : currentPos;
    }
800
801
    // find best pos for groups
    int groupId = m_groups->getRootId(clipId);
802
    std::unordered_set<int> all_items = m_groups->getLeaves(groupId);
Nicolas Carion's avatar
Nicolas Carion committed
803
    QMap<int, int> trackPosition;
804
805

    // First pass, sort clips by track and keep only the first / last depending on move direction
806
    for (int current_clipId : all_items) {
807
        int clipTrack = getItemTrackId(current_clipId);
808
809
810
        if (clipTrack == -1) {
            continue;
        }
811
        int in = getItemPosition(current_clipId);
812
813
814
        if (trackPosition.contains(clipTrack)) {
            if (after) {
                // keep only last clip position for track
815
                int out = in + getItemPlaytime(current_clipId);
816
817
818
819
820
821
822
823
                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);
                }
824
            }
Nicolas Carion's avatar
Nicolas Carion committed
825
        } else {
826
            trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in);
827
828
        }
    }
829

830
831
    // Now check space on each track
    QMapIterator<int, int> i(trackPosition);
832
    int blank_length = 0;
833
834
835
836
837
    while (i.hasNext()) {
        i.next();
        int track_space;
        if (!after) {
            // Check space before the position
838
839
            track_space = i.value() - getTrackById_const(i.key())->getBlankStart(i.value() - 1);
            if (blank_length == 0 || blank_length > track_space) {
840
841
842
                blank_length = track_space;
            }
        } else {
843
844
            // Check space after the position
            track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1;
845
            if (blank_length == 0 || blank_length > track_space) {
846
847
848
849
                blank_length = track_space;
            }
        }
    }
850
851
852
853
854
855
856
    if (snapDistance > 0) {
        if (blank_length > 10 * snapDistance) {
            blank_length = 0;
        }
    } else if (blank_length / m_profile->fps() > 5) {
        blank_length = 0;
    }
857
858
    if (blank_length != 0) {
        int updatedPos = currentPos + (after ? blank_length : -blank_length);
859
        possible = requestClipMove(clipId, trackId, updatedPos, moveMirrorTracks, true, false, false);
860
        if (possible) {
861
            TRACE_RES(updatedPos);
862
            return updatedPos;
Nicolas Carion's avatar
Nicolas Carion committed
863
        }
864
    }
865
    TRACE_RES(currentPos);
866
    return currentPos;
867
868
}

869
int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance)
870
871
{
    QWriteLocker locker(&m_lock);
872
    TRACE(compoId, trackId, position, cursorPosition, snapDistance);
873
    Q_ASSERT(isComposition(compoId));
874
    Q_ASSERT(isTrack(trackId));