timelinefunctions.cpp 95 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"
28
#include "clipmodel.hpp"
29
#include "compositionmodel.hpp"
Nicolas Carion's avatar
linting  
Nicolas Carion committed
30
#include "core.h"
31
#include "doc/kdenlivedoc.h"
32
#include "effects/effectstack/model/effectstackmodel.hpp"
33 34 35
#include "groupsmodel.hpp"
#include "timelineitemmodel.hpp"
#include "trackmodel.hpp"
36
#include "transitions/transitionsrepository.hpp"
37
#include "mainwindow.h"
38

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

Vincent Pinon's avatar
Vincent Pinon committed
46 47
#ifdef CRASH_AUTO_TEST
#include "logger.hpp"
48 49 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))(
            parameter_names("timeline", "clipId", "position"));
}
Vincent Pinon's avatar
Vincent Pinon committed
64 65 66 67 68 69 70 71
#else
#define TRACE_STATIC(...)
#define TRACE_RES(...)
#endif

QStringList waitingBinIds;
QMap<QString, QString> mappedIds;
QMap<int, int> tracksMap;
72
QMap<int, int> spacerUngroupedItems;
Vincent Pinon's avatar
Vincent Pinon committed
73
QSemaphore semaphore(1);
74

75 76
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
77
{
78 79
    // Special case: slowmotion clips
    double clipSpeed = timeline->m_allClips[clipId]->getSpeed();
80
    bool warp_pitch = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch"));
81 82
    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
83
    timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize;
84

85 86 87
    // copy useful timeline properties
    timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]);

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

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

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

Nicolas Carion's avatar
Nicolas Carion committed
195
bool TimelineFunctions::requestClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, Fun &undo, Fun &redo)
196
{
197 198 199 200
    const std::unordered_set<int> clipselect = timeline->getGroupElements(clipId);
    // Remove locked items
    std::unordered_set<int> clips;
    for (int cid : clipselect) {
201 202 203 204
        if (timeline->isSubTitle(cid)) {
            clips.insert(cid);
            continue;
        }
205 206 207
        if (!timeline->isClip(cid)) {
            continue;
        }
208
        int tk = timeline->getClipTrackId(cid);
209
        if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) {
210 211 212
            clips.insert(cid);
        }
    }
213 214 215 216 217 218 219 220 221 222
    // 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);
        }
    }

223 224 225
    // 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();

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

282 283 284 285 286 287
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
288
    for (const auto &track: timeline->m_allTracks) {
289 290 291 292 293 294
        if (!track->isLocked()) {
            affectedTracks << track;
        }
    }

    if (affectedTracks.isEmpty()) {
295
        pCore->displayMessage(i18n("All tracks are locked"), ErrorMessage, 500);
296 297 298 299
        return false;
    }

    unsigned count = 0;
Vincent Pinon's avatar
Vincent Pinon committed
300
    for (auto track: qAsConst(affectedTracks)) {
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
        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) {
317
        pCore->displayMessage(i18n("No clips to cut"), ErrorMessage);
318 319 320 321 322 323 324
    } else {
        pCore->pushUndo(undo, redo, i18n("Cut all clips"));
    }

    return count > 0;
}

Nicolas Carion's avatar
Nicolas Carion committed
325
int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position)
326
{
Nicolas Carion's avatar
Nicolas Carion committed
327
    std::unordered_set<int> clips = timeline->getItemsInRange(trackId, position, -1);
Nicolas Carion's avatar
Nicolas Carion committed
328
    if (!clips.empty()) {
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
        // 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;
        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) {
                    if (timeline->getItemPosition(l) + timeline->getItemPlaytime(l) < position) {
                        leavesToRemove.insert(l);
                    } else {
                        leavesToKeep.insert(l);
                    }
                }
                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);
                    }
                }
            }
        }
        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);
        return (*roots.cbegin());
374 375 376
    }
    return -1;
}
377

