diff --git a/src/timeline2/model/builders/meltBuilder.cpp b/src/timeline2/model/builders/meltBuilder.cpp
index a230a1511..5b0d987fb 100644
--- a/src/timeline2/model/builders/meltBuilder.cpp
+++ b/src/timeline2/model/builders/meltBuilder.cpp
@@ -1,345 +1,345 @@
/***************************************************************************
* 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 . *
***************************************************************************/
#include "meltBuilder.hpp"
#include "../clipmodel.hpp"
#include "../timelineitemmodel.hpp"
#include "../timelinemodel.hpp"
#include "../trackmodel.hpp"
#include "../undohelper.hpp"
#include "bin/bin.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "kdenlivesettings.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
static QStringList m_errorMessage;
bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track,
const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack, QProgressDialog *progressDialog = nullptr);
bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track,
const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack, QProgressDialog *progressDialog = nullptr);
bool constructTimelineFromMelt(const std::shared_ptr &timeline, Mlt::Tractor tractor, QProgressDialog *progressDialog)
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
// First, we destruct the previous tracks
timeline->requestReset(undo, redo);
m_errorMessage.clear();
std::unordered_map binIdCorresp;
pCore->projectItemModel()->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp, progressDialog);
QSet reserved_names{QLatin1String("playlistmain"), QLatin1String("timeline_preview"), QLatin1String("timeline_overlay"),
QLatin1String("black_track")};
bool ok = true;
qDebug() << "//////////////////////\nTrying to construct" << tractor.count() << "tracks.\n////////////////////////////////";
QList videoTracksIndexes;
// Black track index
videoTracksIndexes << 0;
for (int i = 0; i < tractor.count() && ok; i++) {
std::unique_ptr track(tractor.track(i));
QString playlist_name = track->get("id");
if (reserved_names.contains(playlist_name)) {
continue;
}
switch (track->type()) {
case producer_type:
// TODO check that it is the black track, and otherwise log an error
qDebug() << "SUSPICIOUS: we weren't expecting a producer when parsing the timeline";
break;
case tractor_type: {
// that is a double track
int tid;
bool audioTrack = track->get_int("kdenlive:audio_track") == 1;
if (!audioTrack) {
videoTracksIndexes << i;
}
ok = timeline->requestTrackInsertion(-1, tid, QString(), audioTrack, undo, redo, false);
int lockState = track->get_int("kdenlive:locked_track");
Mlt::Tractor local_tractor(*track);
ok = ok && constructTrackFromMelt(timeline, tid, local_tractor, binIdCorresp, undo, redo, audioTrack, progressDialog);
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), track->get("kdenlive:thumbs_format"));
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), track->get("kdenlive:audio_rec"));
if (lockState > 0) {
timeline->setTrackLockedState(tid, true);
}
break;
}
case playlist_type: {
// that is a single track
qDebug() << "Adding track: " << track->get("id");
int tid;
Mlt::Playlist local_playlist(*track);
const QString trackName = local_playlist.get("kdenlive:track_name");
bool audioTrack = local_playlist.get_int("kdenlive:audio_track") == 1;
if (!audioTrack) {
videoTracksIndexes << i;
}
ok = timeline->requestTrackInsertion(-1, tid, trackName, audioTrack, undo, redo, false);
int muteState = track->get_int("hide");
if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) {
timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState));
}
int lockState = local_playlist.get_int("kdenlive:locked_track");
ok = ok && constructTrackFromMelt(timeline, tid, local_playlist, binIdCorresp, undo, redo, audioTrack, progressDialog);
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), local_playlist.get("kdenlive:thumbs_format"));
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), track->get("kdenlive:audio_rec"));
if (lockState > 0) {
timeline->setTrackLockedState(tid, true);
}
break;
}
default:
qDebug() << "ERROR: Unexpected item in the timeline";
}
}
timeline->_resetView();
// Loading compositions
QScopedPointer service(tractor.producer());
QList compositions;
while ((service != nullptr) && service->is_valid()) {
if (service->type() == transition_type) {
Mlt::Transition t((mlt_transition)service->get_service());
QString id(t.get("kdenlive_id"));
QString internal(t.get("internal_added"));
if (internal.isEmpty()) {
compositions << new Mlt::Transition(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"));
}
}
}
service.reset(service->producer());
}
// Sort compositions and insert
bool compositionOk = true;
while (!compositions.isEmpty()) {
QScopedPointer t(compositions.takeFirst());
QString id(t->get("kdenlive_id"));
int compoId;
int aTrack = t->get_a_track();
if (aTrack > tractor.count()) {
m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3, compositing with track %4.", t->get("id"), t->get_b_track(),
t->get_in(), t->get_a_track());
continue;
}
if (t->get_int("force_track") == 0) {
// This is an automatic composition, check that we composite with lower track or warn
int pos = videoTracksIndexes.indexOf(t->get_b_track());
if (pos > 0 && videoTracksIndexes.at(pos - 1) != aTrack) {
t->set("force_track", 1);
m_errorMessage << i18n("Incorrect composition %1 found on track %2 at %3, compositing with track %4 was set to forced track.", t->get("id"), t->get_b_track(),
t->get_in(), t->get_a_track());
}
}
auto transProps = std::make_unique(t->get_properties());
compositionOk = timeline->requestCompositionInsertion(id, timeline->getTrackIndexFromPosition(t->get_b_track() - 1), t->get_a_track(), t->get_in(), t->get_length(), std::move(transProps), compoId, undo, redo);
if (!compositionOk) {
qDebug() << "ERROR : failed to insert composition in track " << t->get_b_track() << ", position" << t->get_in() << ", ID: " << id
<< ", MLT ID: " << t->get("id");
// timeline->requestItemDeletion(compoId, false);
m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3.", t->get("id"), t->get_b_track(), t->get_in());
continue;
}
qDebug() << "Inserted composition in track " << t->get_b_track() << ", position" << t->get_in() << "/" << t->get_out();
}
// build internal track compositing
timeline->buildTrackCompositing();
//timeline->updateDuration();
if (!ok) {
// TODO log error
// Don't abort loading because of failed composition
undo();
return false;
}
if (!m_errorMessage.isEmpty()) {
KMessageBox::sorry(qApp->activeWindow(), m_errorMessage.join("\n"), i18n("Problems found in your project file"));
}
return true;
}
bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track,
const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack, QProgressDialog *progressDialog)
{
if (track.count() != 2) {
// we expect a tractor with two tracks (a "fake" track)
qDebug() << "ERROR : wrong number of subtracks";
return false;
}
for (int i = 0; i < track.count(); i++) {
std::unique_ptr sub_track(track.track(i));
if (sub_track->type() != playlist_type) {
qDebug() << "ERROR : SubTracks must be MLT::Playlist";
return false;
}
Mlt::Playlist playlist(*sub_track);
constructTrackFromMelt(timeline, tid, playlist, binIdCorresp, undo, redo, audioTrack, progressDialog);
if (i == 0) {
// Pass track properties
int height = track.get_int("kdenlive:trackheight");
timeline->setTrackProperty(tid, "kdenlive:trackheight", height == 0 ? "100" : QString::number(height));
timeline->setTrackProperty(tid, "kdenlive:collapsed", QString::number(track.get_int("kdenlive:collapsed")));
QString trackName = track.get("kdenlive:track_name");
if (!trackName.isEmpty()) {
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:track_name"), trackName.toUtf8().constData());
}
if (audioTrack) {
// This is an audio track
timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_track"), QStringLiteral("1"));
timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("1"));
} else {
// video track, hide audio
timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("2"));
}
int muteState = playlist.get_int("hide");
if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) {
timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState));
}
}
}
std::shared_ptr serv = std::make_shared(track.get_service());
timeline->importTrackEffects(tid, serv);
return true;
}
namespace {
// This function tries to recover the state of the producer (audio or video or both)
PlaylistState::ClipState inferState(const std::shared_ptr &prod, bool audioTrack)
{
auto getProperty = [prod](const QString &name) {
if (prod->parent().is_valid()) {
return QString::fromUtf8(prod->parent().get(name.toUtf8().constData()));
}
return QString::fromUtf8(prod->get(name.toUtf8().constData()));
};
auto getIntProperty = [prod](const QString &name) {
if (prod->parent().is_valid()) {
return prod->parent().get_int(name.toUtf8().constData());
}
return prod->get_int(name.toUtf8().constData());
};
QString service = getProperty("mlt_service");
std::pair VidAud{true, true};
VidAud.first = getIntProperty("set.test_image") == 0;
VidAud.second = getIntProperty("set.test_audio") == 0;
if (audioTrack || ((service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("video_index")) == -1))) {
VidAud.first = false;
}
if (!audioTrack || ((service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("audio_index")) == -1))) {
VidAud.second = false;
}
return stateFromBool(VidAud);
}
} // namespace
bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track,
const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack, QProgressDialog *progressDialog)
{
for (int i = 0; i < track.count(); i++) {
if (track.is_blank(i)) {
continue;
}
if (progressDialog) {
progressDialog->setValue(progressDialog->value() + 1);
}
std::shared_ptr clip(track.get_clip(i));
int position = track.clip_start(i);
switch (clip->type()) {
case unknown_type:
case producer_type: {
qDebug() << "Looking for clip clip " << clip->parent().get("kdenlive:id") << " = " << clip->parent().get("kdenlive:clipname");
QString binId;
if (clip->parent().get_int("_kdenlive_processed") == 1) {
// This is a bin clip, already processed no need to change id
binId = QString(clip->parent().get("kdenlive:id"));
} else {
QString clipId = clip->parent().get("kdenlive:id");
if (clipId.startsWith(QStringLiteral("slowmotion"))) {
clipId = clipId.section(QLatin1Char(':'), 1, 1);
}
if (clipId.isEmpty()) {
clipId = clip->get("kdenlive:id");
}
if (binIdCorresp.count(clipId) == 0) {
// Project was somehow corrupted
qDebug()<<"=== WARNING, CANNOT FIND CLIP WITH ID: "<projectItemModel()->getClipByUrl(QFileInfo(clip->parent().get("resource")));
if (!fixedId.isEmpty()) {
binId = fixedId.first();
m_errorMessage << i18n("Invalid clip %1 (%2) not found in project bin, recovered.", clip->parent().get("id"), clipId);
} else {
m_errorMessage << i18n("Project corrupted. Clip %1 (%2) not found in project bin.", clip->parent().get("id"), clipId);
}
} else {
binId = binIdCorresp.at(clipId);
}
Q_ASSERT(!clipId.isEmpty() && !binId.isEmpty());
clip->parent().set("kdenlive:id", binId.toUtf8().constData());
clip->parent().set("_kdenlive_processed", 1);
}
bool ok = false;
int cid = -1;
if (pCore->bin()->getBinClip(binId)) {
PlaylistState::ClipState st = inferState(clip, audioTrack);
cid = ClipModel::construct(timeline, binId, clip, st);
- ok = timeline->requestClipMove(cid, tid, position, true, false, true, undo, redo);
+ ok = timeline->requestClipMove(cid, tid, position, true, true, false, true, undo, redo);
} else {
qDebug() << "// Cannot find bin clip: " << binId << " - " << clip->get("id");
}
if (!ok && cid > -1) {
qDebug() << "ERROR : failed to insert clip in track" << tid << "position" << position;
timeline->requestItemDeletion(cid, false);
m_errorMessage << i18n("Invalid clip %1 found on track %2 at %3.", clip->parent().get("id"), track.get("id"), position);
break;
}
qDebug() << "Inserted clip in track" << tid << "at " << position;
break;
}
case tractor_type: {
// TODO This is a nested timeline
qDebug() << "NOT_IMPLEMENTED: code for parsing nested timeline is not there yet.";
break;
}
default:
qDebug() << "ERROR : unexpected object found on playlist";
return false;
break;
}
}
std::shared_ptr serv = std::make_shared(track.get_service());
timeline->importTrackEffects(tid, serv);
return true;
}
diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp
index bf91e5cb1..0875fa7b6 100644
--- a/src/timeline2/model/timelinefunctions.cpp
+++ b/src/timeline2/model/timelinefunctions.cpp
@@ -1,1478 +1,1478 @@
/*
Copyright (C) 2017 Jean-Baptiste Mardelle
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 .
*/
#include "timelinefunctions.hpp"
#include "bin/bin.h"
#include "bin/projectclip.h"
#include "bin/projectfolder.h"
#include "bin/projectitemmodel.h"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/kdenlivedoc.h"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "groupsmodel.hpp"
#include "logger.hpp"
#include "timelineitemmodel.hpp"
#include "trackmodel.hpp"
#include "transitions/transitionsrepository.hpp"
#include
#include
#include
#include
#include
#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
#pragma GCC diagnostic pop
RTTR_REGISTRATION
{
using namespace rttr;
registration::class_("TimelineFunctions")
.method("requestClipCut", select_overload, int, int)>(&TimelineFunctions::requestClipCut))(
parameter_names("timeline", "clipId", "position"));
}
bool TimelineFunctions::cloneClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo,
Fun &redo)
{
// Special case: slowmotion clips
double clipSpeed = timeline->m_allClips[clipId]->getSpeed();
bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, clipSpeed, undo, redo);
timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize;
// copy useful timeline properties
timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]);
int duration = timeline->getClipPlaytime(clipId);
int init_duration = timeline->getClipPlaytime(newId);
if (duration != init_duration) {
int in = timeline->m_allClips[clipId]->getIn();
res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo);
res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo);
}
if (!res) {
return false;
}
std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId);
std::shared_ptr destStack = timeline->getClipEffectStackModel(newId);
destStack->importEffects(sourceStack, state);
return res;
}
bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr &timeline, const QStringList &binIds, int trackId, int position,
QList &clipIds, bool logUndo, bool refreshView)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
for (const QString &binId : binIds) {
int clipId;
if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) {
clipIds.append(clipId);
position += timeline->getItemPlaytime(clipId);
} else {
undo();
clipIds.clear();
return false;
}
}
if (logUndo) {
pCore->pushUndo(undo, redo, i18n("Insert Clips"));
}
return true;
}
bool TimelineFunctions::processClipCut(const std::shared_ptr &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo)
{
int trackId = timeline->getClipTrackId(clipId);
int trackDuration = timeline->getTrackById_const(trackId)->trackDuration();
int start = timeline->getClipPosition(clipId);
int duration = timeline->getClipPlaytime(clipId);
if (start > position || (start + duration) < position) {
return false;
}
PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState();
bool res = cloneClip(timeline, clipId, newId, state, undo, redo);
timeline->m_blockRefresh = true;
res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo);
int newDuration = timeline->getClipPlaytime(clipId);
// parse effects
std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId);
sourceStack->cleanFadeEffects(true, undo, redo);
std::shared_ptr destStack = timeline->getClipEffectStackModel(newId);
destStack->cleanFadeEffects(false, undo, redo);
res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo);
// 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();
- res = res && timeline->requestClipMove(newId, trackId, position, true, false, true, undo, redo);
+ res = res && timeline->requestClipMove(newId, trackId, position, true, true, false, true, undo, redo);
if (durationChanged) {
// Track length changed, check project duration
Fun updateDuration = [timeline]() {
timeline->updateDuration();
return true;
};
updateDuration();
PUSH_LAMBDA(updateDuration, redo);
}
timeline->m_blockRefresh = false;
return res;
}
bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
TRACE_STATIC(timeline, clipId, position);
bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo);
if (result) {
pCore->pushUndo(undo, redo, i18n("Cut clip"));
}
TRACE_RES(result);
return result;
}
bool TimelineFunctions::requestClipCut(const std::shared_ptr &timeline, int clipId, int position, Fun &undo, Fun &redo)
{
const std::unordered_set clipselect = timeline->getGroupElements(clipId);
// Remove locked items
std::unordered_set clips;
for (int cid : clipselect) {
if (!timeline->isClip(cid)) {
continue;
}
int tk = timeline->getClipTrackId(cid);
if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) {
clips.insert(cid);
}
}
// 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();
std::unordered_set topElements;
std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
int count = 0;
QList newIds;
int mainId = -1;
QList clipsToCut;
for (int cid : clips) {
if (!timeline->isClip(cid)) {
continue;
}
int start = timeline->getClipPosition(cid);
int duration = timeline->getClipPlaytime(cid);
if (start < position && (start + duration) > position) {
clipsToCut << cid;
}
}
if (clipsToCut.isEmpty()) {
return true;
}
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;
}
if (cid == clipId) {
mainId = newId;
}
// splitted elements go temporarily in the same group as original ones.
timeline->m_groups->setInGroupOf(newId, cid, undo, redo);
newIds << newId;
}
if (count > 0 && timeline->m_groups->isInGroup(clipId)) {
// we now split the group hierarchy.
// As a splitting criterion, we compare start point with split position
auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; };
bool res = true;
for (const int topId : topElements) {
qDebug()<<"// CHECKING REGROUP ELMENT: "<isClip(topId)<isGroup(topId);
res = res && timeline->m_groups->split(topId, criterion, undo, redo);
}
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
}
return count > 0;
}
int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr &timeline, int trackId, int position)
{
std::unordered_set clips = timeline->getItemsInRange(trackId, position, -1);
if (!clips.empty()) {
timeline->requestSetSelection(clips);
return (*clips.cbegin());
}
return -1;
}
bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr &timeline, int itemId, int startPosition, int endPosition)
{
// Move group back to original position
int track = timeline->getItemTrackId(itemId);
bool isClip = timeline->isClip(itemId);
if (isClip) {
- timeline->requestClipMove(itemId, track, startPosition, false, false);
+ timeline->requestClipMove(itemId, track, startPosition, true, false, false);
} else {
timeline->requestCompositionMove(itemId, track, startPosition, false, false);
}
std::unordered_set clips = timeline->getGroupElements(itemId);
// Start undoable command
std::function undo = []() { return true; };
std::function redo = []() { return true; };
//int res = timeline->requestClipsGroup(clips, undo, redo, GroupType::Selection);
int res = timeline->m_groups->getRootId(itemId);
bool final = false;
if (res > -1 || clips.size() == 1) {
if (clips.size() > 1) {
final = timeline->requestGroupMove(itemId, res, 0, endPosition - startPosition, true, true, undo, redo);
} else {
// only 1 clip to be moved
if (isClip) {
- final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, undo, redo);
+ final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, true, undo, redo);
} else {
final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo);
}
}
}
timeline->requestClearSelection();
if (final) {
if (startPosition < endPosition) {
pCore->pushUndo(undo, redo, i18n("Insert space"));
} else {
pCore->pushUndo(undo, redo, i18n("Remove space"));
}
return true;
}
return false;
}
bool TimelineFunctions::breakAffectedGroups(const std::shared_ptr &timeline, QVector 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 affectedItems;
// First find all affected items
for (int &trackId : tracks) {
std::unordered_set 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 all_children = timeline->m_groups->getLeaves(groupId);
for (int child: all_children) {
int childTrackId = timeline->getItemTrackId(child);
if (!tracks.contains(childTrackId)) {
// This item should not be affected by the operation, ungroup it
result = result && timeline->requestClipUngroup(child, undo, redo);
}
}
}
}
return result;
}
bool TimelineFunctions::extractZone(const std::shared_ptr &timeline, QVector tracks, QPoint zone, bool liftOnly)
{
// Start undoable command
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool result = true;
result = breakAffectedGroups(timeline, tracks, zone, undo, redo);
for (int &trackId : tracks) {
if (timeline->getTrackById_const(trackId)->isLocked()) {
continue;
}
result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo);
}
if (result && !liftOnly) {
result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo);
}
pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone"));
return result;
}
bool TimelineFunctions::insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone,
bool overwrite, bool useTargets)
{
// Start undoable command
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool result = true;
QVector affectedTracks;
auto it = timeline->m_allTracks.cbegin();
while (it != timeline->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (!trackIds.contains(target_track) && !timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
++it;
continue;
}
affectedTracks << target_track;
++it;
}
result = breakAffectedGroups(timeline, affectedTracks, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
if (overwrite) {
// Cut all tracks
for (int target_track : affectedTracks) {
result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
if (!result) {
qDebug() << "// LIFTING ZONE FAILED\n";
break;
}
}
} else {
// Cut all tracks
for (int target_track : affectedTracks) {
int startClipId = timeline->getClipByPosition(target_track, insertFrame);
if (startClipId > -1) {
// There is a clip, cut it
result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo);
}
}
result = result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
}
if (result) {
if (!trackIds.isEmpty()) {
int newId = -1;
QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1);
result = timeline->requestClipInsertion(binClipId, trackIds.first(), insertFrame, newId, true, true, useTargets, undo, redo);
}
if (result) {
pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
}
}
if (!result) {
qDebug() << "// REQUESTING SPACE FAILED";
undo();
}
return result;
}
bool TimelineFunctions::liftZone(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
{
// Check if there is a clip at start point
int startClipId = timeline->getClipByPosition(trackId, zone.x());
if (startClipId > -1) {
// There is a clip, cut it
if (timeline->getClipPosition(startClipId) < zone.x()) {
qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId;
TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo);
qDebug() << "/// CUTTING AT START DONE";
}
}
int endClipId = timeline->getClipByPosition(trackId, zone.y());
if (endClipId > -1) {
// There is a clip, cut it
if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) {
qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId;
TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo);
qDebug() << "/// CUTTING AT END DONE";
}
}
std::unordered_set clips = timeline->getItemsInRange(trackId, zone.x(), zone.y());
for (const auto &clipId : clips) {
timeline->requestItemDeletion(clipId, undo, redo);
}
return true;
}
bool TimelineFunctions::removeSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
{
Q_UNUSED(trackId)
std::unordered_set clips;
auto it = timeline->m_allTracks.cbegin();
while (it != timeline->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (timeline->m_videoTarget == target_track || timeline->m_audioTarget == target_track ||
timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
std::unordered_set subs = timeline->getItemsInRange(target_track, zone.y() - 1, -1, true);
clips.insert(subs.begin(), subs.end());
}
++it;
}
bool result = false;
if (!clips.empty()) {
int clipId = *clips.begin();
if (clips.size() > 1) {
int clipsGroup = timeline->m_groups->getRootId(clipId);
int res = timeline->requestClipsGroup(clips, undo, redo);
if (res > -1) {
result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo);
if (result && res != clipsGroup) {
// Only ungroup if a group was created
result = timeline->requestClipUngroup(clipId, undo, redo);
}
if (!result) {
undo();
}
}
} else {
// only 1 clip to be moved
int clipStart = timeline->getItemPosition(clipId);
- result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, true, undo, redo);
+ result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, true, true, undo, redo);
}
}
return result;
}
bool TimelineFunctions::requestInsertSpace(const std::shared_ptr &timeline, QPoint zone, Fun &undo, Fun &redo, bool followTargets)
{
timeline->requestClearSelection();
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
std::unordered_set items;
if (!followTargets) {
// Select clips in all tracks
items = timeline->getItemsInRange(-1, zone.x(), -1, true);
} else {
// Select clips in target and active tracks only
auto it = timeline->m_allTracks.cbegin();
while (it != timeline->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (timeline->m_videoTarget == target_track || timeline->m_audioTarget == target_track ||
timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
std::unordered_set subs = timeline->getItemsInRange(target_track, zone.x(), -1, true);
items.insert(subs.begin(), subs.end());
}
++it;
}
}
if (items.empty()) {
return true;
}
timeline->requestSetSelection(items);
bool result = true;
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, true, local_undo, local_redo);
+ result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, 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);
}
UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
return result;
}
bool TimelineFunctions::requestItemCopy(const std::shared_ptr &timeline, int clipId, int trackId, int position)
{
Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId));
Fun undo = []() { return true; };
Fun redo = []() { return true; };
int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId));
int deltaPos = position - timeline->getItemPosition(clipId);
std::unordered_set allIds = timeline->getGroupElements(clipId);
std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips
bool res = true;
for (int id : allIds) {
int newId = -1;
if (timeline->isClip(id)) {
PlaylistState::ClipState state = timeline->m_allClips[id]->clipState();
res = cloneClip(timeline, id, newId, state, undo, redo);
res = res && (newId != -1);
}
int target_position = timeline->getItemPosition(id) + deltaPos;
int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack;
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();
if (timeline->isClip(id)) {
- res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, undo, redo);
+ res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, true, undo, redo);
} else {
const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
std::unique_ptr transProps(timeline->m_allCompositions[id]->properties());
res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo);
}
} else {
res = false;
}
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
mapping[id] = newId;
}
qDebug() << "Successful copy, coping groups...";
res = timeline->m_groups->copyGroups(mapping, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
return true;
}
void TimelineFunctions::showClipKeyframes(const std::shared_ptr &timeline, int clipId, bool value)
{
timeline->m_allClips[clipId]->setShowKeyframes(value);
QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
}
void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr &timeline, int compoId, bool value)
{
timeline->m_allCompositions[compoId]->setShowKeyframes(value);
QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
}
bool TimelineFunctions::switchEnableState(const std::shared_ptr &timeline, int clipId)
{
PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState();
PlaylistState::ClipState state = PlaylistState::Disabled;
bool disable = true;
if (oldState == PlaylistState::Disabled) {
state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType();
disable = false;
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = changeClipState(timeline, clipId, state, undo, redo);
if (result) {
pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip"));
}
return result;
}
bool TimelineFunctions::changeClipState(const std::shared_ptr &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo)
{
int track = timeline->getClipTrackId(clipId);
int start = -1;
int end = -1;
bool invalidate = false;
if (track > -1) {
if (!timeline->getTrackById_const(track)->isAudioTrack()) {
invalidate = true;
}
start = timeline->getItemPosition(clipId);
end = start + timeline->getItemPlaytime(clipId);
}
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
// For the state change to work, we need to unplant/replant the clip
bool result = true;
if (track > -1) {
result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo, false, false);
}
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);
}
UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
return result;
}
bool TimelineFunctions::requestSplitAudio(const std::shared_ptr &timeline, int clipId, int audioTarget)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
const std::unordered_set clips = timeline->getGroupElements(clipId);
bool done = false;
// Now clear selection so we don't mess with groups
timeline->requestClearSelection(false, undo, redo);
for (int cid : clips) {
if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) {
// clip without audio or audio only, skip
pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage);
return false;
}
int position = timeline->getClipPosition(cid);
int track = timeline->getClipTrackId(cid);
QList possibleTracks = audioTarget >= 0 ? QList() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack);
if (possibleTracks.isEmpty()) {
// No available audio track for splitting, abort
undo();
pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage);
return false;
}
int newId;
bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
return false;
}
bool success = false;
while (!success && !possibleTracks.isEmpty()) {
int newTrack = possibleTracks.takeFirst();
- success = timeline->requestClipMove(newId, newTrack, position, true, false, true, undo, redo);
+ success = timeline->requestClipMove(newId, newTrack, position, true, true, false, true, undo, redo);
}
TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo);
success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo);
if (!success) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
return false;
}
done = true;
}
if (done) {
timeline->requestSetSelection(clips, undo, redo);
pCore->pushUndo(undo, redo, i18n("Split Audio"));
}
return done;
}
bool TimelineFunctions::requestSplitVideo(const std::shared_ptr &timeline, int clipId, int videoTarget)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
const std::unordered_set clips = timeline->getGroupElements(clipId);
bool done = false;
// Now clear selection so we don't mess with groups
timeline->requestClearSelection();
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 possibleTracks = QList() << 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;
bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo);
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();
- success = timeline->requestClipMove(newId, newTrack, position, true, true, true, undo, redo);
+ success = timeline->requestClipMove(newId, newTrack, position, true, true, true, true, undo, redo);
}
TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo);
success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo);
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;
}
void TimelineFunctions::setCompositionATrack(const std::shared_ptr &timeline, int cid, int aTrack)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
std::shared_ptr compo = timeline->getCompositionPtr(cid);
int previousATrack = compo->getATrack();
int previousAutoTrack = static_cast(compo->getForcedTrack() == -1);
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);
Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() {
timeline->unplantComposition(cid);
QScopedPointer 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();
timeline->replantCompositions(cid, true);
timeline->invalidateZone(start, end);
timeline->checkRefresh(start, end);
return true;
};
Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() {
timeline->unplantComposition(cid);
QScopedPointer field(timeline->m_tractor->field());
field->lock();
timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0);
timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1));
field->unlock();
timeline->replantCompositions(cid, true);
timeline->invalidateZone(start, end);
timeline->checkRefresh(start, end);
return true;
};
if (local_redo()) {
PUSH_LAMBDA(local_undo, undo);
PUSH_LAMBDA(local_redo, redo);
}
pCore->pushUndo(undo, redo, i18n("Change Composition Track"));
}
void TimelineFunctions::enableMultitrackView(const std::shared_ptr &timeline, bool enable)
{
QList videoTracks;
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 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) {
case 0:
switch (videoTracks.size()) {
case 2:
geometry = QStringLiteral("0 0 50% 100%");
break;
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%");
break;
}
break;
case 1:
switch (videoTracks.size()) {
case 2:
geometry = QStringLiteral("50% 0 50% 100%");
break;
case 3:
geometry = QStringLiteral("33% 0 33% 100%");
break;
case 4:
geometry = QStringLiteral("50% 0 50% 50%");
break;
case 5:
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%");
break;
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:
case 6:
geometry = QStringLiteral("33% 50% 33% 50%");
break;
default:
geometry = QStringLiteral("33% 33% 33% 33%");
break;
}
break;
case 5:
switch (videoTracks.size()) {
case 6:
geometry = QStringLiteral("66% 50% 33% 50%");
break;
default:
geometry = QStringLiteral("66% 33% 33% 33%");
break;
}
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;
}
// 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();
}
void TimelineFunctions::saveTimelineSelection(const std::shared_ptr &timeline, const std::unordered_set &selection,
const QDir &targetDir)
{
bool ok;
QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal,
QString(), &ok);
if (name.isEmpty() || !ok) {
return;
}
if (targetDir.exists(name + QStringLiteral(".mlt"))) {
// TODO: warn and ask for overwrite / rename
}
int offset = -1;
int lowerAudioTrack = -1;
int lowerVideoTrack = -1;
QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt"));
// Build a copy of selected tracks.
QMap sourceTracks;
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());
QScopedPointer field(newTractor.field());
int ix = 0;
QString composite = TransitionsRepository::get()->getCompositingTransition();
QMapIterator i(sourceTracks);
QList compositions;
while (i.hasNext()) {
i.next();
QScopedPointer newTrackPlaylist(new Mlt::Playlist(*newTractor.profile()));
newTractor.set_track(*newTrackPlaylist, ix);
// QScopedPointer trackProducer(newTractor.track(ix));
int trackId = i.value();
sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix);
std::shared_ptr track = timeline->getTrackById_const(trackId);
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;
}
}
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 clip = track->getClipProducer(target_clip);
newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1);
} else if (timeline->isComposition(itemId)) {
// Composition
auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get());
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"));
}
}
}
}
}
ix++;
}
// 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 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;
while (i.hasNext()) {
i.next();
int trackId = i.value();
std::shared_ptr track = timeline->getTrackById_const(trackId);
bool isAudio = track->isAudioTrack();
if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) {
// add track compositing / mix
Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData());
if (isAudio) {
t.set("sum", 1);
}
t.set("always_active", 1);
t.set("internal_added", 237);
field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix);
}
ix++;
}
Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData());
xmlConsumer.set("terminate_on_pause", 1);
xmlConsumer.connect(newTractor);
xmlConsumer.run();
}
int TimelineFunctions::getTrackOffset(const std::shared_ptr &timeline, int startTrack, int destTrack)
{
qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack;
int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
int destTrackMltIndex = timeline->getTrackMltIndex(destTrack);
int offset = 0;
qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex;
if (masterTrackMltIndex == destTrackMltIndex) {
return offset;
}
int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1;
bool isAudio = timeline->isAudioTrack(startTrack);
int track = masterTrackMltIndex;
while (track != destTrackMltIndex) {
track += step;
qDebug() << "+ + +TESTING TRACK: " << track;
int trackId = timeline->getTrackIndexFromPosition(track - 1);
if (isAudio == timeline->isAudioTrack(trackId)) {
offset += step;
}
}
return offset;
}
int TimelineFunctions::getOffsetTrackId(const std::shared_ptr &timeline, int startTrack, int offset, bool audioOffset)
{
int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
bool isAudio = timeline->isAudioTrack(startTrack);
if (isAudio != audioOffset) {
offset = -offset;
}
qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset;
while (offset != 0) {
masterTrackMltIndex += offset > 0 ? 1 : -1;
qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex;
if (masterTrackMltIndex < 0) {
masterTrackMltIndex = 0;
break;
}
if (masterTrackMltIndex > (int)timeline->m_allTracks.size()) {
masterTrackMltIndex = (int)timeline->m_allTracks.size();
break;
}
int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
if (timeline->isAudioTrack(trackId) == isAudio) {
offset += offset > 0 ? -1 : 1;
}
}
return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
}
QPair, QList> TimelineFunctions::getAVTracksIds(const std::shared_ptr &timeline)
{
QList audioTracks;
QList videoTracks;
for (const auto &track : timeline->m_allTracks) {
if (track->isAudioTrack()) {
audioTracks << track->getId();
} else {
videoTracks << track->getId();
}
}
return {audioTracks, videoTracks};
}
QString TimelineFunctions::copyClips(const std::shared_ptr &timeline, const std::unordered_set &itemIds)
{
int clipId = *(itemIds.begin());
// We need to retrieve ALL the involved clips, ie those who are also grouped with the given clips
std::unordered_set allIds;
for (const auto &itemId : itemIds) {
std::unordered_set siblings = timeline->getGroupElements(itemId);
allIds.insert(siblings.begin(), siblings.end());
}
timeline->requestClearSelection();
// TODO better guess for master track
int masterTid = timeline->getItemTrackId(clipId);
bool audioCopy = timeline->isAudioTrack(masterTid);
int masterTrack = timeline->getTrackPosition(masterTid);
QDomDocument copiedItems;
int offset = -1;
QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene"));
copiedItems.appendChild(container);
QStringList binIds;
for (int id : allIds) {
if (offset == -1 || timeline->getItemPosition(id) < offset) {
offset = timeline->getItemPosition(id);
}
if (timeline->isClip(id)) {
container.appendChild(timeline->m_allClips[id]->toXml(copiedItems));
const QString bid = timeline->m_allClips[id]->binId();
if (!binIds.contains(bid)) {
binIds << bid;
}
} else if (timeline->isComposition(id)) {
container.appendChild(timeline->m_allCompositions[id]->toXml(copiedItems));
} else {
Q_ASSERT(false);
}
}
QDomElement container2 = copiedItems.createElement(QStringLiteral("bin"));
container.appendChild(container2);
for (const QString &id : binIds) {
std::shared_ptr clip = pCore->projectItemModel()->getClipByBinID(id);
QDomDocument tmp;
container2.appendChild(clip->toXml(tmp));
}
container.setAttribute(QStringLiteral("offset"), offset);
if (audioCopy) {
container.setAttribute(QStringLiteral("masterAudioTrack"), masterTrack);
int masterMirror = timeline->getMirrorVideoTrackId(masterTid);
if (masterMirror == -1) {
QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline);
if (!projectTracks.second.isEmpty()) {
masterTrack = timeline->getTrackPosition(projectTracks.second.first());
}
} else {
masterTrack = timeline->getTrackPosition(masterMirror);
}
}
/* masterTrack contains the reference track over which we want to paste.
this is a video track, unless audioCopy is defined */
container.setAttribute(QStringLiteral("masterTrack"), masterTrack);
container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid")));
QDomElement grp = copiedItems.createElement(QStringLiteral("groups"));
container.appendChild(grp);
std::unordered_set groupRoots;
std::transform(allIds.begin(), allIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
qDebug() << "==============\n GROUP ROOTS: ";
for (int gp : groupRoots) {
qDebug() << "GROUP: " << gp;
}
qDebug() << "\n=======";
grp.appendChild(copiedItems.createTextNode(timeline->m_groups->toJson(groupRoots)));
qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------";
return copiedItems.toString();
}
bool TimelineFunctions::pasteClips(const std::shared_ptr &timeline, const QString &pasteString, int trackId, int position)
{
timeline->requestClearSelection();
QDomDocument copiedItems;
copiedItems.setContent(pasteString);
if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) {
qDebug() << " / / READING CLIPS FROM CLIPBOARD";
} else {
return false;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid"));
QMap mappedIds;
// Check available tracks
QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline);
int masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack")).toInt();
QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition"));
// find paste tracks
// List of all source audio tracks
QList audioTracks;
// List of all source video tracks
QList videoTracks;
// List of all audio tracks with their corresponding video mirror
std::unordered_map audioMirrors;
// List of all source audio tracks that don't have video mirror
QList singleAudioTracks;
for (int i = 0; i < clips.count(); i++) {
QDomElement prod = clips.at(i).toElement();
int trackPos = prod.attribute(QStringLiteral("track")).toInt();
bool audioTrack = prod.hasAttribute(QStringLiteral("audioTrack"));
if (audioTrack) {
if (!audioTracks.contains(trackPos)) {
audioTracks << trackPos;
}
int videoMirror = prod.attribute(QStringLiteral("mirrorTrack")).toInt();
if (videoMirror == -1) {
if (singleAudioTracks.contains(trackPos)) {
continue;
}
singleAudioTracks << trackPos;
continue;
}
audioMirrors[trackPos] = videoMirror;
if (videoTracks.contains(videoMirror)) {
continue;
}
videoTracks << videoMirror;
} else {
if (videoTracks.contains(trackPos)) {
continue;
}
videoTracks << trackPos;
}
}
for (int i = 0; i < compositions.count(); i++) {
QDomElement prod = compositions.at(i).toElement();
int trackPos = prod.attribute(QStringLiteral("track")).toInt();
if (!videoTracks.contains(trackPos)) {
videoTracks << trackPos;
}
int atrackPos = prod.attribute(QStringLiteral("a_track")).toInt();
if (atrackPos == 0 || videoTracks.contains(atrackPos)) {
continue;
}
videoTracks << atrackPos;
}
// Now we have a list of all source tracks, check that we have enough target tracks
std::sort(videoTracks.begin(), videoTracks.end());
std::sort(audioTracks.begin(), audioTracks.end());
std::sort(singleAudioTracks.begin(), singleAudioTracks.end());
//qDebug()<<"== GOT WANTED TKS\n VIDEO: "< projectTracks.second.size() || requestedAudioTracks > projectTracks.first.size()) {
pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500);
return false;
}
// Find destination master track
// Check we have enough tracks above/below
if (requestedVideoTracks > 0) {
qDebug() << "MASTERSTK: " << masterSourceTrack << ", VTKS: " << videoTracks;
int tracksBelow = masterSourceTrack - videoTracks.first();
int tracksAbove = videoTracks.last() - masterSourceTrack;
qDebug() << "// RQST TKS BELOW: " << tracksBelow << " / ABOVE: " << tracksAbove;
qDebug() << "// EXISTING TKS BELOW: " << projectTracks.second.indexOf(trackId) << ", IX: " << trackId;
qDebug() << "// EXISTING TKS ABOVE: " << projectTracks.second.size() << " - " << projectTracks.second.indexOf(trackId);
if (projectTracks.second.indexOf(trackId) < tracksBelow) {
qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow;
// not enough tracks below, try to paste on upper track
trackId = projectTracks.second.at(tracksBelow);
} else if ((projectTracks.second.size() - (projectTracks.second.indexOf(trackId) + 1)) < tracksAbove) {
// not enough tracks above, try to paste on lower track
qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.second.size() - tracksAbove);
trackId = projectTracks.second.at(projectTracks.second.size() - tracksAbove - 1);
}
} else {
// Audio only
masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterAudioTrack")).toInt();
int tracksBelow = masterSourceTrack - audioTracks.first();
int tracksAbove = audioTracks.last() - masterSourceTrack;
if (projectTracks.first.indexOf(trackId) < tracksBelow) {
qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow;
// not enough tracks below, try to paste on upper track
trackId = projectTracks.first.at(tracksBelow);
} else if ((projectTracks.first.size() - (projectTracks.first.indexOf(trackId) + 1)) < tracksAbove) {
// not enough tracks above, try to paste on lower track
qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.first.size() - tracksAbove);
trackId = projectTracks.first.at(projectTracks.first.size() - tracksAbove - 1);
}
}
QMap tracksMap;
bool audioMaster = false;
int masterIx = projectTracks.second.indexOf(trackId);
if (masterIx == -1) {
masterIx = projectTracks.first.indexOf(trackId);
audioMaster = true;
}
qDebug() << "/// PROJECT VIDEO TKS: " << projectTracks.second << ", MASTER: " << trackId;
qDebug() << "/// PASTE VIDEO TKS: " << videoTracks << " / MASTER: " << masterSourceTrack;
qDebug() << "/// MASTER PASTE: " << masterIx;
for (int tk : videoTracks) {
int newPos = masterIx + tk - masterSourceTrack;
if (newPos < 0 || newPos >= projectTracks.second.size()) {
pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500);
}
tracksMap.insert(tk, projectTracks.second.at(newPos));
}
bool audioOffsetCalculated = false;
int audioOffset = 0;
for (const auto &mirror : audioMirrors) {
int videoIx = tracksMap.value(mirror.second);
tracksMap.insert(mirror.first, timeline->getMirrorAudioTrackId(videoIx));
if (!audioOffsetCalculated) {
int oldPosition = mirror.first;
int currentPosition = timeline->getTrackPosition(tracksMap.value(oldPosition));
audioOffset = currentPosition - oldPosition;
audioOffsetCalculated = true;
}
}
if (!audioOffsetCalculated && audioMaster) {
audioOffset = masterIx - masterSourceTrack;
audioOffsetCalculated = true;
}
for (int i = 0; i < singleAudioTracks.size(); i++) {
int oldPos = singleAudioTracks.at(i);
if (tracksMap.contains(oldPos)) {
continue;
}
int offsetId = oldPos + audioOffset;
if (offsetId < 0 || offsetId >= projectTracks.first.size()) {
pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500);
return false;
}
tracksMap.insert(oldPos, projectTracks.first.at(offsetId));
}
if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) {
// paste from another document, import bin clips
QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips"));
if (folderId.isEmpty()) {
// Folder doe not exist
const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId();
folderId = QString::number(pCore->projectItemModel()->getFreeFolderId());
pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo);
}
QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer"));
for (int i = 0; i < binClips.count(); ++i) {
QDomElement currentProd = binClips.item(i).toElement();
QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id"));
if (!pCore->projectItemModel()->isIdFree(clipId)) {
QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId());
Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId);
mappedIds.insert(clipId, updatedId);
clipId = updatedId;
}
pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo);
}
}
int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt();
bool res = true;
QLocale locale;
std::unordered_map correspondingIds;
QList waitingIds;
for (int i = 0; i < clips.count(); i++) {
waitingIds << i;
}
for (int i = 0; res && !waitingIds.isEmpty();) {
if (i >= waitingIds.size()) {
i = 0;
}
QDomElement prod = clips.at(waitingIds.at(i)).toElement();
QString originalId = prod.attribute(QStringLiteral("binid"));
if (mappedIds.contains(originalId)) {
// Map id
originalId = mappedIds.value(originalId);
}
int in = prod.attribute(QStringLiteral("in")).toInt();
int out = prod.attribute(QStringLiteral("out")).toInt();
int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
int pos = prod.attribute(QStringLiteral("position")).toInt() - offset;
double speed = locale.toDouble(prod.attribute(QStringLiteral("speed")));
int newId;
bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), speed, undo, redo);
if (created) {
// Master producer is ready
// ids.removeAll(originalId);
waitingIds.removeAt(i);
} else {
i++;
qApp->processEvents();
continue;
}
if (timeline->m_allClips[newId]->m_endlessResize) {
out = out - in;
in = 0;
timeline->m_allClips[newId]->m_producer->set("length", out + 1);
}
timeline->m_allClips[newId]->setInOut(in, out);
int targetId = prod.attribute(QStringLiteral("id")).toInt();
correspondingIds[targetId] = newId;
res = res && timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, undo, redo);
// paste effects
if (res) {
std::shared_ptr destStack = timeline->getClipEffectStackModel(newId);
destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), undo, redo);
}
}
// Compositions
for (int i = 0; res && i < compositions.count(); i++) {
QDomElement prod = compositions.at(i).toElement();
QString originalId = prod.attribute(QStringLiteral("composition"));
int in = prod.attribute(QStringLiteral("in")).toInt();
int out = prod.attribute(QStringLiteral("out")).toInt();
int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
int aTrackId = prod.attribute(QStringLiteral("a_track")).toInt();
if (aTrackId > 0) {
aTrackId = timeline->getTrackPosition(tracksMap.value(aTrackId));
}
int pos = prod.attribute(QStringLiteral("position")).toInt() - offset;
int newId;
auto transProps = std::make_unique();
QDomNodeList props = prod.elementsByTagName(QStringLiteral("property"));
for (int j = 0; j < props.count(); j++) {
transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(),
props.at(j).toElement().text().toUtf8().constData());
}
res = timeline->requestCompositionInsertion(originalId, curTrackId, aTrackId, position + pos, out - in + 1, std::move(transProps), newId, undo, redo);
}
if (!res) {
undo();
return false;
}
// Rebuild groups
const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text();
if (!groupsData.isEmpty()) {
timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, undo, redo);
}
// unsure to clear selection in undo/redo too.
Fun unselect = [&]() {
qDebug() << "starting undo or redo. Selection " << timeline->m_currentSelection;
timeline->requestClearSelection();
qDebug() << "after Selection " << timeline->m_currentSelection;
return true;
};
PUSH_FRONT_LAMBDA(unselect, undo);
PUSH_FRONT_LAMBDA(unselect, redo);
pCore->pushUndo(undo, redo, i18n("Paste clips"));
return true;
}
bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr &timeline, int trackId, int position, bool affectAllTracks)
{
// find blank duration
int spaceDuration = timeline->getTrackById_const(trackId)->getBlankSizeAtPos(position);
int cid = requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position);
if (cid == -1) {
return false;
}
int start = timeline->getItemPosition(cid);
requestSpacerEndOperation(timeline, cid, start, start - spaceDuration);
return true;
}
diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp
index 63e1e067e..c048d730c 100644
--- a/src/timeline2/model/timelinemodel.cpp
+++ b/src/timeline2/model/timelinemodel.cpp
@@ -1,3469 +1,3478 @@
/***************************************************************************
* 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 . *
***************************************************************************/
#include "timelinemodel.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "effects/effectsrepository.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "groupsmodel.hpp"
#include "kdenlivesettings.h"
#include "logger.hpp"
#include "snapmodel.hpp"
#include "timelinefunctions.hpp"
#include "trackmodel.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "macros.hpp"
#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
#pragma GCC diagnostic pop
RTTR_REGISTRATION
{
using namespace rttr;
registration::class_("TimelineModel")
.method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock"))
- .method("requestClipMove", select_overload(&TimelineModel::requestClipMove))(
- parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
+ .method("requestClipMove", select_overload(&TimelineModel::requestClipMove))(
+ parameter_names("clipId", "trackId", "position", "moveMirrorTracks", "updateView", "logUndo", "invalidateTimeline"))
.method("requestCompositionMove", select_overload(&TimelineModel::requestCompositionMove))(
parameter_names("compoId", "trackId", "position", "updateView", "logUndo"))
.method("requestClipInsertion", select_overload(&TimelineModel::requestClipInsertion))(
parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets"))
.method("requestItemDeletion", select_overload(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo"))
- .method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove))(
- parameter_names("itemId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
+ .method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove))(
+ parameter_names("itemId", "groupId", "delta_track", "delta_pos", "moveMirrorTracks", "updateView", "logUndo"))
.method("requestGroupDeletion", select_overload(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo"))
.method("requestItemResize", select_overload(&TimelineModel::requestItemResize))(
parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize"))
.method("requestClipsGroup", select_overload &, bool, GroupType)>(&TimelineModel::requestClipsGroup))(
parameter_names("itemIds", "logUndo", "type"))
.method("requestClipUngroup", select_overload(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo"))
.method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo"))
.method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion))(
parameter_names("pos", "id", "trackName", "audioTrack"))
.method("requestTrackDeletion", select_overload(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
.method("requestClearSelection", select_overload(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
.method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear"))
.method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
.method("requestSetSelection", select_overload &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds"))
.method("requestFakeClipMove", select_overload(&TimelineModel::requestFakeClipMove))(
parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
.method("requestFakeGroupMove", select_overload(&TimelineModel::requestFakeGroupMove))(
parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
- .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance"))
+ .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance", "moveMirrorTracks"))
.method("suggestCompositionMove",
&TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance"))
// .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos"))
// .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos"))
// .method("requestCompositionInsertion", select_overload, int &, bool)>(
// &TimelineModel::requestCompositionInsertion))(
// parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo"))
.method("requestClipTimeWarp", select_overload(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed","changeDuration"));
}
int TimelineModel::next_id = 0;
int TimelineModel::seekDuration = 30000;
TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack)
: QAbstractItemModel_shared_from_this()
, m_tractor(new Mlt::Tractor(*profile))
, m_snaps(new SnapModel())
, m_undoStack(std::move(undo_stack))
, m_profile(profile)
, m_blackClip(new Mlt::Producer(*profile, "color:black"))
, m_lock(QReadWriteLock::Recursive)
, m_timelineEffectsEnabled(true)
, m_id(getNextId())
, m_overlayTrackCount(-1)
, m_audioTarget(-1)
, m_videoTarget(-1)
, m_editMode(TimelineMode::NormalEdit)
, m_blockRefresh(false)
, m_closing(false)
{
// Create black background track
m_blackClip->set("id", "black_track");
m_blackClip->set("mlt_type", "producer");
m_blackClip->set("aspect_ratio", 1);
m_blackClip->set("length", INT_MAX);
m_blackClip->set("set.test_audio", 0);
m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
m_tractor->insert_track(*m_blackClip, 0);
TRACE_CONSTR(this);
}
void TimelineModel::prepareClose()
{
requestClearSelection(true);
QWriteLocker locker(&m_lock);
// Unlock all tracks to allow delting clip from tracks
m_closing = true;
auto it = m_allTracks.begin();
while (it != m_allTracks.end()) {
(*it)->unlock();
++it;
}
}
TimelineModel::~TimelineModel()
{
std::vector all_ids;
for (auto tracks : m_iteratorTable) {
all_ids.push_back(tracks.first);
}
for (auto tracks : all_ids) {
deregisterTrack_lambda(tracks, false)();
}
for (const auto &clip : m_allClips) {
clip.second->deregisterClipToBin();
}
}
int TimelineModel::getTracksCount() const
{
READ_LOCK();
int count = m_tractor->count();
if (m_overlayTrackCount > -1) {
count -= m_overlayTrackCount;
}
Q_ASSERT(count >= 0);
// don't count the black background track
Q_ASSERT(count - 1 == static_cast(m_allTracks.size()));
return count - 1;
}
int TimelineModel::getTrackIndexFromPosition(int pos) const
{
Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size());
READ_LOCK();
auto it = m_allTracks.cbegin();
while (pos > 0) {
it++;
pos--;
}
return (*it)->getId();
}
int TimelineModel::getClipsCount() const
{
READ_LOCK();
int size = int(m_allClips.size());
return size;
}
int TimelineModel::getCompositionsCount() const
{
READ_LOCK();
int size = int(m_allCompositions.size());
return size;
}
int TimelineModel::getClipTrackId(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
const auto clip = m_allClips.at(clipId);
return clip->getCurrentTrackId();
}
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();
Q_ASSERT(isItem(itemId));
if (isComposition(itemId)) {
return getCompositionTrackId(itemId);
}
return getClipTrackId(itemId);
}
int TimelineModel::getClipPosition(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
const auto clip = m_allClips.at(clipId);
int pos = clip->getPosition();
return pos;
}
double TimelineModel::getClipSpeed(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
return m_allClips.at(clipId)->getSpeed();
}
int TimelineModel::getClipSplitPartner(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
return m_groups->getSplitPartner(clipId);
}
int TimelineModel::getClipIn(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
const auto clip = m_allClips.at(clipId);
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();
}
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;
}
int TimelineModel::getClipPlaytime(int clipId) const
{
READ_LOCK();
Q_ASSERT(isClip(clipId));
const auto clip = m_allClips.at(clipId);
int playtime = clip->getPlaytime();
return playtime;
}
QSize TimelineModel::getClipFrameSize(int clipId) const
{
READ_LOCK();
Q_ASSERT(isClip(clipId));
const auto clip = m_allClips.at(clipId);
return clip->getFrameSize();
}
int TimelineModel::getTrackClipsCount(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
int count = getTrackById_const(trackId)->getClipsCount();
return count;
}
int TimelineModel::getClipByPosition(int trackId, int position) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
return getTrackById_const(trackId)->getClipByPosition(position);
}
int TimelineModel::getCompositionByPosition(int trackId, int position) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
return getTrackById_const(trackId)->getCompositionByPosition(position);
}
int TimelineModel::getTrackPosition(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_allTracks.cbegin();
int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId));
return pos;
}
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;
}
int TimelineModel::getTrackSortValue(int trackId, int separated) const
{
if (separated == 1) {
return getTrackPosition(trackId) + 1;
}
if (separated == 2) {
// Count audio/video tracks
auto it = m_allTracks.cbegin();
int aCount = 0;
int vCount = 0;
int refPos = 0;
bool isVideo = true;
while (it != m_allTracks.cend()) {
if ((*it)->isAudioTrack()) {
if ((*it)->getId() == trackId) {
refPos = aCount;
isVideo = false;
}
aCount++;
} else {
// video track
if ((*it)->getId() == trackId) {
refPos = vCount;
}
vCount++;
}
++it;
}
return isVideo ? aCount + refPos + 1 : aCount - refPos;
}
auto it = m_allTracks.cend();
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;
}
QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
QList results;
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.cbegin()) {
--it;
if (type == TrackType::AnyTrack) {
results << (*it)->getId();
continue;
}
bool audioTrack = (*it)->isAudioTrack();
if (type == TrackType::AudioTrack && audioTrack) {
results << (*it)->getId();
} else if (type == TrackType::VideoTrack && !audioTrack) {
results << (*it)->getId();
}
}
return results;
}
int TimelineModel::getPreviousVideoTrackIndex(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.cbegin()) {
--it;
if (!(*it)->isAudioTrack()) {
return (*it)->getId();
}
}
return 0;
}
int TimelineModel::getPreviousVideoTrackPos(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.cbegin()) {
--it;
if (!(*it)->isAudioTrack()) {
return getTrackMltIndex((*it)->getId());
}
}
return 0;
}
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;
while (it != m_allTracks.cend()) {
if ((*it)->isAudioTrack()) {
count++;
} else {
count--;
if (count == 0) {
return (*it)->getId();
}
}
++it;
}
return -1;
}
int TimelineModel::getMirrorTrackId(int trackId) const
{
if (isAudioTrack(trackId)) {
return getMirrorVideoTrackId(trackId);
}
return getMirrorAudioTrackId(trackId);
}
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...
qDebug()<<"++++++++\n+++++++ ERROR RQSTNG AUDIO MIRROR FOR AUDIO";
return -1;
}
int count = 0;
while (it != m_allTracks.cbegin()) {
if (!(*it)->isAudioTrack()) {
count++;
} else {
count--;
if (count == 0) {
return (*it)->getId();
}
}
--it;
}
if ((*it)->isAudioTrack() && count == 1) {
return (*it)->getId();
}
return -1;
}
void TimelineModel::setEditMode(TimelineMode::EditMode mode)
{
m_editMode = mode;
}
bool TimelineModel::normalEdit() const
{
return m_editMode == TimelineMode::NormalEdit;
}
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
{
Q_UNUSED(updateView);
Q_UNUSED(invalidateTimeline);
Q_UNUSED(undo);
Q_UNUSED(redo);
Q_ASSERT(isClip(clipId));
m_allClips[clipId]->setFakePosition(position);
bool trackChanged = false;
if (trackId > -1) {
if (trackId != m_allClips[clipId]->getFakeTrackId()) {
if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
m_allClips[clipId]->setFakeTrackId(trackId);
trackChanged = true;
}
}
}
QModelIndex modelIndex = makeClipIndexFromID(clipId);
if (modelIndex.isValid()) {
QVector roles{FakePositionRole};
if (trackChanged) {
roles << FakeTrackIdRole;
}
notifyChange(modelIndex, modelIndex, roles);
return true;
}
return false;
}
-bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
+bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
{
// qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<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()) {
// Move not allowed (audio / video mismatch)
qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState();
return false;
}
std::function local_undo = []() { return true; };
std::function local_redo = []() { return true; };
bool ok = true;
int old_trackId = getClipTrackId(clipId);
bool notifyViewOnly = false;
// qDebug()<<"MOVING CLIP FROM: "<isAudioTrack()) {
int in = getClipPosition(clipId);
emit invalidateZone(in, in + getClipPlaytime(clipId));
}
return true;
};
}
if (old_trackId != -1) {
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_undo);
}
ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove, false);
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
ok = ok & getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove);
if (!ok) {
qDebug() << "-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------";
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
update_model();
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_redo);
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
Q_ASSERT(m_allClips.count(clipId) > 0);
if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
TRACE_RES(true);
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();
bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
TRACE_RES(res);
return res;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move clip"));
}
TRACE_RES(res);
return res;
}
-bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
+bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool logUndo, bool invalidateTimeline)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline);
Q_ASSERT(m_allClips.count(clipId) > 0);
if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
TRACE_RES(true);
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();
- return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
+ return requestGroupMove(clipId, groupId, delta_track, delta_pos, moveMirrorTracks, updateView, logUndo);
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
- bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, logUndo, undo, redo);
+ bool res = requestClipMove(clipId, trackId, position, moveMirrorTracks, updateView, invalidateTimeline, logUndo, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move clip"));
}
TRACE_RES(res);
return res;
}
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 undo = []() { return true; };
std::function 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();
res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false);
} else {
- res = requestClipMove(clipId, trackId, position, false, false, false, undo, redo);
+ res = requestClipMove(clipId, trackId, position, true, false, false, false, undo, redo);
}
if (res) {
undo();
}
return res;
}
int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
{
if (isClip(itemId)) {
return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
}
return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
}
-int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance)
+int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool moveMirrorTracks)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, trackId, position, cursorPosition, snapDistance);
Q_ASSERT(isClip(clipId));
Q_ASSERT(isTrack(trackId));
int currentPos = getClipPosition(clipId);
int sourceTrackId = getClipTrackId(clipId);
if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) {
// Trying move on incompatible track type, stay on same track
trackId = sourceTrackId;
}
if (currentPos == position && sourceTrackId == trackId) {
TRACE_RES(position);
return position;
}
bool after = position > currentPos;
if (snapDistance > 0) {
// For snapping, we must ignore all in/outs of the clips of the group being moved
std::vector ignored_pts;
std::unordered_set all_items = {clipId};
if (m_groups->isInGroup(clipId)) {
int groupId = m_groups->getRootId(clipId);
all_items = m_groups->getLeaves(groupId);
}
for (int current_clipId : all_items) {
if (getItemTrackId(current_clipId) != -1) {
int in = getItemPosition(current_clipId);
int out = in + getItemPlaytime(current_clipId);
ignored_pts.push_back(in);
ignored_pts.push_back(out);
}
}
int snapped = getBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector(),
cursorPosition, snapDistance);
// qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped;
if (snapped >= 0) {
position = snapped;
}
}
// we check if move is possible
- bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false)
+ bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false)
: requestFakeClipMove(clipId, trackId, position, true, false, false);
/*} else {
possible = requestClipMoveAttempt(clipId, trackId, position);
}*/
if (possible) {
TRACE_RES(position);
return position;
}
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;
}
// Find best possible move
if (!m_groups->isInGroup(clipId)) {
// Try same track move
if (trackId != sourceTrackId && sourceTrackId != -1) {
qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId;
trackId = sourceTrackId;
- possible = requestClipMove(clipId, trackId, position, true, false, false);
+ possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
if (!possible) {
qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position;
} else {
TRACE_RES(position);
return position;
}
}
int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after);
qDebug() << "Found blank" << blank_length;
if (blank_length < INT_MAX) {
if (after) {
position = currentPos + blank_length;
} else {
position = currentPos - blank_length;
}
} else {
TRACE_RES(currentPos);
return currentPos;
}
- possible = requestClipMove(clipId, trackId, position, true, false, false);
+ possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
TRACE_RES(possible ? position : currentPos);
return possible ? position : currentPos;
}
// find best pos for groups
int groupId = m_groups->getRootId(clipId);
std::unordered_set all_items = m_groups->getLeaves(groupId);
QMap trackPosition;
// First pass, sort clips by track and keep only the first / last depending on move direction
for (int current_clipId : all_items) {
int clipTrack = getItemTrackId(current_clipId);
if (clipTrack == -1) {
continue;
}
int in = getItemPosition(current_clipId);
if (trackPosition.contains(clipTrack)) {
if (after) {
// keep only last clip position for track
int out = in + getItemPlaytime(current_clipId);
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);
}
}
} else {
trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in);
}
}
// Now check space on each track
QMapIterator 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 {
// Check space after the position
track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1;
if (blank_length == -1 || blank_length > track_space) {
blank_length = track_space;
}
}
}
if (blank_length != 0) {
int updatedPos = currentPos + (after ? blank_length : -blank_length);
- possible = requestClipMove(clipId, trackId, updatedPos, true, false, false);
+ possible = requestClipMove(clipId, trackId, updatedPos, moveMirrorTracks, true, false, false);
if (possible) {
TRACE_RES(updatedPos);
return updatedPos;
}
}
TRACE_RES(currentPos);
return currentPos;
}
int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance)
{
QWriteLocker locker(&m_lock);
TRACE(compoId, trackId, position, cursorPosition, snapDistance);
Q_ASSERT(isComposition(compoId));
Q_ASSERT(isTrack(trackId));
int currentPos = getCompositionPosition(compoId);
int currentTrack = getCompositionTrackId(compoId);
if (getTrackById_const(trackId)->isAudioTrack()) {
// Trying move on incompatible track type, stay on same track
trackId = currentTrack;
}
if (currentPos == position && currentTrack == trackId) {
TRACE_RES(position);
return position;
}
if (snapDistance > 0) {
// For snapping, we must ignore all in/outs of the clips of the group being moved
std::vector ignored_pts;
if (m_groups->isInGroup(compoId)) {
int groupId = m_groups->getRootId(compoId);
auto all_items = m_groups->getLeaves(groupId);
for (int current_compoId : all_items) {
// TODO: fix for composition
int in = getItemPosition(current_compoId);
int out = in + getItemPlaytime(current_compoId);
ignored_pts.push_back(in);
ignored_pts.push_back(out);
}
} else {
int in = currentPos;
int out = in + getCompositionPlaytime(compoId);
qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out;
ignored_pts.push_back(in);
ignored_pts.push_back(out);
}
int snapped = getBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance);
qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped;
if (snapped >= 0) {
position = snapped;
}
}
// we check if move is possible
bool possible = requestCompositionMove(compoId, trackId, position, true, false);
qDebug() << "Original move success" << possible;
if (possible) {
TRACE_RES(position);
return position;
}
/*bool after = position > currentPos;
int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after);
qDebug() << "Found blank" << blank_length;
if (blank_length < INT_MAX) {
if (after) {
return currentPos + blank_length;
}
return currentPos - blank_length;
}
return position;*/
TRACE_RES(currentPos);
return currentPos;
}
bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo)
{
qDebug() << "requestClipCreation " << binClipId;
QString bid = binClipId;
if (binClipId.contains(QLatin1Char('/'))) {
bid = binClipId.section(QLatin1Char('/'), 0, 0);
}
if (!pCore->projectItemModel()->hasClip(bid)) {
qDebug() << " / / / /MASTER CLIP NOT FOUND";
return false;
}
std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid);
if (!master->isReady() || !master->isCompatible(state)) {
qDebug() << "// CLIP NOT READY OR NOT COMPATIBLE: " << state;
return false;
}
int clipId = TimelineModel::getNextId();
id = clipId;
Fun local_undo = deregisterClip_lambda(clipId);
ClipModel::construct(shared_from_this(), bid, clipId, state, speed);
auto clip = m_allClips[clipId];
Fun local_redo = [clip, this, state]() {
// We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is
// sufficient to register it.
registerClip(clip, true);
clip->refreshProducerFromBin(state);
return true;
};
if (binClipId.contains(QLatin1Char('/'))) {
int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt();
int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt();
int initLength = m_allClips[clipId]->getPlaytime();
bool res = true;
if (in != 0) {
res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo);
}
res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo);
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets)
{
QWriteLocker locker(&m_lock);
TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo);
if (result && logUndo) {
PUSH_UNDO(undo, redo, i18n("Insert Clip"));
}
TRACE_RES(result);
return result;
}
bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets,
Fun &undo, Fun &redo)
{
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
qDebug() << "requestClipInsertion " << binClipId << " "
<< " " << trackId << " " << position;
bool res = false;
ClipType::ProducerType type = ClipType::Unknown;
QString bid = binClipId.section(QLatin1Char('/'), 0, 0);
// dropType indicates if we want a normal drop (disabled), audio only or video only drop
PlaylistState::ClipState dropType = PlaylistState::Disabled;
if (bid.startsWith(QLatin1Char('A'))) {
dropType = PlaylistState::AudioOnly;
bid = bid.remove(0, 1);
} else if (bid.startsWith(QLatin1Char('V'))) {
dropType = PlaylistState::VideoOnly;
bid = bid.remove(0, 1);
}
if (!pCore->projectItemModel()->hasClip(bid)) {
return false;
}
std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid);
type = master->clipType();
if (useTargets && m_audioTarget == -1 && m_videoTarget == -1) {
useTargets = false;
}
if (dropType == PlaylistState::Disabled && (type == ClipType::AV || type == ClipType::Playlist)) {
if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) {
// If audio target is set but no video target, only insert audio
trackId = m_audioTarget;
if (trackId > -1 && getTrackById_const(trackId)->isLocked()) {
trackId = -1;
}
} else if (useTargets && getTrackById_const(trackId)->isLocked()) {
// Video target set but locked
trackId = m_audioTarget;
if (trackId > -1 && getTrackById_const(trackId)->isLocked()) {
trackId = -1;
}
}
if (trackId == -1) {
pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage);
return false;
}
bool audioDrop = getTrackById_const(trackId)->isAudioTrack();
res = requestClipCreation(binClipId, id, getTrackById_const(trackId)->trackType(), 1.0, local_undo, local_redo);
- res = res && requestClipMove(id, trackId, position, refreshView, logUndo, logUndo, local_undo, local_redo);
+ res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo);
int target_track;
if (audioDrop) {
target_track = m_videoTarget == -1 ? -1 : getTrackById_const(m_videoTarget)->isLocked() ? -1 : m_videoTarget;
} else {
target_track = m_audioTarget == -1 ? -1 : getTrackById_const(m_audioTarget)->isLocked() ? -1 : m_audioTarget;
}
qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo();
int mirror = getMirrorTrackId(trackId);
if (mirror > -1 && getTrackById_const(mirror)->isLocked()) {
mirror = -1;
}
bool canMirrorDrop = !useTargets && mirror > -1;
if (res && (canMirrorDrop || target_track > -1) && master->hasAudioAndVideo()) {
if (!useTargets) {
target_track = mirror;
}
// QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack);
QList possibleTracks;
qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets;
if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) {
possibleTracks << target_track;
}
if (possibleTracks.isEmpty()) {
// No available audio track for splitting, abort
pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage);
res = false;
} else {
std::function audio_undo = []() { return true; };
std::function audio_redo = []() { return true; };
int newId;
res = requestClipCreation(binClipId, newId, audioDrop ? PlaylistState::VideoOnly : PlaylistState::AudioOnly, 1.0, audio_undo, audio_redo);
if (res) {
bool move = false;
while (!move && !possibleTracks.isEmpty()) {
int newTrack = possibleTracks.takeFirst();
- move = requestClipMove(newId, newTrack, position, true, true, true, audio_undo, audio_redo);
+ move = requestClipMove(newId, newTrack, position, true, true, true, true, audio_undo, audio_redo);
}
// use lazy evaluation to group only if move was successful
res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit);
if (!res || !move) {
pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage);
bool undone = audio_undo();
Q_ASSERT(undone);
} else {
UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo);
}
} else {
pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage);
bool undone = audio_undo();
Q_ASSERT(undone);
}
}
}
} else {
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid);
if (dropType == PlaylistState::Disabled) {
dropType = getTrackById_const(trackId)->trackType();
} else if (dropType != getTrackById_const(trackId)->trackType()) {
qDebug() << "// INCORRECT DRAG, ABORTING";
return false;
}
QString normalisedBinId = binClipId;
if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) {
normalisedBinId.remove(0, 1);
}
res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo);
- res = res && requestClipMove(id, trackId, position, refreshView, logUndo, logUndo, local_undo, local_redo);
+ res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo);
}
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
id = -1;
return false;
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestItemDeletion(int itemId, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
if (m_groups->isInGroup(itemId)) {
return requestGroupDeletion(itemId, undo, redo);
}
if (isClip(itemId)) {
return requestClipDeletion(itemId, undo, redo);
}
if (isComposition(itemId)) {
return requestCompositionDeletion(itemId, undo, redo);
}
Q_ASSERT(false);
return false;
}
bool TimelineModel::requestItemDeletion(int itemId, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(itemId, logUndo);
Q_ASSERT(isItem(itemId));
QString actionLabel;
if (m_groups->isInGroup(itemId)) {
actionLabel = i18n("Remove group");
} else {
if (isClip(itemId)) {
actionLabel = i18n("Delete Clip");
} else {
actionLabel = i18n("Delete Composition");
}
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = requestItemDeletion(itemId, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, actionLabel);
}
TRACE_RES(res);
requestClearSelection(true);
return res;
}
bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo)
{
int trackId = getClipTrackId(clipId);
if (trackId != -1) {
bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo, false, true);
if (!res) {
undo();
return false;
}
}
auto operation = deregisterClip_lambda(clipId);
auto clip = m_allClips[clipId];
Fun reverse = [this, clip]() {
// We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is
// sufficient to register it.
registerClip(clip, true);
return true;
};
if (operation()) {
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
undo();
return false;
}
bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo)
{
int trackId = getCompositionTrackId(compositionId);
if (trackId != -1) {
bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo, true);
if (!res) {
undo();
return false;
} else {
Fun unplant_op = [this, compositionId]() {
unplantComposition(compositionId);
return true;
};
unplant_op();
PUSH_LAMBDA(unplant_op, redo);
}
}
Fun operation = deregisterComposition_lambda(compositionId);
auto composition = m_allCompositions[compositionId];
int new_in = composition->getPosition();
int new_out = new_in + composition->getPlaytime();
Fun reverse = [this, composition, compositionId, trackId, new_in, new_out]() {
// We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it
// back it is sufficient to register it.
registerComposition(composition);
composition->setCurrentTrackId(trackId, true);
replantCompositions(compositionId, false);
checkRefresh(new_in, new_out);
return true;
};
if (operation()) {
Fun update_monitor = [this, new_in, new_out]() {
checkRefresh(new_in, new_out);
return true;
};
update_monitor();
PUSH_LAMBDA(update_monitor, operation);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
undo();
return false;
}
std::unordered_set TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions)
{
Q_UNUSED(listCompositions)
std::unordered_set allClips;
if (trackId == -1) {
for (const auto &track : m_allTracks) {
if (track->isLocked()) {
continue;
}
std::unordered_set clipTracks = getItemsInRange(track->getId(), start, end, listCompositions);
allClips.insert(clipTracks.begin(), clipTracks.end());
}
} else {
std::unordered_set clipTracks = getTrackById(trackId)->getClipsInRange(start, end);
allClips.insert(clipTracks.begin(), clipTracks.end());
if (listCompositions) {
std::unordered_set compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end);
allClips.insert(compoTracks.begin(), compoTracks.end());
}
}
return allClips;
}
bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
{
TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move group"));
}
TRACE_RES(res);
return res;
}
bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo,
bool allowViewRefresh)
{
Q_UNUSED(updateView);
Q_UNUSED(finalMove);
Q_UNUSED(undo);
Q_UNUSED(redo);
Q_UNUSED(allowViewRefresh);
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allGroups.count(groupId) > 0);
bool ok = true;
auto all_items = m_groups->getLeaves(groupId);
Q_ASSERT(all_items.size() > 1);
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
// Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
// This way, we ensure that no conflict will arise with clips inside the group being moved
// Check if there is a track move
// First, remove clips
std::unordered_map old_track_ids, old_position, old_forced_track;
for (int item : all_items) {
int old_trackId = getItemTrackId(item);
old_track_ids[item] = old_trackId;
if (old_trackId != -1) {
if (isClip(item)) {
old_position[item] = m_allClips[item]->getPosition();
} else {
old_position[item] = m_allCompositions[item]->getPosition();
old_forced_track[item] = m_allCompositions[item]->getForcedTrack();
}
}
}
// Second step, calculate delta
int audio_delta, video_delta;
audio_delta = video_delta = delta_track;
if (getTrackById(old_track_ids[clipId])->isAudioTrack()) {
// Master clip is audio, so reverse delta for video clips
video_delta = -delta_track;
} else {
audio_delta = -delta_track;
}
bool trackChanged = false;
// Reverse sort. We need to insert from left to right to avoid confusing the view
for (int item : all_items) {
int current_track_id = old_track_ids[item];
int current_track_position = getTrackPosition(current_track_id);
int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
int target_track_position = current_track_position + d;
if (target_track_position >= 0 && target_track_position < getTracksCount()) {
auto it = m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
int target_position = old_position[item] + delta_pos;
if (isClip(item)) {
qDebug() << "/// SETTING FAKE CLIP: " << target_track << ", POSITION: " << target_position;
m_allClips[item]->setFakePosition(target_position);
if (m_allClips[item]->getFakeTrackId() != target_track) {
trackChanged = true;
}
m_allClips[item]->setFakeTrackId(target_track);
} else {
}
} else {
qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n..";
ok = false;
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
QModelIndex modelIndex;
QVector roles{FakePositionRole};
if (trackChanged) {
roles << FakeTrackIdRole;
}
for (int item : all_items) {
if (isClip(item)) {
modelIndex = makeClipIndexFromID(item);
} else {
modelIndex = makeCompositionIndexFromID(item);
}
notifyChange(modelIndex, modelIndex, roles);
}
return true;
}
-bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
+bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool moveMirrorTracks, bool updateView, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(itemId, groupId, delta_track, delta_pos, updateView, logUndo);
std::function undo = []() { return true; };
std::function redo = []() { return true; };
- bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
+ bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo, moveMirrorTracks);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move group"));
}
TRACE_RES(res);
return res;
}
-bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo,
+bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool moveMirrorTracks,
bool allowViewRefresh)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allGroups.count(groupId) > 0);
Q_ASSERT(isItem(itemId));
if (getGroupElements(groupId).count(itemId) == 0) {
// this group doesn't contain the clip, abort
return false;
}
bool ok = true;
auto all_items = m_groups->getLeaves(groupId);
Q_ASSERT(all_items.size() > 1);
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
std::unordered_set all_clips;
std::unordered_set all_compositions;
// Separate clips from compositions to sort
for (int affectedItemId : all_items) {
if (isClip(affectedItemId)) {
all_clips.insert(affectedItemId);
} else {
all_compositions.insert(affectedItemId);
}
}
// Sort clips first
std::vector sorted_clips(all_clips.begin(), all_clips.end());
std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_pos](int clipId1, int clipId2) {
int p1 = m_allClips[clipId1]->getPosition();
int p2 = m_allClips[clipId2]->getPosition();
return delta_pos > 0 ? p2 <= p1 : p1 <= p2;
});
// Sort compositions. We need to delete in the move direction from top to bottom
std::vector sorted_compositions(all_compositions.begin(), all_compositions.end());
std::sort(sorted_compositions.begin(), sorted_compositions.end(), [this, delta_track, delta_pos](int clipId1, int clipId2) {
int p1 = delta_track < 0
? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition();
int p2 = delta_track < 0
? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition();
return delta_track == 0 ? (delta_pos > 0 ? p2 <= p1 : p1 <= p2) : p1 <= p2;
});
sorted_clips.insert(sorted_clips.end(), sorted_compositions.begin(), sorted_compositions.end());
// Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
// This way, we ensure that no conflict will arise with clips inside the group being moved
Fun update_model = [this, finalMove]() {
if (finalMove) {
updateDuration();
}
return true;
};
// Check if there is a track move
bool updatePositionOnly = false;
// Second step, reinsert clips at correct positions
int audio_delta, video_delta;
audio_delta = video_delta = delta_track;
if (delta_track == 0 && updateView) {
updateView = false;
allowViewRefresh = false;
updatePositionOnly = true;
update_model = [sorted_clips, finalMove, this]() {
QModelIndex modelIndex;
QVector roles{StartRole};
for (int item : sorted_clips) {
if (isClip(item)) {
modelIndex = makeClipIndexFromID(item);
} else {
modelIndex = makeCompositionIndexFromID(item);
}
notifyChange(modelIndex, modelIndex, roles);
}
if (finalMove) {
updateDuration();
}
return true;
};
}
std::unordered_map old_track_ids, old_position, old_forced_track;
// First, remove clips
if (delta_track != 0) {
// We delete our clips only if changing track
for (int item : sorted_clips) {
int old_trackId = getItemTrackId(item);
old_track_ids[item] = old_trackId;
if (old_trackId != -1) {
bool updateThisView = allowViewRefresh;
if (isClip(item)) {
ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo, true, false);
old_position[item] = m_allClips[item]->getPosition();
} else {
// ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, finalMove, local_undo, local_redo);
old_position[item] = m_allCompositions[item]->getPosition();
old_forced_track[item] = m_allCompositions[item]->getForcedTrack();
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
}
- if (getTrackById(old_track_ids[itemId])->isAudioTrack()) {
- // Master clip is audio, so reverse delta for video clips
- video_delta = -delta_track;
+ if (!moveMirrorTracks) {
+ if (getTrackById(old_track_ids[itemId])->isAudioTrack()) {
+ // Master clip is audio, so reverse delta for video clips
+ video_delta = 0;
+ } else {
+ audio_delta = 0;
+ }
} else {
- audio_delta = -delta_track;
+ if (getTrackById(old_track_ids[itemId])->isAudioTrack()) {
+ // Master clip is audio, so reverse delta for video clips
+ video_delta = -delta_track;
+ } else {
+ audio_delta = -delta_track;
+ }
}
}
// We need to insert depending on the move direction to avoid confusing the view
// std::reverse(std::begin(sorted_clips), std::end(sorted_clips));
bool updateThisView = allowViewRefresh;
if (delta_track == 0) {
// Special case, we are moving on same track, avoid too many calculations
for (int item : sorted_clips) {
int current_track_id = getItemTrackId(item);
int target_position = getItemPosition(item) + delta_pos;
if (isClip(item)) {
- ok = ok && requestClipMove(item, current_track_id, target_position, updateThisView, finalMove, finalMove, local_undo, local_redo, true);
+ ok = ok && requestClipMove(item, current_track_id, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true);
} else {
ok = ok &&
requestCompositionMove(item, current_track_id, m_allCompositions[item]->getForcedTrack(), target_position, updateThisView, finalMove, local_undo, local_redo);
}
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
} else {
// Track changed
for (int item : sorted_clips) {
int current_track_id = old_track_ids[item];
int current_track_position = getTrackPosition(current_track_id);
int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
int target_track_position = current_track_position + d;
if (target_track_position >= 0 && target_track_position < getTracksCount()) {
auto it = m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
int target_position = old_position[item] + delta_pos;
if (isClip(item)) {
- ok = ok && requestClipMove(item, target_track, target_position, updateThisView, finalMove, finalMove, local_undo, local_redo, true);
+ ok = ok && requestClipMove(item, target_track, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true);
} else {
ok = ok &&
requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, finalMove, local_undo, local_redo);
}
} else {
qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n..";
ok = false;
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
}
if (updatePositionOnly) {
update_model();
PUSH_LAMBDA(update_model, local_redo);
PUSH_LAMBDA(update_model, local_undo);
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(clipId, logUndo);
if (!m_groups->isInGroup(clipId)) {
TRACE_RES(false);
return false;
}
bool res = requestItemDeletion(clipId, logUndo);
TRACE_RES(res);
return res;
}
bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo)
{
// we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves.
std::queue group_queue;
group_queue.push(m_groups->getRootId(clipId));
std::unordered_set all_items;
std::unordered_set all_compositions;
while (!group_queue.empty()) {
int current_group = group_queue.front();
bool isSelection = m_currentSelection == current_group;
if (isSelection) {
m_currentSelection = -1;
}
group_queue.pop();
Q_ASSERT(isGroup(current_group));
auto children = m_groups->getDirectChildren(current_group);
int one_child = -1; // we need the id on any of the indices of the elements of the group
for (int c : children) {
if (isClip(c)) {
all_items.insert(c);
one_child = c;
} else if (isComposition(c)) {
all_compositions.insert(c);
one_child = c;
} else {
Q_ASSERT(isGroup(c));
one_child = c;
group_queue.push(c);
}
}
if (one_child != -1) {
if (m_groups->getType(current_group) == GroupType::Selection) {
Q_ASSERT(isSelection);
// in the case of a selection group, we delete the group but don't log it in the undo object
Fun tmp_undo = []() { return true; };
Fun tmp_redo = []() { return true; };
m_groups->ungroupItem(one_child, tmp_undo, tmp_redo);
} else {
bool res = m_groups->ungroupItem(one_child, undo, redo);
if (!res) {
undo();
return false;
}
}
}
}
for (int clip : all_items) {
bool res = requestClipDeletion(clip, undo, redo);
if (!res) {
undo();
return false;
}
}
for (int compo : all_compositions) {
bool res = requestCompositionDeletion(compo, undo, redo);
if (!res) {
undo();
return false;
}
}
return true;
}
const QVariantList TimelineModel::getGroupData(int itemId)
{
QWriteLocker locker(&m_lock);
if (!m_groups->isInGroup(itemId)) {
return {itemId, getItemPosition(itemId), getItemPlaytime(itemId)};
}
int groupId = m_groups->getRootId(itemId);
QVariantList result;
std::unordered_set items = m_groups->getLeaves(groupId);
for (int id : items) {
result << id << getItemPosition(id) << getItemPlaytime(id);
}
return result;
}
void TimelineModel::processGroupResize(QVariantList startPos, QVariantList endPos, bool right)
{
Q_ASSERT(startPos.size() == endPos.size());
QMap> startData;
QMap> endData;
while (!startPos.isEmpty()) {
int id = startPos.takeFirst().toInt();
int in = startPos.takeFirst().toInt();
int duration = startPos.takeFirst().toInt();
startData.insert(id, {in, duration});
id = endPos.takeFirst().toInt();
in = endPos.takeFirst().toInt();
duration = endPos.takeFirst().toInt();
endData.insert(id, {in, duration});
}
QMapIterator