timelinemodel.cpp 121 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
/***************************************************************************
 *   Copyright (C) 2017 by Nicolas Carion                                  *
 *   This file is part of Kdenlive. See www.kdenlive.org.                  *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) version 3 or any later version accepted by the       *
 *   membership of KDE e.V. (or its successor approved  by the membership  *
 *   of KDE e.V.), which shall act as a proxy defined in Section 14 of     *
 *   version 3 of the license.                                             *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 ***************************************************************************/

#include "timelinemodel.hpp"
23
#include "assets/model/assetparametermodel.hpp"
24
#include "bin/projectclip.h"
25
#include "bin/projectitemmodel.h"
26
#include "clipmodel.hpp"
27
#include "compositionmodel.hpp"
Nicolas Carion's avatar
style  
Nicolas Carion committed
28
#include "core.h"
Nicolas Carion's avatar
Nicolas Carion committed
29
#include "doc/docundostack.hpp"
30
#include "effects/effectsrepository.hpp"
31
#include "effects/effectstack/model/effectstackmodel.hpp"
32
#include "groupsmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
33
#include "kdenlivesettings.h"
34
#include "logger.hpp"
35
#include "snapmodel.hpp"
36
#include "timelinefunctions.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
37
#include "trackmodel.hpp"
38

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

49 50
#include "macros.hpp"

