timelinefunctions.cpp 62.8 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  Jean-Baptiste Mardelle <jb@kdenlive.org>
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 "timelinefunctions.hpp"
23
24
25
26
#include "bin/bin.h"
#include "bin/projectclip.h"
#include "bin/projectfolder.h"
#include "bin/projectitemmodel.h"
27
#include "clipmodel.hpp"
28
#include "compositionmodel.hpp"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
29
#include "core.h"
30
#include "doc/kdenlivedoc.h"
31
#include "effects/effectstack/model/effectstackmodel.hpp"
32
#include "groupsmodel.hpp"
33
#include "logger.hpp"
34
35
#include "timelineitemmodel.hpp"
#include "trackmodel.hpp"
36
#include "transitions/transitionsrepository.hpp"
37

38
#include <QApplication>
39
#include <QDebug>
40
#include <QInputDialog>
Nicolas Carion's avatar
linting    
Nicolas Carion committed
41
#include <klocalizedstring.h>
42
#include <unordered_map>
43

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#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_<TimelineFunctions>("TimelineFunctions")
        .method("requestClipCut", select_overload<bool(std::shared_ptr<TimelineItemModel>, int, int)>(&TimelineFunctions::requestClipCut))(
            parameter_names("timeline", "clipId", "position"));
}

61
62
bool TimelineFunctions::cloneClip(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo,
                                  Fun &redo)
Nicolas Carion's avatar
Nicolas Carion committed
63
{
64
65
    // Special case: slowmotion clips
    double clipSpeed = timeline->m_allClips[clipId]->getSpeed();
66
    bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, clipSpeed, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
67
    timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize;
68

69
70
71
    // copy useful timeline properties
    timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]);

Nicolas Carion's avatar
Nicolas Carion committed
72
73
74
75
    int duration = timeline->getClipPlaytime(clipId);
    int init_duration = timeline->getClipPlaytime(newId);
    if (duration != init_duration) {
        int in = timeline->m_allClips[clipId]->getIn();
76
77
        res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo);
        res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
78
79
80
81
82
83
    }
    if (!res) {
        return false;
    }
    std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(clipId);
    std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId);
84
    destStack->importEffects(sourceStack, state);
Nicolas Carion's avatar
Nicolas Carion committed
85
86
87
    return res;
}

Nicolas Carion's avatar
Nicolas Carion committed
88
bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr<TimelineItemModel> &timeline, const QStringList &binIds, int trackId, int position,
89
                                                      QList<int> &clipIds, bool logUndo, bool refreshView)
90
91
92
93
94
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    for (const QString &binId : binIds) {
        int clipId;
95
        if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) {
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
            clipIds.append(clipId);
            position += timeline->getItemPlaytime(clipId);
        } else {
            undo();
            clipIds.clear();
            return false;
        }
    }

    if (logUndo) {
        pCore->pushUndo(undo, redo, i18n("Insert Clips"));
    }

    return true;
}

Nicolas Carion's avatar
Nicolas Carion committed
112
bool TimelineFunctions::processClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo)
113
{
114
115
    int trackId = timeline->getClipTrackId(clipId);
    int trackDuration = timeline->getTrackById_const(trackId)->trackDuration();
116
117
118
119
120
    int start = timeline->getClipPosition(clipId);
    int duration = timeline->getClipPlaytime(clipId);
    if (start > position || (start + duration) < position) {
        return false;
    }
121
    PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState();
122
    bool res = cloneClip(timeline, clipId, newId, state, undo, redo);
123
    timeline->m_blockRefresh = true;
124
    res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
125
    int newDuration = timeline->getClipPlaytime(clipId);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
126
127
128
129
130
    // parse effects
    std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(clipId);
    sourceStack->cleanFadeEffects(true, undo, redo);
    std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId);
    destStack->cleanFadeEffects(false, undo, redo);
131
    res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo);
132
133
    // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now
    bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration();
134
    res = res && timeline->requestClipMove(newId, trackId, position, true, false, true, undo, redo);
135
136
    if (durationChanged) {
        // Track length changed, check project duration
137
        Fun updateDuration = [timeline]() {
138
139
140
141
142
143
            timeline->updateDuration();
            return true;
        };
        updateDuration();
        PUSH_LAMBDA(updateDuration, redo);
    }
