timelinemodel.cpp 204 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 "bin/model/subtitlemodel.hpp"
32
#include "effects/effectstack/model/effectstackmodel.hpp"
33
#include "jobs/jobmanager.h"
34
#include "groupsmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
35
#include "kdenlivesettings.h"
36
#include "snapmodel.hpp"
37
#include "timelinefunctions.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
38
#include "trackmodel.hpp"
39

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

52
53
#include "macros.hpp"

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

113
int TimelineModel::next_id = 0;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
114
int TimelineModel::seekDuration = 30000;
115

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

143
    TRACE_CONSTR(this);
144
145
}

146
147
void TimelineModel::prepareClose()
{
148
    requestClearSelection(true);
149
    QWriteLocker locker(&m_lock);
150
    // Unlock all tracks to allow deleting clip from tracks
151
152
153
    m_closing = true;
    auto it = m_allTracks.begin();
    while (it != m_allTracks.end()) {
154
        (*it)->unlock();
155
156
        ++it;
    }
157
158
    m_subtitleModel.reset();
    //m_subtitleModel->removeAllSubtitles();
159
160
}

161
162
TimelineModel::~TimelineModel()
{
163
    std::vector<int> all_ids;
Nicolas Carion's avatar
Nicolas Carion committed
164
    for (auto tracks : m_iteratorTable) {
165
166
        all_ids.push_back(tracks.first);
    }
Nicolas Carion's avatar
Nicolas Carion committed
167
    for (auto tracks : all_ids) {
168
        deregisterTrack_lambda(tracks)();
169
    }
170
    for (const auto &clip : m_allClips) {
171
172
        clip.second->deregisterClipToBin();
    }
173
174
}

175
176
int TimelineModel::getTracksCount() const
{
177
    READ_LOCK();
178
    int count = m_tractor->count();
179
180
    if (m_overlayTrackCount > -1) {
        count -= m_overlayTrackCount;
181
    }
182
    Q_ASSERT(count >= 0);
183
    // don't count the black background track
Nicolas Carion's avatar
Nicolas Carion committed
184
    Q_ASSERT(count - 1 == static_cast<int>(m_allTracks.size()));
185
    return count - 1;
186
}
187

188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
QPair<int, int> TimelineModel::getAVtracksCount() const
{
    QPair <int, int> tracks{0, 0};
    auto it = m_allTracks.cbegin();
    while (it != m_allTracks.cend()) {
        if ((*it)->isAudioTrack()) {
            tracks.second++;
        } else {
            tracks.first++;
        }
        ++it;
    }
    if (m_overlayTrackCount > -1) {
        tracks.first -= m_overlayTrackCount;
    }
    return tracks;
}

QList<int> TimelineModel::getTracksIds(bool audio) const
{
    QList <int> trackIds;
    auto it = m_allTracks.cbegin();
    while (it != m_allTracks.cend()) {
        if ((*it)->isAudioTrack() == audio) {
            trackIds.insert(-1, (*it)->getId());
        }
        ++it;
    }
    return trackIds;
}

219
220
int TimelineModel::getTrackIndexFromPosition(int pos) const
{
221
    Q_ASSERT(pos >= 0 && pos < int(m_allTracks.size()));
222
    READ_LOCK();
223
    auto it = m_allTracks.cbegin();
224
225
226
227
228
229
230
    while (pos > 0) {
        it++;
        pos--;
    }
    return (*it)->getId();
}

231
int TimelineModel::getClipsCount() const
232
{
233
234
235
    READ_LOCK();
    int size = int(m_allClips.size());
    return size;
236
}
237

238
239
240
241
242
243
int TimelineModel::getCompositionsCount() const
{
    READ_LOCK();
    int size = int(m_allCompositions.size());
    return size;
}
244

245
int TimelineModel::getClipTrackId(int clipId) const
246
{
247
    READ_LOCK();
248
249
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
250
    return clip->getCurrentTrackId();
251
252
}

253
254
255
256
257
258
259
260
261
262
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();
263
    Q_ASSERT(isItem(itemId));
264
265
266
    if (isClip(itemId)) {
        return getClipTrackId(itemId);
    }
267
268
269
    if (isComposition(itemId)) {
        return getCompositionTrackId(itemId);
    }
270
    return -1;
