timelinefunctions.cpp 59.1 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, 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
    timeline->requestClearSelection();
173
    std::unordered_set<int> topElements;
174
175
    std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });

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

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

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

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

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

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

Nicolas Carion's avatar
Nicolas Carion committed
374
bool TimelineFunctions::removeSpace(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
375
{
376
377
    Q_UNUSED(trackId)

Nicolas Carion's avatar
Nicolas Carion committed
378
    std::unordered_set<int> clips = timeline->getItemsInRange(-1, zone.y() - 1, -1, true);
379
    bool result = false;
Nicolas Carion's avatar
Nicolas Carion committed
380
    if (!clips.empty()) {
381
382
        int clipId = *clips.begin();
        if (clips.size() > 1) {
383
            int res = timeline->requestClipsGroup(clips, undo, redo);
384
            if (res > -1) {
385
                result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo);
386
                if (result) {
387
                    result = timeline->requestClipUngroup(clipId, undo, redo);
388
                }
389
390
391
                if (!result) {
                    undo();
                }
392
393
394
395
            }
        } else {
            // only 1 clip to be moved
            int clipStart = timeline->getItemPosition(clipId);
396
            result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, undo, redo);
397
398
399
400
401
        }
    }
    return result;
}

402
bool TimelineFunctions::requestInsertSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo)
403
{
404
405
406
407
408
409
410
411
    timeline->requestClearSelection();
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
    std::unordered_set<int> items = timeline->getItemsInRange(-1, zone.x(), -1, true);
    if (items.empty()) {
        return true;
    }
    timeline->requestSetSelection(items);
412
    bool result = true;
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
    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)) {
        result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, local_undo, local_redo);
    } 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);
432
    }
433
    UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
434
    return result;
435
}
436

Nicolas Carion's avatar
Nicolas Carion committed
437
bool TimelineFunctions::requestItemCopy(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int trackId, int position)
438
439
440
441
{
    Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId));
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
442
443
    int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId));
    int deltaPos = position - timeline->getItemPosition(clipId);
444
    std::unordered_set<int> allIds = timeline->getGroupElements(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
445
    std::unordered_map<int, int> mapping; // keys are ids of the source clips, values are ids of the copied clips
446
    bool res = true;
447
448
    for (int id : allIds) {
        int newId = -1;
449
        if (timeline->isClip(id)) {
450
            PlaylistState::ClipState state = timeline->m_allClips[id]->clipState();
451
            res = cloneClip(timeline, id, newId, state, undo, redo);
452
453
454
455
            res = res && (newId != -1);
        }
        int target_position = timeline->getItemPosition(id) + deltaPos;
        int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack;
456
457
458
459
        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();
460
            if (timeline->isClip(id)) {
461
                res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo);
462
463
            } else {
                const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
464
                std::unique_ptr<Mlt::Properties> transProps(timeline->m_allCompositions[id]->properties());
Nicolas Carion's avatar
Nicolas Carion committed
465
466
                res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
                                                                   timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo);
467
            }
468
469
470
471
472
473
474
475
        } else {
            res = false;
        }
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            return false;
        }
Nicolas Carion's avatar
Nicolas Carion committed
476
477
        mapping[id] = newId;
    }
478
    qDebug() << "Successful copy, coping groups...";
479
    res = timeline->m_groups->copyGroups(mapping, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
480
481
482
483
    if (!res) {
        bool undone = undo();
        Q_ASSERT(undone);
        return false;
484
485
486
487
    }
    return true;
}

Nicolas Carion's avatar
Nicolas Carion committed
488
void TimelineFunctions::showClipKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, bool value)
489
490
491
{
    timeline->m_allClips[clipId]->setShowKeyframes(value);
    QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId);
492
    timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
493
}
494

Nicolas Carion's avatar
Nicolas Carion committed
495
void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int compoId, bool value)
496
497
498
{
    timeline->m_allCompositions[compoId]->setShowKeyframes(value);
    QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId);
499
    timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
500
501
}