144
    timeline->m_blockRefresh = false;
Nicolas Carion's avatar
Nicolas Carion committed
145
146
    return res;
}
147

Nicolas Carion's avatar
Nicolas Carion committed
148
149
bool TimelineFunctions::requestClipCut(std::shared_ptr<TimelineItemModel> timeline, int clipId, int position)
{
150
151
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
152
    TRACE_STATIC(timeline, clipId, position);
Nicolas Carion's avatar
Nicolas Carion committed
153
    bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo);
154
155
156
    if (result) {
        pCore->pushUndo(undo, redo, i18n("Cut clip"));
    }
157
    TRACE_RES(result);
158
159
160
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
161
bool TimelineFunctions::requestClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, Fun &undo, Fun &redo)
162
{
163
164
165
166
167
    const std::unordered_set<int> clipselect = timeline->getGroupElements(clipId);
    // Remove locked items
    std::unordered_set<int> clips;
    for (int cid : clipselect) {
        int tk = timeline->getClipTrackId(cid);
168
        if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) {
169
170
171
            clips.insert(cid);
        }
    }
172
    std::unordered_set<int> topElements;
173
174
    std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });

175
    // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support)
176
    timeline->requestClearSelection();
Nicolas Carion's avatar
Nicolas Carion committed
177
    int count = 0;
178
    QList<int> newIds;
179
    int mainId = -1;
Nicolas Carion's avatar
Nicolas Carion committed
180
    QList<int> clipsToCut;
Nicolas Carion's avatar
Nicolas Carion committed
181
182
183
184
    for (int cid : clips) {
        int start = timeline->getClipPosition(cid);
        int duration = timeline->getClipPlaytime(cid);
        if (start < position && (start + duration) > position) {
185
            clipsToCut << cid;
Nicolas Carion's avatar
Nicolas Carion committed
186
187
        }
    }
188
189
190
    if (clipsToCut.isEmpty()) {
        return true;
    }
191
192
193
194
195
196
197
198
    for (int cid : clipsToCut) {
        count++;
        int newId;
        bool res = processClipCut(timeline, cid, position, newId, undo, redo);
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            return false;
Nicolas Carion's avatar
Nicolas Carion committed
199
200
        }
        if (cid == clipId) {
201
202
203
204
205
206
            mainId = newId;
        }
        // splitted elements go temporarily in the same group as original ones.
        timeline->m_groups->setInGroupOf(newId, cid, undo, redo);
        newIds << newId;
    }
Nicolas Carion's avatar
Nicolas Carion committed
207
    if (count > 0 && timeline->m_groups->isInGroup(clipId)) {
208
        // we now split the group hierarchy.
Nicolas Carion's avatar
Nicolas Carion committed
209
        // As a splitting criterion, we compare start point with split position
210
        auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; };
211
212
        bool res = true;
        for (const int topId : topElements) {
Nicolas Carion's avatar
Nicolas Carion committed
213
            res = res && timeline->m_groups->split(topId, criterion, undo, redo);
214
        }
Nicolas Carion's avatar
Nicolas Carion committed
215
216
217
218
219
220
221
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            return false;
        }
    }
    return count > 0;
222
223
}

Nicolas Carion's avatar
Nicolas Carion committed
224
int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position)
225
{
Nicolas Carion's avatar
Nicolas Carion committed
226
    std::unordered_set<int> clips = timeline->getItemsInRange(trackId, position, -1);
Nicolas Carion's avatar
Nicolas Carion committed
227
    if (!clips.empty()) {
228
        timeline->requestSetSelection(clips);
229
        return (*clips.cbegin());
230
231
232
    }
    return -1;
}
233

