timelinemodel.cpp 119 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")
63
        .method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock"))
Nicolas Carion's avatar
Nicolas Carion committed
64
65
66
67
68
69
70
71
        .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))(
72
            parameter_names("itemId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
Nicolas Carion's avatar
Nicolas Carion committed
73
74
75
76
        .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))(
77
78
79
            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
80
81
        .method("requestTrackInsertion", select_overload<bool(int, int &, const QString &, bool)>(&TimelineModel::requestTrackInsertion))(
            parameter_names("pos", "id", "trackName", "audioTrack"))
82
        .method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
83
        .method("requestClearSelection", select_overload<bool(bool)>(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
84
85
        .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear"))
        .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
86
87
88
        .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"))
89
90
        .method("requestFakeGroupMove", select_overload<bool(int, int, int, int, bool, bool)>(&TimelineModel::requestFakeGroupMove))(
            parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
91
92
93
        .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance"))
        .method("suggestCompositionMove",
                &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance"))
94
95
        // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos"))
        // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos"))
96
97
98
99
        // .method("requestCompositionInsertion", select_overload<bool(const QString &, int, int, int, std::unique_ptr<Mlt::Properties>, int &, bool)>(
        //                                            &TimelineModel::requestCompositionInsertion))(
        //     parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo"))
        .method("requestClipTimeWarp", select_overload<bool(int, double)>(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed"));
Nicolas Carion's avatar
Nicolas Carion committed
100
101
}

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

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

131
    TRACE_CONSTR(this);
132
133
}

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

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

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

173
174
int TimelineModel::getTrackIndexFromPosition(int pos) const
{
175
    Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size());
176
177
178
179
180
181
182
183
184
    READ_LOCK();
    auto it = m_allTracks.begin();
    while (pos > 0) {
        it++;
        pos--;
    }
    return (*it)->getId();
}

185
int TimelineModel::getClipsCount() const
186
{
187
188
189
    READ_LOCK();
    int size = int(m_allClips.size());
    return size;
190
}
191

192
193
194
195
196
197
int TimelineModel::getCompositionsCount() const
{
    READ_LOCK();
    int size = int(m_allCompositions.size());
    return size;
}
198

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

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

224
int TimelineModel::getClipPosition(int clipId) const
225
{
226
    READ_LOCK();
227
228
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
229
230
    int pos = clip->getPosition();
    return pos;
231
232
}

233
234
235
236
237
238
239
double TimelineModel::getClipSpeed(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_allClips.at(clipId)->getSpeed();
}

240
241
242
243
244
245
246
int TimelineModel::getClipSplitPartner(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_groups->getSplitPartner(clipId);
}

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

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

272
int TimelineModel::getClipPlaytime(int clipId) const
273
{
274
    READ_LOCK();
275
    Q_ASSERT(isClip(clipId));
276
    const auto clip = m_allClips.at(clipId);
277
278
    int playtime = clip->getPlaytime();
    return playtime;
279
280
}

281
282
283
284
285
286
287
288
QSize TimelineModel::getClipFrameSize(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(isClip(clipId));
    const auto clip = m_allClips.at(clipId);
    return clip->getFrameSize();
}

289
int TimelineModel::getTrackClipsCount(int trackId) const
290
{
291
    READ_LOCK();
292
    Q_ASSERT(isTrack(trackId));
293
    int count = getTrackById_const(trackId)->getClipsCount();
294
    return count;
295
296
}

297
298
299
300
301
302
303
int TimelineModel::getClipByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getClipByPosition(position);
}

304
305
306
307
308
309
310
int TimelineModel::getCompositionByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getCompositionByPosition(position);
}

311
int TimelineModel::getTrackPosition(int trackId) const
312
{
313
    READ_LOCK();
314
    Q_ASSERT(isTrack(trackId));
315
    auto it = m_allTracks.begin();
316
    int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId));
317
    return pos;
318
319
}

320
321
322
323
324
325
326
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;
}

327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
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
362
QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const
363
364
365
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
Nicolas Carion's avatar
Nicolas Carion committed
366
    QList<int> results;
367
    auto it = m_iteratorTable.at(trackId);
368
369
370
    while (it != m_allTracks.begin()) {
        --it;
        if (type == TrackType::AnyTrack) {
371
372
            results << (*it)->getId();
            continue;
373
        }
374
375
        bool audioTrack = (*it)->isAudioTrack();
        if (type == TrackType::AudioTrack && audioTrack) {
376
            results << (*it)->getId();
377
        } else if (type == TrackType::VideoTrack && !audioTrack) {
378
            results << (*it)->getId();
379
380
        }
    }
381
    return results;
382
383
}