378
bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int startPosition, int endPosition, int affectedTrack, Fun &undo, Fun &redo)
379 380
{
    // Move group back to original position
381 382 383
    int track = timeline->getItemTrackId(itemId);
    bool isClip = timeline->isClip(itemId);
    if (isClip) {
384
        timeline->requestClipMove(itemId, track, startPosition, true, false, false);
385
    } else if (timeline->isComposition(itemId)) {
386
        timeline->requestCompositionMove(itemId, track, startPosition, false, false);
387 388
    } else {
        timeline->requestSubtitleMove(itemId, startPosition, false, false);
389 390
    }
    std::unordered_set<int> clips = timeline->getGroupElements(itemId);
391
    int mainGroup = timeline->m_groups->getRootId(itemId);
392
    bool final = false;
393 394 395 396 397 398 399 400 401 402 403 404 405
    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;
            }
406
        } else if (timeline->isTrack(affectedTrack)) {
407 408 409
            liftOk = TimelineFunctions::liftZone(timeline, affectedTrack, QPoint(endPosition, startPosition), undo, redo);
        }
        // The lift operation destroys selection group, so regroup now
410
        if (clips.size() > 1) {
411 412 413 414 415 416 417
            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);
418 419
        } else {
            // only 1 clip to be moved
420
            if (isClip) {
421
                final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, true, undo, redo);
422
            } else if (timeline->isComposition(itemId)) {
423
                final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo);
424
            } else {
425
                final = timeline->requestSubtitleMove(itemId, endPosition, true, true, true, true, undo, redo);
426
            }
427
        }
428
    }
429
    timeline->requestClearSelection();
430
    if (final) {
431 432
        if (startPosition < endPosition) {
            pCore->pushUndo(undo, redo, i18n("Insert space"));
Nicolas Carion's avatar
Nicolas Carion committed
433 434
        } else {
            pCore->pushUndo(undo, redo, i18n("Remove space"));
435
        }
436 437 438 439 440 441 442 443 444 445 446 447 448 449
        // 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();
450
        return true;
451 452
    } else {
        undo();
453 454 455
    }
    return false;
}
456

457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473

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);
474
                if (!tracks.contains(childTrackId) && timeline->m_groups->isInGroup(child)) {
475 476 477 478 479 480 481 482 483
                    // 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
484
bool TimelineFunctions::extractZone(const std::shared_ptr<TimelineItemModel> &timeline, QVector<int> tracks, QPoint zone, bool liftOnly)
485 486 487 488
{
    // Start undoable command
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
489
    bool result = true;
490
    result = breakAffectedGroups(timeline, tracks, zone, undo, redo);
491

492
    for (int &trackId : tracks) {
493 494 495
        if (timeline->getTrackById_const(trackId)->isLocked()) {
            continue;
        }
496
        result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo);
497 498
    }
    if (result && !liftOnly) {
499
        result = TimelineFunctions::removeSpace(timeline, zone, undo, redo, tracks);
500
    }
501
    pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone"));
502 503 504
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
505
bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, QList<int> trackIds, const QString &binId, int insertFrame, QPoint zone,
506
                                   bool overwrite, bool useTargets)
507 508 509
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
510 511 512 513
    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 {
514
        pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage);
515 516 517 518 519 520 521 522 523
        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
524
    bool result = true;
525 526
    QVector<int> affectedTracks;
    auto it = timeline->m_allTracks.cbegin();
527
    if (!useTargets) {
528 529 530 531 532 533 534 535 536
        // 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();
537
            if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
538 539 540 541 542 543
                affectedTracks << target_track;
            } else if (trackIds.contains(target_track)) {
                // Track is marked as target but not active, remove it
                trackIds.removeAll(target_track);
            }
            ++it;
544 545
        }
    }
546
    if (affectedTracks.isEmpty()) {
547
        pCore->displayMessage(i18n("Please activate a track by clicking on a track's label"), ErrorMessage);
548 549
        return false;
    }