Nicolas Carion's avatar
Nicolas Carion committed
234
bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int startPosition, int endPosition)
235
236
{
    // Move group back to original position
237
238
239
240
241
242
243
244
    int track = timeline->getItemTrackId(itemId);
    bool isClip = timeline->isClip(itemId);
    if (isClip) {
        timeline->requestClipMove(itemId, track, startPosition, false, false);
    } else {
        timeline->requestCompositionMove(itemId, track, startPosition, false, false);
    }
    std::unordered_set<int> clips = timeline->getGroupElements(itemId);
245
246
247
    // Start undoable command
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
248
249
    //int res = timeline->requestClipsGroup(clips, undo, redo, GroupType::Selection);
    int res = timeline->m_groups->getRootId(itemId);
250
    bool final = false;
251
    if (res > -1 || clips.size() == 1) {
252
        if (clips.size() > 1) {
253
            final = timeline->requestGroupMove(itemId, res, 0, endPosition - startPosition, true, true, undo, redo);
254
255
        } else {
            // only 1 clip to be moved
256
            if (isClip) {
257
                final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, undo, redo);
258
            } else {
259
                final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo);
260
            }
261
        }
262
    }
263
    timeline->requestClearSelection();
264
    if (final) {
265
266
        if (startPosition < endPosition) {
            pCore->pushUndo(undo, redo, i18n("Insert space"));
Nicolas Carion's avatar
Nicolas Carion committed
267
268
        } else {
            pCore->pushUndo(undo, redo, i18n("Remove space"));
269
        }
270
271
272
273
        return true;
    }
    return false;
}
274

Nicolas Carion's avatar
Nicolas Carion committed
275
bool TimelineFunctions::extractZone(const std::shared_ptr<TimelineItemModel> &timeline, QVector<int> tracks, QPoint zone, bool liftOnly)
276
277
278
279
{
    // Start undoable command
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
280
    bool result = true;
281
    for (int trackId : tracks) {
282
283
284
        if (timeline->getTrackById_const(trackId)->isLocked()) {
            continue;
        }
285
        result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo);
286
287
    }
    if (result && !liftOnly) {
288
        result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo);
289
    }
290
    pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone"));
291
292
293
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
294
bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, QList<int> trackIds, const QString &binId, int insertFrame, QPoint zone,
295
                                   bool overwrite)
296
297
298
299
{
    // Start undoable command
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
300
    bool result = true;
301
    if (overwrite) {
302
303
304
305
        // Cut all tracks
        auto it = timeline->m_allTracks.cbegin();
        while (it != timeline->m_allTracks.cend()) {
            int target_track = (*it)->getId();
306
            if (!trackIds.contains(target_track) && !timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
307
308
309
                ++it;
                continue;
            }
310
            result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
311
            if (!result) {
312
                qDebug() << "// LIFTING ZONE FAILED\n";
313
314
315
                break;
            }
            ++it;
316
        }
317
    } else {
318
319
320
321
        // Cut all tracks
        auto it = timeline->m_allTracks.cbegin();
        while (it != timeline->m_allTracks.cend()) {
            int target_track = (*it)->getId();
322
            if (!trackIds.contains(target_track) && !timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
323
324
325
                ++it;
                continue;
            }
326
327
328
            int startClipId = timeline->getClipByPosition(target_track, insertFrame);
            if (startClipId > -1) {
                // There is a clip, cut it
329
                result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo);
330
331
            }
            ++it;
332
        }
333
        result = result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
334
    }
335
    if (result) {
336
337
338
339
340
        if (!trackIds.isEmpty()) {
            int newId = -1;
            QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1);
            result = timeline->requestClipInsertion(binClipId, trackIds.first(), insertFrame, newId, true, true, true, undo, redo);
        }
341
342
343
        if (result) {
            pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
        }
344
    }
345
    if (!result) {
346
        qDebug() << "// REQUESTING SPACE FAILED";
347
348
349
        undo();
    }
    return result;
350
351
}

Nicolas Carion's avatar
Nicolas Carion committed
352
bool TimelineFunctions::liftZone(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
353
354
355
{
    // Check if there is a clip at start point
    int startClipId = timeline->getClipByPosition(trackId, zone.x());
356
357
    if (startClipId > -1) {
        // There is a clip, cut it
358
        if (timeline->getClipPosition(startClipId) < zone.x()) {
359
            qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId;
360
            TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo);
361
            qDebug() << "/// CUTTING AT START DONE";
362
        }
363
364
365
366
    }
    int endClipId = timeline->getClipByPosition(trackId, zone.y());
    if (endClipId > -1) {
        // There is a clip, cut it
367
        if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) {
368
            qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId;
369
            TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo);
370
            qDebug() << "/// CUTTING AT END DONE";
371
        }