384
385
386
387
388
389
390
391
392
393
394
395
396
397
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();
}

398
int TimelineModel::getPreviousVideoTrackPos(int trackId) const
399
400
401
402
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
403
404
    while (it != m_allTracks.begin()) {
        --it;
405
        if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) {
406
407
            break;
        }
408
    }
409
    return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId());
410
411
}

412
413
414
415
416
417
418
419
420
421
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;
422
423
424
    if (it != m_allTracks.end()) {
        ++it;
    }
425
426
427
428
429
430
431
432
433
434
435
    while (it != m_allTracks.end()) {
        if ((*it)->isAudioTrack()) {
            count++;
        } else {
            if (count == 0) {
                return (*it)->getId();
            }
            count--;
        }
        ++it;
    }
436
    if (it != m_allTracks.end() && !(*it)->isAudioTrack() && count == 0) {
437
438
439
440
441
        return (*it)->getId();
    }
    return -1;
}

442
443
444
445
446
447
448
449
int TimelineModel::getMirrorTrackId(int trackId) const
{
    if (isAudioTrack(trackId)) {
        return getMirrorVideoTrackId(trackId);
    }
    return getMirrorAudioTrackId(trackId);
}

450
451
452
453
454
455
456
457
458
459
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;
460
461
462
    if (it != m_allTracks.begin()) {
        --it;
    }
463
464
465
466
467
468
469
470
471
472
473
    while (it != m_allTracks.begin()) {
        if (!(*it)->isAudioTrack()) {
            count++;
        } else {
            if (count == 0) {
                return (*it)->getId();
            }
            count--;
        }
        --it;
    }
474
    if ((*it)->isAudioTrack() && count == 0) {
475
476
        return (*it)->getId();
    }
477
478
479
    return -1;
}

480
481
482
483
484
485
486
487
488
489
void TimelineModel::setEditMode(TimelineMode::EditMode mode)
{
    m_editMode = mode;
}

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

490
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
491
{
492
493
494
495
    Q_UNUSED(updateView);
    Q_UNUSED(invalidateTimeline);
    Q_UNUSED(undo);
    Q_UNUSED(redo);
496
497
498
499
500
    Q_ASSERT(isClip(clipId));
    m_allClips[clipId]->setFakePosition(position);
    bool trackChanged = false;
    if (trackId > -1) {
        if (trackId != m_allClips[clipId]->getFakeTrackId()) {
501
            if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
502
503
504
505
506
507
508
                m_allClips[clipId]->setFakeTrackId(trackId);
                trackChanged = true;
            }
        }
    }
    QModelIndex modelIndex = makeClipIndexFromID(clipId);
    if (modelIndex.isValid()) {
509
        QVector<int> roles{FakePositionRole};
510
511
512
513
514
515
516
517
518
        if (trackChanged) {
            roles << FakeTrackIdRole;
        }
        notifyChange(modelIndex, modelIndex, roles);
        return true;
    }
    return false;
}

519
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo)
520
{
521
    // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<<finalMove;
522
523
524
    if (trackId == -1) {
        return false;
    }
525
    Q_ASSERT(isClip(clipId));
526
527
528
529
530
531
532
533
    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()) {
534
        // Move not allowed (audio / video mismatch)
535
        qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState();
536
537
        return false;
    }
Nicolas Carion's avatar
Nicolas Carion committed
538
539
    std::function<bool(void)> local_undo = []() { return true; };
    std::function<bool(void)> local_redo = []() { return true; };
540
    bool ok = true;
541
    int old_trackId = getClipTrackId(clipId);
542
    bool notifyViewOnly = false;
543
    // qDebug()<<"MOVING CLIP FROM: "<<old_trackId<<" == "<<trackId;
544
545
546
    Fun update_model = []() { return true; };
    if (old_trackId == trackId) {
        // Move on same track, simply inform the view
Nicolas Carion's avatar
Nicolas Carion committed
547
        updateView = false;
548
        notifyViewOnly = true;
549
        update_model = [clipId, this, trackId, invalidateTimeline]() {
550
            QModelIndex modelIndex = makeClipIndexFromID(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
551
            notifyChange(modelIndex, modelIndex, StartRole);
552
            if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
553
554
555
                int in = getClipPosition(clipId);
                emit invalidateZone(in, in + getClipPlaytime(clipId));
            }
556
557
558
            return true;
        };
    }
559
    if (old_trackId != -1) {
560
561
562
        if (notifyViewOnly) {
            PUSH_LAMBDA(update_model, local_undo);
        }
563
        ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo);
564
        if (!ok) {
565
566
            bool undone = local_undo();
            Q_ASSERT(undone);
567
568
569
            return false;
        }
    }