271
272
}

273
int TimelineModel::getClipPosition(int clipId) const
274
{
275
    READ_LOCK();
276
277
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
278
279
    int pos = clip->getPosition();
    return pos;
280
281
}

282
283
284
285
286
287
288
double TimelineModel::getClipSpeed(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_allClips.at(clipId)->getSpeed();
}

289
290
291
292
293
294
295
int TimelineModel::getClipSplitPartner(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_groups->getSplitPartner(clipId);
}

296
297
298
299
300
int TimelineModel::getClipIn(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
301
302
303
304
305
306
307
308
309
    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();
310
311
312
313
314
315
316
317
318
319
320
}

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

321
int TimelineModel::getClipPlaytime(int clipId) const
322
{
323
    READ_LOCK();
324
    Q_ASSERT(isClip(clipId));
325
    const auto clip = m_allClips.at(clipId);
326
327
    int playtime = clip->getPlaytime();
    return playtime;
328
329
}

330
331
332
333
334
335
336
337
QSize TimelineModel::getClipFrameSize(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(isClip(clipId));
    const auto clip = m_allClips.at(clipId);
    return clip->getFrameSize();
}

338
int TimelineModel::getTrackClipsCount(int trackId) const
339
{
340
    READ_LOCK();
341
    Q_ASSERT(isTrack(trackId));
342
    int count = getTrackById_const(trackId)->getClipsCount();
343
    return count;
344
345
}

346
347
348
349
350
351
352
int TimelineModel::getClipByStartPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getClipByStartPosition(position);
}

353
354
355
356
357
358
359
int TimelineModel::getClipByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getClipByPosition(position);
}

360
361
362
363
364
365
366
int TimelineModel::getCompositionByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getCompositionByPosition(position);
}

367
int TimelineModel::getSubtitleByStartPosition(int position) const
368
369
370
371
372
373
374
375
376
377
378
379
{
    READ_LOCK();
    GenTime startTime(position, pCore->getCurrentFps());
    auto findResult = std::find_if(std::begin(m_allSubtitles), std::end(m_allSubtitles), [&](const std::pair<int, GenTime> &pair) {
        return pair.second == startTime;
    });
    if (findResult != std::end(m_allSubtitles)) {
        return findResult->first;
    }
    return -1;
}

380
381
382
383
384
385
386
387
388
389
390
391
392
int TimelineModel::getSubtitleByPosition(int position) const
{
    READ_LOCK();
    GenTime startTime(position, pCore->getCurrentFps());
    if (m_subtitleModel) {
        std::unordered_set<int> sids = m_subtitleModel->getItemsInRange(position, position);
        if (!sids.empty()) {
            return *sids.begin();
        }
    }
    return -1;
}

393
int TimelineModel::getTrackPosition(int trackId) const
394
{
395
    READ_LOCK();
396
    Q_ASSERT(isTrack(trackId));
397
    auto it = m_allTracks.cbegin();
Vincent Pinon's avatar
Vincent Pinon committed
398
    int pos = int(std::distance(it, static_cast<decltype(it)>(m_iteratorTable.at(trackId))));
399
    return pos;
400
401
}

402
403
404
405
406
407
408
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;
}

409
int TimelineModel::getTrackSortValue(int trackId, int separated) const
410
{
411
    if (separated == 1) {
412
        // This will be A2, A1, V1, V2
413
414
        return getTrackPosition(trackId) + 1;
    }
415
    if (separated == 2) {
416
        // This will be A1, A2, V1, V2
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
        // Count audio/video tracks
        auto it = m_allTracks.cbegin();
        int aCount = 0;
        int vCount = 0;
        int refPos = 0;
        bool isVideo = true;
        while (it != m_allTracks.cend()) {
            if ((*it)->isAudioTrack()) {
                if ((*it)->getId() == trackId) {
                    refPos = aCount;
                    isVideo = false;
                }
                aCount++;
            } else {
                // video track
                if ((*it)->getId() == trackId) {
                    refPos = vCount;
                }
                vCount++;
            }
            ++it;
        }
        return isVideo ? aCount + refPos + 1 : aCount - refPos;
    }
441
    // This will be A1, V1, A2, V2
442
    auto it = m_allTracks.cend();
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
    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;
        }
    }