372
    }
Nicolas Carion's avatar
Nicolas Carion committed
373
    std::unordered_set<int> clips = timeline->getItemsInRange(trackId, zone.x(), zone.y());
374
    for (const auto &clipId : clips) {
375
        timeline->requestItemDeletion(clipId, undo, redo);
376
    }
377
378
379
    return true;
}

Nicolas Carion's avatar
Nicolas Carion committed
380
bool TimelineFunctions::removeSpace(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
381
{
382
    Q_UNUSED(trackId)
383

384
385
386
387
    std::unordered_set<int> clips;
    auto it = timeline->m_allTracks.cbegin();
    while (it != timeline->m_allTracks.cend()) {
        int target_track = (*it)->getId();
388
389
        if (timeline->m_videoTarget == target_track || timeline->m_audioTarget == target_track ||
            timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
390
391
392
393
394
            std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.y() - 1, -1, true);
            clips.insert(subs.begin(), subs.end());
        }
        ++it;
    }
395
    bool result = false;
Nicolas Carion's avatar
Nicolas Carion committed
396
    if (!clips.empty()) {
397
398
        int clipId = *clips.begin();
        if (clips.size() > 1) {
399
            int clipsGroup = timeline->m_groups->getRootId(clipId);
400
            int res = timeline->requestClipsGroup(clips, undo, redo);
401
            if (res > -1) {
402
                result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo);
403
404
                if (result && res != clipsGroup) {
                    // Only ungroup if a group was created
405
                    result = timeline->requestClipUngroup(clipId, undo, redo);
406
                }
407
408
409
                if (!result) {
                    undo();
                }
410
411
412
413
            }
        } else {
            // only 1 clip to be moved
            int clipStart = timeline->getItemPosition(clipId);
414
            result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, true, undo, redo);
415
416
417
418
419
        }
    }
    return result;
}

420
bool TimelineFunctions::requestInsertSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, bool followTargets)
421
{
422
423
424
    timeline->requestClearSelection();
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
425
426
427
428
429
430
431
432
433
    std::unordered_set<int> items;
    if (!followTargets) {
        // Select clips in all tracks
        items = timeline->getItemsInRange(-1, zone.x(), -1, true);
    } else {
        // Select clips in target and active tracks only
        auto it = timeline->m_allTracks.cbegin();
        while (it != timeline->m_allTracks.cend()) {
            int target_track = (*it)->getId();
434
435
            if (timeline->m_videoTarget == target_track || timeline->m_audioTarget == target_track ||
                timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
436
437
438
439
440
441
                std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.x(), -1, true);
                items.insert(subs.begin(), subs.end());
            }
            ++it;
        }
    }
442
443
444
445
    if (items.empty()) {
        return true;
    }
    timeline->requestSetSelection(items);
446
    bool result = true;
447
448
449
450
451
452
453
454
455
    int itemId = *(items.begin());
    int targetTrackId = timeline->getItemTrackId(itemId);
    int targetPos = timeline->getItemPosition(itemId) + zone.y() - zone.x();

    // TODO the three move functions should be unified in a "requestItemMove" function
    if (timeline->m_groups->isInGroup(itemId)) {
        result =
            result && timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.y() - zone.x(), true, true, local_undo, local_redo);
    } else if (timeline->isClip(itemId)) {
456
        result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, local_undo, local_redo);
457
458
459
460
461
462
463
464
465
    } else {
        result = result && timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true,
                                                            local_undo, local_redo);
    }
    timeline->requestClearSelection();
    if (!result) {
        bool undone = local_undo();
        Q_ASSERT(undone);
        pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage);
466
    }
467
    UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
468
    return result;
469
}
470

Nicolas Carion's avatar
Nicolas Carion committed
471
bool TimelineFunctions::requestItemCopy(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int trackId, int position)
472
473
474
475
{
    Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId));
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
476
477
    int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId));
    int deltaPos = position - timeline->getItemPosition(clipId);