570
    ok = ok & getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo);
571
    if (!ok) {
572
        qDebug() << "-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------";
573
574
        bool undone = local_undo();
        Q_ASSERT(undone);
575
        return false;
576
    }
577
578
579
580
    update_model();
    if (notifyViewOnly) {
        PUSH_LAMBDA(update_model, local_redo);
    }
Nicolas Carion's avatar
Nicolas Carion committed
581
582
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
583
584
}

585
586
587
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
    QWriteLocker locker(&m_lock);
588
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
589
590
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
591
        TRACE_RES(true);
592
593
594
595
596
597
598
599
600
601
        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();
602
603
604
        bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
        TRACE_RES(res);
        return res;
605
606
607
    }
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
608
    bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
609
610
611
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
    }
612
    TRACE_RES(res);
613
614
615
    return res;
}

616
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
617
{
618
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
619
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline);
620
621
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
Nicolas Carion's avatar
Nicolas Carion committed
622
        TRACE_RES(true);
623
624
        return true;
    }
625
    if (m_groups->isInGroup(clipId)) {
Nicolas Carion's avatar
Nicolas Carion committed
626
        // element is in a group.
627
628
629
630
        int groupId = m_groups->getRootId(clipId);
        int current_trackId = getClipTrackId(clipId);
        int track_pos1 = getTrackPosition(trackId);
        int track_pos2 = getTrackPosition(current_trackId);
631
        int delta_track = track_pos1 - track_pos2;
632
633
        int delta_pos = position - m_allClips[clipId]->getPosition();
        return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
634
    }
Nicolas Carion's avatar
Nicolas Carion committed
635
636
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
637
    bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, logUndo, undo, redo);
638
639
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
640
    }
Nicolas Carion's avatar
Nicolas Carion committed
641
    TRACE_RES(res);
642
643
644
    return res;
}

645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
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();
663
        res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false);
664
    } else {
665
        res = requestClipMove(clipId, trackId, position, false, false, false, undo, redo);
666
667
668
669
670
671
672
    }
    if (res) {
        undo();
    }
    return res;
}

673
int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
674
675
{
    if (isClip(itemId)) {
676
        return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
677
    }
678
    return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
679
680
}

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

715
716
        int snapped = getBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector<int>(),
                                     cursorPosition, snapDistance);
Nicolas Carion's avatar
Nicolas Carion committed
717
        // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped;
718
719
720
        if (snapped >= 0) {
            position = snapped;
        }
721
    }
Nicolas Carion's avatar
Nicolas Carion committed
722
    // we check if move is possible
723
724
    bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false)
                                                           : requestFakeClipMove(clipId, trackId, position, true, false, false);
725
    /*} else {
726
        possible = requestClipMoveAttempt(clipId, trackId, position);
727
    }*/
728
    if (possible) {
729
        TRACE_RES(position);
730
731
        return position;
    }
732
733
734
735
736
    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;
    }
737
738
    // Find best possible move
    if (!m_groups->isInGroup(clipId)) {
739
        // Try same track move
740
        if (trackId != sourceTrackId && sourceTrackId != -1) {
741
742
743
744
745
746
            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 {
747
                TRACE_RES(position);
748
749
                return position;
            }
750
751
        }

752
753
754
755
        int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after);
        qDebug() << "Found blank" << blank_length;
        if (blank_length < INT_MAX) {
            if (after) {
756
757
758
759
760
                position = currentPos + blank_length;
            } else {
                position = currentPos - blank_length;
            }
        } else {
761
            TRACE_RES(currentPos);
762
            return currentPos;
763
        }
764
        possible = requestClipMove(clipId, trackId, position, true, false, false);
765
        TRACE_RES(possible ? position : currentPos);
766
767
768
769
        return possible ? position : currentPos;
    }
    // find best pos for groups
    int groupId = m_groups->getRootId(clipId);
770
    std::unordered_set<int> all_items = m_groups->getLeaves(groupId);
Nicolas Carion's avatar
Nicolas Carion committed
771
    QMap<int, int> trackPosition;
772
773

    // First pass, sort clips by track and keep only the first / last depending on move direction
774
    for (int current_clipId : all_items) {
775
        int clipTrack = getItemTrackId(current_clipId);
776
777
778
        if (clipTrack == -1) {
            continue;
        }
779
        int in = getItemPosition(current_clipId);
780
781
782
        if (trackPosition.contains(clipTrack)) {
            if (after) {
                // keep only last clip position for track
783
                int out = in + getItemPlaytime(current_clipId);
784
785
786
787
788
789
790
791
                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);
                }
792
            }
