timelinefunctions.cpp 99.4 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 "bin/model/subtitlemodel.hpp"
Julius Künzel's avatar
Julius Künzel committed
28
#include "bin/model/markerlistmodel.hpp"
29
#include "clipmodel.hpp"
30
#include "compositionmodel.hpp"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
31
#include "core.h"
32
#include "doc/kdenlivedoc.h"
33
#include "effects/effectstack/model/effectstackmodel.hpp"
34
35
36
#include "groupsmodel.hpp"
#include "timelineitemmodel.hpp"
#include "trackmodel.hpp"
37
#include "transitions/transitionsrepository.hpp"
38
#include "mainwindow.h"
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
39
#include "project/projectmanager.h"
40

41
#include <QApplication>
42
#include <QDebug>
43
#include <QInputDialog>
44
#include <QSemaphore>
Nicolas Carion's avatar
linting    
Nicolas Carion committed
45
#include <klocalizedstring.h>
46
#include <unordered_map>
47

Vincent Pinon's avatar
Vincent Pinon committed
48
49
#ifdef CRASH_AUTO_TEST
#include "logger.hpp"
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#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))(
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
64
65
66
            parameter_names("timeline", "clipId", "position"))
        .method("requestDeleteBlankAt", select_overload<bool(const std::shared_ptr<TimelineItemModel>&, int, int, bool)>(&TimelineFunctions::requestDeleteBlankAt))(
            parameter_names("timeline", "trackId", "position", "affectAllTracks"));
67
}
Vincent Pinon's avatar
Vincent Pinon committed
68
69
70
71
72
73
74
75
#else
#define TRACE_STATIC(...)
#define TRACE_RES(...)
#endif

QStringList waitingBinIds;
QMap<QString, QString> mappedIds;
QMap<int, int> tracksMap;
76
QMap<int, int> spacerUngroupedItems;
77
int spacerMinPosition;
Vincent Pinon's avatar
Vincent Pinon committed
78
QSemaphore semaphore(1);
79

80
81
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
82
{
83
84
    // Special case: slowmotion clips
    double clipSpeed = timeline->m_allClips[clipId]->getSpeed();
85
    bool warp_pitch = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch"));
86
87
    int audioStream = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("audio_index"));
    bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, audioStream, clipSpeed, warp_pitch, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
88
    timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize;
89

90
91
92
    // copy useful timeline properties
    timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]);

Nicolas Carion's avatar
Nicolas Carion committed
93
94
95
96
    int duration = timeline->getClipPlaytime(clipId);
    int init_duration = timeline->getClipPlaytime(newId);
    if (duration != init_duration) {
        int in = timeline->m_allClips[clipId]->getIn();
97
98
        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
99
100
101
102
103
104
    }
    if (!res) {
        return false;
    }
    std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(clipId);
    std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId);
105
    destStack->importEffects(sourceStack, state);
Nicolas Carion's avatar
Nicolas Carion committed
106
107
108
    return res;
}

Nicolas Carion's avatar
Nicolas Carion committed
109
bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr<TimelineItemModel> &timeline, const QStringList &binIds, int trackId, int position,
110
                                                      QList<int> &clipIds, bool logUndo, bool refreshView)
111
112
113
114
115
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    for (const QString &binId : binIds) {
        int clipId;
116
        if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) {
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
            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
133
bool TimelineFunctions::processClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo)
134
{
135
136
137
138
139
    bool isSubtitle = timeline->isSubTitle(clipId);
    int trackId = isSubtitle ? -1 : timeline->getClipTrackId(clipId);
    int trackDuration = isSubtitle ? -1 : timeline->getTrackById_const(trackId)->trackDuration();
    int start = timeline->getItemPosition(clipId);
    int duration = timeline->getItemPlaytime(clipId);
140
141
142
    if (start > position || (start + duration) < position) {
        return false;
    }
143
144
145
    if (isSubtitle) {
        return timeline->cutSubtitle(position, undo, redo);
    }
146
    PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState();
147
    // Check if clip has an end Mix
148
    bool res = cloneClip(timeline, clipId, newId, state, undo, redo);
149
    timeline->m_blockRefresh = true;
150
    res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
151
    int newDuration = timeline->getClipPlaytime(clipId);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
152
153
154
155
156
    // 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);
157
    res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo);