478
    std::unordered_set<int> allIds = timeline->getGroupElements(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
479
    std::unordered_map<int, int> mapping; // keys are ids of the source clips, values are ids of the copied clips
480
    bool res = true;
481
482
    for (int id : allIds) {
        int newId = -1;
483
        if (timeline->isClip(id)) {
484
            PlaylistState::ClipState state = timeline->m_allClips[id]->clipState();
485
            res = cloneClip(timeline, id, newId, state, undo, redo);
486
487
488
489
            res = res && (newId != -1);
        }
        int target_position = timeline->getItemPosition(id) + deltaPos;
        int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack;
490
491
492
493
        if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) {
            auto it = timeline->m_allTracks.cbegin();
            std::advance(it, target_track_position);
            int target_track = (*it)->getId();
494
            if (timeline->isClip(id)) {
495
                res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, undo, redo);
496
497
            } else {
                const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
498
                std::unique_ptr<Mlt::Properties> transProps(timeline->m_allCompositions[id]->properties());
Nicolas Carion's avatar
Nicolas Carion committed
499
500
                res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
                                                                   timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo);
501
            }
502
503
504
505
506
507
508
509
        } else {
            res = false;
        }
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            return false;
        }
Nicolas Carion's avatar
Nicolas Carion committed
510
511
        mapping[id] = newId;
    }
512
    qDebug() << "Successful copy, coping groups...";
513
    res = timeline->m_groups->copyGroups(mapping, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
514
515
516
517
    if (!res) {
        bool undone = undo();
        Q_ASSERT(undone);
        return false;
518
519
520
521
    }
    return true;
}

Nicolas Carion's avatar
Nicolas Carion committed
522
void TimelineFunctions::showClipKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, bool value)
523
524
525
{
    timeline->m_allClips[clipId]->setShowKeyframes(value);
    QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId);
526
    timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
527
}
528

Nicolas Carion's avatar
Nicolas Carion committed
529
void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int compoId, bool value)
530
531
532
{
    timeline->m_allCompositions[compoId]->setShowKeyframes(value);
    QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId);
533
    timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
534
535
}

Nicolas Carion's avatar
Nicolas Carion committed
536
bool TimelineFunctions::switchEnableState(const std::shared_ptr<TimelineItemModel> &timeline, int clipId)
537
{
538
539
540
541
    PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState();
    PlaylistState::ClipState state = PlaylistState::Disabled;
    bool disable = true;
    if (oldState == PlaylistState::Disabled) {
542
        state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType();
543
        disable = false;
544
    }
545
546
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
547
    bool result = changeClipState(timeline, clipId, state, undo, redo);
548
    if (result) {
549
        pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip"));
550
551
552
553
    }
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
554
bool TimelineFunctions::changeClipState(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo)
555
{
556
557
558
    int track = timeline->getClipTrackId(clipId);
    int start = -1;
    int end = -1;
559
    bool invalidate = false;
560
561
    if (track > -1) {
        if (!timeline->getTrackById_const(track)->isAudioTrack()) {
562
            invalidate = true;
563
        }
564
565
        start = timeline->getItemPosition(clipId);
        end = start + timeline->getItemPlaytime(clipId);
566
    }
567
568
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
569
570
571
572
573
574
575
576
    // For the state change to work, we need to unplant/replant the clip
    bool result = true;
    if (track > -1) {
        result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo);
    }
    result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo);
    if (result && track > -1) {
        result = timeline->getTrackById(track)->requestClipInsertion(clipId, start, true, true, local_undo, local_redo);
577
    }
578
    UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
579
580
    return result;
}
581

Nicolas Carion's avatar
Nicolas Carion committed
582
bool TimelineFunctions::requestSplitAudio(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int audioTarget)
583
584
585
586
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
587
    bool done = false;
588
    // Now clear selection so we don't mess with groups
589
    timeline->requestClearSelection(false, undo, redo);
590
    for (int cid : clips) {
591
        if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) {
592
            // clip without audio or audio only, skip
Pino Toscano's avatar
Pino Toscano committed
593
            pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
594
            return false;
595
        }
596
597
        int position = timeline->getClipPosition(cid);
        int track = timeline->getClipTrackId(cid);
598
        QList<int> possibleTracks = audioTarget >= 0 ? QList<int>() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack);
599
        if (possibleTracks.isEmpty()) {
600
601
602
603
604
            // No available audio track for splitting, abort
            undo();
            pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage);
            return false;
        }