550
    result = breakAffectedGroups(timeline, affectedTracks, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
551
    if (overwrite) {
552
        // Cut all tracks
Vincent Pinon's avatar
Vincent Pinon committed
553
        for (int target_track : qAsConst(affectedTracks)) {
554
            result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
555
            if (!result) {
556
                qDebug() << "// LIFTING ZONE FAILED\n";
557 558
                break;
            }
559
        }
560
    } else {
561
        // Cut all tracks
Vincent Pinon's avatar
Vincent Pinon committed
562
        for (int target_track : qAsConst(affectedTracks)) {
563 564 565
            int startClipId = timeline->getClipByPosition(target_track, insertFrame);
            if (startClipId > -1) {
                // There is a clip, cut it
566
                result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo);
567
            }
568
        }
569
        result = result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo, affectedTracks);
570
    }
571
    if (result) {
572 573
        if (!trackIds.isEmpty()) {
            int newId = -1;
574 575 576 577 578 579
            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);
            }
580
            result = timeline->requestClipInsertion(binClipId, trackIds.first(), insertFrame, newId, true, true, useTargets, undo, redo, affectedTracks);
581
        }
582
    }
583
    return result;
584 585
}

Nicolas Carion's avatar
Nicolas Carion committed
586
bool TimelineFunctions::liftZone(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
587 588 589
{
    // Check if there is a clip at start point
    int startClipId = timeline->getClipByPosition(trackId, zone.x());
590 591
    if (startClipId > -1) {
        // There is a clip, cut it
592
        if (timeline->getClipPosition(startClipId) < zone.x()) {
593
            qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId;
594
            TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo);
595
            qDebug() << "/// CUTTING AT START DONE";
596
        }
597 598 599 600
    }
    int endClipId = timeline->getClipByPosition(trackId, zone.y());
    if (endClipId > -1) {
        // There is a clip, cut it
601
        if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) {
602
            qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId;
603
            TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo);
604
            qDebug() << "/// CUTTING AT END DONE";
605
        }
606
    }
Nicolas Carion's avatar
Nicolas Carion committed
607
    std::unordered_set<int> clips = timeline->getItemsInRange(trackId, zone.x(), zone.y());
608
    for (const auto &clipId : clips) {
609
        timeline->requestClipUngroup(clipId, undo, redo);
610
        timeline->requestItemDeletion(clipId, undo, redo);
611
    }
612 613 614
    return true;
}

615
bool TimelineFunctions::removeSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, QVector<int> allowedTracks, bool useTargets)
616
{
617
    std::unordered_set<int> clips;
618 619 620 621 622 623 624 625 626 627 628 629 630
    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);
631 632 633
            clips.insert(subs.begin(), subs.end());
        }
    }
634 635 636 637
    if (clips.size() == 0) {
        // TODO: inform user no change will be performed
        return true;
    }
638
    bool result = false;
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
    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)) {
        result = timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.x() - zone.y(), true, true, undo, redo, true, true, allowedTracks);
    } 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();
654 655 656 657
    }
    return result;
}

658
bool TimelineFunctions::requestInsertSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, QVector<int> allowedTracks)
659
{
660 661 662
    timeline->requestClearSelection();
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
663
    std::unordered_set<int> items;
664
    if (allowedTracks.isEmpty()) {
665 666 667 668
        // Select clips in all tracks
        items = timeline->getItemsInRange(-1, zone.x(), -1, true);
    } else {
        // Select clips in target and active tracks only
669 670 671
        for (int target_track : allowedTracks) {
            std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.x(), -1, true);
            items.insert(subs.begin(), subs.end());
672 673
        }
    }
674 675 676 677
    if (items.empty()) {
        return true;
    }
    timeline->requestSetSelection(items);
678
    bool result = true;
679 680 681 682 683 684 685
    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 =
686
            result && timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.y() - zone.x(), true, true, local_undo, local_redo, true, true, allowedTracks);
687
    } else if (timeline->isClip(itemId)) {
688
        result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, local_undo, local_redo);
