timelinemodel.cpp 109 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
82
83
84
        .method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
        .method("requestClearSelection", select_overload<void(bool)>(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
        .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear"))
        .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
Nicolas Carion's avatar
Nicolas Carion committed
85
        .method("requestSetSelection", select_overload<bool(const std::unordered_set<int> &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds"));
Nicolas Carion's avatar
Nicolas Carion committed
86
87
}

88
int TimelineModel::next_id = 0;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
89
int TimelineModel::seekDuration = 30000;
90

91
TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr<DocUndoStack> undo_stack)
92
    : QAbstractItemModel_shared_from_this()
93
    , m_tractor(new Mlt::Tractor(*profile))
94
    , m_snaps(new SnapModel())
Nicolas Carion's avatar
Nicolas Carion committed
95
    , m_undoStack(std::move(undo_stack))
96
    , m_profile(profile)
97
    , m_blackClip(new Mlt::Producer(*profile, "color:black"))
98
99
    , m_lock(QReadWriteLock::Recursive)
    , m_timelineEffectsEnabled(true)
Nicolas Carion's avatar
Nicolas Carion committed
100
    , m_id(getNextId())
101
    , m_overlayTrackCount(-1)
102
103
    , m_audioTarget(-1)
    , m_videoTarget(-1)
104
    , m_editMode(TimelineMode::NormalEdit)
105
    , m_blockRefresh(false)
106
{
107
108
109
    // Create black background track
    m_blackClip->set("id", "black_track");
    m_blackClip->set("mlt_type", "producer");
110
    m_blackClip->set("aspect_ratio", 1);
111
    m_blackClip->set("length", INT_MAX);
112
    m_blackClip->set("set.test_audio", 0);
113
    m_blackClip->set("length", INT_MAX);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
114
    m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
115
    m_tractor->insert_track(*m_blackClip, 0);
116

117
    TRACE_CONSTR(this);
118
119
}

120
121
TimelineModel::~TimelineModel()
{
122
    std::vector<int> all_ids;
Nicolas Carion's avatar
Nicolas Carion committed
123
    for (auto tracks : m_iteratorTable) {
124
125
        all_ids.push_back(tracks.first);
    }
Nicolas Carion's avatar
Nicolas Carion committed
126
    for (auto tracks : all_ids) {
127
        deregisterTrack_lambda(tracks, false)();
128
    }
129
    for (const auto &clip : m_allClips) {
130
131
        clip.second->deregisterClipToBin();
    }
132
133
}

134
135
int TimelineModel::getTracksCount() const
{
136
    READ_LOCK();
137
    int count = m_tractor->count();
138
139
    if (m_overlayTrackCount > -1) {
        count -= m_overlayTrackCount;
140
    }
141
    Q_ASSERT(count >= 0);
142
    // don't count the black background track
Nicolas Carion's avatar
Nicolas Carion committed
143
    Q_ASSERT(count - 1 == static_cast<int>(m_allTracks.size()));
144
    return count - 1;
145
}
146

147
148
int TimelineModel::getTrackIndexFromPosition(int pos) const
{
149
    Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size());
150
151
152
153
154
155
156
157
158
    READ_LOCK();
    auto it = m_allTracks.begin();
    while (pos > 0) {
        it++;
        pos--;
    }
    return (*it)->getId();
}

159
int TimelineModel::getClipsCount() const
160
{
161
162
163
    READ_LOCK();
    int size = int(m_allClips.size());
    return size;
164
}
165

166
167
168
169
170
171
int TimelineModel::getCompositionsCount() const
{
    READ_LOCK();
    int size = int(m_allCompositions.size());
    return size;
}
172

173
int TimelineModel::getClipTrackId(int clipId) const
174
{
175
    READ_LOCK();
176
177
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
178
    return clip->getCurrentTrackId();
179
180
}

181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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();
    Q_ASSERT(isClip(itemId) || isComposition(itemId));
    if (isComposition(itemId)) {
        return getCompositionTrackId(itemId);
    }
    return getClipTrackId(itemId);
}

198
int TimelineModel::getClipPosition(int clipId) const
199
{
200
    READ_LOCK();
201
202
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
203
204
    int pos = clip->getPosition();
    return pos;
205
206
}

207
208
209
210
211
212
213
double TimelineModel::getClipSpeed(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_allClips.at(clipId)->getSpeed();
}

214
215
216
217
218
219
220
int TimelineModel::getClipSplitPartner(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_groups->getSplitPartner(clipId);
}