158
159
    // 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();
160
    timeline->m_allClips[newId]->setSubPlaylistIndex(timeline->m_allClips[clipId]->getSubPlaylistIndex(), trackId);
161
    res = res && timeline->requestClipMove(newId, trackId, position, true, true, false, true, undo, redo);
162
163
164
165
166
167
168
169
170
171
172
173
    
    if (timeline->getTrackById_const(trackId)->hasEndMix(clipId)) {
        Fun local_undo = [timeline, trackId, clipId, newId]() { 
            timeline->getTrackById_const(trackId)->reAssignEndMix(newId, clipId);
            return true; };
        Fun local_redo = [timeline, trackId, clipId, newId]() {
            timeline->getTrackById_const(trackId)->reAssignEndMix(clipId, newId);
            return true; };
        local_redo();
        UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
    }
    
174
175
    if (durationChanged) {
        // Track length changed, check project duration
176
        Fun updateDuration = [timeline]() {
177
178
179
180
181
182
            timeline->updateDuration();
            return true;
        };
        updateDuration();
        PUSH_LAMBDA(updateDuration, redo);
    }
183
    timeline->m_blockRefresh = false;
Nicolas Carion's avatar
Nicolas Carion committed
184
185
    return res;
}
186

Nicolas Carion's avatar
Nicolas Carion committed
187
188
bool TimelineFunctions::requestClipCut(std::shared_ptr<TimelineItemModel> timeline, int clipId, int position)
{
189
190
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
191
    TRACE_STATIC(timeline, clipId, position);
Nicolas Carion's avatar
Nicolas Carion committed
192
    bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo);
193
194
195
    if (result) {
        pCore->pushUndo(undo, redo, i18n("Cut clip"));
    }
196
    TRACE_RES(result);
197
198
199
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
200
bool TimelineFunctions::requestClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, Fun &undo, Fun &redo)
201
{
202
203
204
205
    const std::unordered_set<int> clipselect = timeline->getGroupElements(clipId);
    // Remove locked items
    std::unordered_set<int> clips;
    for (int cid : clipselect) {
206
207
208
209
        if (timeline->isSubTitle(cid)) {
            clips.insert(cid);
            continue;
        }
210
211
212
        if (!timeline->isClip(cid)) {
            continue;
        }
213
        int tk = timeline->getClipTrackId(cid);
214
        if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) {
215
216
217
            clips.insert(cid);
        }
    }
218
219
220
221
222
223
224
225
226
227
    // Shall we reselect after the split
    int trackToSelect = -1;
    if (timeline->isClip(clipId) && timeline->m_allClips[clipId]->selected) {
        int mainIn = timeline->getItemPosition(clipId);
        int mainOut = mainIn + timeline->getItemPlaytime(clipId);
        if (position > mainIn && position < mainOut) {
            trackToSelect = timeline->getItemTrackId(clipId);
        }
    }

228
229
230
    // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support)
    timeline->requestClearSelection();

231
    std::unordered_set<int> topElements;
232
233
    std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });

Nicolas Carion's avatar
Nicolas Carion committed
234
    int count = 0;
235
    QList<int> newIds;
Nicolas Carion's avatar
Nicolas Carion committed
236
    QList<int> clipsToCut;
Nicolas Carion's avatar
Nicolas Carion committed
237
    for (int cid : clips) {
238
        if (!timeline->isClip(cid) && !timeline->isSubTitle(cid)) {
239
240
            continue;
        }
241
242
        int start = timeline->getItemPosition(cid);
        int duration = timeline->getItemPlaytime(cid);
Nicolas Carion's avatar
Nicolas Carion committed
243
        if (start < position && (start + duration) > position) {
244
            clipsToCut << cid;
Nicolas Carion's avatar
Nicolas Carion committed
245
246
        }
    }
247
248
249
    if (clipsToCut.isEmpty()) {
        return true;
    }
Vincent Pinon's avatar
Vincent Pinon committed
250
    for (int cid : qAsConst(clipsToCut)) {
251
252
253
254
255
256
257
        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
258
        }
259
260
261
262
        // 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