Nicolas Carion's avatar
Nicolas Carion committed
51 52 53 54 55 56 57 58 59 60 61 62
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wfloat-equal"
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wpedantic"
#include <rttr/registration>
#pragma GCC diagnostic pop
RTTR_REGISTRATION
{
    using namespace rttr;
    registration::class_<TimelineModel>("TimelineModel")
63
        .method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock"))
Nicolas Carion's avatar
Nicolas Carion committed
64 65 66 67 68 69 70 71
        .method("requestClipMove", select_overload<bool(int, int, int, bool, bool, bool)>(&TimelineModel::requestClipMove))(
            parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
        .method("requestCompositionMove", select_overload<bool(int, int, int, bool, bool)>(&TimelineModel::requestCompositionMove))(
            parameter_names("compoId", "trackId", "position", "updateView", "logUndo"))
        .method("requestClipInsertion", select_overload<bool(const QString &, int, int, int &, bool, bool, bool)>(&TimelineModel::requestClipInsertion))(
            parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets"))
        .method("requestItemDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo"))
        .method("requestGroupMove", select_overload<bool(int, int, int, int, bool, bool)>(&TimelineModel::requestGroupMove))(
72
            parameter_names("itemId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
Nicolas Carion's avatar
Nicolas Carion committed
73 74 75 76
        .method("requestGroupDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo"))
        .method("requestItemResize", select_overload<int(int, int, bool, bool, int, bool)>(&TimelineModel::requestItemResize))(
            parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize"))
        .method("requestClipsGroup", select_overload<int(const std::unordered_set<int> &, bool, GroupType)>(&TimelineModel::requestClipsGroup))(
77 78 79
            parameter_names("itemIds", "logUndo", "type"))
        .method("requestClipUngroup", select_overload<bool(int, bool)>(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo"))
        .method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo"))
Nicolas Carion's avatar
Nicolas Carion committed
80 81
        .method("requestTrackInsertion", select_overload<bool(int, int &, const QString &, bool)>(&TimelineModel::requestTrackInsertion))(
            parameter_names("pos", "id", "trackName", "audioTrack"))
82
        .method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
83
        .method("requestClearSelection", select_overload<bool(bool)>(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
84 85
        .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear"))
        .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
86 87 88
        .method("requestSetSelection", select_overload<bool(const std::unordered_set<int> &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds"))
        .method("requestFakeClipMove", select_overload<bool(int, int, int, bool, bool, bool)>(&TimelineModel::requestFakeClipMove))(
            parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
89 90
        .method("requestFakeGroupMove", select_overload<bool(int, int, int, int, bool, bool)>(&TimelineModel::requestFakeGroupMove))(
            parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
91 92 93
        .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance"))
        .method("suggestCompositionMove",
                &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance"))
94 95
        // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos"))
        // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos"))
96 97 98 99
        // .method("requestCompositionInsertion", select_overload<bool(const QString &, int, int, int, std::unique_ptr<Mlt::Properties>, int &, bool)>(
        //                                            &TimelineModel::requestCompositionInsertion))(
        //     parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo"))
        .method("requestClipTimeWarp", select_overload<bool(int, double)>(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed"));
Nicolas Carion's avatar
Nicolas Carion committed
100 101
}

102
int TimelineModel::next_id = 0;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
103
int TimelineModel::seekDuration = 30000;
104

105
TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr<DocUndoStack> undo_stack)
106
    : QAbstractItemModel_shared_from_this()
107
    , m_tractor(new Mlt::Tractor(*profile))
108
    , m_snaps(new SnapModel())
Nicolas Carion's avatar
Nicolas Carion committed
109
    , m_undoStack(std::move(undo_stack))
110
    , m_profile(profile)
111
    , m_blackClip(new Mlt::Producer(*profile, "color:black"))
112 113
    , m_lock(QReadWriteLock::Recursive)
    , m_timelineEffectsEnabled(true)
Nicolas Carion's avatar
Nicolas Carion committed
114
    , m_id(getNextId())
115
    , m_overlayTrackCount(-1)
116 117
    , m_audioTarget(-1)
    , m_videoTarget(-1)
118
    , m_editMode(TimelineMode::NormalEdit)
119
    , m_blockRefresh(false)
120
    , m_closing(false)
121
{
122 123 124
    // Create black background track
    m_blackClip->set("id", "black_track");
    m_blackClip->set("mlt_type", "producer");
125
    m_blackClip->set("aspect_ratio", 1);
126
    m_blackClip->set("length", INT_MAX);
127
    m_blackClip->set("set.test_audio", 0);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
128
    m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
129
    m_tractor->insert_track(*m_blackClip, 0);
130

131
    TRACE_CONSTR(this);
132 133
}

134 135
void TimelineModel::prepareClose()
{
136
    QWriteLocker locker(&m_lock);
137 138 139 140
    // Unlock all tracks to allow delting clip from tracks
    m_closing = true;
    auto it = m_allTracks.begin();
    while (it != m_allTracks.end()) {
141
        (*it)->unlock();
142 143 144 145
        ++it;
    }
}

146 147
TimelineModel::~TimelineModel()
{
148
    std::vector<int> all_ids;
Nicolas Carion's avatar
Nicolas Carion committed
149
    for (auto tracks : m_iteratorTable) {
150 151
        all_ids.push_back(tracks.first);
    }
Nicolas Carion's avatar
Nicolas Carion committed
152
    for (auto tracks : all_ids) {
153
        deregisterTrack_lambda(tracks, false)();
154
    }
155
    for (const auto &clip : m_allClips) {
156 157
        clip.second->deregisterClipToBin();
    }
158 159
}

160 161
int TimelineModel::getTracksCount() const
{
162
    READ_LOCK();
163
    int count = m_tractor->count();
164 165
    if (m_overlayTrackCount > -1) {
        count -= m_overlayTrackCount;
166
    }
167
    Q_ASSERT(count >= 0);
168
    // don't count the black background track
Nicolas Carion's avatar
Nicolas Carion committed
169
    Q_ASSERT(count - 1 == static_cast<int>(m_allTracks.size()));
170
    return count - 1;
171
}
172

173 174
int TimelineModel::getTrackIndexFromPosition(int pos) const
{
175
    Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size());
176
    READ_LOCK();
177
    auto it = m_allTracks.cbegin();
178 179 180 181 182 183 184
    while (pos > 0) {
        it++;
        pos--;
    }
    return (*it)->getId();
}

185
int TimelineModel::getClipsCount() const
186
{
187 188 189
    READ_LOCK();
    int size = int(m_allClips.size());
    return size;
190
}
191

192 193 194 195 196 197
int TimelineModel::getCompositionsCount() const
{
    READ_LOCK();
    int size = int(m_allCompositions.size());
    return size;
}
198

199
int TimelineModel::getClipTrackId(int clipId) const
200
{
201
    READ_LOCK();
202 203
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
204
    return clip->getCurrentTrackId();
205 206
}

207 208 209 210 211 212 213 214 215 216
int TimelineModel::getCompositionTrackId(int compoId) const
{
    Q_ASSERT(m_allCompositions.count(compoId) > 0);
    const auto trans = m_allCompositions.at(compoId);
    return trans->getCurrentTrackId();
}

int TimelineModel::getItemTrackId(int itemId) const
{
    READ_LOCK();
217
    Q_ASSERT(isItem(itemId));
218 219 220 221 222 223
    if (isComposition(itemId)) {
        return getCompositionTrackId(itemId);
    }
    return getClipTrackId(itemId);
}

224
int TimelineModel::getClipPosition(int clipId) const
225
{
226
    READ_LOCK();
227 228
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
229 230
    int pos = clip->getPosition();
    return pos;
231 232
}

233 234 235 236 237 238 239
double TimelineModel::getClipSpeed(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_allClips.at(clipId)->getSpeed();
}

240 241 242 243 244 245 246
int TimelineModel::getClipSplitPartner(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    return m_groups->getSplitPartner(clipId);
}

247 248 249 250 251
int TimelineModel::getClipIn(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
252 253 254 255 256 257 258 259 260
    return clip->getIn();
}

PlaylistState::ClipState TimelineModel::getClipState(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
    return clip->clipState();
261 262 263 264 265 266 267 268 269 270 271
}

const QString TimelineModel::getClipBinId(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(m_allClips.count(clipId) > 0);
    const auto clip = m_allClips.at(clipId);
    QString id = clip->binId();
    return id;
}

272
int TimelineModel::getClipPlaytime(int clipId) const
273
{
274
    READ_LOCK();
275
    Q_ASSERT(isClip(clipId));
276
    const auto clip = m_allClips.at(clipId);
277 278
    int playtime = clip->getPlaytime();
    return playtime;
279 280
}

281 282 283 284 285 286 287 288
QSize TimelineModel::getClipFrameSize(int clipId) const
{
    READ_LOCK();
    Q_ASSERT(isClip(clipId));
    const auto clip = m_allClips.at(clipId);
    return clip->getFrameSize();
}

289
int TimelineModel::getTrackClipsCount(int trackId) const
290
{
291
    READ_LOCK();
292
    Q_ASSERT(isTrack(trackId));
293
    int count = getTrackById_const(trackId)->getClipsCount();
294
    return count;
295 296
}

297 298 299 300 301 302 303
int TimelineModel::getClipByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getClipByPosition(position);
}

304 305 306 307 308 309 310
int TimelineModel::getCompositionByPosition(int trackId, int position) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    return getTrackById_const(trackId)->getCompositionByPosition(position);
}

311
int TimelineModel::getTrackPosition(int trackId) const
312
{
313
    READ_LOCK();
314
    Q_ASSERT(isTrack(trackId));
315
    auto it = m_allTracks.cbegin();
316
    int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId));
317
    return pos;
318 319
}

320 321 322 323 324 325 326
int TimelineModel::getTrackMltIndex(int trackId) const
{
    READ_LOCK();
    // Because of the black track that we insert in first position, the mlt index is the position + 1
    return getTrackPosition(trackId) + 1;
}

327 328 329 330 331
int TimelineModel::getTrackSortValue(int trackId, bool separated) const
{
    if (separated) {
        return getTrackPosition(trackId) + 1;
    }
332
    auto it = m_allTracks.cend();
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
    int aCount = 0;
    int vCount = 0;
    bool isAudio = false;
    int trackPos = 0;
    while (it != m_allTracks.begin()) {
        --it;
        bool audioTrack = (*it)->isAudioTrack();
        if (audioTrack) {
            aCount++;
        } else {
            vCount++;
        }
        if (trackId == (*it)->getId()) {
            isAudio = audioTrack;
            trackPos = audioTrack ? aCount : vCount;
        }
    }
    int trackDiff = aCount - vCount;
    if (trackDiff > 0) {
        // more audio tracks
        if (!isAudio) {
            trackPos -= trackDiff;
        } else if (trackPos > vCount) {
            return -trackPos;
        }
    }
    return isAudio ? ((aCount * trackPos) - 1) : (vCount + 1 - trackPos) * 2;
}

Nicolas Carion's avatar
Nicolas Carion committed
362
QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const
363 364 365
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
Nicolas Carion's avatar
Nicolas Carion committed
366
    QList<int> results;
367
    auto it = m_iteratorTable.at(trackId);
368
    while (it != m_allTracks.cbegin()) {
369 370
        --it;
        if (type == TrackType::AnyTrack) {
371 372
            results << (*it)->getId();
            continue;
373
        }
374 375
        bool audioTrack = (*it)->isAudioTrack();
        if (type == TrackType::AudioTrack && audioTrack) {
376
            results << (*it)->getId();
377
        } else if (type == TrackType::VideoTrack && !audioTrack) {
378
            results << (*it)->getId();
379 380
        }
    }
381
    return results;
382 383
}

384 385 386 387 388
int TimelineModel::getPreviousVideoTrackIndex(int trackId) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
389
    while (it != m_allTracks.cbegin()) {
390
        --it;
391 392
        if (!(*it)->isAudioTrack()) {
            return (*it)->getId();
393 394
        }
    }
395
    return 0;
396 397
}

398
int TimelineModel::getPreviousVideoTrackPos(int trackId) const
399 400 401 402
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
403
    while (it != m_allTracks.cbegin()) {
404
        --it;
405 406
        if (!(*it)->isAudioTrack()) {
            return getTrackMltIndex((*it)->getId());
407
        }
408
    }
409
    return 0;
410 411
}

412 413 414 415 416 417 418 419 420 421
int TimelineModel::getMirrorVideoTrackId(int trackId) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
    if (!(*it)->isAudioTrack()) {
        // we expected an audio track...
        return -1;
    }
    int count = 0;
422
    if (it != m_allTracks.cend()) {
423 424
        ++it;
    }
425
    while (it != m_allTracks.cend()) {
426 427 428 429 430 431 432 433 434 435
        if ((*it)->isAudioTrack()) {
            count++;
        } else {
            if (count == 0) {
                return (*it)->getId();
            }
            count--;
        }
        ++it;
    }
436
    if (it != m_allTracks.cend() && !(*it)->isAudioTrack() && count == 0) {
437 438 439 440 441
        return (*it)->getId();
    }
    return -1;
}

442 443 444 445 446 447 448 449
int TimelineModel::getMirrorTrackId(int trackId) const
{
    if (isAudioTrack(trackId)) {
        return getMirrorVideoTrackId(trackId);
    }
    return getMirrorAudioTrackId(trackId);
}

450 451 452 453 454 455 456 457 458 459
int TimelineModel::getMirrorAudioTrackId(int trackId) const
{
    READ_LOCK();
    Q_ASSERT(isTrack(trackId));
    auto it = m_iteratorTable.at(trackId);
    if ((*it)->isAudioTrack()) {
        // we expected a video track...
        return -1;
    }
    int count = 0;
460
    while (it != m_allTracks.cbegin()) {
461
        --it;
462 463 464 465 466 467 468 469 470
        if (!(*it)->isAudioTrack()) {
            count++;
        } else {
            if (count == 0) {
                return (*it)->getId();
            }
            count--;
        }
    }
471
    if ((*it)->isAudioTrack() && count == 0) {
472 473
        return (*it)->getId();
    }
474 475 476
    return -1;
}

477 478 479 480 481 482 483 484 485 486
void TimelineModel::setEditMode(TimelineMode::EditMode mode)
{
    m_editMode = mode;
}

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

487
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
488
{
489 490 491 492
    Q_UNUSED(updateView);
    Q_UNUSED(invalidateTimeline);
    Q_UNUSED(undo);
    Q_UNUSED(redo);
493 494 495 496 497
    Q_ASSERT(isClip(clipId));
    m_allClips[clipId]->setFakePosition(position);
    bool trackChanged = false;
    if (trackId > -1) {
        if (trackId != m_allClips[clipId]->getFakeTrackId()) {
498
            if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
499 500 501 502 503 504 505
                m_allClips[clipId]->setFakeTrackId(trackId);
                trackChanged = true;
            }
        }
    }
    QModelIndex modelIndex = makeClipIndexFromID(clipId);
    if (modelIndex.isValid()) {
506
        QVector<int> roles{FakePositionRole};
507 508 509 510 511 512 513 514 515
        if (trackChanged) {
            roles << FakeTrackIdRole;
        }
        notifyChange(modelIndex, modelIndex, roles);
        return true;
    }
    return false;
}

516
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo)
517
{
518
    // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<<finalMove;
519 520 521
    if (trackId == -1) {
        return false;
    }
522
    Q_ASSERT(isClip(clipId));
523 524 525 526 527 528 529 530
    if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) {
        if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) {
            return false;
        }
        if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) {
            return false;
        }
    } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) {
531
        // Move not allowed (audio / video mismatch)
532
        qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState();
533 534
        return false;
    }