221
222
223
224
225
int TimelineModel::getClipIn(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
226
227
228
229
230
231
232
233
234
    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();
235
236
237
238
239
240
241
242
243
244
245
}

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

246
int TimelineModel::getClipPlaytime(int clipId) const
247
{
248
    READ_LOCK();
249
    Q_ASSERT(isClip(clipId));
250
    const auto clip = m_allClips.at(clipId);
251
252
    int playtime = clip->getPlaytime();
    return playtime;
253
254
}

255
256
257
258
259
260
261
262
QSize TimelineModel::getClipFrameSize(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(isClip(clipId));
    const auto clip = m_allClips.at(clipId);
    return clip->getFrameSize();
}

263
int TimelineModel::getTrackClipsCount(int trackId) const
264
{
265
    READ_LOCK();
266
    Q_ASSERT(isTrack(trackId));
267
    int count = getTrackById_const(trackId)->getClipsCount();
268
    return count;
269
270
}

271
272
273
274
275
276
277
int TimelineModel::getClipByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getClipByPosition(position);
}

278
279
280
281
282
283
284
int TimelineModel::getCompositionByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getCompositionByPosition(position);
}

285
int TimelineModel::getTrackPosition(int trackId) const
286
{
287
    READ_LOCK();
288
    Q_ASSERT(isTrack(trackId));
289
    auto it = m_allTracks.begin();
290
    int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId));
291
    return pos;
292
293
}

294
295
296
297
298
299
300
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;
}

301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
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
336
QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const
337
338
339
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
Nicolas Carion's avatar
Nicolas Carion committed
340
    QList<int> results;
341
    auto it = m_iteratorTable.at(trackId);
342
343
344
    while (it != m_allTracks.begin()) {
        --it;
        if (type == TrackType::AnyTrack) {
345
346
            results << (*it)->getId();
            continue;
347
        }
348
349
        bool audioTrack = (*it)->isAudioTrack();
        if (type == TrackType::AudioTrack && audioTrack) {
350
            results << (*it)->getId();
351
        } else if (type == TrackType::VideoTrack && !audioTrack) {
352
            results << (*it)->getId();
353
354
        }
    }
355
    return results;
356
357
}

358
359
360
361
362
363
364
365
366
367
368
369
370
371
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();
}

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

386
387
388
389
390
391
392
393
394
395
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;
396
397
398
    if (it != m_allTracks.end()) {
        ++it;
    }
399
400
401
402
403
404
405
406
407
408
409
    while (it != m_allTracks.end()) {
        if ((*it)->isAudioTrack()) {
            count++;
        } else {
            if (count == 0) {
                return (*it)->getId();
            }
            count--;
        }
        ++it;
    }
410
    if (it != m_allTracks.end() && !(*it)->isAudioTrack() && count == 0) {
411
412
413
414
415
        return (*it)->getId();
    }
    return -1;
}

416
417
418
419
420
421
422
423
int TimelineModel::getMirrorTrackId(int trackId) const
{
    if (isAudioTrack(trackId)) {
        return getMirrorVideoTrackId(trackId);
    }
    return getMirrorAudioTrackId(trackId);
}

424
425
426
427
428
429
430
431
432
433
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;
434
435
436
    if (it != m_allTracks.begin()) {
        --it;
    }
437
438
439
440
441
442
443
444
445
446
447
    while (it != m_allTracks.begin()) {
        if (!(*it)->isAudioTrack()) {
            count++;
        } else {
            if (count == 0) {
                return (*it)->getId();
            }
            count--;
        }
        --it;
    }
448
    if ((*it)->isAudioTrack() && count == 0) {
449
450
        return (*it)->getId();
    }
451
452
453
    return -1;
}

454
455
456
457
458
459
460
461
462
463
464
465
void TimelineModel::setEditMode(TimelineMode::EditMode mode)
{
    m_editMode = mode;
}

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

bool TimelineModel::fakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
{
466
467
468
469
    Q_UNUSED(updateView);
    Q_UNUSED(invalidateTimeline);
    Q_UNUSED(undo);
    Q_UNUSED(redo);
470
471
472
473
474
    Q_ASSERT(isClip(clipId));
    m_allClips[clipId]->setFakePosition(position);
    bool trackChanged = false;
    if (trackId > -1) {
        if (trackId != m_allClips[clipId]->getFakeTrackId()) {
475
            if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
476
477
478
479
480
481
482
                m_allClips[clipId]->setFakeTrackId(trackId);
                trackChanged = true;
            }
        }
    }
    QModelIndex modelIndex = makeClipIndexFromID(clipId);
    if (modelIndex.isValid()) {
483
        QVector<int> roles{FakePositionRole};
484
485
486
487
488
489
490
491
492
        if (trackChanged) {
            roles << FakeTrackIdRole;
        }
        notifyChange(modelIndex, modelIndex, roles);
        return true;
    }
    return false;
}

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