263
    if (count > 0 && timeline->m_groups->isInGroup(clipId)) {
264
        // we now split the group hierarchy.
Nicolas Carion's avatar
Nicolas Carion committed
265
        // As a splitting criterion, we compare start point with split position
266
        auto criterion = [timeline, position](int cid) { return timeline->getItemPosition(cid) < position; };
267
268
        bool res = true;
        for (const int topId : topElements) {
269
            qDebug()<<"// CHECKING REGROUP ELEMENT: "<<topId<<", ISCLIP: "<<timeline->isClip(topId)<<timeline->isGroup(topId);
Nicolas Carion's avatar
Nicolas Carion committed
270
            res = res && timeline->m_groups->split(topId, criterion, undo, redo);
271
        }
Nicolas Carion's avatar
Nicolas Carion committed
272
273
274
275
276
277
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            return false;
        }
    }
278
279
280
281
    if (count > 0 && trackToSelect > -1) {
        int newClip = timeline->getClipByPosition(trackToSelect, position);
        if (newClip > -1) {
            timeline->requestSetSelection({newClip});
282
        }
283
    }
Nicolas Carion's avatar
Nicolas Carion committed
284
    return count > 0;
285
}   
286

287
288
289
290
291
292
bool TimelineFunctions::requestClipCutAll(std::shared_ptr<TimelineItemModel> timeline, int position)
{
    QVector<std::shared_ptr<TrackModel>> affectedTracks;
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };

Vincent Pinon's avatar
Vincent Pinon committed
293
    for (const auto &track: timeline->m_allTracks) {
294
295
296
297
298
299
        if (!track->isLocked()) {
            affectedTracks << track;
        }
    }

    if (affectedTracks.isEmpty()) {
300
        pCore->displayMessage(i18n("All tracks are locked"), ErrorMessage, 500);
301
302
303
304
        return false;
    }

    unsigned count = 0;
Vincent Pinon's avatar
Vincent Pinon committed
305
    for (auto track: qAsConst(affectedTracks)) {
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
        int clipId = track->getClipByPosition(position);
        if (clipId > -1) {
            // Found clip at position in track, cut it. Update undo/redo as we go.
            if (!TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo)) {
                qWarning() << "Failed to cut clip " << clipId << " at " << position;
                pCore->displayMessage(i18n("Failed to cut clip"), ErrorMessage, 500);
                // Undo all cuts made, assert successful undo.
                bool undone = undo();
                Q_ASSERT(undone);
                return false;
            }
            count++;
        }
    }

    if (!count) {
322
        pCore->displayMessage(i18n("No clips to cut"), ErrorMessage);
323
324
325
326
327
328
329
    } else {
        pCore->pushUndo(undo, redo, i18n("Cut all clips"));
    }

    return count > 0;
}