460
    if (isAudio) {
461
462
463
464
465
466
        if (aCount > vCount) {
            if (trackPos - 1 > aCount - vCount) {
                // We have more audio tracks than video tracks
                return (aCount - vCount + 1) + 2 * (trackPos - (aCount - vCount +1));
            }
            return trackPos;
467
        }
468
        return 2 * trackPos;
469
    }
470
    return 2 * (vCount + 1 - trackPos) + 1;
471
472
}

Nicolas Carion's avatar
Nicolas Carion committed
473
QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const
474
475
476
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
Nicolas Carion's avatar
Nicolas Carion committed
477
    QList<int> results;
478
    auto it = m_iteratorTable.at(trackId);
479
    while (it != m_allTracks.cbegin()) {
480
481
        --it;
        if (type == TrackType::AnyTrack) {
482
483
            results << (*it)->getId();
            continue;
484
        }
485
486
        bool audioTrack = (*it)->isAudioTrack();
        if (type == TrackType::AudioTrack && audioTrack) {
487
            results << (*it)->getId();
488
        } else if (type == TrackType::VideoTrack && !audioTrack) {
489
            results << (*it)->getId();
490
491
        }
    }
492
    return results;
493
494
}

495
496
497
498
499
int TimelineModel::getPreviousVideoTrackIndex(int trackId) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
500
    while (it != m_allTracks.cbegin()) {
501
        --it;
502
503
        if (!(*it)->isAudioTrack()) {
            return (*it)->getId();
504
505
        }
    }
506
    return 0;
507
508
}

509
int TimelineModel::getPreviousVideoTrackPos(int trackId) const
510
511
512
513
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
514
    while (it != m_allTracks.cbegin()) {
515
        --it;
516
517
        if (!(*it)->isAudioTrack()) {
            return getTrackMltIndex((*it)->getId());
518
        }
519
    }
520
    return 0;
521
522
}

523
524
525
526
527
528
529
530
531
532
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;
533
    while (it != m_allTracks.cend()) {
534
535
536
        if ((*it)->isAudioTrack()) {
            count++;
        } else {
537
            count--;
538
539
540
541
542
543
544
545
546
            if (count == 0) {
                return (*it)->getId();
            }
        }
        ++it;
    }
    return -1;
}

547
548
549
550
551
552
553
554
int TimelineModel::getMirrorTrackId(int trackId) const
{
    if (isAudioTrack(trackId)) {
        return getMirrorVideoTrackId(trackId);
    }
    return getMirrorAudioTrackId(trackId);
}

555
556
557
558
559
560
561
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...
562
        qWarning() << "requesting mirror audio track for audio track";
563
564
565
        return -1;
    }
    int count = 0;
566
    while (it != m_allTracks.cbegin()) {
567
568
569
        if (!(*it)->isAudioTrack()) {
            count++;
        } else {
570
            count--;
571
572
573
574
            if (count == 0) {
                return (*it)->getId();
            }
        }
575
        --it;
576
    }
577
    if ((*it)->isAudioTrack() && count == 1) {
578
579
        return (*it)->getId();
    }
580
581
582
    return -1;
}

583
584
585
586
587
588
589
590
591
592
void TimelineModel::setEditMode(TimelineMode::EditMode mode)
{
    m_editMode = mode;
}

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

593
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
594
{
595
596
597
598
    Q_UNUSED(updateView);
    Q_UNUSED(invalidateTimeline);
    Q_UNUSED(undo);
    Q_UNUSED(redo);
599
600
601
602
603
    Q_ASSERT(isClip(clipId));
    m_allClips[clipId]->setFakePosition(position);
    bool trackChanged = false;
    if (trackId > -1) {
        if (trackId != m_allClips[clipId]->getFakeTrackId()) {
604
            if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
605
606
607
608
609
610
611
                m_allClips[clipId]->setFakeTrackId(trackId);
                trackChanged = true;
            }
        }
    }
    QModelIndex modelIndex = makeClipIndexFromID(clipId);
    if (modelIndex.isValid()) {
612
        QVector<int> roles{FakePositionRole};
613
614
615
616
617
618
619
620
621
        if (trackChanged) {
            roles << FakeTrackIdRole;
        }
        notifyChange(modelIndex, modelIndex, roles);
        return true;
    }
    return false;
}