Nicolas Carion's avatar
Nicolas Carion committed
535 536
    std::function<bool(void)> local_undo = []() { return true; };
    std::function<bool(void)> local_redo = []() { return true; };
537
    bool ok = true;
538
    int old_trackId = getClipTrackId(clipId);
539
    bool notifyViewOnly = false;
540
    // qDebug()<<"MOVING CLIP FROM: "<<old_trackId<<" == "<<trackId;
541 542 543
    Fun update_model = []() { return true; };
    if (old_trackId == trackId) {
        // Move on same track, simply inform the view
Nicolas Carion's avatar
Nicolas Carion committed
544
        updateView = false;
545
        notifyViewOnly = true;
546
        update_model = [clipId, this, trackId, invalidateTimeline]() {
547
            QModelIndex modelIndex = makeClipIndexFromID(clipId);
Nicolas Carion's avatar
Nicolas Carion committed
548
            notifyChange(modelIndex, modelIndex, StartRole);
549
            if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
550 551 552
                int in = getClipPosition(clipId);
                emit invalidateZone(in, in + getClipPlaytime(clipId));
            }
553 554 555
            return true;
        };
    }
556
    if (old_trackId != -1) {
557 558 559
        if (notifyViewOnly) {
            PUSH_LAMBDA(update_model, local_undo);
        }
560
        ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo);