Nicolas Carion's avatar
Nicolas Carion committed
330
int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position)
331
{
Nicolas Carion's avatar
Nicolas Carion committed
332
    std::unordered_set<int> clips = timeline->getItemsInRange(trackId, position, -1);
333
    timeline->requestClearSelection();
334
    spacerMinPosition = -1;
Nicolas Carion's avatar
Nicolas Carion committed
335
    if (!clips.empty()) {
336
337
338
339
340
341
        // Remove grouped items that are before the click position
        // First get top groups ids
        std::unordered_set<int> roots;
        spacerUngroupedItems.clear();
        std::transform(clips.begin(), clips.end(), std::inserter(roots, roots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
        std::unordered_set<int> groupsToRemove;
342
343
        int firstCid = -1;
        int firstPosition = -1;
344
345
346
347
348
349
        for (int r : roots) {
            if (timeline->isGroup(r)) {
                std::unordered_set<int> leaves = timeline->m_groups->getLeaves(r);
                std::unordered_set<int> leavesToRemove;
                std::unordered_set<int> leavesToKeep;
                for (int l : leaves) {
350
351
                    int pos = timeline->getItemPosition(l);
                    if (pos + timeline->getItemPlaytime(l) < position) {
352
353
354
                        leavesToRemove.insert(l);
                    } else {
                        leavesToKeep.insert(l);
355
356
357
358
359
                        // Find first item
                        if (firstPosition == -1 || pos < firstPosition) {
                            firstCid = l;
                            firstPosition = pos;
                        }
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
                    }
                }
                if (leavesToKeep.size() == 1) {
                    // Only 1 item left in group, group will be deleted
                    int master = *leavesToKeep.begin();
                    roots.insert(master);
                    for (int l : leavesToRemove) {
                        spacerUngroupedItems.insert(l, master);
                    }
                    groupsToRemove.insert(r);
                } else {
                    for (int l : leavesToRemove) {
                        spacerUngroupedItems.insert(l, r);
                    }
                }
375
376
377
378
379
380
            } else {
                int pos = timeline->getItemPosition(r);
                if (firstPosition == -1 || pos < firstPosition) {
                    firstCid = r;
                    firstPosition = pos;
                }
381
382
383
384
385
386
387
388
389
390
391
392
393
            }
        }
        for (int r : groupsToRemove) {
            roots.erase(r);
        }
        Fun undo = []() { return true; };
        Fun redo = []() { return true; };
        QMapIterator<int, int> i(spacerUngroupedItems);
        while (i.hasNext()) {
            i.next();
            timeline->m_groups->ungroupItem(i.key(), undo, redo);
        }
        timeline->requestSetSelection(roots);
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
        if (firstPosition > 0) {
            // Find minimum position, parse all tracks
            if (trackId > -1) {
                // Easy, check blank size
                int spaceDuration = timeline->getTrackById_const(trackId)->getBlankSizeAtPos(firstPosition - 1);
                if (spaceDuration > 0 ) {
                    spacerMinPosition = firstPosition - spaceDuration;
                }
            } else {
                // Check space in all tracks
                auto it = timeline->m_allTracks.cbegin();
                int space = -1;
                while (it != timeline->m_allTracks.cend()) {
                    int spaceDuration = timeline->getTrackById_const((*it)->getId())->getBlankSizeAtPos(firstPosition - 1);
                    if (space == -1 || spaceDuration < space) {
                        space = spaceDuration;
                    }
                    ++it;
                }
                if (space > -1) {
                    spacerMinPosition = firstPosition - space;
                }
            }
        }
418
        return (firstCid);
419
420
421
    }
    return -1;
}
422

423
bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int startPosition, int endPosition, int affectedTrack, bool moveGuides, Fun &undo, Fun &redo)
424
425
{
    // Move group back to original position
426
    spacerMinPosition = -1;
427
428
429
    int track = timeline->getItemTrackId(itemId);
    bool isClip = timeline->isClip(itemId);
    if (isClip) {
430
        timeline->requestClipMove(itemId, track, startPosition, true, false, false);
431
    } else if (timeline->isComposition(itemId)) {
432
        timeline->requestCompositionMove(itemId, track, startPosition, false, false);
433
434
    } else {
        timeline->requestSubtitleMove(itemId, startPosition, false, false);
435
    }
436
    // Move guides
437
    if (moveGuides) {
438
439
        GenTime fromPos(startPosition, pCore->getCurrentFps());
        GenTime toPos(endPosition, pCore->getCurrentFps());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
440
441
        QList<CommentedTime> guides = pCore->projectManager()->getGuideModel()->getMarkersInRange(startPosition, -1);
        pCore->projectManager()->getGuideModel()->moveMarkers(guides, fromPos, toPos, undo, redo);
442
443
    }

444
    std::unordered_set<int> clips = timeline->getGroupElements(itemId);
445
    int mainGroup = timeline->m_groups->getRootId(itemId);
446
    bool final = false;
447
448
449
450
451
452
453
454
455
456
457
458
459
    bool liftOk = true;
    if (timeline->m_editMode == TimelineMode::OverwriteEdit && endPosition < startPosition) {
        // Remove zone between end and start pos
        if (affectedTrack == -1) {
            // touch all tracks
            auto it = timeline->m_allTracks.cbegin();
            while (it != timeline->m_allTracks.cend()) {
                int target_track = (*it)->getId();
                if (!timeline->getTrackById_const(target_track)->isLocked()) {
                    liftOk = liftOk && TimelineFunctions::liftZone(timeline, target_track, QPoint(endPosition, startPosition), undo, redo);
                }
                ++it;
            }
460
        } else if (timeline->isTrack(affectedTrack)) {
461
462
463
            liftOk = TimelineFunctions::liftZone(timeline, affectedTrack, QPoint(endPosition, startPosition), undo, redo);
        }
        // The lift operation destroys selection group, so regroup now
464
        if (clips.size() > 1) {
465
466
467
468
469
470
471
            timeline->requestSetSelection(clips);
            mainGroup = timeline->m_groups->getRootId(itemId);
        }
    }
    if (liftOk && (mainGroup > -1 || clips.size() == 1)) {
        if (clips.size() > 1) {
            final = timeline->requestGroupMove(itemId, mainGroup, 0, endPosition - startPosition, true, true, undo, redo);
472
473
        } else {
            // only 1 clip to be moved
474
            if (isClip) {
475
                final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, true, undo, redo);
476
            } else if (timeline->isComposition(itemId)) {
477
                final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo);
478
            } else {
479
                final = timeline->requestSubtitleMove(itemId, endPosition, true, true, true, true, undo, redo);
480
            }
481
        }
482
    }