622
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove, QMap <int, int> moving_clips)
623
{
Vincent Pinon's avatar
Vincent Pinon committed
624
    Q_UNUSED(moveMirrorTracks)
625
626
627
    if (trackId == -1) {
        return false;
    }
628
    Q_ASSERT(isClip(clipId));
629
630
631
632
633
634
635
636
    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()) {
637
638
639
        // Move not allowed (audio / video mismatch)
        return false;
    }
Nicolas Carion's avatar
Nicolas Carion committed
640
641
    std::function<bool(void)> local_undo = []() { return true; };
    std::function<bool(void)> local_redo = []() { return true; };
642
    bool ok = true;
643
    int old_trackId = getClipTrackId(clipId);
644
645
646
647
    int previous_track = moving_clips.value(clipId, -1);
    if (old_trackId == -1) {
        //old_trackId = previous_track;
    }
648
649
650
651
    bool notifyViewOnly = false;
    Fun update_model = []() { return true; };
    if (old_trackId == trackId) {
        // Move on same track, simply inform the view
Nicolas Carion's avatar
Nicolas Carion committed
652
        updateView = false;
653
        notifyViewOnly = true;
654
        update_model = [clipId, this, trackId, invalidateTimeline]() {
655
            QModelIndex modelIndex = makeClipIndexFromID(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
656
            notifyChange(modelIndex, modelIndex, StartRole);
657
            if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
658
659
660
                int in = getClipPosition(clipId);
                emit invalidateZone(in, in + getClipPlaytime(clipId));
            }
661
662
663
            return true;
        };
    }
664
665
666
667
668
    Fun sync_mix = []() { return true; };
    Fun update_playlist = []() { return true; };
    Fun update_playlist_undo = []() { return true; };
    Fun simple_move_mix = []() { return true; };
    Fun simple_restore_mix = []() { return true; };