561
        if (!ok) {
562 563
            bool undone = local_undo();
            Q_ASSERT(undone);
564 565 566
            return false;
        }
    }
567
    ok = ok & getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo);
568
    if (!ok) {
569
        qDebug() << "-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------";
570 571
        bool undone = local_undo();
        Q_ASSERT(undone);
572
        return false;
573
    }
574 575 576 577
    update_model();
    if (notifyViewOnly) {
        PUSH_LAMBDA(update_model, local_redo);
    }
Nicolas Carion's avatar
Nicolas Carion committed
578 579
    UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    return true;
580 581
}

582 583 584
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
    QWriteLocker locker(&m_lock);
585
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
586 587
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
588
        TRACE_RES(true);
589 590 591 592 593 594 595 596 597 598
        return true;
    }
    if (m_groups->isInGroup(clipId)) {
        // element is in a group.
        int groupId = m_groups->getRootId(clipId);
        int current_trackId = getClipTrackId(clipId);
        int track_pos1 = getTrackPosition(trackId);
        int track_pos2 = getTrackPosition(current_trackId);
        int delta_track = track_pos1 - track_pos2;
        int delta_pos = position - m_allClips[clipId]->getPosition();
599 600 601
        bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
        TRACE_RES(res);
        return res;
602 603 604
    }
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
605
    bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