689 690 691 692 693 694 695 696 697
    } 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);
698
    }
699
    UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
700
    return result;
701
}
702

Nicolas Carion's avatar
Nicolas Carion committed
703
bool TimelineFunctions::requestItemCopy(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int trackId, int position)
704 705 706 707
{
    Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId));
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
708 709
    int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId));
    int deltaPos = position - timeline->getItemPosition(clipId);
710
    std::unordered_set<int> allIds = timeline->getGroupElements(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
711
    std::unordered_map<int, int> mapping; // keys are ids of the source clips, values are ids of the copied clips
712
    bool res = true;
713 714
    for (int id : allIds) {
        int newId = -1;
715
        if (timeline->isClip(id)) {
716
            PlaylistState::ClipState state = timeline->m_allClips[id]->clipState();
717
            res = cloneClip(timeline, id, newId, state, undo, redo);
718 719 720 721
            res = res && (newId != -1);
        }
        int target_position = timeline->getItemPosition(id) + deltaPos;
        int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack;
722 723 724 725
        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();
726
            if (timeline->isClip(id)) {
727
                res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, true, undo, redo);
728 729
            } else {
                const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
730
                std::unique_ptr<Mlt::Properties> transProps(timeline->m_allCompositions[id]->properties());
Nicolas Carion's avatar
Nicolas Carion committed
731 732
                res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
                                                                   timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo);
733
            }
734 735 736 737 738 739 740 741
        } else {
            res = false;
        }
        if (!res) {
            bool undone = undo();
            Q_ASSERT(undone);
            return false;
        }
Nicolas Carion's avatar
Nicolas Carion committed
742 743
        mapping[id] = newId;
    }
744
    qDebug() << "Successful copy, copying groups...";
745
    res = timeline->m_groups->copyGroups(mapping, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
746 747 748 749
    if (!res) {
        bool undone = undo();
        Q_ASSERT(undone);
        return false;
750 751 752 753
    }
    return true;
}

Nicolas Carion's avatar
Nicolas Carion committed
754
void TimelineFunctions::showClipKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, bool value)
755 756 757
{
    timeline->m_allClips[clipId]->setShowKeyframes(value);
    QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId);
Vincent Pinon's avatar
Vincent Pinon committed
758
    emit timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
759
}
760

Nicolas Carion's avatar
Nicolas Carion committed
761
void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int compoId, bool value)
762 763 764
{
    timeline->m_allCompositions[compoId]->setShowKeyframes(value);
    QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId);
Vincent Pinon's avatar
Vincent Pinon committed
765
    emit timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
766 767
}

768
bool TimelineFunctions::switchEnableState(const std::shared_ptr<TimelineItemModel> &timeline, std::unordered_set<int> selection)
769
{
770 771
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
    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;
        }
    }
790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
    // 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;
        };
    }
806
    if (result) {
807 808 809
        local_redo();
        UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
        pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip"));
810 811 812 813
    }
    return result;
}

Nicolas Carion's avatar
Nicolas Carion committed
814
bool TimelineFunctions::changeClipState(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo)
815
{
816 817
    int track = timeline->getClipTrackId(clipId);
    int start = -1;
818
    bool invalidate = false;
819 820
    if (track > -1) {
        if (!timeline->getTrackById_const(track)->isAudioTrack()) {
821
            invalidate = true;
822
        }
823
        start = timeline->getItemPosition(clipId);
824
    }
825 826
    Fun local_undo = []() { return true; };
    Fun local_redo = []() { return true; };
827 828 829
    // For the state change to work, we need to unplant/replant the clip
    bool result = true;
    if (track > -1) {
830
        result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo, false, false);
831 832 833 834
    }
    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);
835
    }
836
    UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
837 838
    return result;
}
839

Nicolas Carion's avatar
Nicolas Carion committed
840
bool TimelineFunctions::requestSplitAudio(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int audioTarget)
841 842 843 844
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
845
    bool done = false;