669
    if (old_trackId == -1 && isTrack(previous_track) && getTrackById_const(previous_track)->hasMix(clipId) && previous_track != trackId) {
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
        // Clip is moved to another track
        std::pair<MixInfo, MixInfo> mixData = getTrackById_const(previous_track)->getMixInfo(clipId);
        bool mixGroupMove = false;
        if (m_groups->isInGroup(clipId)) {
            int parentGroup = m_groups->getRootId(clipId);
            if (parentGroup > -1) {
                std::unordered_set<int> sub = m_groups->getLeaves(parentGroup);
                if (sub.count(mixData.first.firstClipId) > 0 && sub.count(mixData.first.secondClipId) > 0) {
                    mixGroupMove = true;
                }
            }
        }
        if (mixGroupMove) {
            // We are moving a group on another track, delete and re-add
            bool isAudio = getTrackById_const(previous_track)->isAudioTrack();
Vincent Pinon's avatar
Vincent Pinon committed
685
            simple_move_mix = [this, previous_track, trackId, finalMove, mixData, isAudio]() {
686
687
688
689
690
691
692
693
694
695
696
697
698
699
                getTrackById_const(previous_track)->syncronizeMixes(finalMove);
                bool result = getTrackById_const(trackId)->createMix(mixData.first, isAudio);
                return result;
            };
            simple_restore_mix = [this, previous_track, trackId, finalMove, mixData, isAudio]() {
                bool result = true;
                if (finalMove) {
                    result = getTrackById_const(previous_track)->createMix(mixData.first, isAudio);
                    getTrackById_const(trackId)->syncronizeMixes(finalMove);
                    //getTrackById_const(previous_track)->syncronizeMixes(finalMove);
                }
                return result;
            };
        }
700
    } else if (finalMove && !groupMove && isTrack(old_trackId) && getTrackById_const(old_trackId)->hasMix(clipId)) {
701
        // Clip has a mix
702
703
        std::pair<MixInfo, MixInfo> mixData = getTrackById_const(old_trackId)->getMixInfo(clipId);
        if (mixData.first.firstClipId > -1) {
704
            if (old_trackId == trackId) {
705
                // We are moving a clip on same track
706
                if (finalMove && position >= mixData.first.firstClipInOut.second) {
707
                    position += m_allClips[clipId]->getMixDuration() - m_allClips[clipId]->getMixCutPosition();
708
                    removeMixWithUndo(clipId, local_undo, local_redo);
709
                }
710
            } else {
711
                // Clip moved to another track, delete mix
712
                removeMixWithUndo(clipId, local_undo, local_redo);
713
            }
714
        }
715
716
        if (mixData.second.firstClipId > -1) {
            // We have a mix at clip end
717
718
719
            int clipDuration = mixData.second.firstClipInOut.second - mixData.second.firstClipInOut.first;
            sync_mix = [this, old_trackId, finalMove]() {
                getTrackById_const(old_trackId)->syncronizeMixes(finalMove);
720
721
                return true;
            };
722
723
            if (old_trackId == trackId) {
                if (finalMove && (position + clipDuration <= mixData.second.secondClipInOut.first)) {
724
                    // Moved outside mix zone
725
                    removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
726
                    
727
                }
728
729
730
731
            } else {
                // Clip moved to another track, delete mix
                // Mix will be deleted by syncronizeMixes operation, only
                // re-add it on undo
732
                removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
733
734
            }
        }
735
    } else if (finalMove && groupMove && isTrack(old_trackId) && getTrackById_const(old_trackId)->hasMix(clipId) && old_trackId == trackId) {
736
737
738
739
740
741
742
        std::pair<MixInfo, MixInfo> mixData = getTrackById_const(old_trackId)->getMixInfo(clipId);
        if (mixData.first.firstClipId > -1) {
            // Mix on clip start, check if mix is still in range
            if (!moving_clips.contains(mixData.first.firstClipId)) {
                int mixEnd = m_allClips[mixData.first.firstClipId]->getPosition() + m_allClips[mixData.first.firstClipId]->getPlaytime();
                if (mixEnd < position) {
                    // Mix will be deleted, recreate on undo
743
744
                    position += m_allClips[mixData.first.secondClipId]->getMixDuration() - m_allClips[mixData.first.secondClipId]->getMixCutPosition();
                    removeMixWithUndo(mixData.first.secondClipId, local_undo, local_redo);
745
746
747
748
749
750
751
752
753
                }
            }
        }
        if (mixData.second.firstClipId > -1) {
            // Mix on clip end, check if mix is still in range
            if (!moving_clips.contains(mixData.second.secondClipId)) {
                int mixEnd = m_allClips[mixData.second.secondClipId]->getPosition();
                if (mixEnd > position + m_allClips[clipId]->getPlaytime()) {
                    // Mix will be deleted, recreate on undo
754
                    removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
755
                }
756
757
            }
        }
758
    }
759
760
761
762
    PUSH_LAMBDA(simple_restore_mix, local_undo);
    if (finalMove) {
        PUSH_LAMBDA(sync_mix, local_undo);
    }
763
    if (old_trackId != -1) {
764
765
766
        if (notifyViewOnly) {
            PUSH_LAMBDA(update_model, local_undo);
        }
767
        ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove, false);
768
        if (!ok) {
769
770
            bool undone = local_undo();
            Q_ASSERT(undone);
771
772
773
            return false;
        }
    }
774
775
    update_playlist();
    UPDATE_UNDO_REDO(update_playlist, update_playlist_undo, local_undo, local_redo);
776
    ok = getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove);
777
    if (!ok) {
778
        qWarning() << "clip insertion failed";
779
780
        bool undone = local_undo();
        Q_ASSERT(undone);
781
        return false;
782
    }
783
    qDebug()<<":::MOVED CLIP: "<<clipId<<" TO "<<position;
784
    sync_mix();
785
    update_model();
786
787
788
789
790
    simple_move_mix();
    PUSH_LAMBDA(simple_move_mix, local_redo);
    if (finalMove) {
        PUSH_LAMBDA(sync_mix, local_redo);
    }
791
792
793
    if (notifyViewOnly) {
        PUSH_LAMBDA(update_model, local_redo);
    }
Nicolas Carion's avatar
Nicolas Carion committed
794
795
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
796
797
}