606 607 608
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
    }
609
    TRACE_RES(res);
610 611 612
    return res;
}

613
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
614
{
615
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
616
    TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline);
617 618
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
Nicolas Carion's avatar
Nicolas Carion committed
619
        TRACE_RES(true);
620 621
        return true;
    }
622
    if (m_groups->isInGroup(clipId)) {
Nicolas Carion's avatar
Nicolas Carion committed
623
        // element is in a group.
624 625 626 627
        int groupId = m_groups->getRootId(clipId);
        int current_trackId = getClipTrackId(clipId);
        int track_pos1 = getTrackPosition(trackId);
        int track_pos2 = getTrackPosition(current_trackId);
628
        int delta_track = track_pos1 - track_pos2;
629 630
        int delta_pos = position - m_allClips[clipId]->getPosition();
        return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
631
    }
Nicolas Carion's avatar
Nicolas Carion committed
632 633
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
634
    bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, logUndo, undo, redo);
635 636
    if (res && logUndo) {
        PUSH_UNDO(undo, redo, i18n("Move clip"));
637
    }
Nicolas Carion's avatar
Nicolas Carion committed
638
    TRACE_RES(res);
639 640 641
    return res;
}

642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position)
{
    QWriteLocker locker(&m_lock);
    Q_ASSERT(m_allClips.count(clipId) > 0);
    if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
        return true;
    }
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    bool res = true;
    if (m_groups->isInGroup(clipId)) {
        // element is in a group.
        int groupId = m_groups->getRootId(clipId);
        int current_trackId = getClipTrackId(clipId);
        int track_pos1 = getTrackPosition(trackId);
        int track_pos2 = getTrackPosition(current_trackId);
        int delta_track = track_pos1 - track_pos2;
        int delta_pos = position - m_allClips[clipId]->getPosition();
660
        res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false);