560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
    QWriteLocker locker(&m_lock);
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
        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();
575
        return requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
576
577
578
579
580
581
582
583
584
585
    }
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    bool res = fakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
    }
    return res;
}

586
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
587
{
588
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
589
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline);
590
591
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
Nicolas Carion's avatar
Nicolas Carion committed
592
        TRACE_RES(true);
593
594
        return true;
    }
595
    if (m_groups->isInGroup(clipId)) {
Nicolas Carion's avatar
Nicolas Carion committed
596
        // element is in a group.
597
598
599
600
        int groupId = m_groups->getRootId(clipId);
        int current_trackId = getClipTrackId(clipId);
        int track_pos1 = getTrackPosition(trackId);
        int track_pos2 = getTrackPosition(current_trackId);
601
        int delta_track = track_pos1 - track_pos2;
602
603
        int delta_pos = position - m_allClips[clipId]->getPosition();
        return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
604
    }
Nicolas Carion's avatar
Nicolas Carion committed
605
606
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
607
608
609
    bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
610
    }
Nicolas Carion's avatar
Nicolas Carion committed
611
    TRACE_RES(res);
612
613
614
    return res;
}

615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
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();
633
        res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false);
634
    } else {
635
        res = requestClipMove(clipId, trackId, position, false, false, undo, redo);
636
637
638
639
640
641
642
    }
    if (res) {
        undo();
    }
    return res;
}

643
int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
644
645
{
    if (isClip(itemId)) {
646
        return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
647
    }
648
    return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
649
650
}

651
int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool allowViewUpdate)
652
{
653
    Q_UNUSED(allowViewUpdate);
654
    QWriteLocker locker(&m_lock);
655
656
657
    Q_ASSERT(isClip(clipId));
    Q_ASSERT(isTrack(trackId));
    int currentPos = getClipPosition(clipId);
658
    int sourceTrackId = getClipTrackId(clipId);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
659
    if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) {
660
661
662
        // Trying move on incompatible track type, stay on same track
        trackId = sourceTrackId;
    }
663
    if (currentPos == position && sourceTrackId == trackId) {
664
665
        return position;
    }
666
    bool after = position > currentPos;
667
668
669
    if (snapDistance > 0) {
        // For snapping, we must ignore all in/outs of the clips of the group being moved
        std::vector<int> ignored_pts;
670
        std::unordered_set<int> all_items = {clipId};
671
672
        if (m_groups->isInGroup(clipId)) {
            int groupId = m_groups->getRootId(clipId);
673
            all_items = m_groups->getLeaves(groupId);
674
        }
675
        for (int current_clipId : all_items) {
676
            if (getItemTrackId(current_clipId) != -1) {
677
678
679
680
681
                int in = getItemPosition(current_clipId);
                int out = in + getItemPlaytime(current_clipId);
                ignored_pts.push_back(in);
                ignored_pts.push_back(out);
            }
682
683
        }

684
685
        int snapped = requestBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector<int>(),
                                         cursorPosition, snapDistance);
Nicolas Carion's avatar
Nicolas Carion committed
686
        // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped;
687
688
689
        if (snapped >= 0) {
            position = snapped;
        }
690
    }
Nicolas Carion's avatar
Nicolas Carion committed
691
    // we check if move is possible
692
693
    bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false)
                                                           : requestFakeClipMove(clipId, trackId, position, true, false, false);
694
    /*} else {
695
        possible = requestClipMoveAttempt(clipId, trackId, position);
696
    }*/
697
698
699
    if (possible) {
        return position;
    }
700
701
    // Find best possible move
    if (!m_groups->isInGroup(clipId)) {
702
        // Try same track move
703
704
705
706
707
708
709
710
711
        if (trackId != sourceTrackId) {
            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 {
                return position;
            }
712
713
        }

714
715
716
717
        int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after);
        qDebug() << "Found blank" << blank_length;
        if (blank_length < INT_MAX) {
            if (after) {
718
719
720
721
722
                position = currentPos + blank_length;
            } else {
                position = currentPos - blank_length;
            }
        } else {
723
            return currentPos;
724
        }
725
        possible = requestClipMove(clipId, trackId, position, true, false, false);