Nicolas Carion's avatar
Nicolas Carion committed
502
bool TimelineFunctions::switchEnableState(const std::shared_ptr<TimelineItemModel> &timeline, int clipId)
503
{
504
505
506
507
    PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState();
    PlaylistState::ClipState state = PlaylistState::Disabled;
    bool disable = true;
    if (oldState == PlaylistState::Disabled) {
508
        state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType();
509
        disable = false;
510
    }
511
512
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
513
    bool result = changeClipState(timeline, clipId, state, undo, redo);
514
    if (result) {
515
        pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip"));
516
517
518
519
    }
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
520
bool TimelineFunctions::changeClipState(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo)
521
{
522
523
524
525
526
527
528
529
530
    int track = timeline->getClipTrackId(clipId);
    int start = -1;
    int end = -1;
    if (track > -1) {
        if (!timeline->getTrackById_const(track)->isAudioTrack()) {
            start = timeline->getItemPosition(clipId);
            end = start + timeline->getItemPlaytime(clipId);
        }
    }
531
532
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
533

534
    bool result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo);
535
536
537
538
539
540
541
542
543
544
545
546
    Fun local_update = [start, end, timeline]() {
        if (start > -1) {
            timeline->invalidateZone(start, end);
            timeline->checkRefresh(start, end);
        }
        return true;
    };
    if (start > -1) {
        local_update();
        PUSH_LAMBDA(local_update, local_redo);
        PUSH_LAMBDA(local_update, local_undo);
    }
547
    UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
548
549
    return result;
}
550

Nicolas Carion's avatar
Nicolas Carion committed
551
bool TimelineFunctions::requestSplitAudio(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int audioTarget)
552
553
554
555
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
556
    bool done = false;
557
    // Now clear selection so we don't mess with groups
558
    timeline->requestClearSelection(false, undo, redo);
559
    for (int cid : clips) {
560
        if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) {
561
            // clip without audio or audio only, skip
Pino Toscano's avatar
Pino Toscano committed
562
            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
563
            return false;
564
        }
565
566
        int position = timeline->getClipPosition(cid);
        int track = timeline->getClipTrackId(cid);
567
        QList<int> possibleTracks = audioTarget >= 0 ? QList<int>() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack);
568
        if (possibleTracks.isEmpty()) {
569
570
571
572
573
            // No available audio track for splitting, abort
            undo();
            pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage);
            return false;
        }
574
        int newId;
575
        bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo);
576
577
578
579
580
581
582
583
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
            return false;
        }
        bool success = false;
        while (!success && !possibleTracks.isEmpty()) {
584
            int newTrack = possibleTracks.takeFirst();
585
            success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo);
586
        }
587
588
        TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo);
        success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo);
589
        if (!success) {
590
591
            bool undone = undo();
            Q_ASSERT(undone);
592
            pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
593
594
            return false;
        }
595
        done = true;
596
    }
597
    if (done) {
598
        timeline->requestSetSelection(clips, undo, redo);
599
600
601
        pCore->pushUndo(undo, redo, i18n("Split Audio"));
    }
    return done;
602
}
603

Nicolas Carion's avatar
Nicolas Carion committed
604
bool TimelineFunctions::requestSplitVideo(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int videoTarget)
605
606
607
608
609
610
{
    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
611
    timeline->requestClearSelection();
612
613
614
615
616
617
618
619
620
621
622
623
624
625
    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;
626
        bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo);
627
628
629
630
631
632
633
634
635
        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();
636
            success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo);
637
        }
638
639
        TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo);
        success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo);
640
641
642
643
644
645
646
647
648
649
650
651
652
653
        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
654
void TimelineFunctions::setCompositionATrack(const std::shared_ptr<TimelineItemModel> &timeline, int cid, int aTrack)
655
{
656
657
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
658
    std::shared_ptr<CompositionModel> compo = timeline->getCompositionPtr(cid);
659
    int previousATrack = compo->getATrack();
Nicolas Carion's avatar
Nicolas Carion committed
660
    int previousAutoTrack = static_cast<int>(compo->getForcedTrack() == -1);
661
662
663
664
665
666
667
    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);
668
    Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() {
669
670
671
672
673
674
675
676
677
678
679
        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();
        QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid);
        timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack});
        timeline->invalidateZone(start, end);
        timeline->checkRefresh(start, end);
        return true;
    };
680
    Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() {
681
682
        QScopedPointer<Mlt::Field> field(timeline->m_tractor->field());
        field->lock();
Nicolas Carion's avatar
Nicolas Carion committed
683
        timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0);
684
        timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1));
685
686
687
688
689
690
691
        field->unlock();
        QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid);
        timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack});
        timeline->invalidateZone(start, end);
        timeline->checkRefresh(start, end);
        return true;
    };
692
693
694
    if (local_redo()) {
        PUSH_LAMBDA(local_undo, undo);
        PUSH_LAMBDA(local_redo, redo);
695
    }
696
    pCore->pushUndo(undo, redo, i18n("Change Composition Track"));
697
}
698

