timelinemodel.cpp 105 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
71
72
73
74
75
        .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))(
            parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
        .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
81
        .method("requestTrackInsertion", select_overload<bool(int, int &, const QString &, bool)>(&TimelineModel::requestTrackInsertion))(
            parameter_names("pos", "id", "trackName", "audioTrack"))
        .method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"));
Nicolas Carion's avatar
Nicolas Carion committed
82
83
}

84
int TimelineModel::next_id = 0;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
85
int TimelineModel::seekDuration = 30000;
86

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

114
    TRACE_CONSTR(this);
115
116
}

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

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

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

156
int TimelineModel::getClipsCount() const
157
{
158
159
160
    READ_LOCK();
    int size = int(m_allClips.size());
    return size;
161
}
162

163
164
165
166
167
168
int TimelineModel::getCompositionsCount() const
{
    READ_LOCK();
    int size = int(m_allCompositions.size());
    return size;
}
169

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

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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);
}

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

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

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

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

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

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

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

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

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

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

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

291
292
293
294
295
296
297
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;
}

298
299
300
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
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
333
QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const
334
335
336
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
Nicolas Carion's avatar
Nicolas Carion committed
337
    QList<int> results;
338
    auto it = m_iteratorTable.at(trackId);
339
340
341
    while (it != m_allTracks.begin()) {
        --it;
        if (type == TrackType::AnyTrack) {
342
343
            results << (*it)->getId();
            continue;
344
        }
345
346
        bool audioTrack = (*it)->isAudioTrack();
        if (type == TrackType::AudioTrack && audioTrack) {
347
            results << (*it)->getId();
348
        } else if (type == TrackType::VideoTrack && !audioTrack) {
349
            results << (*it)->getId();
350
351
        }
    }
352
    return results;
353
354
}

355
356
357
358
359
360
361
362
363
364
365
366
367
368
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();
}

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

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

413
414
415
416
417
418
419
420
int TimelineModel::getMirrorTrackId(int trackId) const
{
    if (isAudioTrack(trackId)) {
        return getMirrorVideoTrackId(trackId);
    }
    return getMirrorAudioTrackId(trackId);
}

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

451
452
453
454
455
456
457
458
459
460
461
462
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)
{
463
464
465
466
    Q_UNUSED(updateView);
    Q_UNUSED(invalidateTimeline);
    Q_UNUSED(undo);
    Q_UNUSED(redo);
467
468
469
470
471
    Q_ASSERT(isClip(clipId));
    m_allClips[clipId]->setFakePosition(position);
    bool trackChanged = false;
    if (trackId > -1) {
        if (trackId != m_allClips[clipId]->getFakeTrackId()) {
472
            if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
473
474
475
476
477
478
479
                m_allClips[clipId]->setFakeTrackId(trackId);
                trackChanged = true;
            }
        }
    }
    QModelIndex modelIndex = makeClipIndexFromID(clipId);
    if (modelIndex.isValid()) {
480
        QVector<int> roles{FakePositionRole};
481
482
483
484
485
486
487
488
489
        if (trackChanged) {
            roles << FakeTrackIdRole;
        }
        notifyChange(modelIndex, modelIndex, roles);
        return true;
    }
    return false;
}

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

557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
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();
572
        return requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
573
574
575
576
577
578
579
580
581
582
    }
    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;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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