798
bool TimelineModel::mixClip(int idToMove, int delta)
799
800
{
    int selectedTrack = -1;
801
    qDebug()<<"==== REQUEST CLIP MIX STEP 1";
802
803
    std::unordered_set<int> initialSelection = getCurrentSelection();
    if (idToMove == -1 && initialSelection.empty()) {
804
        pCore->displayMessage(i18n("Select a clip to apply the mix"), ErrorMessage, 500);
805
806
807
        return false;
    }
    std::pair<int, int> clipsToMix;
808
    int mixPosition = 0;
809
    int previousClip = -1;
810
    int noSpaceInClip = 0;
811
812
813
814
815
816
817
    if (idToMove != -1) {
        initialSelection = {idToMove};
        idToMove = -1;
    }
    for (int s : initialSelection) {
        if (!isClip(s)) {
            continue;
818
        }
819
820
821
        selectedTrack = getClipTrackId(s);
        if (selectedTrack == -1 || !isTrack(selectedTrack)) {
            continue;
822
        }
823
824
825
826
        mixPosition = getItemPosition(s);
        int clipDuration =  getItemPlaytime(s);    
        // Check if we have a clip before and/or after
        int nextClip = -1;
827
        previousClip = -1;
828
        // Check if clip already has a mix
829
        if (delta > -1 && getTrackById_const(selectedTrack)->hasStartMix(s)) {
830
831
832
833
            if (getTrackById_const(selectedTrack)->hasEndMix(s)) {
                continue;
            }
            nextClip = getTrackById_const(selectedTrack)->getClipByPosition(mixPosition + clipDuration + 1);
834
        } else if (delta < 1 && getTrackById_const(selectedTrack)->hasEndMix(s)) {
835
836
837
838
839
            previousClip = getTrackById_const(selectedTrack)->getClipByPosition(mixPosition - 1);
            if (previousClip > -1 && getTrackById_const(selectedTrack)->hasEndMix(previousClip)) {
                // Could happen if 2 clips before are mixed to full length
                previousClip = -1;
            }
840
        } else {
841
842
843
844
845
846
            if (delta < 1) {
                previousClip = getTrackById_const(selectedTrack)->getClipByPosition(mixPosition - 1);
            }
            if (delta > -1) {
                nextClip = getTrackById_const(selectedTrack)->getClipByPosition(mixPosition + clipDuration + 1);
            }
847
        }
848
849
850
851
852
853
854
855
856
857
858
        if (previousClip > -1 && nextClip > -1) {
            // We have a clip before and a clip after, check timeline cursor position to decide where to mix
            int cursor = pCore->getTimelinePosition();
            if (cursor < mixPosition + clipDuration / 2) {
                nextClip = -1;
            } else {
                previousClip = -1;
            }
        }
        if (nextClip == -1) {
            if (previousClip == -1) {
859
            // No clip to mix, abort
860
861
                continue;
            }
862
863
864
865
866
867
            // Make sure we have enough space in clip to resize
            int maxLength = m_allClips[previousClip]->getMaxDuration();
            if ((m_allClips[s]->getMaxDuration() > -1 && m_allClips[s]->getIn() < 2) || (maxLength > -1 && m_allClips[previousClip]->getOut() + 2 >= maxLength)) {
                noSpaceInClip = 1;
                continue;
            }
868
869
870
871
872
873
874
            // Mix at start of selected clip
            clipsToMix.first = previousClip;
            clipsToMix.second = s;
            idToMove = s;
            break;
        } else {
            // Mix at end of selected clip
875
876
877
878
879
880
            // Make sure we have enough space in clip to resize
            int maxLength = m_allClips[s]->getMaxDuration();
            if ((m_allClips[nextClip]->getMaxDuration() > -1 && m_allClips[nextClip]->getIn() < 2) || (maxLength > -1 && m_allClips[s]->getOut() + 2 >= maxLength)) {
                noSpaceInClip = 2;
                continue;
            }
881
882
883
884
885
            mixPosition += clipDuration;
            clipsToMix.first = s;
            clipsToMix.second = nextClip;
            idToMove = s;
            break;
886
887
        }
    }
888
889
890
891
    if (noSpaceInClip > 0) {
        pCore->displayMessage(i18n("Not enough frames at clip %1 to apply the mix", noSpaceInClip == 1 ? i18n("start") : i18n("end")), ErrorMessage, 500);
        return false;
    }
892
    if (idToMove == -1 || !isClip(idToMove)) {
893
        pCore->displayMessage(i18n("Select a clip to apply the mix"), ErrorMessage, 500);
894
895
896
        return false;
    }

897
898
899
900
901
902
903
904
905
906
907
908
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    bool result = requestClipMix(clipsToMix, selectedTrack, mixPosition, true, true, true, undo,
 redo, false);
    if (result) {
        // Check if this is an AV split group
        if (m_groups->isInGroup(idToMove)) {
            int parentGroup = m_groups->getRootId(idToMove);
            if (parentGroup > -1 && m_groups->getType(parentGroup) == GroupType::AVSplit) {
                std::unordered_set<int> sub = m_groups->getLeaves(parentGroup);
                // Perform mix on split clip
                for (int current_id : sub) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
909
                    if (idToMove == current_id) {
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
                        continue;
                    }
                    int splitTrack = m_allClips[current_id]->getCurrentTrackId();
                    int splitId;
                    if (previousClip == -1) {
                        splitId = getTrackById_const(splitTrack)->getClipByPosition(mixPosition + 1);
                        clipsToMix.first = current_id;
                        clipsToMix.second = splitId;
                    } else {
                        splitId = getTrackById_const(splitTrack)->getClipByPosition(mixPosition - 1);
                        clipsToMix.first = splitId;
                        clipsToMix.second = current_id;
                    }
                    if (splitId > -1 && clipsToMix.first != clipsToMix.second) {
                        result = requestClipMix(clipsToMix, splitTrack, mixPosition, true, true, true, undo, redo, false);
                    }
                }
            }
        }
        pCore->pushUndo(undo, redo, i18n("Create mix"));
930
931
932
933
        // Reselect clips
        if (!initialSelection.empty()) {
            requestSetSelection(initialSelection);
        }
934
935
        return result;
    } else {
936
        qWarning() << "mix failed";
937
        undo();
938
939
940
        if (!initialSelection.empty()) {
            requestSetSelection(initialSelection);
        }
941
942
943
        return false;
    }
}
944