483
    timeline->requestClearSelection();
484
    if (final) {
485
486
        if (startPosition < endPosition) {
            pCore->pushUndo(undo, redo, i18n("Insert space"));
Nicolas Carion's avatar
Nicolas Carion committed
487
488
        } else {
            pCore->pushUndo(undo, redo, i18n("Remove space"));
489
        }
490
491
492
493
494
495
496
497
498
499
500
501
502
503
        // Regroup temporarily ungrouped items
        QMapIterator<int, int> i(spacerUngroupedItems);
        Fun local_undo = []() { return true; };
        Fun local_redo = []() { return true; };
        while (i.hasNext()) {
            i.next();
            if (timeline->isGroup(i.value())) {
                timeline->m_groups->setInGroupOf(i.key(), i.value(), local_undo, local_redo);
            } else {
                std::unordered_set<int> items = {i.key(), i.value()};
                timeline->m_groups->groupItems(items, local_undo, local_redo);
            }
        }
        spacerUngroupedItems.clear();
504
        return true;
505
506
    } else {
        undo();
507
508
509
    }
    return false;
}
510

511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527

bool TimelineFunctions::breakAffectedGroups(const std::shared_ptr<TimelineItemModel> &timeline, QVector<int> tracks, QPoint zone, Fun &undo, Fun &redo)
{
    // Check if we have grouped clips that are on unaffected tracks, and ungroup them
    bool result = true;
    std::unordered_set<int> affectedItems;
    // First find all affected items
    for (int &trackId : tracks) {
        std::unordered_set<int> items = timeline->getItemsInRange(trackId, zone.x(), zone.y());
        affectedItems.insert(items.begin(), items.end());
    }
    for (int item : affectedItems) {
        if (timeline->m_groups->isInGroup(item)) {
            int groupId = timeline->m_groups->getRootId(item);
            std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId);
            for (int child: all_children) {
                int childTrackId = timeline->getItemTrackId(child);
528
                if (!tracks.contains(childTrackId) && timeline->m_groups->isInGroup(child)) {
529
530
531
532
533
534
535
536
537
                    // This item should not be affected by the operation, ungroup it
                    result = result && timeline->requestClipUngroup(child, undo, redo);
                }
            }
        }
    }
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
538
bool TimelineFunctions::extractZone(const std::shared_ptr<TimelineItemModel> &timeline, QVector<int> tracks, QPoint zone, bool liftOnly)
539
540
541
542
{
    // Start undoable command
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
543
    bool result = true;
544
    result = breakAffectedGroups(timeline, tracks, zone, undo, redo);
545

546
    for (int &trackId : tracks) {
547
548
549
        if (timeline->getTrackById_const(trackId)->isLocked()) {
            continue;
        }
550
        result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo);
551
552
    }
    if (result && !liftOnly) {
553
        result = TimelineFunctions::removeSpace(timeline, zone, undo, redo, tracks);
554
    }
555
    pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone"));
556
557
558
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
559
bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, QList<int> trackIds, const QString &binId, int insertFrame, QPoint zone,
560
                                   bool overwrite, bool useTargets)
561
562
563
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
564
565
566
567
    bool res = TimelineFunctions::insertZone(timeline, trackIds, binId, insertFrame, zone, overwrite, useTargets, undo, redo);
    if (res) {
        pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
    } else {
568
        pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage);