605
        int newId;
606
        bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo);
607
608
609
610
611
612
613
614
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
            return false;
        }
        bool success = false;
        while (!success && !possibleTracks.isEmpty()) {
615
            int newTrack = possibleTracks.takeFirst();
616
            success = timeline->requestClipMove(newId, newTrack, position, true, false, true, undo, redo);
617
        }
618
619
        TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo);
        success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo);
620
        if (!success) {
621
622
            bool undone = undo();
            Q_ASSERT(undone);
623
            pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
624
625
            return false;
        }
626
        done = true;
627
    }
628
    if (done) {
629
        timeline->requestSetSelection(clips, undo, redo);
630
631
632
        pCore->pushUndo(undo, redo, i18n("Split Audio"));
    }
    return done;
633
}
634

Nicolas Carion's avatar
Nicolas Carion committed
635
bool TimelineFunctions::requestSplitVideo(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int videoTarget)
636
637
638
639
640
641
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
    bool done = false;
    // Now clear selection so we don't mess with groups
642
    timeline->requestClearSelection();
643
644
645
646
647
648
649
650
651
652
653
654
655
656
    for (int cid : clips) {
        if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) {
            // clip without audio or audio only, skip
            continue;
        }
        int position = timeline->getClipPosition(cid);
        QList<int> possibleTracks = QList<int>() << videoTarget;
        if (possibleTracks.isEmpty()) {
            // No available audio track for splitting, abort
            undo();
            pCore->displayMessage(i18n("No available video track for split operation"), ErrorMessage);
            return false;
        }
        int newId;
657
        bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo);
658
659
660
661
662
663
664
665
666
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            pCore->displayMessage(i18n("Video split failed"), ErrorMessage);
            return false;
        }
        bool success = false;
        while (!success && !possibleTracks.isEmpty()) {
            int newTrack = possibleTracks.takeFirst();
667
            success = timeline->requestClipMove(newId, newTrack, position, true, true, true, undo, redo);
668
        }
669
670
        TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo);
        success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo);
671
672
673
674
675
676
677
678
679
680
681
682
683
684
        if (!success) {
            bool undone = undo();
            Q_ASSERT(undone);
            pCore->displayMessage(i18n("Video split failed"), ErrorMessage);
            return false;
        }
        done = true;
    }
    if (done) {
        pCore->pushUndo(undo, redo, i18n("Split Video"));
    }
    return done;
}

Nicolas Carion's avatar
Nicolas Carion committed
685
void TimelineFunctions::setCompositionATrack(const std::shared_ptr<TimelineItemModel> &timeline, int cid, int aTrack)
686
{
687
688
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
689
    std::shared_ptr<CompositionModel> compo = timeline->getCompositionPtr(cid);
690
    int previousATrack = compo->getATrack();
Nicolas Carion's avatar
Nicolas Carion committed
691
    int previousAutoTrack = static_cast<int>(compo->getForcedTrack() == -1);
692
693
694
695
696
697
698
    bool autoTrack = aTrack < 0;
    if (autoTrack) {
        // Automatic track compositing, find lower video track
        aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId());
    }
    int start = timeline->getItemPosition(cid);
    int end = start + timeline->getItemPlaytime(cid);
699
    Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() {
700
        timeline->unplantComposition(cid);
701
702
703
704
705
        QScopedPointer<Mlt::Field> field(timeline->m_tractor->field());
        field->lock();
        timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack);
        timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1));
        field->unlock();
706
        timeline->replantCompositions(cid, true);
707
708
709
710
        timeline->invalidateZone(start, end);
        timeline->checkRefresh(start, end);
        return true;
    };
711
    Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() {
712
        timeline->unplantComposition(cid);
713
714
        QScopedPointer<Mlt::Field> field(timeline->m_tractor->field());
        field->lock();
Nicolas Carion's avatar
Nicolas Carion committed
715
        timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0);
716
        timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1));
717
        field->unlock();
718
        timeline->replantCompositions(cid, true);
719
720
721
722
        timeline->invalidateZone(start, end);
        timeline->checkRefresh(start, end);
        return true;
    };
723
724
725
    if (local_redo()) {
        PUSH_LAMBDA(local_undo, undo);
        PUSH_LAMBDA(local_redo, redo);
726
    }