661
    } else {
662
        res = requestClipMove(clipId, trackId, position, false, false, false, undo, redo);
663 664 665 666 667 668 669
    }
    if (res) {
        undo();
    }
    return res;
}

670
int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
671 672
{
    if (isClip(itemId)) {
673
        return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
674
    }
675
    return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
676 677
}

678
int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance)
679
{
680
    QWriteLocker locker(&m_lock);
681
    TRACE(clipId, trackId, position, cursorPosition, snapDistance);
682 683 684
    Q_ASSERT(isClip(clipId));
    Q_ASSERT(isTrack(trackId));
    int currentPos = getClipPosition(clipId);
685
    int sourceTrackId = getClipTrackId(clipId);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
686
    if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) {
687 688 689
        // Trying move on incompatible track type, stay on same track
        trackId = sourceTrackId;
    }
690
    if (currentPos == position && sourceTrackId == trackId) {
691
        TRACE_RES(position);
692 693
        return position;
    }
694
    bool after = position > currentPos;
695 696 697
    if (snapDistance > 0) {
        // For snapping, we must ignore all in/outs of the clips of the group being moved
        std::vector<int> ignored_pts;
698
        std::unordered_set<int> all_items = {clipId};
699 700
        if (m_groups->isInGroup(clipId)) {
            int groupId = m_groups->getRootId(clipId);
701
            all_items = m_groups->getLeaves(groupId);
702
        }
703
        for (int current_clipId : all_items) {
704
            if (getItemTrackId(current_clipId) != -1) {
705 706 707 708 709
                int in = getItemPosition(current_clipId);
                int out = in + getItemPlaytime(current_clipId);
                ignored_pts.push_back(in);
                ignored_pts.push_back(out);
            }
710 711
        }

712 713
        int snapped = getBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector<int>(),
                                     cursorPosition, snapDistance);