Nicolas Carion's avatar
Nicolas Carion committed
699
void TimelineFunctions::enableMultitrackView(const std::shared_ptr<TimelineItemModel> &timeline, bool enable)
700
{
701
    QList<int> videoTracks;
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
    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) {
741
742
743
744
            case 0:
                switch (videoTracks.size()) {
                case 2:
                    geometry = QStringLiteral("0 0 50% 100%");
745
                    break;
746
747
748
749
750
751
752
753
754
755
756
757
                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%");
758
                    break;
759
760
761
762
                }
                break;
            case 1:
                switch (videoTracks.size()) {
763
                case 2:
764
                    geometry = QStringLiteral("50% 0 50% 100%");
765
766
                    break;
                case 3:
767
                    geometry = QStringLiteral("33% 0 33% 100%");
768
769
                    break;
                case 4:
770
                    geometry = QStringLiteral("50% 0 50% 50%");
771
772
                    break;
                case 5:
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
                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%");
802
                    break;
803
804
805
806
807
808
809
810
811
812
813
814
                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:
815
                case 6:
816
817
818
819
                    geometry = QStringLiteral("33% 50% 33% 50%");
                    break;
                default:
                    geometry = QStringLiteral("33% 33% 33% 33%");
820
                    break;
821
822
823
824
825
826
                }
                break;
            case 5:
                switch (videoTracks.size()) {
                case 6:
                    geometry = QStringLiteral("66% 50% 33% 50%");
827
828
                    break;
                default:
829
                    geometry = QStringLiteral("66% 33% 33% 33%");
830
                    break;
831
832
833
834
835
836
837
838
839
840
841
                }
                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;
842
843
844
845
846
847
848
849
850
851
            }
            // 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();
}
852

853
854
void TimelineFunctions::saveTimelineSelection(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &selection,
                                              const QDir &targetDir)
855
856
{
    bool ok;
857
858
    QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal,
                                         QString(), &ok);
859
860
861
862
863
864
865
    if (name.isEmpty() || !ok) {
        return;
    }
    if (targetDir.exists(name + QStringLiteral(".mlt"))) {
        // TODO: warn and ask for overwrite / rename
    }
    int offset = -1;
866
867
    int lowerAudioTrack = -1;
    int lowerVideoTrack = -1;
868
869
    QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt"));
    // Build a copy of selected tracks.
870
    QMap<int, int> sourceTracks;
871
872
873
874
875
876
877
878
879
880
881
882
883
    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());
884
    QScopedPointer<Mlt::Field> field(newTractor.field());
885
    int ix = 0;
886
    QString composite = TransitionsRepository::get()->getCompositingTransition();
887
    QMapIterator<int, int> i(sourceTracks);
888
    QList<Mlt::Transition *> compositions;
889
890
891
    while (i.hasNext()) {
        i.next();
        QScopedPointer<Mlt::Playlist> newTrackPlaylist(new Mlt::Playlist(*newTractor.profile()));
892
        newTractor.set_track(*newTrackPlaylist, ix);
893
        // QScopedPointer<Mlt::Producer> trackProducer(newTractor.track(ix));
894
        int trackId = i.value();
895
        sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix);
896
        std::shared_ptr<TrackModel> track = timeline->getTrackById_const(trackId);
897
898
899
900
901
902
903
904
905
906
907
908
        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;
            }
        }
909
910
911
912
913
914
915
916
        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
Jean-Baptiste Mardelle committed
917
                    newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1);
918
                } else if (timeline->isComposition(itemId)) {
919
                    // Composition
Nicolas Carion's avatar
Nicolas Carion committed
920
                    auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get());
921
922
923
924
925
926
927
928
929
                    QString id(t->get("kdenlive_id"));
                    QString internal(t->get("internal_added"));
                    if (internal.isEmpty()) {
                        compositions << t;
                        if (id.isEmpty()) {
                            qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service");
                            t->set("kdenlive_id", t->get("mlt_service"));
                        }
                    }
930
931
932
933
934
                }
            }
        }
        ix++;
    }
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
    // Sort compositions and insert
    if (!compositions.isEmpty()) {
        std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); });
        while (!compositions.isEmpty()) {
            QScopedPointer<Mlt::Transition> t(compositions.takeFirst());
            if (sourceTracks.contains(t->get_a_track()) && sourceTracks.contains(t->get_b_track())) {
                Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service"));
                Mlt::Properties sourceProps(t->get_properties());
                newComposition.inherit(sourceProps);
                QString id(t->get("kdenlive_id"));
                int in = qMax(0, t->get_in() - offset);
                int out = t->get_out() - offset;
                newComposition.set_in_and_out(in, out);
                int a_track = sourceTracks.value(t->get_a_track());
                int b_track = sourceTracks.value(t->get_b_track());
                field->plant_transition(newComposition, a_track, b_track);
            }
        }
    }
    // Track compositing
    i.toFront();
    ix = 0;