726
727
728
729
        return possible ? position : currentPos;
    }
    // find best pos for groups
    int groupId = m_groups->getRootId(clipId);
730
    std::unordered_set<int> all_items = m_groups->getLeaves(groupId);
Nicolas Carion's avatar
Nicolas Carion committed
731
    QMap<int, int> trackPosition;
732
733

    // First pass, sort clips by track and keep only the first / last depending on move direction
734
    for (int current_clipId : all_items) {
735
        int clipTrack = getItemTrackId(current_clipId);
736
737
738
        if (clipTrack == -1) {
            continue;
        }
739
        int in = getItemPosition(current_clipId);
740
741
742
        if (trackPosition.contains(clipTrack)) {
            if (after) {
                // keep only last clip position for track
743
                int out = in + getItemPlaytime(current_clipId);
744
745
746
747
748
749
750
751
                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);
                }
752
            }
Nicolas Carion's avatar
Nicolas Carion committed
753
        } else {
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
            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 {
770
771
            // Check space after the position
            track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1;
772
773
774
775
776
777
778
            if (blank_length == -1 || blank_length > track_space) {
                blank_length = track_space;
            }
        }
    }
    if (blank_length != 0) {
        int updatedPos = currentPos + (after ? blank_length : -blank_length);
779
        possible = requestClipMove(clipId, trackId, updatedPos, true, false, false);
780
781
        if (possible) {
            return updatedPos;
Nicolas Carion's avatar
Nicolas Carion committed
782
        }
783
    }
784
    return currentPos;
785
786
}

787
int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance)
788
789
{
    QWriteLocker locker(&m_lock);
790
    Q_ASSERT(isComposition(compoId));
791
    Q_ASSERT(isTrack(trackId));
792
793
    int currentPos = getCompositionPosition(compoId);
    int currentTrack = getCompositionTrackId(compoId);
794
    if (getTrackById_const(trackId)->isAudioTrack()) {
795
796
797
        // Trying move on incompatible track type, stay on same track
        trackId = currentTrack;
    }
798
    if (currentPos == position && currentTrack == trackId) {
799
800
801
        return position;
    }

802
803
804
805
806
    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);
807
808
            auto all_items = m_groups->getLeaves(groupId);
            for (int current_compoId : all_items) {
809
810
811
812
813
814
815
816
817
818
                // 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;
819
820
821
822
            ignored_pts.push_back(in);
            ignored_pts.push_back(out);
        }

823
        int snapped = requestBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance);
824
825
826
827
        qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped;
        if (snapped >= 0) {
            position = snapped;
        }
828
    }
Nicolas Carion's avatar
Nicolas Carion committed
829
    // we check if move is possible
830
    bool possible = requestCompositionMove(compoId, trackId, position, true, false);
831
832
833
834
    qDebug() << "Original move success" << possible;
    if (possible) {
        return position;
    }
835
    /*bool after = position > currentPos;
836
    int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after);
837
838
839
840
    qDebug() << "Found blank" << blank_length;
    if (blank_length < INT_MAX) {
        if (after) {
            return currentPos + blank_length;
Nicolas Carion's avatar
Nicolas Carion committed
841
842
        }
        return currentPos - blank_length;
843
    }
844
845
    return position;*/
    return currentPos;
846
847
}

848
bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo)
849
{
850
    qDebug() << "requestClipCreation " << binClipId;
Nicolas Carion's avatar
Nicolas Carion committed
851
    QString bid = binClipId;
852
853
    if (binClipId.contains(QLatin1Char('/'))) {
        bid = binClipId.section(QLatin1Char('/'), 0, 0);
Nicolas Carion's avatar
Nicolas Carion committed
854
    }
855
    if (!pCore->projectItemModel()->hasClip(bid)) {
856
        qDebug() << " / / / /MASTER CLIP NOT FOUND";
857
858
        return false;
    }
859
    std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid);
860
    if (!master->isReady() || !master->isCompatible(state)) {
Nicolas Carion's avatar
Nicolas Carion committed
861
        qDebug() << "// CLIP NOT READY OR NOT COMPATIBLE: " << state;
862
863
864
865
866
        return false;
    }
    int clipId = TimelineModel::getNextId();
    id = clipId;
    Fun local_undo = deregisterClip_lambda(clipId);
867
    ClipModel::construct(shared_from_this(), bid, clipId, state, speed);
868
    auto clip = m_allClips[clipId];
869
    Fun local_redo = [clip, this, state]() {
870
871
        // 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.
872
        registerClip(clip, true);
873
        clip->refreshProducerFromBin(state);
874
875
        return true;
    };