Nicolas Carion's avatar
Nicolas Carion committed
714
        // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped;
715 716 717
        if (snapped >= 0) {
            position = snapped;
        }
718
    }
Nicolas Carion's avatar
Nicolas Carion committed
719
    // we check if move is possible
720 721
    bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false)
                                                           : requestFakeClipMove(clipId, trackId, position, true, false, false);
722
    /*} else {
723
        possible = requestClipMoveAttempt(clipId, trackId, position);
724
    }*/
725
    if (possible) {
726
        TRACE_RES(position);
727 728
        return position;
    }
729 730 731 732 733
    if (sourceTrackId == -1) {
        // not clear what to do hear, if the current move doesn't work. We could try to find empty space, but it might end up being far away...
        TRACE_RES(currentPos);
        return currentPos;
    }
734 735
    // Find best possible move
    if (!m_groups->isInGroup(clipId)) {
736
        // Try same track move
737
        if (trackId != sourceTrackId && sourceTrackId != -1) {
738 739 740 741 742 743
            qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId;
            trackId = sourceTrackId;
            possible = requestClipMove(clipId, trackId, position, true, false, false);
            if (!possible) {
                qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position;
            } else {
744
                TRACE_RES(position);
745 746
                return position;
            }
747 748
        }

749 750 751 752
        int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after);
        qDebug() << "Found blank" << blank_length;
        if (blank_length < INT_MAX) {
            if (after) {
753 754 755 756 757
                position = currentPos + blank_length;
            } else {
                position = currentPos - blank_length;
            }
        } else {
758
            TRACE_RES(currentPos);
759
            return currentPos;
760
        }
761
        possible = requestClipMove(clipId, trackId, position, true, false, false);
762
        TRACE_RES(possible ? position : currentPos);
763 764 765 766
        return possible ? position : currentPos;
    }
    // find best pos for groups
    int groupId = m_groups->getRootId(clipId);
767
    std::unordered_set<int> all_items = m_groups->getLeaves(groupId);
Nicolas Carion's avatar
Nicolas Carion committed
768
    QMap<int, int> trackPosition;
769 770

    // First pass, sort clips by track and keep only the first / last depending on move direction
771
    for (int current_clipId : all_items) {
772
        int clipTrack = getItemTrackId(current_clipId);
773 774 775
        if (clipTrack == -1) {
            continue;
        }
776
        int in = getItemPosition(current_clipId);
777 778 779
        if (trackPosition.contains(clipTrack)) {
            if (after) {
                // keep only last clip position for track
780
                int out = in + getItemPlaytime(current_clipId);
781 782 783 784 785 786 787 788
                if (trackPosition.value(clipTrack) < out) {
                    trackPosition.insert(clipTrack, out);
                }
            } else {
                // keep only first clip position for track
                if (trackPosition.value(clipTrack) > in) {
                    trackPosition.insert(clipTrack, in);
                }
789
            }
Nicolas Carion's avatar
Nicolas Carion committed
790
        } else {
791
            trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in);
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
        }
    }
    // Now check space on each track
    QMapIterator<int, int> i(trackPosition);
    int blank_length = -1;
    while (i.hasNext()) {
        i.next();
        int track_space;
        if (!after) {
            // Check space before the position
            track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1);
            if (blank_length == -1 || blank_length > track_space) {
                blank_length = track_space;
            }
        } else {
807 808
            // Check space after the position
            track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1;
809 810 811 812 813 814 815
            if (blank_length == -1 || blank_length > track_space) {
                blank_length = track_space;
            }
        }
    }
    if (blank_length != 0) {
        int updatedPos = currentPos + (after ? blank_length : -blank_length);
816
        possible = requestClipMove(clipId, trackId, updatedPos, true, false, false);
817
        if (possible) {
818
            TRACE_RES(updatedPos);
819
            return updatedPos;
Nicolas Carion's avatar
Nicolas Carion committed
820
        }
821
    }
822
    TRACE_RES(currentPos);