945
bool TimelineModel::requestClipMix(std::pair<int, int> clipIds, int trackId, int position, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
946
947
948
949
{
    if (trackId == -1) {
        return false;
    }
950
    Q_ASSERT(isClip(clipIds.first));
951
952
953
954
955
956
957
958
    std::function<bool(void)> local_undo = []() { return true; };
    std::function<bool(void)> local_redo = []() { return true; };
    bool ok = true;
    bool notifyViewOnly = false;
    Fun update_model = []() { return true; };
    // Move on same track, simply inform the view
    updateView = false;
    notifyViewOnly = true;
959
    int mixDuration = pCore->getDurationFromString(KdenliveSettings::mix_duration());
960
961
    update_model = [clipIds, this, trackId, position, invalidateTimeline, mixDuration]() {
        QModelIndex modelIndex = makeClipIndexFromID(clipIds.second);
962
        notifyChange(modelIndex, modelIndex, {StartRole,DurationRole});
963
        QModelIndex modelIndex2 = makeClipIndexFromID(clipIds.first);
Vincent Pinon's avatar
Vincent Pinon committed
964
        notifyChange(modelIndex2, modelIndex2, DurationRole);
965
        if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
966
            emit invalidateZone(position - mixDuration / 2, position + mixDuration);
967
968
969
970
971
972
        }
        return true;
    };
    if (notifyViewOnly) {
        PUSH_LAMBDA(update_model, local_undo);
    }
973
    ok = getTrackById(trackId)->requestClipMix(clipIds, mixDuration, updateView, finalMove, local_undo, local_redo, groupMove);
974
    if (!ok) {
975
        qWarning() << "mix failed, reverting";
976
977
978
979
980
981
982
983
984
985
986
987
988
        bool undone = local_undo();
        Q_ASSERT(undone);
        return false;
    }
    update_model();
    if (notifyViewOnly) {
        PUSH_LAMBDA(update_model, local_redo);
    }
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return ok;

}

989
990
991
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
    QWriteLocker locker(&m_lock);
992
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
993
994
995
996
997
998
999
1000
1001
    Q_ASSERT(m_allClips.count(clipId) > 0);
    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();
1002
1003
1004
        bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
        TRACE_RES(res);
        return res;
1005
1006
1007
    }
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
1008
    bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
1009
1010
1011