569
570
571
572
573
574
575
576
577
        undo();
    }
    return res;
}

bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, QList<int> trackIds, const QString &binId, int insertFrame, QPoint zone,
                                   bool overwrite, bool useTargets, Fun &undo, Fun &redo)
{
    // Start undoable command
578
    bool result = true;
579
580
    QVector<int> affectedTracks;
    auto it = timeline->m_allTracks.cbegin();
581
    if (!useTargets) {
582
583
584
585
586
587
588
589
590
        // Timeline drop in overwrite mode
        for (int target_track : trackIds) {
            if (!timeline->getTrackById_const(target_track)->isLocked()) {
                affectedTracks << target_track;
            }
        }
    } else {
        while (it != timeline->m_allTracks.cend()) {
            int target_track = (*it)->getId();
591
            if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
592
593
594
595
596
597
                affectedTracks << target_track;
            } else if (trackIds.contains(target_track)) {
                // Track is marked as target but not active, remove it
                trackIds.removeAll(target_track);
            }
            ++it;
598
599
        }
    }
600
    if (affectedTracks.isEmpty()) {
601
        pCore->displayMessage(i18n("Please activate a track by clicking on a track's label"), ErrorMessage);
602
603
        return false;
    }
604
    result = breakAffectedGroups(timeline, affectedTracks, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
605
    if (overwrite) {
606
        // Cut all tracks
Vincent Pinon's avatar
Vincent Pinon committed
607
        for (int target_track : qAsConst(affectedTracks)) {
608
            result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
609
            if (!result) {
610
                qDebug() << "// LIFTING ZONE FAILED\n";
611
612
                break;
            }
613
        }
614
    } else {
615
        // Cut all tracks
Vincent Pinon's avatar
Vincent Pinon committed
616
        for (int target_track : qAsConst(affectedTracks)) {
617
618
619
            int startClipId = timeline->getClipByPosition(target_track, insertFrame);
            if (startClipId > -1) {
                // There is a clip, cut it
620
                result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo);
621
            }
622
        }
623
        result = result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo, affectedTracks);
624
    }
625
    if (result) {
626
627
        if (!trackIds.isEmpty()) {
            int newId = -1;
628
629
630
631
632
633
            QString binClipId;
            if (binId.contains(QLatin1Char('/'))) {
                binClipId = QString("%1/%2/%3").arg(binId.section(QLatin1Char('/'), 0, 0)).arg(zone.x()).arg(zone.y() - 1);
            } else {
                binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1);
            }
634
            result = timeline->requestClipInsertion(binClipId, trackIds.first(), insertFrame, newId, true, true, useTargets, undo, redo, affectedTracks);
635
        }
636
    }
637
    return result;
638
639
}

Nicolas Carion's avatar
Nicolas Carion committed
640
bool TimelineFunctions::liftZone(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
641
642
643
{
    // Check if there is a clip at start point
    int startClipId = timeline->getClipByPosition(trackId, zone.x());
644
645
    if (startClipId > -1) {
        // There is a clip, cut it
646
        if (timeline->getClipPosition(startClipId) < zone.x()) {
647
            qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId;
648
            TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo);
649
            qDebug() << "/// CUTTING AT START DONE";
650
        }
651
652
653
654
    }
    int endClipId = timeline->getClipByPosition(trackId, zone.y());
    if (endClipId > -1) {
        // There is a clip, cut it
655
        if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) {
656
            qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId;
657
            TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo);
658
            qDebug() << "/// CUTTING AT END DONE";
659
        }
660
    }
Nicolas Carion's avatar
Nicolas Carion committed
661
    std::unordered_set<int> clips = timeline->getItemsInRange(trackId, zone.x(), zone.y());
662
    for (const auto &clipId : clips) {
663
        timeline->requestClipUngroup(clipId, undo, redo);
664
        timeline->requestItemDeletion(clipId, undo, redo);
665
    }
666
667
668
    return true;
}