727
    pCore->pushUndo(undo, redo, i18n("Change Composition Track"));
728
}
729

Nicolas Carion's avatar
Nicolas Carion committed
730
void TimelineFunctions::enableMultitrackView(const std::shared_ptr<TimelineItemModel> &timeline, bool enable)
731
{
732
    QList<int> videoTracks;
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
    for (const auto &track : timeline->m_iteratorTable) {
        if (timeline->getTrackById_const(track.first)->isAudioTrack() || timeline->getTrackById_const(track.first)->isHidden()) {
            continue;
        }
        videoTracks << track.first;
    }
    if (videoTracks.size() < 2) {
        pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), InformationMessage);
    }
    // First, dis/enable track compositing
    QScopedPointer<Mlt::Service> service(timeline->m_tractor->field());
    Mlt::Field *field = timeline->m_tractor->field();
    field->lock();
    while ((service != nullptr) && service->is_valid()) {
        if (service->type() == transition_type) {
            Mlt::Transition t((mlt_transition)service->get_service());
            QString serviceName = t.get("mlt_service");
            int added = t.get_int("internal_added");
            if (added == 237 && serviceName != QLatin1String("mix")) {
                // remove all compositing transitions
                t.set("disable", enable ? "1" : nullptr);
            } else if (!enable && added == 200) {
                field->disconnect_service(t);
            }
        }
        service.reset(service->producer());
    }
    if (enable) {
        for (int i = 0; i < videoTracks.size(); ++i) {
            Mlt::Transition transition(*timeline->m_tractor->profile(), "composite");
            transition.set("mlt_service", "composite");
            transition.set("a_track", 0);
            transition.set("b_track", timeline->getTrackMltIndex(videoTracks.at(i)));
            transition.set("distort", 0);
            transition.set("aligned", 0);
            // 200 is an arbitrary number so we can easily remove these transition later
            transition.set("internal_added", 200);
            QString geometry;
            switch (i) {
772
773
774
775
            case 0:
                switch (videoTracks.size()) {
                case 2:
                    geometry = QStringLiteral("0 0 50% 100%");
776
                    break;
777
778
779
780
781
782
783
784
785
786
787
788
                case 3:
                    geometry = QStringLiteral("0 0 33% 100%");
                    break;
                case 4:
                    geometry = QStringLiteral("0 0 50% 50%");
                    break;
                case 5:
                case 6:
                    geometry = QStringLiteral("0 0 33% 50%");
                    break;
                default:
                    geometry = QStringLiteral("0 0 33% 33%");
789
                    break;
790
791
792
793
                }
                break;
            case 1:
                switch (videoTracks.size()) {
794
                case 2:
795
                    geometry = QStringLiteral("50% 0 50% 100%");
796
797
                    break;
                case 3:
798
                    geometry = QStringLiteral("33% 0 33% 100%");
799
800
                    break;
                case 4:
801
                    geometry = QStringLiteral("50% 0 50% 50%");
802
803
                    break;
                case 5:
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
                case 6:
                    geometry = QStringLiteral("33% 0 33% 50%");
                    break;
                default:
                    geometry = QStringLiteral("33% 0 33% 33%");
                    break;
                }
                break;
            case 2:
                switch (videoTracks.size()) {
                case 3:
                    geometry = QStringLiteral("66% 0 33% 100%");
                    break;
                case 4:
                    geometry = QStringLiteral("0 50% 50% 50%");
                    break;
                case 5:
                case 6:
                    geometry = QStringLiteral("66% 0 33% 50%");
                    break;
                default:
                    geometry = QStringLiteral("66% 0 33% 33%");
                    break;
                }
                break;
            case 3:
                switch (videoTracks.size()) {
                case 4:
                    geometry = QStringLiteral("50% 50% 50% 50%");
833
                    break;
834
835
836
837
838
839
840
841
842
843
844
845
                case 5:
                case 6:
                    geometry = QStringLiteral("0 50% 33% 50%");
                    break;
                default:
                    geometry = QStringLiteral("0 33% 33% 33%");
                    break;
                }
                break;
            case 4:
                switch (videoTracks.size()) {
                case 5:
846
                case 6:
847
848
849
850
                    geometry = QStringLiteral("33% 50% 33% 50%");
                    break;
                default:
                    geometry = QStringLiteral("33% 33% 33% 33%");
851
                    break;
852
853
854
855
856
857
                }
                break;
            case 5:
                switch (videoTracks.size()) {
                case 6:
                    geometry = QStringLiteral("66% 50% 33% 50%");
858
859
                    break;
                default:
860
                    geometry = QStringLiteral("66% 33% 33% 33%");
861
                    break;
862
863
864
865
866
867
868
869
870
871
872
                }
                break;
            case 6:
                geometry = QStringLiteral("0 66% 33% 33%");
                break;
            case 7:
                geometry = QStringLiteral("33% 66% 33% 33%");
                break;
            default:
                geometry = QStringLiteral("66% 66% 33% 33%");
                break;
873
874
875
876
877
878
879
880
881
882
            }
            // Add transition to track:
            transition.set("geometry", geometry.toUtf8().constData());
            transition.set("always_active", 1);
            field->plant_transition(transition, 0, timeline->getTrackMltIndex(videoTracks.at(i)));
        }
    }
    field->unlock();
    timeline->requestMonitorRefresh();
}
883