Nicolas Carion's avatar
Nicolas Carion committed
793
        } else {
794
            trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in);
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
        }
    }
    // 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 {
810
811
            // Check space after the position
            track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1;
812
813
814
815
816
817
818
            if (blank_length == -1 || blank_length > track_space) {
                blank_length = track_space;
            }
        }
    }
    if (blank_length != 0) {
        int updatedPos = currentPos + (after ? blank_length : -blank_length);
819
        possible = requestClipMove(clipId, trackId, updatedPos, true, false, false);
820
        if (possible) {
821
            TRACE_RES(updatedPos);
822
            return updatedPos;
Nicolas Carion's avatar
Nicolas Carion committed
823
        }
824
    }
825
    TRACE_RES(currentPos);
826
    return currentPos;
827
828
}

829
int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance)
830
831
{
    QWriteLocker locker(&m_lock);
832
    TRACE(compoId, trackId, position, cursorPosition, snapDistance);
833
    Q_ASSERT(isComposition(compoId));
834
    Q_ASSERT(isTrack(trackId));
835
836
    int currentPos = getCompositionPosition(compoId);
    int currentTrack = getCompositionTrackId(compoId);
837
    if (getTrackById_const(trackId)->isAudioTrack()) {
838
839
840
        // Trying move on incompatible track type, stay on same track
        trackId = currentTrack;
    }
841
    if (currentPos == position && currentTrack == trackId) {
842
        TRACE_RES(position);
843
844
845
        return position;
    }

846
847
848
849
850
    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);
851
852
            auto all_items = m_groups->getLeaves(groupId);
            for (int current_compoId : all_items) {
853
854
855
856
857
858
859
860
861
862
                // 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;
863
864
865
866
            ignored_pts.push_back(in);
            ignored_pts.push_back(out);
        }

867
        int snapped = getBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance);
868
869
870
871
        qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped;
        if (snapped >= 0) {
            position = snapped;
        }
872
    }
Nicolas Carion's avatar
Nicolas Carion committed
873
    // we check if move is possible
874
    bool possible = requestCompositionMove(compoId, trackId, position, true, false);
875
876
    qDebug() << "Original move success" << possible;
    if (possible) {
877
        TRACE_RES(position);
878
879
        return position;
    }
880
    /*bool after = position > currentPos;
881
    int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after);
882
883
884
885
    qDebug() << "Found blank" << blank_length;
    if (blank_length < INT_MAX) {
        if (after) {
            return currentPos + blank_length;
Nicolas Carion's avatar
Nicolas Carion committed
886
887
        }
        return currentPos - blank_length;
888
    }
889
    return position;*/
890
    TRACE_RES(currentPos);
891
    return currentPos;
892
893
}

894
bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo)
895
{
896
    qDebug() << "requestClipCreation " << binClipId;
Nicolas Carion's avatar
Nicolas Carion committed
897
    QString bid = binClipId;
898
899
    if (binClipId.contains(QLatin1Char('/'))) {
        bid = binClipId.section(QLatin1Char('/'), 0, 0);
Nicolas Carion's avatar
Nicolas Carion committed
900
    }
901
    if (!pCore->projectItemModel()->hasClip(bid)) {
902
        qDebug() << " / / / /MASTER CLIP NOT FOUND";
903
904
        return false;
    }
905
    std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid);
906
    if (!master->isReady() || !master->isCompatible(state)) {
Nicolas Carion's avatar
Nicolas Carion committed
907
        qDebug() << "// CLIP NOT READY OR NOT COMPATIBLE: " << state;
908
909
910
911
912
        return false;
    }
    int clipId = TimelineModel::getNextId();
    id = clipId;
    Fun local_undo = deregisterClip_lambda(clipId);
913
    ClipModel::construct(shared_from_this(), bid, clipId, state, speed);
914
    auto clip = m_allClips[clipId];
915
    Fun local_redo = [clip, this, state]() {
916
917
        // 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.
918
        registerClip(clip, true);
919
        clip->refreshProducerFromBin(state);
920
921
        return true;
    };
Nicolas Carion's avatar
Nicolas Carion committed
922

923
924
925
    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
926
        int initLength = m_allClips[clipId]->getPlaytime();
927
928
        bool res = true;
        if (in != 0) {
929
            res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo);
930
        }
931
        res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo);
Nicolas Carion's avatar
Nicolas Carion committed
932
933
934
935
936
        if (!res) {
            bool undone = local_undo();
            Q_ASSERT(undone);
            return false;
        }
937
938
939
940
941
    }
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
}

942
943
944
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
945
    TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets);
946
947
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
948
949
950
    bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo);
    if (result && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Insert Clip"));
951
    }
Nicolas Carion's avatar