669
bool TimelineFunctions::removeSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, QVector<int> allowedTracks, bool useTargets)
670
{
671
    std::unordered_set<int> clips;
672
673
674
675
676
677
678
679
680
681
682
683
684
    if (useTargets) {
        auto it = timeline->m_allTracks.cbegin();
        while (it != timeline->m_allTracks.cend()) {
            int target_track = (*it)->getId();
            if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
                std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.y() - 1, -1, true);
                clips.insert(subs.begin(), subs.end());
            }
            ++it;
        }
    } else {
        for (int &tid : allowedTracks) {
            std::unordered_set<int> subs = timeline->getItemsInRange(tid, zone.y() - 1, -1, true);
685
686
687
            clips.insert(subs.begin(), subs.end());
        }
    }
688
689
690
691
    if (clips.size() == 0) {
        // TODO: inform user no change will be performed
        return true;
    }
692
    bool result = false;
693
694
695
696
697
698
    timeline->requestSetSelection(clips);
    int itemId = *clips.begin();
    int targetTrackId = timeline->getItemTrackId(itemId);
    int targetPos = timeline->getItemPosition(itemId) + zone.x() - zone.y();

    if (timeline->m_groups->isInGroup(itemId)) {
699
        result = timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.x() - zone.y(), true, true, undo, redo, true, true, true, allowedTracks);
700
701
702
703
704
705
706
707
    } else if (timeline->isClip(itemId)) {
        result = timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, undo, redo);
    } else {
        result = timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true, undo, redo);
    }
    timeline->requestClearSelection();
    if (!result) {
        undo();
708
709
710
711
    }
    return result;
}

712
bool TimelineFunctions::requestInsertSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, QVector<int> allowedTracks)
713
{
714
715
716
    timeline->requestClearSelection();
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
717
    std::unordered_set<int> items;
718
    if (allowedTracks.isEmpty()) {
719
720
721
722
        // Select clips in all tracks
        items = timeline->getItemsInRange(-1, zone.x(), -1, true);
    } else {
        // Select clips in target and active tracks only
723
724
725
        for (int target_track : allowedTracks) {
            std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.x(), -1, true);
            items.insert(subs.begin(), subs.end());
726
727
        }
    }
728
729
730
731
    if (items.empty()) {
        return true;
    }
    timeline->requestSetSelection(items);
732
    bool result = true;
733
734
735
736
737
738
739
    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 =
740
            result && timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.y() - zone.x(), true, true, local_undo, local_redo, true, true, true, allowedTracks);
741
    } else if (timeline->isClip(itemId)) {
742
        result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, local_undo, local_redo);
743
744
745
746
747
748
749
750
751
    } 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);
752
    }
753
    UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
754
    return result;
755
}
756

Nicolas Carion's avatar
Nicolas Carion committed
757
bool TimelineFunctions::requestItemCopy(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int trackId, int position)
758
759
760
761
{
    Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId));
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
762
763
    int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId));
    int deltaPos = position - timeline->getItemPosition(clipId);
764
    std::unordered_set<int> allIds = timeline->getGroupElements(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
765
    std::unordered_map<int, int> mapping; // keys are ids of the source clips, values are ids of the copied clips
766
    bool res = true;
767
768
    for (int id : allIds) {
        int newId = -1;
769
        if (timeline->isClip(id)) {
770
            PlaylistState::ClipState state = timeline->m_allClips[id]->clipState();
771
            res = cloneClip(timeline, id, newId, state, undo, redo);
772
773
774
775
            res = res && (newId != -1);
        }
        int target_position = timeline->getItemPosition(id) + deltaPos;
        int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack;
776
777
778
779
        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();
780
            if (timeline->isClip(id)) {
781
                res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, true, undo, redo);
782
783
            } else {
                const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
784
                std::unique_ptr<Mlt::Properties> transProps(timeline->m_allCompositions[id]->properties());
Nicolas Carion's avatar
Nicolas Carion committed
785
786
                res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
                                                                   timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo);
787
            }
788
789
790
791
792
793
794
795
        } else {
            res = false;
        }
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            return false;
        }
Nicolas Carion's avatar
Nicolas Carion committed
796
797
        mapping[id] = newId;
    }
798
    qDebug() << "Successful copy, copying groups...";
799
    res = timeline->m_groups->copyGroups(mapping, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
800
801
802
803
    if (!res) {
        bool undone = undo();
        Q_ASSERT(undone);
        return false;
804
805
806
807
    }
    return true;
}