884
885
void TimelineFunctions::saveTimelineSelection(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &selection,
                                              const QDir &targetDir)
886
887
{
    bool ok;
888
889
    QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal,
                                         QString(), &ok);
890
891
892
893
894
895
896
    if (name.isEmpty() || !ok) {
        return;
    }
    if (targetDir.exists(name + QStringLiteral(".mlt"))) {
        // TODO: warn and ask for overwrite / rename
    }
    int offset = -1;
897
898
    int lowerAudioTrack = -1;
    int lowerVideoTrack = -1;
899
900
    QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt"));
    // Build a copy of selected tracks.
901
    QMap<int, int> sourceTracks;
902
903
904
905
906
907
908
909
910
911
912
913
914
    for (int i : selection) {
        int sourceTrack = timeline->getItemTrackId(i);
        int clipPos = timeline->getItemPosition(i);
        if (offset < 0 || clipPos < offset) {
            offset = clipPos;
        }
        int trackPos = timeline->getTrackMltIndex(sourceTrack);
        if (!sourceTracks.contains(trackPos)) {
            sourceTracks.insert(trackPos, sourceTrack);
        }
    }
    // Build target timeline
    Mlt::Tractor newTractor(*timeline->m_tractor->profile());
915
    QScopedPointer<Mlt::Field> field(newTractor.field());
916
    int ix = 0;
917
    QString composite = TransitionsRepository::get()->getCompositingTransition();
918
    QMapIterator<int, int> i(sourceTracks);
919
    QList<Mlt::Transition *> compositions;
920
921
922
    while (i.hasNext()) {
        i.next();
        QScopedPointer<Mlt::Playlist> newTrackPlaylist(new Mlt::Playlist(*newTractor.profile()));
923
        newTractor.set_track(*newTrackPlaylist, ix);
924
        // QScopedPointer<Mlt::Producer> trackProducer(newTractor.track(ix));
925
        int trackId = i.value();
926
        sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix);
927
        std::shared_ptr<TrackModel> track = timeline->getTrackById_const(trackId);
928
929
930
931
932
933
934
935
936
937
938
939
        bool isAudio = track->isAudioTrack();
        if (isAudio) {
            newTrackPlaylist->set("hide", 1);
            if (lowerAudioTrack < 0) {
                lowerAudioTrack = ix;
            }
        } else {
            newTrackPlaylist->set("hide", 2);
            if (lowerVideoTrack < 0) {
                lowerVideoTrack = ix;
            }
        }
940
941
942
943
944
945
946
947
        for (int itemId : selection) {
            if (timeline->getItemTrackId(itemId) == trackId) {
                // Copy clip on the destination track
                if (timeline->isClip(itemId)) {
                    int clip_position = timeline->m_allClips[itemId]->getPosition();
                    auto clip_loc = track->getClipIndexAt(clip_position);
                    int target_clip = clip_loc.second;
                    QSharedPointer<Mlt::Producer> clip = track->getClipProducer(target_clip);
Jean-Baptiste Mardelle's avatar