Nicolas Carion's avatar
Nicolas Carion committed
876

877
878
879
    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
880
        int initLength = m_allClips[clipId]->getPlaytime();
881
882
        bool res = true;
        if (in != 0) {
883
            res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo);
884
        }
885
        res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo);
Nicolas Carion's avatar
Nicolas Carion committed
886
887
888
889
890
        if (!res) {
            bool undone = local_undo();
            Q_ASSERT(undone);
            return false;
        }
891
892
893
894
895
    }
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
}

896
897
898
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
899
    TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets);
900
901
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
902
903
904
    bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo);
    if (result && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Insert Clip"));
905
    }
Nicolas Carion's avatar
Nicolas Carion committed
906
    TRACE_RES(result);
907
908
909
910
    return result;
}

bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets,
911
                                         Fun &undo, Fun &redo)
912
{
Nicolas Carion's avatar
Nicolas Carion committed
913
914
    std::function<bool(void)> local_undo = []() { return true; };
    std::function<bool(void)> local_redo = []() { return true; };
915
916
    qDebug() << "requestClipInsertion " << binClipId << " "
             << " " << trackId << " " << position;
917
    bool res = false;
918
    ClipType::ProducerType type = ClipType::Unknown;
919
    QString bid = binClipId.section(QLatin1Char('/'), 0, 0);
920
921
922
923
924
925
926
927
928
    // dropType indicates if we want a normal drop (disabled), audio only or video only drop
    PlaylistState::ClipState dropType = PlaylistState::Disabled;
    if (bid.startsWith(QLatin1Char('A'))) {
        dropType = PlaylistState::AudioOnly;
        bid = bid.remove(0, 1);
    } else if (bid.startsWith(QLatin1Char('V'))) {
        dropType = PlaylistState::VideoOnly;
        bid = bid.remove(0, 1);
    }
929
930
    if (!pCore->projectItemModel()->hasClip(bid)) {
        return false;
931
    }
932
933
    std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid);
    type = master->clipType();
934
935
936
    if (useTargets && m_audioTarget == -1 && m_videoTarget == -1) {
        useTargets = false;
    }
937
    if (dropType == PlaylistState::Disabled && (type == ClipType::AV || type == ClipType::Playlist)) {
938
        if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) {
939
940
            // If audio target is set but no video target, only insert audio
            trackId = m_audioTarget;
941
942
943
944
945
946
947
948
949
950
951
952
953
            if (trackId > -1 && getTrackById_const(trackId)->isLocked()) {
                trackId = -1;
            }
        } else if (useTargets && getTrackById_const(trackId)->isLocked()) {
            // Video target set but locked
            trackId = m_audioTarget;
            if (trackId > -1 && getTrackById_const(trackId)->isLocked()) {
                trackId = -1;
            }
        }
        if (trackId == -1) {
            pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage);
            return false;
954
        }
955
        bool audioDrop = getTrackById_const(trackId)->isAudioTrack();
956
        res = requestClipCreation(binClipId, id, getTrackById_const(trackId)->trackType(), 1.0, local_undo, local_redo);
957
        res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo);
958
959
960
961
962
963
        int target_track;
        if (audioDrop) {
            target_track = m_videoTarget == -1 ? -1 : getTrackById_const(m_videoTarget)->isLocked() ? -1 : m_videoTarget;
        } else {
            target_track = m_audioTarget == -1 ? -1 : getTrackById_const(m_audioTarget)->isLocked() ? -1 : m_audioTarget;
        }
964
        qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo();
965
        int mirror = getMirrorTrackId(trackId);
966
967
968
        if (mirror > -1 && getTrackById_const(mirror)->isLocked()) {
            mirror = -1;
        }
969
970
        bool canMirrorDrop = !useTargets && mirror > -1;
        if (res && (canMirrorDrop || target_track > -1) && master->hasAudioAndVideo()) {
971
            if (!useTargets) {
972
                target_track = mirror;
973
974
975
976
            }
            // QList<int> possibleTracks = m_audioTarget >= 0 ? QList<int>() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack);
            QList<int> possibleTracks;
            qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets;
977
            if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) {
978
979
                possibleTracks << target_track;
            }
980
981
            if (possibleTracks.isEmpty()) {
                // No available audio track for splitting, abort
982
                pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage);
983
984
                res = false;
            } else {
985
986
                std::function<bool(void)> audio_undo = []() { return true; };
                std::function<bool(void)> audio_redo = []() { return true; };