Nicolas Carion's avatar
Nicolas Carion committed
808
void TimelineFunctions::showClipKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, bool value)
809
810
811
{
    timeline->m_allClips[clipId]->setShowKeyframes(value);
    QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId);
Vincent Pinon's avatar
Vincent Pinon committed
812
    emit timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
813
}
814

Nicolas Carion's avatar
Nicolas Carion committed
815
void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int compoId, bool value)
816
817
818
{
    timeline->m_allCompositions[compoId]->setShowKeyframes(value);
    QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId);
Vincent Pinon's avatar
Vincent Pinon committed
819
    emit timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
820
821
}

822
bool TimelineFunctions::switchEnableState(const std::shared_ptr<TimelineItemModel> &timeline, std::unordered_set<int> selection)
823
{
824
825
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
    bool result = false;
    bool disable = true;
    for (int clipId : selection) {
        if (!timeline->isClip(clipId)) {
            continue;
        }
        PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState();
        PlaylistState::ClipState state = PlaylistState::Disabled;
        disable = true;
        if (oldState == PlaylistState::Disabled) {
            state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType();
            disable = false;
        }
        result = changeClipState(timeline, clipId, state, undo, redo);
        if (!result) {
            break;
        }
    }
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
    // Update action name since clip will be switched
    int id = *selection.begin();
    Fun local_redo = []() { return true; };
    Fun local_undo = []() { return true; };
    if (timeline->isClip(id)) {
        bool disabled = timeline->m_allClips[id]->clipState() == PlaylistState::Disabled;
        QAction *action = pCore->window()->actionCollection()->action(QStringLiteral("clip_switch"));
        local_redo = [disabled, action]() {
            action->setText(disabled ? i18n("Enable clip") : i18n("Disable clip"));
            return true;
        };
        local_undo = [disabled, action]() {
            action->setText(disabled ? i18n("Disable clip") : i18n("Enable clip"));
            return true;
        };
    }
860
    if (result) {
861
862
863
        local_redo();
        UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
        pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip"));
864
865
866
867
    }
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
868
bool TimelineFunctions::changeClipState(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo)
869
{
870
871
    int track = timeline->getClipTrackId(clipId);
    int start = -1;
872
    bool invalidate = false;
873
874
    if (track > -1) {
        if (!timeline->getTrackById_const(track)->isAudioTrack()) {
875
            invalidate = true;
876
        }
877
        start = timeline->getItemPosition(clipId);
878
    }
879
880
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
881
882
883
    // For the state change to work, we need to unplant/replant the clip
    bool result = true;
    if (track > -1) {
884
        result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo, false, false);
885
886
887
888
    }
    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);
889
    }
890
    UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
891
892
    return result;
}
893

Nicolas Carion's avatar
Nicolas Carion committed
894
bool TimelineFunctions::requestSplitAudio(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int audioTarget)
895
896
897
898
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
899
    bool done = false;
900
    // Now clear selection so we don't mess with groups
901
    timeline->requestClearSelection(false, undo, redo);
902
    for (int cid : clips) {
903
        if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) {
904
            // clip without audio or audio only, skip
Pino Toscano's avatar
Pino Toscano committed
905
            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
906
            return false;
907
        }
908
909
        int position = timeline->getClipPosition(cid);
        int track = timeline->getClipTrackId(cid);
910
911
912
913
914
915
916
917
918
        QList<int> possibleTracks;
        if (audioTarget >= 0) {
            possibleTracks = {audioTarget};
        } else {
            int mirror = timeline->getMirrorAudioTrackId(track);
            if (mirror > -1) {
                possibleTracks = {mirror};
            }
        }
919
        if (possibleTracks.isEmpty()) {
920
921
            // No available audio track for splitting, abort
            undo();
922
            pCore->displayMessage(i18n("No available audio track for restore operation"), ErrorMessage);
923
924
            return false;
        }
925
        int newId;
926
        bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo);
927
928
929
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
930
            pCore->displayMessage(i18n("Audio restore failed"), ErrorMessage);
931
932
933
934
            return false;
        }
        bool success = false;
        while (!success && !possibleTracks.isEmpty()) {
935