diff --git a/src/logger.cpp b/src/logger.cpp
new file mode 100644
index 000000000..283fcf4d1
--- /dev/null
+++ b/src/logger.cpp
@@ -0,0 +1,194 @@
+/***************************************************************************
+ * Copyright (C) 2019 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 "logger.hpp"
+#include "timeline2/model/timelinemodel.hpp"
+#include
+#include
+#include
+#include
+#include
+
+thread_local bool Logger::is_executing = false;
+std::mutex Logger::mut;
+std::vector Logger::operations;
+std::vector Logger::invoks;
+std::unordered_map> Logger::constr;
+thread_local size_t Logger::result_awaiting = INT_MAX;
+
+bool Logger::start_logging()
+{
+ std::unique_lock lk(mut);
+ if (is_executing) {
+ return false;
+ }
+ is_executing = true;
+ return true;
+}
+void Logger::stop_logging()
+{
+ std::unique_lock lk(mut);
+ is_executing = false;
+}
+std::string Logger::get_ptr_name(rttr::variant ptr)
+{
+ if (ptr.can_convert()) {
+ return "timeline_" + std::to_string(get_id_from_ptr(ptr.convert()));
+ } else {
+ std::cout << "Error: unhandled ptr type " << ptr.get_type().get_name().to_string() << std::endl;
+ }
+ return "unknown";
+}
+
+void Logger::log_res(rttr::variant result)
+{
+ std::unique_lock lk(mut);
+ Q_ASSERT(result_awaiting < invoks.size());
+ invoks[result_awaiting].res = std::move(result);
+}
+
+namespace {
+bool isIthParamARef(const rttr::method &method, size_t i)
+{
+ QString sig = QString::fromStdString(method.get_signature().to_string());
+ int deb = sig.indexOf("(");
+ int end = sig.lastIndexOf(")");
+ sig = sig.mid(deb + 1, deb - end - 1);
+ QStringList args = sig.split(QStringLiteral(","));
+ return args[(int)i].contains("&") && !args[(int)i].contains("const &");
+}
+} // namespace
+
+void Logger::print_trace()
+{
+ auto process_args = [&](const std::vector &args, const std::unordered_set &refs = {}) {
+ std::stringstream ss;
+ bool deb = true;
+ size_t i = 0;
+ for (const auto &a : args) {
+ if (deb) {
+ deb = false;
+ } else {
+ ss << ", ";
+ }
+ if (refs.count(i) > 0) {
+ ss << "dummy_" << i;
+ } else if (a.get_type() == rttr::type::get()) {
+ ss << a.convert();
+ } else if (a.can_convert()) {
+ ss << (a.convert() ? "true" : "false");
+ } else if (a.can_convert()) {
+ ss << std::quoted(a.convert().toStdString());
+ } else if (a.get_type().is_pointer()) {
+ ss << get_ptr_name(a);
+ } else {
+ std::cout << "Error: unhandled arg type " << a.get_type().get_name().to_string() << std::endl;
+ }
+ ++i;
+ }
+ return ss.str();
+ };
+ auto test_file = std::ofstream("test_case.cpp");
+ test_file << "TEST_CASE(\"Regression\") {" << std::endl;
+ test_file << "auto binModel = pCore->projectItemModel();" << std::endl;
+ test_file << "std::shared_ptr undoStack = std::make_shared(nullptr);" << std::endl;
+ test_file << "std::shared_ptr guideModel = std::make_shared(undoStack);" << std::endl;
+ test_file << "Mock pmMock;" << std::endl;
+ test_file << "When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);" << std::endl;
+ test_file << "ProjectManager &mocked = pmMock.get();" << std::endl;
+ test_file << "pCore->m_projectManager = &mocked;" << std::endl;
+
+ auto check_consistancy = [&]() {
+ if (constr.count("TimelineModel") > 0) {
+ for (size_t i = 0; i < constr["TimelineModel"].size(); ++i) {
+ test_file << "REQUIRE(timeline_" << i << "->checkConsistency());" << std::endl;
+ }
+ }
+ };
+ for (const auto &o : operations) {
+ if (o.can_convert()) {
+ InvokId id = o.convert();
+ Invok &invok = invoks[id.id];
+ std::unordered_set refs;
+ rttr::method m = invok.ptr.get_type().get_method(invok.method);
+ test_file << "{" << std::endl;
+ for (const auto &a : m.get_parameter_infos()) {
+ if (isIthParamARef(m, a.get_index())) {
+ refs.insert(a.get_index());
+ test_file << a.get_type().get_name().to_string() << " dummy_" << std::to_string(a.get_index()) << ";" << std::endl;
+ }
+ }
+ if (m.get_return_type() != rttr::type::get()) {
+ test_file << m.get_return_type().get_name().to_string() << " res = ";
+ }
+ test_file << get_ptr_name(invok.ptr) << "->" << invok.method << "(" << process_args(invok.args, refs) << ");" << std::endl;
+ if (m.get_return_type() != rttr::type::get() && invok.res.is_valid()) {
+ test_file << "REQUIRE( res == " << invok.res.to_string() << ");" << std::endl;
+ }
+ test_file << "}" << std::endl;
+
+ } else if (o.can_convert()) {
+ ConstrId id = o.convert();
+ if (id.type == "TimelineModel") {
+ test_file << "TimelineItemModel tim_" << id.id << "(new Mlt::Profile(), undoStack);" << std::endl;
+ test_file << "Mock timMock_" << id.id << "(tim_" << id.id << ");" << std::endl;
+ test_file << "auto timeline_" << id.id << " = std::shared_ptr(&timMock_" << id.id << ".get(), [](...) {});" << std::endl;
+ test_file << "TimelineItemModel::finishConstruct(timeline_" << id.id << ", guideModel);" << std::endl;
+ test_file << "Fake(Method(timMock_" << id.id << ", adjustAssetRange));" << std::endl;
+ } else if (id.type == "TrackModel") {
+ std::string params = process_args(constr[id.type][id.id].second);
+ test_file << "TrackModel::construct(" << params << ");" << std::endl;
+
+ } else {
+ std::cout << "Error: unknown constructor " << id.type << std::endl;
+ }
+ } else {
+ std::cout << "Error: unknown operation" << std::endl;
+ }
+ check_consistancy();
+ test_file << "undoStack->undo();" << std::endl;
+ check_consistancy();
+ test_file << "undoStack->redo();" << std::endl;
+ check_consistancy();
+ }
+ test_file << "}" << std::endl;
+}
+void Logger::clear()
+{
+ is_executing = false;
+ invoks.clear();
+ operations.clear();
+}
+
+LogGuard::LogGuard()
+{
+ m_hasGuard = Logger::start_logging();
+}
+LogGuard::~LogGuard()
+{
+ if (m_hasGuard) {
+ Logger::stop_logging();
+ }
+}
+bool LogGuard::hasGuard() const
+{
+ return m_hasGuard;
+}
diff --git a/src/logger.hpp b/src/logger.hpp
new file mode 100644
index 000000000..eaec2a52e
--- /dev/null
+++ b/src/logger.hpp
@@ -0,0 +1,156 @@
+/***************************************************************************
+ * Copyright (C) 2019 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 stdd::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 . *
+ ***************************************************************************/
+
+#pragma once
+#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
+
+/** @brief This class is meant to provide an easy way to reproduce bugs involving the model.
+ * The idea is to log any modifier function involving a model class, and trace the parameters that were passed, to be able to generate a test-case producing the
+ * same behaviour. Note that many modifier functions of the models are nested. We are only interested in the top-most call, and we must ignore bottom calls.
+ */
+class Logger
+{
+public:
+ /** @brief Notify the logger that the current thread wants to start logging.
+ * This function returns true if this is a top-level call, meaning that we indeed want to log it. If the function returns false, the caller must not log.
+ */
+ static bool start_logging();
+ template static void log_constr(T *inst, std::vector args);
+ template static void log(T *inst, std::string str, std::vector args);
+
+ static void log_res(rttr::variant result);
+
+ /// @brief Notify that we are done with our function. Must not be called if start_logging returned false.
+ static void stop_logging();
+ static void print_trace();
+
+ static void clear();
+
+protected:
+ template static size_t get_id_from_ptr(T *ptr);
+ static std::string get_ptr_name(rttr::variant ptr);
+ struct InvokId
+ {
+ size_t id;
+ };
+ struct ConstrId
+ {
+ std::string type;
+ size_t id;
+ };
+ // a construction log contains the pointer as first parameter, and the vector of parameters
+ using Constr = std::pair>;
+ struct Invok
+ {
+ rttr::variant ptr;
+ std::string method;
+ std::vector args;
+ rttr::variant res;
+ };
+ thread_local static bool is_executing;
+ thread_local static size_t result_awaiting;
+ static std::mutex mut;
+ static std::vector operations;
+ static std::unordered_map> constr;
+ static std::vector invoks;
+};
+
+/** @brief This class provides a RAII mechanism to log the execution of a function */
+class LogGuard
+{
+public:
+ LogGuard();
+ ~LogGuard();
+ // @brief Returns true if we are the top-level caller.
+ bool hasGuard() const;
+
+protected:
+ bool m_hasGuard = false;
+};
+
+#define TRACE_CONSTR(ptr, ...) \
+ LogGuard __guard; \
+ if (__guard.hasGuard()) { \
+ Logger::log_constr((ptr), {__VA_ARGS__}); \
+ }
+#define TRACE(...) \
+ LogGuard __guard; \
+ if (__guard.hasGuard()) { \
+ Logger::log(this, __FUNCTION__, {__VA_ARGS__}); \
+ }
+
+#define TRACE_RES(res) \
+ if (__guard.hasGuard()) { \
+ Logger::log_res(res); \
+ }
+/******* Implementations ***********/
+template void Logger::log_constr(T *inst, std::vector args)
+{
+ std::unique_lock lk(mut);
+ for (auto &a : args) {
+ // this will rewove shared/weak/unique ptrs
+ if (a.get_type().is_wrapper()) {
+ a = a.extract_wrapped_value();
+ }
+ }
+ std::string class_name = rttr::type::get().get_name().to_string();
+ constr[class_name].push_back({inst, std::move(args)});
+ operations.push_back(ConstrId{class_name, constr[class_name].size() - 1});
+}
+
+template void Logger::log(T *inst, std::string fctName, std::vector args)
+{
+ std::unique_lock lk(mut);
+ for (auto &a : args) {
+ // this will rewove shared/weak/unique ptrs
+ if (a.get_type().is_wrapper()) {
+ a = a.extract_wrapped_value();
+ }
+ }
+ std::string class_name = rttr::type::get().get_name().to_string();
+ invoks.push_back({inst, std::move(fctName), std::move(args), rttr::variant()});
+ operations.push_back(InvokId{invoks.size() - 1});
+ result_awaiting = invoks.size() - 1;
+}
+
+template size_t Logger::get_id_from_ptr(T *ptr)
+{
+ const std::string class_name = rttr::type::get().get_name().to_string();
+ for (size_t i = 0; i < constr.at(class_name).size(); ++i) {
+ if (constr.at(class_name)[i].first.convert() == ptr) {
+ return i;
+ }
+ }
+ std::cerr << "Error: ptr of type " << class_name << " not found" << std::endl;
+ return INT_MAX;
+}
diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp
index 48a1d2ac8..4764ac103 100644
--- a/src/timeline2/model/timelinemodel.cpp
+++ b/src/timeline2/model/timelinemodel.cpp
@@ -1,2805 +1,2810 @@
/***************************************************************************
* 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 "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
#ifdef LOGGING
#include
#include
#endif
#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("requestClipMove", select_overload(&TimelineModel::requestClipMove));
+ .method("requestClipMove", select_overload(&TimelineModel::requestClipMove))
+ .method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion));
}
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(undo_stack)
, m_profile(profile)
, m_blackClip(new Mlt::Producer(*profile, "color:black"))
, m_lock(QReadWriteLock::Recursive)
, m_timelineEffectsEnabled(true)
, m_id(getNextId())
, m_temporarySelectionGroup(-1)
, m_overlayTrackCount(-1)
, m_audioTarget(-1)
, m_videoTarget(-1)
, m_editMode(TimelineMode::NormalEdit)
{
// 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("length", INT_MAX);
m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
m_tractor->insert_track(*m_blackClip, 0);
+ TRACE_CONSTR(this);
#ifdef LOGGING
m_logFile = std::ofstream("log.txt");
m_logFile << "TEST_CASE(\"Regression\") {" << std::endl;
m_logFile << "Mlt::Profile profile;" << std::endl;
m_logFile << "std::shared_ptr undoStack = std::make_shared(nullptr);" << std::endl;
m_logFile << "std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), undoStack);" << std::endl;
m_logFile << "TimelineModel::next_id = 0;" << std::endl;
m_logFile << "int dummy_id;" << std::endl;
#endif
}
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.begin();
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(isClip(itemId) || isComposition(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.begin();
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, bool separated) const
{
if (separated) {
return getTrackPosition(trackId) + 1;
}
auto it = m_allTracks.end();
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.begin()) {
--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.begin()) {
--it;
if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) {
break;
}
}
return it == m_allTracks.begin() ? 0 : (*it)->getId();
}
int TimelineModel::getPreviousVideoTrackPos(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.begin()) {
--it;
if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) {
break;
}
}
return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId());
}
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;
if (it != m_allTracks.end()) {
++it;
}
while (it != m_allTracks.end()) {
if ((*it)->isAudioTrack()) {
count++;
} else {
if (count == 0) {
return (*it)->getId();
}
count--;
}
++it;
}
if (!(*it)->isAudioTrack() && count == 0) {
return (*it)->getId();
}
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...
return -1;
}
int count = 0;
if (it != m_allTracks.begin()) {
--it;
}
while (it != m_allTracks.begin()) {
if (!(*it)->isAudioTrack()) {
count++;
} else {
if (count == 0) {
return (*it)->getId();
}
count--;
}
--it;
}
if ((*it)->isAudioTrack() && count == 0) {
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::fakeClipMove(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, Fun &undo, Fun &redo)
{
// qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView;
if (trackId == -1) {
return false;
}
Q_ASSERT(isClip(clipId));
if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) {
if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) {
return false;
}
if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) {
return false;
}
} else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) {
// 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;
bool localUpdateView = updateView;
// qDebug()<<"MOVING CLIP FROM: "<requestClipDeletion(clipId, localUpdateView, invalidateTimeline, local_undo, local_redo);
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
ok = ok & getTrackById(trackId)->requestClipInsertion(clipId, position, localUpdateView, invalidateTimeline, local_undo, local_redo);
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)
{
#ifdef LOGGING
m_logFile << "timeline->requestClipMove(" << clipId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", "
<< (logUndo ? "true" : "false") << " ); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allClips.count(clipId) > 0);
if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
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 requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = fakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move clip"));
}
return res;
}
bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
#ifdef LOGGING
m_logFile << "timeline->requestClipMove(" << clipId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", "
<< (logUndo ? "true" : "false") << " ); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allClips.count(clipId) > 0);
if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
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);
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move clip"));
}
return res;
}
bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position)
{
#ifdef LOGGING
m_logFile << "timeline->requestClipMove(" << clipId << "," << trackId << " ," << position << std::endl;
#endif
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, undo, redo);
}
if (res) {
undo();
}
return res;
}
int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int snapDistance)
{
if (isClip(itemId)) {
return suggestClipMove(itemId, trackId, position, snapDistance);
}
return suggestCompositionMove(itemId, trackId, position, snapDistance);
}
int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int snapDistance, bool allowViewUpdate)
{
#ifdef LOGGING
m_logFile << "timeline->suggestClipMove(" << clipId << "," << trackId << " ," << position << "); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
Q_ASSERT(isClip(clipId));
Q_ASSERT(isTrack(trackId));
int currentPos = getClipPosition(clipId);
int sourceTrackId = getClipTrackId(clipId);
if (currentPos == position && sourceTrackId == trackId) {
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 = requestBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector(),
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)
: requestFakeClipMove(clipId, trackId, position, true, false, false);
/*} else {
possible = requestClipMoveAttempt(clipId, trackId, position);
}*/
if (possible) {
return position;
}
// Find best possible move
if (!m_groups->isInGroup(clipId)) {
// Easy
// int currentTrackId = getClipTrackId(clipId);
// Try same track move
qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId;
trackId = sourceTrackId;
possible = requestClipMove(clipId, trackId, position, true, false, false);
if (!possible) {
qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position;
} else {
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 {
return currentPos;
}
possible = requestClipMove(clipId, trackId, position, true, false, false);
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) : 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 after before the position
track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value();
if (blank_length == -1 || blank_length > track_space) {
blank_length = track_space;
}
}
}
if (blank_length != 0) {
int updatedPos = currentPos + (after ? blank_length : -blank_length);
if (allowViewUpdate) {
possible = requestClipMove(clipId, trackId, updatedPos, false, false, false);
} else {
possible = requestClipMoveAttempt(clipId, trackId, updatedPos);
}
if (possible) {
return updatedPos;
}
}
return currentPos;
}
int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int snapDistance)
{
#ifdef LOGGING
m_logFile << "timeline->suggestCompositionMove(" << compoId << "," << trackId << " ," << position << "); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
Q_ASSERT(isComposition(compoId));
Q_ASSERT(isTrack(trackId));
int currentPos = getCompositionPosition(compoId);
int currentTrack = getCompositionTrackId(compoId);
if (currentPos == position && currentTrack == trackId) {
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 = requestBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, 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) {
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;*/
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)) {
return false;
}
std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid);
if (!master->isCompatible(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, clipId]() {
// 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)
{
#ifdef LOGGING
m_logFile << "timeline->requestClipInsertion(" << binClipId.toStdString() << "," << trackId << " ," << position << ", dummy_id );" << std::endl;
#endif
QWriteLocker locker(&m_lock);
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"));
}
return result;
}
bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets,
Fun &undo, Fun &redo)
{
std::function local_undo = []() { return true; };
std::function 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;
}
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, local_undo, local_redo);
int target_track = audioDrop ? m_videoTarget : m_audioTarget;
qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo();
if (res && (!useTargets || target_track > -1) && master->hasAudioAndVideo()) {
if (!useTargets) {
target_track = audioDrop ? getMirrorVideoTrackId(trackId) : getMirrorAudioTrackId(trackId);
}
// 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, false, 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, 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 clipId, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
if (m_groups->isInGroup(clipId)) {
return requestGroupDeletion(clipId, undo, redo);
}
return requestClipDeletion(clipId, undo, redo);
}
bool TimelineModel::requestItemDeletion(int itemId, bool logUndo)
{
#ifdef LOGGING
m_logFile << "timeline->requestItemDeletion(" << itemId << "); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
Q_ASSERT(isClip(itemId) || isComposition(itemId));
if (m_groups->isInGroup(itemId)) {
return requestGroupDeletion(itemId, logUndo);
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = false;
QString actionLabel;
if (isClip(itemId)) {
actionLabel = i18n("Delete Clip");
res = requestClipDeletion(itemId, undo, redo);
} else {
actionLabel = i18n("Delete Composition");
res = requestCompositionDeletion(itemId, undo, redo);
}
if (res && logUndo) {
PUSH_UNDO(undo, redo, actionLabel);
}
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);
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()) {
emit removeFromSelection(clipId);
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);
if (!res) {
undo();
return false;
} else {
unplantComposition(compositionId);
}
}
Fun operation = deregisterComposition_lambda(compositionId);
auto composition = m_allCompositions[compositionId];
Fun reverse = [this, composition]() {
// 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);
return true;
};
if (operation()) {
emit removeFromSelection(compositionId);
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) {
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)
{
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"));
}
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
Fun update_model = []() { return true; };
// 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 clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move group"));
}
return res;
}
bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo,
bool allowViewRefresh)
{
#ifdef LOGGING
m_logFile << "timeline->requestGroupMove(" << clipId << "," << groupId << " ," << delta_track << ", " << delta_pos << ", "
<< (updateView ? "true" : "false") << " ); " << std::endl;
#endif
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; };
// Sort clips. We need to delete from right to left to avoid confusing the view
std::vector sorted_clips(all_items.begin(), all_items.end());
std::sort(sorted_clips.begin(), sorted_clips.end(), [this](int clipId1, int clipId2) {
int p1 = isClip(clipId1) ? m_allClips[clipId1]->getPosition() : m_allCompositions[clipId1]->getPosition();
int p2 = isClip(clipId2) ? m_allClips[clipId2]->getPosition() : m_allCompositions[clipId2]->getPosition();
return p2 <= p1;
});
// 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 = []() { return true; };
// Check if there is a track move
bool updatePositionOnly = false;
if (delta_track == 0 && updateView) {
updateView = false;
allowViewRefresh = false;
updatePositionOnly = true;
update_model = [sorted_clips, 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);
}
return true;
};
}
// First, remove clips
std::unordered_map old_track_ids, old_position, old_forced_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);
old_position[item] = m_allClips[item]->getPosition();
} else {
// ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, 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;
}
}
}
// Second step, reinsert clips at correct positions
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;
}
// Reverse sort. We need to insert from left to right to avoid confusing the view
std::reverse(std::begin(sorted_clips), std::end(sorted_clips));
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;
bool updateThisView = allowViewRefresh;
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, local_undo, local_redo);
} 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)
{
#ifdef LOGGING
m_logFile << "timeline->requestGroupDeletion(" << clipId << " ); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = requestGroupDeletion(clipId, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Remove group"));
}
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();
if (m_temporarySelectionGroup == current_group) {
m_temporarySelectionGroup = -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) {
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;
}
int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize)
{
#ifdef LOGGING
m_logFile << "timeline->requestItemResize(" << itemId << "," << size << " ," << (right ? "true" : "false") << ", " << (logUndo ? "true" : "false") << ", "
<< (snapDistance > 0 ? "true" : "false") << " ); " << std::endl;
#endif
if (logUndo) {
qDebug() << "---------------------\n---------------------\nRESIZE W/UNDO CALLED\n++++++++++++++++\n++++";
}
QWriteLocker locker(&m_lock);
Q_ASSERT(isClip(itemId) || isComposition(itemId));
if (size <= 0) return -1;
int in = getItemPosition(itemId);
int out = in + getItemPlaytime(itemId);
if (snapDistance > 0) {
Fun temp_undo = []() { return true; };
Fun temp_redo = []() { return true; };
int proposed_size = m_snaps->proposeSize(in, out, size, right, snapDistance);
if (proposed_size > 0) {
// only test move if proposed_size is valid
bool success = false;
if (isClip(itemId)) {
success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false);
} else {
success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false);
}
if (success) {
temp_undo(); // undo temp move
size = proposed_size;
}
}
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
std::unordered_set all_items;
if (!allowSingleResize && m_groups->isInGroup(itemId)) {
int groupId = m_groups->getRootId(itemId);
auto items = m_groups->getLeaves(groupId);
for (int id : items) {
if (id == itemId) {
all_items.insert(id);
continue;
}
int start = getItemPosition(id);
int end = in + getItemPlaytime(id);
if (right) {
if (out == end) {
all_items.insert(id);
}
} else if (start == in) {
all_items.insert(id);
}
}
} else {
all_items.insert(itemId);
}
bool result = true;
for (int id : all_items) {
result = result && requestItemResize(id, size, right, logUndo, undo, redo);
}
if (!result) {
bool undone = undo();
Q_ASSERT(undone);
return -1;
}
if (result && logUndo) {
if (isClip(itemId)) {
PUSH_UNDO(undo, redo, i18n("Resize clip"));
} else {
PUSH_UNDO(undo, redo, i18n("Resize composition"));
}
}
return result ? size : -1;
}
bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo)
{
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
Fun update_model = [itemId, right, logUndo, this]() {
Q_ASSERT(isClip(itemId) || isComposition(itemId));
if (getItemTrackId(itemId) != -1) {
qDebug() << "++++++++++\nRESIZING ITEM: " << itemId << "\n+++++++";
QModelIndex modelIndex = isClip(itemId) ? makeClipIndexFromID(itemId) : makeCompositionIndexFromID(itemId);
notifyChange(modelIndex, modelIndex, !right, true, logUndo);
}
return true;
};
bool result = false;
if (isClip(itemId)) {
result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo);
} else {
Q_ASSERT(isComposition(itemId));
result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo);
}
if (result) {
if (!blockUndo) {
PUSH_LAMBDA(update_model, local_undo);
}
PUSH_LAMBDA(update_model, local_redo);
update_model();
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
}
return result;
}
int TimelineModel::requestClipsGroup(const std::unordered_set &ids, bool logUndo, GroupType type)
{
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
if (m_temporarySelectionGroup > -1) {
m_groups->destructGroupItem(m_temporarySelectionGroup);
// We don't log in undo the selection changes
// int firstChild = *m_groups->getDirectChildren(m_temporarySelectionGroup).begin();
// requestClipUngroup(firstChild, undo, redo);
m_temporarySelectionGroup = -1;
}
int result = requestClipsGroup(ids, undo, redo, type);
if (type == GroupType::Selection) {
m_temporarySelectionGroup = result;
}
if (result > -1 && logUndo && type != GroupType::Selection) {
PUSH_UNDO(undo, redo, i18n("Group clips"));
}
return result;
}
int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type)
{
#ifdef LOGGING
std::stringstream group;
m_logFile << "{" << std::endl;
m_logFile << "auto group = {";
bool deb = true;
for (int clipId : ids) {
if (deb)
deb = false;
else
group << ", ";
group << clipId;
}
m_logFile << group.str() << "};" << std::endl;
m_logFile << "timeline->requestClipsGroup(group);" << std::endl;
m_logFile << std::endl << "}" << std::endl;
#endif
QWriteLocker locker(&m_lock);
for (int id : ids) {
if (isClip(id)) {
if (getClipTrackId(id) == -1) {
return -1;
}
} else if (isComposition(id)) {
if (getCompositionTrackId(id) == -1) {
return -1;
}
} else if (!isGroup(id)) {
return -1;
}
}
int groupId = m_groups->groupItems(ids, undo, redo, type);
if (type == GroupType::Selection && *(ids.begin()) == groupId) {
// only one element selected, no group created
return -1;
}
return groupId;
}
bool TimelineModel::requestClipUngroup(int id, bool logUndo)
{
#ifdef LOGGING
m_logFile << "timeline->requestClipUngroup(" << id << " ); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = true;
if (id == m_temporarySelectionGroup) {
// Ungrouping selection group, so get id of all children
std::unordered_set leaves = m_groups->getDirectChildren(id);
// Delete selection group without undo
Fun tmp_undo = []() { return true; };
Fun tmp_redo = []() { return true; };
requestClipUngroup(id, tmp_undo, tmp_redo);
m_temporarySelectionGroup = -1;
} else {
result = requestClipUngroup(id, undo, redo);
}
if (result && logUndo) {
PUSH_UNDO(undo, redo, i18n("Ungroup clips"));
}
return result;
}
bool TimelineModel::requestClipUngroup(int id, Fun &undo, Fun &redo)
{
return m_groups->ungroupItem(id, undo, redo);
}
bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack)
{
#ifdef LOGGING
m_logFile << "timeline->requestTrackInsertion(" << position << ", dummy_id ); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
+ TRACE(position, id, trackName, audioTrack);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo);
if (result) {
PUSH_UNDO(undo, redo, i18n("Insert Track"));
}
+ TRACE_RES(result);
return result;
}
bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView)
{
// TODO: make sure we disable overlayTrack before inserting a track
if (position == -1) {
position = (int)(m_allTracks.size());
}
if (position < 0 || position > (int)m_allTracks.size()) {
return false;
}
int trackId = TimelineModel::getNextId();
id = trackId;
Fun local_undo = deregisterTrack_lambda(trackId, true);
TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack);
auto track = getTrackById(trackId);
Fun local_redo = [track, position, updateView, this]() {
// We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
// sufficient to register it.
registerTrack(track, position, updateView);
return true;
};
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestTrackDeletion(int trackId)
{
// TODO: make sure we disable overlayTrack before deleting a track
#ifdef LOGGING
m_logFile << "timeline->requestTrackDeletion(" << trackId << "); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = requestTrackDeletion(trackId, undo, redo);
if (result) {
if (m_videoTarget == trackId) {
m_videoTarget = -1;
}
if (m_audioTarget == trackId) {
m_audioTarget = -1;
}
PUSH_UNDO(undo, redo, i18n("Delete Track"));
}
return result;
}
bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo)
{
Q_ASSERT(isTrack(trackId));
std::vector clips_to_delete;
for (const auto &it : getTrackById(trackId)->m_allClips) {
clips_to_delete.push_back(it.first);
}
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
for (int clip : clips_to_delete) {
bool res = true;
while (res && m_groups->isInGroup(clip)) {
res = requestClipUngroup(clip, local_undo, local_redo);
}
if (res) {
res = requestClipDeletion(clip, local_undo, local_redo);
}
if (!res) {
bool u = local_undo();
Q_ASSERT(u);
return false;
}
}
int old_position = getTrackPosition(trackId);
auto operation = deregisterTrack_lambda(trackId, true);
std::shared_ptr track = getTrackById(trackId);
Fun reverse = [this, track, old_position]() {
// We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
// sufficient to register it.
registerTrack(track, old_position);
return true;
};
if (operation()) {
UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
local_undo();
return false;
}
void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert, bool reloadView)
{
qDebug() << "REGISTER TRACK" << track->getId() << pos;
int id = track->getId();
if (pos == -1) {
pos = static_cast(m_allTracks.size());
}
Q_ASSERT(pos >= 0);
Q_ASSERT(pos <= static_cast(m_allTracks.size()));
// effective insertion (MLT operation), add 1 to account for black background track
if (doInsert) {
int error = m_tractor->insert_track(*track, pos + 1);
Q_ASSERT(error == 0); // we might need better error handling...
}
// we now insert in the list
auto posIt = m_allTracks.begin();
std::advance(posIt, pos);
auto it = m_allTracks.insert(posIt, std::move(track));
// it now contains the iterator to the inserted element, we store it
Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen)
m_iteratorTable[id] = it;
if (reloadView) {
// don't reload view on each track load on project opening
_resetView();
}
}
void TimelineModel::registerClip(const std::shared_ptr &clip, bool registerProducer)
{
int id = clip->getId();
qDebug() << " // /REQUEST TL CLP REGSTR: " << id << "\n--------\nCLIPS COUNT: " << m_allClips.size();
Q_ASSERT(m_allClips.count(id) == 0);
m_allClips[id] = clip;
clip->registerClipToBin(clip->getProducer(), registerProducer);
m_groups->createGroupItem(id);
clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled);
}
void TimelineModel::registerGroup(int groupId)
{
Q_ASSERT(m_allGroups.count(groupId) == 0);
m_allGroups.insert(groupId);
}
Fun TimelineModel::deregisterTrack_lambda(int id, bool updateView)
{
return [this, id, updateView]() {
qDebug() << "DEREGISTER TRACK" << id;
auto it = m_iteratorTable[id]; // iterator to the element
int index = getTrackPosition(id); // compute index in list
m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track
// send update to the model
m_allTracks.erase(it); // actual deletion of object
m_iteratorTable.erase(id); // clean table
if (updateView) {
QModelIndex root;
_resetView();
}
return true;
};
}
Fun TimelineModel::deregisterClip_lambda(int clipId)
{
return [this, clipId]() {
// qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size();
clearAssetView(clipId);
Q_ASSERT(m_allClips.count(clipId) > 0);
Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point
Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point
auto clip = m_allClips[clipId];
m_allClips.erase(clipId);
clip->deregisterClipToBin();
m_groups->destructGroupItem(clipId);
return true;
};
}
void TimelineModel::deregisterGroup(int id)
{
Q_ASSERT(m_allGroups.count(id) > 0);
m_allGroups.erase(id);
}
std::shared_ptr TimelineModel::getTrackById(int trackId)
{
Q_ASSERT(m_iteratorTable.count(trackId) > 0);
return *m_iteratorTable[trackId];
}
const std::shared_ptr TimelineModel::getTrackById_const(int trackId) const
{
Q_ASSERT(m_iteratorTable.count(trackId) > 0);
return *m_iteratorTable.at(trackId);
}
bool TimelineModel::addTrackEffect(int trackId, const QString &effectId)
{
Q_ASSERT(m_iteratorTable.count(trackId) > 0);
if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) {
QString effectName = EffectsRepository::get()->getName(effectId);
pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500);
return false;
}
return true;
}
bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId)
{
QStringList source = sourceId.split(QLatin1Char('-'));
Q_ASSERT(m_iteratorTable.count(trackId) > 0 && source.count() == 3);
int itemType = source.at(0).toInt();
int itemId = source.at(1).toInt();
int itemRow = source.at(2).toInt();
std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId);
if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) {
pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500);
return false;
}
return true;
}
std::shared_ptr TimelineModel::getClipPtr(int clipId) const
{
Q_ASSERT(m_allClips.count(clipId) > 0);
return m_allClips.at(clipId);
}
bool TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify)
{
Q_ASSERT(m_allClips.count(clipId) > 0);
bool result = m_allClips.at(clipId)->addEffect(effectId);
if (!result && notify) {
QString effectName = EffectsRepository::get()->getName(effectId);
pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500);
}
return result;
}
bool TimelineModel::removeFade(int clipId, bool fromStart)
{
Q_ASSERT(m_allClips.count(clipId) > 0);
return m_allClips.at(clipId)->removeFade(fromStart);
}
std::shared_ptr TimelineModel::getClipEffectStack(int itemId)
{
Q_ASSERT(m_allClips.count(itemId));
return m_allClips.at(itemId)->m_effectStack;
}
bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId)
{
QStringList source = sourceId.split(QLatin1Char('-'));
Q_ASSERT(m_allClips.count(clipId) && source.count() == 3);
int itemType = source.at(0).toInt();
int itemId = source.at(1).toInt();
int itemRow = source.at(2).toInt();
std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId);
return m_allClips.at(clipId)->copyEffect(effectStack, itemRow);
}
bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration)
{
Q_ASSERT(m_allClips.count(clipId));
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo);
if (res && initialDuration > 0) {
PUSH_UNDO(undo, redo, i18n("Adjust Fade"));
}
return res;
}
std::shared_ptr TimelineModel::getCompositionPtr(int compoId) const
{
Q_ASSERT(m_allCompositions.count(compoId) > 0);
return m_allCompositions.at(compoId);
}
int TimelineModel::getNextId()
{
return TimelineModel::next_id++;
}
bool TimelineModel::isClip(int id) const
{
return m_allClips.count(id) > 0;
}
bool TimelineModel::isComposition(int id) const
{
return m_allCompositions.count(id) > 0;
}
bool TimelineModel::isTrack(int id) const
{
return m_iteratorTable.count(id) > 0;
}
bool TimelineModel::isGroup(int id) const
{
return m_allGroups.count(id) > 0;
}
void TimelineModel::updateDuration()
{
int current = m_blackClip->get_playtime() - TimelineModel::seekDuration;
int duration = 0;
for (const auto &tck : m_iteratorTable) {
auto track = (*tck.second);
duration = qMax(duration, track->trackDuration());
}
if (duration != current) {
// update black track length
m_blackClip->set_in_and_out(0, duration + TimelineModel::seekDuration);
emit durationUpdated();
}
}
int TimelineModel::duration() const
{
return m_tractor->get_playtime() - TimelineModel::seekDuration;
}
std::unordered_set TimelineModel::getGroupElements(int clipId)
{
int groupId = m_groups->getRootId(clipId);
return m_groups->getLeaves(groupId);
}
Mlt::Profile *TimelineModel::getProfile()
{
return m_profile;
}
bool TimelineModel::requestReset(Fun &undo, Fun &redo)
{
std::vector all_ids;
for (const auto &track : m_iteratorTable) {
all_ids.push_back(track.first);
}
bool ok = true;
for (int trackId : all_ids) {
ok = ok && requestTrackDeletion(trackId, undo, redo);
}
return ok;
}
void TimelineModel::setUndoStack(std::weak_ptr undo_stack)
{
m_undoStack = std::move(undo_stack);
}
int TimelineModel::suggestSnapPoint(int pos, int snapDistance)
{
int snapped = m_snaps->getClosestPoint(pos);
return (qAbs(snapped - pos) < snapDistance ? snapped : pos);
}
int TimelineModel::requestBestSnapPos(int pos, int length, const std::vector &pts, int snapDistance)
{
if (!pts.empty()) {
m_snaps->ignore(pts);
}
int snapped_start = m_snaps->getClosestPoint(pos);
int snapped_end = m_snaps->getClosestPoint(pos + length);
m_snaps->unIgnore();
int startDiff = qAbs(pos - snapped_start);
int endDiff = qAbs(pos + length - snapped_end);
if (startDiff < endDiff && startDiff <= snapDistance) {
// snap to start
return snapped_start;
}
if (endDiff <= snapDistance) {
// snap to end
return snapped_end - length;
}
return -1;
}
int TimelineModel::requestNextSnapPos(int pos)
{
return m_snaps->getNextPoint(pos);
}
int TimelineModel::requestPreviousSnapPos(int pos)
{
return m_snaps->getPreviousPoint(pos);
}
void TimelineModel::addSnap(int pos)
{
return m_snaps->addPoint(pos);
}
void TimelineModel::removeSnap(int pos)
{
return m_snaps->removePoint(pos);
}
void TimelineModel::registerComposition(const std::shared_ptr &composition)
{
int id = composition->getId();
Q_ASSERT(m_allCompositions.count(id) == 0);
m_allCompositions[id] = composition;
m_groups->createGroupItem(id);
}
bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, Mlt::Properties *transProps, int &id,
bool logUndo)
{
#ifdef LOGGING
m_logFile << "timeline->requestCompositionInsertion(\"composite\"," << trackId << " ," << position << "," << length << ", dummy_id );" << std::endl;
#endif
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, transProps, id, undo, redo, logUndo);
if (result && logUndo) {
PUSH_UNDO(undo, redo, i18n("Insert Composition"));
}
return result;
}
bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length,
Mlt::Properties *transProps, int &id, Fun &undo, Fun &redo, bool finalMove)
{
qDebug() << "Inserting compo track" << trackId << "pos" << position << "length" << length;
int compositionId = TimelineModel::getNextId();
id = compositionId;
Fun local_undo = deregisterComposition_lambda(compositionId);
CompositionModel::construct(shared_from_this(), transitionId, compositionId, transProps);
auto composition = m_allCompositions[compositionId];
Fun local_redo = [composition, this]() {
// 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);
return true;
};
bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo);
qDebug() << "trying to move" << trackId << "pos" << position << "success " << res;
if (res) {
res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true);
qDebug() << "trying to resize" << compositionId << "length" << length << "success " << res;
}
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
id = -1;
return false;
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
Fun TimelineModel::deregisterComposition_lambda(int compoId)
{
return [this, compoId]() {
Q_ASSERT(m_allCompositions.count(compoId) > 0);
Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point
clearAssetView(compoId);
m_allCompositions.erase(compoId);
m_groups->destructGroupItem(compoId);
return true;
};
}
int TimelineModel::getCompositionPosition(int compoId) const
{
Q_ASSERT(m_allCompositions.count(compoId) > 0);
const auto trans = m_allCompositions.at(compoId);
return trans->getPosition();
}
int TimelineModel::getCompositionPlaytime(int compoId) const
{
READ_LOCK();
Q_ASSERT(m_allCompositions.count(compoId) > 0);
const auto trans = m_allCompositions.at(compoId);
int playtime = trans->getPlaytime();
return playtime;
}
int TimelineModel::getItemPosition(int itemId) const
{
if (isClip(itemId)) {
return getClipPosition(itemId);
}
return getCompositionPosition(itemId);
}
int TimelineModel::getItemPlaytime(int itemId) const
{
if (isClip(itemId)) {
return getClipPlaytime(itemId);
}
return getCompositionPlaytime(itemId);
}
int TimelineModel::getTrackCompositionsCount(int trackId) const
{
Q_ASSERT(isTrack(trackId));
return getTrackById_const(trackId)->getCompositionsCount();
}
bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo)
{
#ifdef LOGGING
m_logFile << "timeline->requestCompositionMove(" << compoId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", "
<< (logUndo ? "true" : "false") << " ); " << std::endl;
#endif
QWriteLocker locker(&m_lock);
Q_ASSERT(isComposition(compoId));
if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) {
return true;
}
if (m_groups->isInGroup(compoId)) {
// element is in a group.
int groupId = m_groups->getRootId(compoId);
int current_trackId = getCompositionTrackId(compoId);
int track_pos1 = getTrackPosition(trackId);
int track_pos2 = getTrackPosition(current_trackId);
int delta_track = track_pos1 - track_pos2;
int delta_pos = position - m_allCompositions[compoId]->getPosition();
return requestGroupMove(compoId, groupId, delta_track, delta_pos, updateView, logUndo);
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
int min = getCompositionPosition(compoId);
int max = min + getCompositionPlaytime(compoId);
int tk = getCompositionTrackId(compoId);
bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo);
if (tk > -1) {
min = qMin(min, getCompositionPosition(compoId));
max = qMax(max, getCompositionPosition(compoId));
} else {
min = getCompositionPosition(compoId);
max = min + getCompositionPlaytime(compoId);
}
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move composition"));
checkRefresh(min, max);
}
return res;
}
bool TimelineModel::isAudioTrack(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
return (*it)->isAudioTrack();
}
bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(isComposition(compoId));
Q_ASSERT(isTrack(trackId));
if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) {
// qDebug() << "// compo track: " << trackId << ", PREVIOUS TK: " << getPreviousVideoTrackPos(trackId);
compositionTrack = getPreviousVideoTrackPos(trackId);
}
if (compositionTrack == -1) {
// it doesn't make sense to insert a composition on the last track
qDebug() << "Move failed because of last track";
return false;
}
qDebug() << "Requesting composition move" << trackId << "," << position << " ( " << compositionTrack << " / "
<< (compositionTrack > 0 ? getTrackIndexFromPosition(compositionTrack - 1) : 0);
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
bool ok = true;
int old_trackId = getCompositionTrackId(compoId);
bool notifyViewOnly = false;
Fun update_model = []() { return true; };
if (updateView && old_trackId == trackId) {
// Move on same track, only send view update
updateView = false;
notifyViewOnly = true;
update_model = [compoId, finalMove, this]() {
QModelIndex modelIndex = makeCompositionIndexFromID(compoId);
notifyChange(modelIndex, modelIndex, {StartRole});
return true;
};
}
if (old_trackId != -1) {
Fun delete_operation = []() { return true; };
Fun delete_reverse = []() { return true; };
if (old_trackId != trackId) {
delete_operation = [this, compoId]() {
bool res = unplantComposition(compoId);
if (res) m_allCompositions[compoId]->setATrack(-1, -1);
return res;
};
int oldAtrack = m_allCompositions[compoId]->getATrack();
delete_reverse = [this, compoId, oldAtrack, updateView]() {
m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack <= 0 ? -1 : getTrackIndexFromPosition(oldAtrack - 1));
return replantCompositions(compoId, updateView);
};
}
ok = delete_operation();
if (!ok) qDebug() << "Move failed because of first delete operation";
if (ok) {
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_undo);
}
UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo);
ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo);
}
if (!ok) {
qDebug() << "Move failed because of first deletion request";
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo);
if (!ok) qDebug() << "Move failed because of second insertion request";
if (ok) {
Fun insert_operation = []() { return true; };
Fun insert_reverse = []() { return true; };
if (old_trackId != trackId) {
insert_operation = [this, compoId, trackId, compositionTrack, updateView]() {
qDebug() << "-------------- ATRACK ----------------\n" << compositionTrack << " = " << getTrackIndexFromPosition(compositionTrack);
m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack <= 0 ? -1 : getTrackIndexFromPosition(compositionTrack - 1));
return replantCompositions(compoId, updateView);
};
insert_reverse = [this, compoId]() {
bool res = unplantComposition(compoId);
if (res) m_allCompositions[compoId]->setATrack(-1, -1);
return res;
};
}
ok = insert_operation();
if (!ok) qDebug() << "Move failed because of second insert operation";
if (ok) {
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_redo);
}
UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo);
}
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
update_model();
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::replantCompositions(int currentCompo, bool updateView)
{
// We ensure that the compositions are planted in a decreasing order of b_track.
// For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order.
std::vector> compos;
for (const auto &compo : m_allCompositions) {
int trackId = compo.second->getCurrentTrackId();
if (trackId == -1 || compo.second->getATrack() == -1) {
continue;
}
// Note: we need to retrieve the position of the track, that is its melt index.
int trackPos = getTrackMltIndex(trackId);
compos.push_back({trackPos, compo.first});
if (compo.first != currentCompo) {
unplantComposition(compo.first);
}
}
// sort by decreasing b_track
std::sort(compos.begin(), compos.end(), [](const std::pair &a, const std::pair &b) { return a.first > b.first; });
// replant
QScopedPointer field(m_tractor->field());
field->lock();
// Unplant track compositing
mlt_service nextservice = mlt_service_get_producer(field->get_service());
mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice);
QString resource = mlt_properties_get(properties, "mlt_service");
mlt_service_type mlt_type = mlt_service_identify(nextservice);
QList trackCompositions;
while (mlt_type == transition_type) {
Mlt::Transition transition((mlt_transition)nextservice);
nextservice = mlt_service_producer(nextservice);
int internal = transition.get_int("internal_added");
if (internal > 0 && resource != QLatin1String("mix")) {
trackCompositions << new Mlt::Transition(transition);
field->disconnect_service(transition);
transition.disconnect_all_producers();
}
if (nextservice == nullptr) {
break;
}
mlt_type = mlt_service_identify(nextservice);
properties = MLT_SERVICE_PROPERTIES(nextservice);
resource = mlt_properties_get(properties, "mlt_service");
}
// Sort track compositing
std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); });
for (const auto &compo : compos) {
int aTrack = m_allCompositions[compo.second]->getATrack();
Q_ASSERT(aTrack != -1);
int ret = field->plant_transition(*m_allCompositions[compo.second].get(), aTrack, compo.first);
qDebug() << "Planting composition " << compo.second << "in " << aTrack << "/" << compo.first << "IN = " << m_allCompositions[compo.second]->getIn()
<< "OUT = " << m_allCompositions[compo.second]->getOut() << "ret=" << ret;
Mlt::Transition &transition = *m_allCompositions[compo.second].get();
transition.set_tracks(aTrack, compo.first);
mlt_service consumer = mlt_service_consumer(transition.get_service());
Q_ASSERT(consumer != nullptr);
if (ret != 0) {
field->unlock();
return false;
}
}
// Replant last tracks compositing
while (!trackCompositions.isEmpty()) {
Mlt::Transition *firstTr = trackCompositions.takeFirst();
field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track());
}
field->unlock();
if (updateView) {
QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo);
notifyChange(modelIndex, modelIndex, {ItemATrack});
}
return true;
}
bool TimelineModel::unplantComposition(int compoId)
{
qDebug() << "Unplanting" << compoId;
Mlt::Transition &transition = *m_allCompositions[compoId].get();
mlt_service consumer = mlt_service_consumer(transition.get_service());
Q_ASSERT(consumer != nullptr);
QScopedPointer field(m_tractor->field());
field->lock();
field->disconnect_service(transition);
int ret = transition.disconnect_all_producers();
mlt_service nextservice = mlt_service_get_producer(transition.get_service());
// mlt_service consumer = mlt_service_consumer(transition.get_service());
Q_ASSERT(nextservice == nullptr);
// Q_ASSERT(consumer == nullptr);
field->unlock();
return ret != 0;
}
bool TimelineModel::checkConsistency()
{
for (const auto &tck : m_iteratorTable) {
auto track = (*tck.second);
// Check parent/children link for tracks
if (auto ptr = track->m_parent.lock()) {
if (ptr.get() != this) {
qDebug() << "Wrong parent for track" << tck.first;
return false;
}
} else {
qDebug() << "NULL parent for track" << tck.first;
return false;
}
// check consistency of track
if (!track->checkConsistency()) {
qDebug() << "Consistency check failed for track" << tck.first;
return false;
}
}
// We store all in/outs of clips to check snap points
std::map snaps;
// Check parent/children link for clips
for (const auto &cp : m_allClips) {
auto clip = (cp.second);
// Check parent/children link for tracks
if (auto ptr = clip->m_parent.lock()) {
if (ptr.get() != this) {
qDebug() << "Wrong parent for clip" << cp.first;
return false;
}
} else {
qDebug() << "NULL parent for clip" << cp.first;
return false;
}
if (getClipTrackId(cp.first) != -1) {
snaps[clip->getPosition()] += 1;
snaps[clip->getPosition() + clip->getPlaytime()] += 1;
}
}
for (const auto &cp : m_allCompositions) {
auto clip = (cp.second);
// Check parent/children link for tracks
if (auto ptr = clip->m_parent.lock()) {
if (ptr.get() != this) {
qDebug() << "Wrong parent for compo" << cp.first;
return false;
}
} else {
qDebug() << "NULL parent for compo" << cp.first;
return false;
}
if (getCompositionTrackId(cp.first) != -1) {
snaps[clip->getPosition()] += 1;
snaps[clip->getPosition() + clip->getPlaytime()] += 1;
}
}
// Check snaps
auto stored_snaps = m_snaps->_snaps();
if (snaps.size() != stored_snaps.size()) {
qDebug() << "Wrong number of snaps: " << snaps.size() << " == " << stored_snaps.size();
return false;
}
for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) {
if (*i != *j) {
qDebug() << "Wrong snap info at point" << (*i).first;
return false;
}
}
// We check consistency with bin model
auto binClips = pCore->projectItemModel()->getAllClipIds();
// First step: all clips referenced by the bin model exist and are inserted
for (const auto &binClip : binClips) {
auto projClip = pCore->projectItemModel()->getClipByBinID(binClip);
for (const auto &insertedClip : projClip->m_registeredClips) {
if (auto ptr = insertedClip.second.lock()) {
if (ptr.get() == this) { // check we are talking of this timeline
if (!isClip(insertedClip.first)) {
qDebug() << "Bin model registers a bad clip ID" << insertedClip.first;
return false;
}
}
} else {
qDebug() << "Bin model registers a clip in a NULL timeline" << insertedClip.first;
return false;
}
}
}
// Second step: all clips are referenced
for (const auto &clip : m_allClips) {
auto binId = clip.second->m_binClipId;
auto projClip = pCore->projectItemModel()->getClipByBinID(binId);
if (projClip->m_registeredClips.count(clip.first) == 0) {
qDebug() << "Clip " << clip.first << "not registered in bin";
return false;
}
}
// We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our
// m_allCompositions
std::unordered_set remaining_compo;
for (const auto &compo : m_allCompositions) {
if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) {
remaining_compo.insert(compo.first);
// check validity of the consumer
Mlt::Transition &transition = *m_allCompositions[compo.first].get();
mlt_service consumer = mlt_service_consumer(transition.get_service());
Q_ASSERT(consumer != nullptr);
}
}
QScopedPointer field(m_tractor->field());
field->lock();
mlt_service nextservice = mlt_service_get_producer(field->get_service());
mlt_service_type mlt_type = mlt_service_identify(nextservice);
while (nextservice != nullptr) {
if (mlt_type == transition_type) {
mlt_transition tr = (mlt_transition)nextservice;
int currentTrack = mlt_transition_get_b_track(tr);
int currentATrack = mlt_transition_get_a_track(tr);
int currentIn = (int)mlt_transition_get_in(tr);
int currentOut = (int)mlt_transition_get_out(tr);
qDebug() << "looking composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack;
int foundId = -1;
// we iterate to try to find a matching compo
for (int compoId : remaining_compo) {
if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack &&
m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) {
foundId = compoId;
break;
}
}
if (foundId == -1) {
qDebug() << "Error, we didn't find matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / "
<< currentATrack;
field->unlock();
return false;
}
qDebug() << "Found";
remaining_compo.erase(foundId);
}
nextservice = mlt_service_producer(nextservice);
if (nextservice == nullptr) {
break;
}
mlt_type = mlt_service_identify(nextservice);
}
field->unlock();
if (!remaining_compo.empty()) {
qDebug() << "Error: We found less compositions than expected. Compositions that have not been found:";
for (int compoId : remaining_compo) {
qDebug() << compoId;
}
return false;
}
// We check consistency of groups
if (!m_groups->checkConsistency(true, true)) {
qDebug() << "== ERROR IN GROUP CONSISTENCY";
return false;
}
return true;
}
void TimelineModel::setTimelineEffectsEnabled(bool enabled)
{
m_timelineEffectsEnabled = enabled;
// propagate info to clips
for (const auto &clip : m_allClips) {
clip.second->setTimelineEffectsEnabled(enabled);
}
// TODO if we support track effects, they should be disabled here too
}
Mlt::Producer *TimelineModel::producer()
{
auto *prod = new Mlt::Producer(tractor());
return prod;
}
void TimelineModel::checkRefresh(int start, int end)
{
int currentPos = tractor()->position();
if (currentPos >= start && currentPos < end) {
emit requestMonitorRefresh();
}
}
void TimelineModel::clearAssetView(int itemId)
{
emit requestClearAssetView(itemId);
}
std::shared_ptr TimelineModel::getCompositionParameterModel(int compoId) const
{
READ_LOCK();
Q_ASSERT(isComposition(compoId));
return std::static_pointer_cast(m_allCompositions.at(compoId));
}
std::shared_ptr TimelineModel::getClipEffectStackModel(int clipId) const
{
READ_LOCK();
Q_ASSERT(isClip(clipId));
return std::static_pointer_cast(m_allClips.at(clipId)->m_effectStack);
}
std::shared_ptr TimelineModel::getTrackEffectStackModel(int trackId)
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
return getTrackById(trackId)->m_effectStack;
}
QStringList TimelineModel::extractCompositionLumas() const
{
QStringList urls;
for (const auto &compo : m_allCompositions) {
QString luma = compo.second->getProperty(QStringLiteral("resource"));
if (!luma.isEmpty()) {
urls << QUrl::fromLocalFile(luma).toLocalFile();
}
}
urls.removeDuplicates();
return urls;
}
void TimelineModel::adjustAssetRange(int clipId, int in, int out)
{
Q_UNUSED(clipId)
Q_UNUSED(in)
Q_UNUSED(out)
// pCore->adjustAssetRange(clipId, in, out);
}
void TimelineModel::requestClipReload(int clipId)
{
std::function local_undo = []() { return true; };
std::function local_redo = []() { return true; };
// in order to make the producer change effective, we need to unplant / replant the clip in int track
int old_trackId = getClipTrackId(clipId);
int oldPos = getClipPosition(clipId);
int oldOut = getClipIn(clipId) + getClipPlaytime(clipId);
// Check if clip out is longer than actual producer duration (if user forced duration)
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId));
bool refreshView = oldOut > (int)binClip->frameDuration();
if (old_trackId != -1) {
getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo);
}
m_allClips[clipId]->refreshProducerFromBin();
if (old_trackId != -1) {
getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo);
}
}
void TimelineModel::replugClip(int clipId)
{
int old_trackId = getClipTrackId(clipId);
if (old_trackId != -1) {
getTrackById(old_trackId)->replugClip(clipId);
}
}
void TimelineModel::requestClipUpdate(int clipId, const QVector &roles)
{
QModelIndex modelIndex = makeClipIndexFromID(clipId);
if (roles.contains(TimelineModel::ReloadThumbRole)) {
m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload;
}
notifyChange(modelIndex, modelIndex, roles);
}
bool TimelineModel::requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed())) {
return true;
}
std::function local_undo = []() { return true; };
std::function local_redo = []() { return true; };
int oldPos = getClipPosition(clipId);
// in order to make the producer change effective, we need to unplant / replant the clip in int track
bool success = true;
int trackId = getClipTrackId(clipId);
if (trackId != -1) {
success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo);
}
if (success) {
success = m_allClips[clipId]->useTimewarpProducer(speed, local_undo, local_redo);
}
if (trackId != -1) {
success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo);
}
if (!success) {
local_undo();
return false;
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return success;
}
bool TimelineModel::requestClipTimeWarp(int clipId, double speed)
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
// Get main clip info
int trackId = getClipTrackId(clipId);
bool result = true;
if (trackId != -1) {
// Check if clip has a split partner
int splitId = m_groups->getSplitPartner(clipId);
if (splitId > -1) {
result = requestClipTimeWarp(splitId, speed / 100.0, undo, redo);
}
if (result) {
result = requestClipTimeWarp(clipId, speed / 100.0, undo, redo);
} else {
pCore->displayMessage(i18n("Change speed failed"), ErrorMessage);
undo();
return false;
}
} else {
// If clip is not inserted on a track, we just change the producer
m_allClips[clipId]->useTimewarpProducer(speed, undo, redo);
}
if (result) {
PUSH_UNDO(undo, redo, i18n("Change clip speed"));
return true;
}
return false;
}
const QString TimelineModel::getTrackTagById(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
bool isAudio = getTrackById_const(trackId)->isAudioTrack();
int count = 1;
int totalAudio = 2;
auto it = m_allTracks.begin();
bool found = false;
while ((isAudio || !found) && it != m_allTracks.end()) {
if ((*it)->isAudioTrack()) {
totalAudio++;
if (isAudio && !found) {
count++;
}
} else if (!isAudio) {
count++;
}
if ((*it)->getId() == trackId) {
found = true;
}
it++;
}
return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1);
}
void TimelineModel::updateProfile(Mlt::Profile *profile)
{
m_profile = profile;
m_tractor->set_profile(*m_profile);
}
int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
int trackId = getClipTrackId(clipId);
if (trackId != -1) {
return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after);
}
return 0;
}
int TimelineModel::getPreviousTrackId(int trackId)
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
bool audioWanted = (*it)->isAudioTrack();
while (it != m_allTracks.begin()) {
--it;
if (it != m_allTracks.begin() && (*it)->isAudioTrack() == audioWanted) {
break;
}
}
return it == m_allTracks.begin() ? trackId : (*it)->getId();
}
int TimelineModel::getNextTrackId(int trackId)
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
bool audioWanted = (*it)->isAudioTrack();
while (it != m_allTracks.end()) {
++it;
if (it != m_allTracks.end() && (*it)->isAudioTrack() == audioWanted) {
break;
}
}
return it == m_allTracks.end() ? trackId : (*it)->getId();
}
diff --git a/src/timeline2/model/trackmodel.cpp b/src/timeline2/model/trackmodel.cpp
index 5fc6cfaed..e56483a83 100644
--- a/src/timeline2/model/trackmodel.cpp
+++ b/src/timeline2/model/trackmodel.cpp
@@ -1,1139 +1,1141 @@
/***************************************************************************
* 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 "trackmodel.hpp"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "kdenlivesettings.h"
+#include "logger.hpp"
#include "snapmodel.hpp"
#include "timelinemodel.hpp"
#include
#include
#include
#include
TrackModel::TrackModel(const std::weak_ptr &parent, int id, const QString &trackName, bool audioTrack)
: m_parent(parent)
, m_id(id == -1 ? TimelineModel::getNextId() : id)
, m_lock(QReadWriteLock::Recursive)
{
if (auto ptr = parent.lock()) {
m_track = std::shared_ptr(new Mlt::Tractor(*ptr->getProfile()));
m_playlists[0].set_profile(*ptr->getProfile());
m_playlists[1].set_profile(*ptr->getProfile());
m_track->insert_track(m_playlists[0], 0);
m_track->insert_track(m_playlists[1], 1);
if (!trackName.isEmpty()) {
m_track->set("kdenlive:track_name", trackName.toUtf8().constData());
}
if (audioTrack) {
m_track->set("kdenlive:audio_track", 1);
for (int i = 0; i < 2; i++) {
m_playlists[i].set("hide", 1);
}
}
m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight());
m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector roles) {
if (auto ptr2 = m_parent.lock()) {
QModelIndex ix = ptr2->makeTrackIndexFromID(m_id);
ptr2->dataChanged(ix, ix, roles);
}
});
} else {
qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
Q_ASSERT(false);
}
}
TrackModel::TrackModel(const std::weak_ptr &parent, Mlt::Tractor mltTrack, int id)
: m_parent(parent)
, m_id(id == -1 ? TimelineModel::getNextId() : id)
{
if (auto ptr = parent.lock()) {
m_track = std::shared_ptr(new Mlt::Tractor(mltTrack));
m_playlists[0] = *m_track->track(0);
m_playlists[1] = *m_track->track(1);
m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
} else {
qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
Q_ASSERT(false);
}
}
TrackModel::~TrackModel()
{
m_track->remove_track(1);
m_track->remove_track(0);
}
int TrackModel::construct(const std::weak_ptr &parent, int id, int pos, const QString &trackName, bool audioTrack)
{
std::shared_ptr track(new TrackModel(parent, id, trackName, audioTrack));
+ TRACE_CONSTR(track.get(), parent, id, pos, trackName, audioTrack);
id = track->m_id;
if (auto ptr = parent.lock()) {
ptr->registerTrack(std::move(track), pos);
} else {
qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
Q_ASSERT(false);
}
return id;
}
int TrackModel::getClipsCount()
{
READ_LOCK();
#ifdef QT_DEBUG
int count = 0;
for (int j = 0; j < 2; j++) {
for (int i = 0; i < m_playlists[j].count(); i++) {
if (!m_playlists[j].is_blank(i)) {
count++;
}
}
}
Q_ASSERT(count == static_cast(m_allClips.size()));
#else
int count = (int)m_allClips.size();
#endif
return count;
}
Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove)
{
QWriteLocker locker(&m_lock);
// By default, insertion occurs in topmost track
// Find out the clip id at position
int target_clip = m_playlists[0].get_clip_index_at(position);
int count = m_playlists[0].count();
// we create the function that has to be executed after the melt order. This is essentially book-keeping
auto end_function = [clipId, this, position, updateView, finalMove]() {
if (auto ptr = m_parent.lock()) {
std::shared_ptr clip = ptr->getClipPtr(clipId);
m_allClips[clip->getId()] = clip; // store clip
// update clip position and track
clip->setPosition(position);
clip->setCurrentTrackId(m_id);
int new_in = clip->getPosition();
int new_out = new_in + clip->getPlaytime();
ptr->m_snaps->addPoint(new_in);
ptr->m_snaps->addPoint(new_out);
if (updateView) {
int clip_index = getRowfromClip(clipId);
ptr->_beginInsertRows(ptr->makeTrackIndexFromID(m_id), clip_index, clip_index);
ptr->_endInsertRows();
bool audioOnly = clip->isAudioOnly();
if (!audioOnly && !isHidden() && !isAudioTrack()) {
// only refresh monitor if not an audio track and not hidden
ptr->checkRefresh(new_in, new_out);
}
if (!audioOnly && finalMove && !isAudioTrack()) {
ptr->invalidateZone(new_in, new_out);
}
}
return true;
}
qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
return false;
};
if (target_clip >= count && isBlankAt(position)) {
// In that case, we append after, in the first playlist
return [this, position, clipId, end_function, finalMove]() {
if (auto ptr = m_parent.lock()) {
// Lock MLT playlist so that we don't end up with an invalid frame being displayed
m_playlists[0].lock();
std::shared_ptr clip = ptr->getClipPtr(clipId);
int index = m_playlists[0].insert_at(position, *clip, 1);
m_playlists[0].consolidate_blanks();
m_playlists[0].unlock();
if (finalMove) {
ptr->updateDuration();
}
return index != -1 && end_function();
}
qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
return false;
};
}
if (isBlankAt(position)) {
int blank_end = getBlankEnd(position);
int length = -1;
if (auto ptr = m_parent.lock()) {
std::shared_ptr clip = ptr->getClipPtr(clipId);
length = clip->getPlaytime();
}
if (blank_end >= position + length) {
return [this, position, clipId, end_function]() {
if (auto ptr = m_parent.lock()) {
// Lock MLT playlist so that we don't end up with an invalid frame being displayed
m_playlists[0].lock();
std::shared_ptr clip = ptr->getClipPtr(clipId);
int index = m_playlists[0].insert_at(position, *clip, 1);
m_playlists[0].consolidate_blanks();
m_playlists[0].unlock();
return index != -1 && end_function();
}
qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
return false;
};
}
}
return []() { return false; };
}
bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
if (isLocked()) {
return false;
}
if (auto ptr = m_parent.lock()) {
if (isAudioTrack() && !ptr->getClipPtr(clipId)->canBeAudio()) {
qDebug() << "// ATTEMPTING TO INSERT NON AUDIO CLIP ON AUDIO TRACK";
return false;
}
if (!isAudioTrack() && !ptr->getClipPtr(clipId)->canBeVideo()) {
qDebug() << "// ATTEMPTING TO INSERT NON VIDEO CLIP ON VIDEO TRACK";
return false;
}
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
bool res = true;
if (ptr->getClipPtr(clipId)->clipState() != PlaylistState::Disabled) {
res = res && ptr->getClipPtr(clipId)->setClipState(isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo);
}
auto operation = requestClipInsertion_lambda(clipId, position, updateView, finalMove);
res = res && operation();
if (res) {
auto reverse = requestClipDeletion_lambda(clipId, updateView, finalMove);
UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
return false;
}
void TrackModel::replugClip(int clipId)
{
QWriteLocker locker(&m_lock);
int clip_position = m_allClips[clipId]->getPosition();
auto clip_loc = getClipIndexAt(clip_position);
int target_track = clip_loc.first;
int target_clip = clip_loc.second;
// lock MLT playlist so that we don't end up with invalid frames in monitor
m_playlists[target_track].lock();
Q_ASSERT(target_clip < m_playlists[target_track].count());
Q_ASSERT(!m_playlists[target_track].is_blank(target_clip));
std::unique_ptr prod(m_playlists[target_track].replace_with_blank(target_clip));
if (auto ptr = m_parent.lock()) {
std::shared_ptr clip = ptr->getClipPtr(clipId);
m_playlists[target_track].insert_at(clip_position, *clip, 1);
if (!clip->isAudioOnly() && !isAudioTrack()) {
ptr->invalidateZone(clip->getIn(), clip->getOut());
}
if (!clip->isAudioOnly() && !isHidden() && !isAudioTrack()) {
// only refresh monitor if not an audio track and not hidden
ptr->checkRefresh(clip->getIn(), clip->getOut());
}
}
m_playlists[target_track].consolidate_blanks();
m_playlists[target_track].unlock();
}
Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove)
{
QWriteLocker locker(&m_lock);
// Find index of clip
int clip_position = m_allClips[clipId]->getPosition();
bool audioOnly = m_allClips[clipId]->isAudioOnly();
int old_in = clip_position;
int old_out = old_in + m_allClips[clipId]->getPlaytime();
return [clip_position, clipId, old_in, old_out, updateView, audioOnly, finalMove, this]() {
auto clip_loc = getClipIndexAt(clip_position);
if (updateView) {
int old_clip_index = getRowfromClip(clipId);
auto ptr = m_parent.lock();
ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index);
ptr->_endRemoveRows();
}
int target_track = clip_loc.first;
int target_clip = clip_loc.second;
// lock MLT playlist so that we don't end up with invalid frames in monitor
m_playlists[target_track].lock();
Q_ASSERT(target_clip < m_playlists[target_track].count());
Q_ASSERT(!m_playlists[target_track].is_blank(target_clip));
auto prod = m_playlists[target_track].replace_with_blank(target_clip);
if (prod != nullptr) {
m_playlists[target_track].consolidate_blanks();
m_allClips[clipId]->setCurrentTrackId(-1);
m_allClips.erase(clipId);
delete prod;
m_playlists[target_track].unlock();
if (auto ptr = m_parent.lock()) {
ptr->m_snaps->removePoint(old_in);
ptr->m_snaps->removePoint(old_out);
if (finalMove) {
if (!audioOnly && !isAudioTrack()) {
ptr->invalidateZone(old_in, old_out);
}
if (target_clip >= m_playlists[target_track].count()) {
// deleted last clip in playlist
ptr->updateDuration();
}
}
if (!audioOnly && !isHidden() && !isAudioTrack()) {
// only refresh monitor if not an audio track and not hidden
ptr->checkRefresh(old_in, old_out);
}
}
return true;
}
m_playlists[target_track].unlock();
return false;
};
}
bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allClips.count(clipId) > 0);
if (isLocked()) {
return false;
}
auto old_clip = m_allClips[clipId];
int old_position = old_clip->getPosition();
// qDebug() << "/// REQUESTOING CLIP DELETION_: " << updateView;
auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove);
if (operation()) {
auto reverse = requestClipInsertion_lambda(clipId, old_position, updateView, finalMove);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
return false;
}
int TrackModel::getBlankSizeAtPos(int frame)
{
READ_LOCK();
int min_length = 0;
for (int i = 0; i < 2; ++i) {
int ix = m_playlists[i].get_clip_index_at(frame);
if (m_playlists[i].is_blank(ix)) {
int blank_length = m_playlists[i].clip_length(ix);
if (min_length == 0 || (blank_length > 0 && blank_length < min_length)) {
min_length = blank_length;
}
}
}
return min_length;
}
int TrackModel::suggestCompositionLength(int position)
{
READ_LOCK();
if (m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position)) {
return -1;
}
auto clip_loc = getClipIndexAt(position);
int track = clip_loc.first;
int index = clip_loc.second;
int other_index; // index in the other track
int other_track = (track + 1) % 2;
int end_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index);
other_index = m_playlists[other_track].get_clip_index_at(end_pos);
if (other_index < m_playlists[other_track].count()) {
end_pos = std::min(end_pos, m_playlists[other_track].clip_start(other_index) + m_playlists[other_track].clip_length(other_index));
}
int min = -1;
std::unordered_set existing = getCompositionsInRange(position, end_pos);
if (existing.size() > 0) {
for (int id : existing) {
if (min < 0) {
min = m_allCompositions[id]->getPosition();
} else {
min = qMin(min, m_allCompositions[id]->getPosition());
}
}
}
if (min >= 0) {
// An existing composition is limiting the space
end_pos = min;
}
return end_pos - position;
}
int TrackModel::getBlankSizeNearClip(int clipId, bool after)
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
int clip_position = m_allClips[clipId]->getPosition();
auto clip_loc = getClipIndexAt(clip_position);
int track = clip_loc.first;
int index = clip_loc.second;
int other_index; // index in the other track
int other_track = (track + 1) % 2;
if (after) {
int first_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index);
other_index = m_playlists[other_track].get_clip_index_at(first_pos);
index++;
} else {
int last_pos = m_playlists[track].clip_start(index) - 1;
other_index = m_playlists[other_track].get_clip_index_at(last_pos);
index--;
}
if (index < 0) return 0;
int length = INT_MAX;
if (index < m_playlists[track].count()) {
if (!m_playlists[track].is_blank(index)) {
return 0;
}
length = std::min(length, m_playlists[track].clip_length(index));
}
if (other_index < m_playlists[other_track].count()) {
if (!m_playlists[other_track].is_blank(other_index)) {
return 0;
}
length = std::min(length, m_playlists[other_track].clip_length(other_index));
}
return length;
}
int TrackModel::getBlankSizeNearComposition(int compoId, bool after)
{
READ_LOCK();
Q_ASSERT(m_allCompositions.count(compoId) > 0);
int clip_position = m_allCompositions[compoId]->getPosition();
Q_ASSERT(m_compoPos.count(clip_position) > 0);
Q_ASSERT(m_compoPos[clip_position] == compoId);
auto it = m_compoPos.find(clip_position);
int clip_length = m_allCompositions[compoId]->getPlaytime();
int length = INT_MAX;
if (after) {
++it;
if (it != m_compoPos.end()) {
return it->first - clip_position - clip_length;
}
} else {
if (it != m_compoPos.begin()) {
--it;
return clip_position - it->first - m_allCompositions[it->second]->getPlaytime();
}
return clip_position;
}
return length;
}
Fun TrackModel::requestClipResize_lambda(int clipId, int in, int out, bool right)
{
QWriteLocker locker(&m_lock);
int clip_position = m_allClips[clipId]->getPosition();
int old_in = clip_position;
int old_out = old_in + m_allClips[clipId]->getPlaytime();
auto clip_loc = getClipIndexAt(clip_position);
int target_track = clip_loc.first;
int target_clip = clip_loc.second;
Q_ASSERT(target_clip < m_playlists[target_track].count());
int size = out - in + 1;
bool checkRefresh = false;
if (!isHidden() && !isAudioTrack()) {
checkRefresh = true;
}
auto update_snaps = [clipId, old_in, old_out, checkRefresh, this](int new_in, int new_out) {
if (auto ptr = m_parent.lock()) {
ptr->m_snaps->removePoint(old_in);
ptr->m_snaps->removePoint(old_out);
ptr->m_snaps->addPoint(new_in);
ptr->m_snaps->addPoint(new_out);
if (checkRefresh) {
ptr->checkRefresh(old_in, old_out);
ptr->checkRefresh(new_in, new_out);
// ptr->adjustAssetRange(clipId, m_allClips[clipId]->getIn(), m_allClips[clipId]->getOut());
}
} else {
qDebug() << "Error : clip resize failed because parent timeline is not available anymore";
Q_ASSERT(false);
}
};
int delta = m_allClips[clipId]->getPlaytime() - size;
if (delta == 0) {
return []() { return true; };
}
// qDebug() << "RESIZING CLIP: " << clipId << " FROM: " << delta;
if (delta > 0) { // we shrink clip
return [right, target_clip, target_track, clip_position, delta, in, out, clipId, update_snaps, this]() {
int target_clip_mutable = target_clip;
int blank_index = right ? (target_clip_mutable + 1) : target_clip_mutable;
// insert blank to space that is going to be empty
// The second is parameter is delta - 1 because this function expects an out time, which is basically size - 1
m_playlists[target_track].insert_blank(blank_index, delta - 1);
if (!right) {
m_allClips[clipId]->setPosition(clip_position + delta);
// Because we inserted blank before, the index of our clip has increased
target_clip_mutable++;
}
int err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out);
// make sure to do this after, to avoid messing the indexes
m_playlists[target_track].consolidate_blanks();
if (err == 0) {
update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
if (right && m_playlists[target_track].count() - 1 == target_clip_mutable) {
// deleted last clip in playlist
if (auto ptr = m_parent.lock()) {
ptr->updateDuration();
}
}
}
return err == 0;
};
}
int blank = -1;
int other_blank_end = getBlankEnd(clip_position, (target_track + 1) % 2);
if (right) {
if (target_clip == m_playlists[target_track].count() - 1 && other_blank_end >= out) {
// clip is last, it can always be extended
return [this, target_clip, target_track, in, out, update_snaps, clipId]() {
// color, image and title clips can have unlimited resize
QScopedPointer clip(m_playlists[target_track].get_clip(target_clip));
if (out >= clip->get_length()) {
clip->parent().set("length", out + 1);
clip->parent().set("out", out);
clip->set("length", out + 1);
}
int err = m_playlists[target_track].resize_clip(target_clip, in, out);
if (err == 0) {
update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
}
m_playlists[target_track].consolidate_blanks();
if (m_playlists[target_track].count() - 1 == target_clip) {
// deleted last clip in playlist
if (auto ptr = m_parent.lock()) {
ptr->updateDuration();
}
}
return err == 0;
};
}
blank = target_clip + 1;
} else {
if (target_clip == 0) {
// clip is first, it can never be extended on the left
return []() { return false; };
}
blank = target_clip - 1;
}
if (m_playlists[target_track].is_blank(blank)) {
int blank_length = m_playlists[target_track].clip_length(blank);
if (blank_length + delta >= 0 && other_blank_end >= out) {
return [blank_length, blank, right, clipId, delta, update_snaps, this, in, out, target_clip, target_track]() {
int target_clip_mutable = target_clip;
int err = 0;
if (blank_length + delta == 0) {
err = m_playlists[target_track].remove(blank);
if (!right) {
target_clip_mutable--;
}
} else {
err = m_playlists[target_track].resize_clip(blank, 0, blank_length + delta - 1);
}
if (err == 0) {
QScopedPointer clip(m_playlists[target_track].get_clip(target_clip_mutable));
if (out >= clip->get_length()) {
clip->parent().set("length", out + 1);
clip->parent().set("out", out);
clip->set("length", out + 1);
}
err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out);
}
if (!right && err == 0) {
m_allClips[clipId]->setPosition(m_playlists[target_track].clip_start(target_clip_mutable));
}
if (err == 0) {
update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
}
m_playlists[target_track].consolidate_blanks();
return err == 0;
};
}
}
return []() { return false; };
}
int TrackModel::getId() const
{
return m_id;
}
int TrackModel::getClipByPosition(int position)
{
READ_LOCK();
QSharedPointer prod(nullptr);
if (m_playlists[0].count() > 0) {
prod = QSharedPointer(m_playlists[0].get_clip_at(position));
}
if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) {
prod = QSharedPointer(m_playlists[1].get_clip_at(position));
}
if (!prod || prod->is_blank()) {
return -1;
}
return prod->get_int("_kdenlive_cid");
}
QSharedPointer TrackModel::getClipProducer(int clipId)
{
READ_LOCK();
QSharedPointer prod(nullptr);
if (m_playlists[0].count() > 0) {
prod = QSharedPointer(m_playlists[0].get_clip(clipId));
}
if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) {
prod = QSharedPointer(m_playlists[1].get_clip(clipId));
}
return prod;
}
int TrackModel::getCompositionByPosition(int position)
{
READ_LOCK();
for (const auto &comp : m_compoPos) {
if (comp.first == position) {
return comp.second;
} else if (comp.first < position) {
if (comp.first + m_allCompositions[comp.second]->getPlaytime() >= position) {
return comp.second;
}
}
}
return -1;
}
int TrackModel::getClipByRow(int row) const
{
READ_LOCK();
if (row >= static_cast(m_allClips.size())) {
return -1;
}
auto it = m_allClips.cbegin();
std::advance(it, row);
return (*it).first;
}
std::unordered_set TrackModel::getClipsInRange(int position, int end)
{
READ_LOCK();
std::unordered_set ids;
for (const auto &clp : m_allClips) {
int pos = clp.second->getPosition();
int length = clp.second->getPlaytime();
if (end > -1 && pos >= end) {
continue;
}
if (pos >= position || pos + length - 1 >= position) {
ids.insert(clp.first);
}
}
return ids;
}
int TrackModel::getRowfromClip(int clipId) const
{
READ_LOCK();
Q_ASSERT(m_allClips.count(clipId) > 0);
return (int)std::distance(m_allClips.begin(), m_allClips.find(clipId));
}
std::unordered_set TrackModel::getCompositionsInRange(int position, int end)
{
READ_LOCK();
// TODO: this function doesn't take into accounts the fact that there are two tracks
std::unordered_set ids;
for (const auto &compo : m_allCompositions) {
int pos = compo.second->getPosition();
int length = compo.second->getPlaytime();
if (end > -1 && pos >= end) {
continue;
}
if (pos >= position || pos + length - 1 >= position) {
ids.insert(compo.first);
}
}
return ids;
}
int TrackModel::getRowfromComposition(int tid) const
{
READ_LOCK();
Q_ASSERT(m_allCompositions.count(tid) > 0);
return (int)m_allClips.size() + (int)std::distance(m_allCompositions.begin(), m_allCompositions.find(tid));
}
QVariant TrackModel::getProperty(const QString &name) const
{
READ_LOCK();
return QVariant(m_track->get(name.toUtf8().constData()));
}
void TrackModel::setProperty(const QString &name, const QString &value)
{
QWriteLocker locker(&m_lock);
m_track->set(name.toUtf8().constData(), value.toUtf8().constData());
// Hide property mus be defined at playlist level or it won't be saved
if (name == QLatin1String("kdenlive:audio_track") || name == QLatin1String("hide")) {
for (int i = 0; i < 2; i++) {
m_playlists[i].set(name.toUtf8().constData(), value.toInt());
}
}
}
bool TrackModel::checkConsistency()
{
auto ptr = m_parent.lock();
if (!ptr) {
return false;
}
std::vector> clips; // clips stored by (position, id)
for (const auto &c : m_allClips) {
Q_ASSERT(c.second);
Q_ASSERT(c.second.get() == ptr->getClipPtr(c.first).get());
clips.push_back({c.second->getPosition(), c.first});
}
std::sort(clips.begin(), clips.end());
size_t current_clip = 0;
int playtime = std::max(m_playlists[0].get_playtime(), m_playlists[1].get_playtime());
for (int i = 0; i < playtime; i++) {
int track, index;
if (isBlankAt(i)) {
track = 0;
index = m_playlists[0].get_clip_index_at(i);
} else {
auto clip_loc = getClipIndexAt(i);
track = clip_loc.first;
index = clip_loc.second;
}
Q_ASSERT(m_playlists[(track + 1) % 2].is_blank_at(i));
if (current_clip < clips.size() && i >= clips[current_clip].first) {
auto clip = m_allClips[clips[current_clip].second];
if (i >= clips[current_clip].first + clip->getPlaytime()) {
current_clip++;
i--;
continue;
}
if (isBlankAt(i)) {
qDebug() << "ERROR: Found blank when clip was required at position " << i;
return false;
}
auto pr = m_playlists[track].get_clip(index);
Mlt::Producer prod(pr);
if (!prod.same_clip(*clip)) {
qDebug() << "ERROR: Wrong clip at position " << i;
delete pr;
return false;
}
delete pr;
} else {
if (!isBlankAt(i)) {
qDebug() << "ERROR: Found clip when blank was required at position " << i;
return false;
}
}
}
// We now check compositions positions
if (m_allCompositions.size() != m_compoPos.size()) {
qDebug() << "Error: the number of compositions position doesn't match number of compositions";
return false;
}
for (const auto &compo : m_allCompositions) {
int pos = compo.second->getPosition();
if (m_compoPos.count(pos) == 0) {
qDebug() << "Error: the position of composition " << compo.first << " is not properly stored";
return false;
}
if (m_compoPos[pos] != compo.first) {
qDebug() << "Error: found composition" << m_compoPos[pos] << "instead of " << compo.first << "at position" << pos;
return false;
}
}
for (auto it = m_compoPos.begin(); it != m_compoPos.end(); ++it) {
int compoId = it->second;
int cur_in = m_allCompositions[compoId]->getPosition();
Q_ASSERT(cur_in == it->first);
int cur_out = cur_in + m_allCompositions[compoId]->getPlaytime() - 1;
++it;
if (it != m_compoPos.end()) {
int next_compoId = it->second;
int next_in = m_allCompositions[next_compoId]->getPosition();
int next_out = next_in + m_allCompositions[next_compoId]->getPlaytime() - 1;
if (next_in <= cur_out) {
qDebug() << "Error: found collision between composition " << compoId << "[ " << cur_in << ", " << cur_out << "] and " << next_compoId << "[ "
<< next_in << ", " << next_out << "]";
return false;
}
}
--it;
}
return true;
}
std::pair TrackModel::getClipIndexAt(int position)
{
READ_LOCK();
for (int j = 0; j < 2; j++) {
if (!m_playlists[j].is_blank_at(position)) {
return {j, m_playlists[j].get_clip_index_at(position)};
}
}
Q_ASSERT(false);
return {-1, -1};
}
bool TrackModel::isBlankAt(int position)
{
READ_LOCK();
return m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position);
}
int TrackModel::getBlankStart(int position)
{
READ_LOCK();
int result = 0;
for (int j = 0; j < 2; j++) {
if (m_playlists[j].count() == 0) {
break;
}
if (!m_playlists[j].is_blank_at(position)) {
result = position;
break;
}
int clip_index = m_playlists[j].get_clip_index_at(position);
int start = m_playlists[j].clip_start(clip_index);
if (start > result) {
result = start;
}
}
return result;
}
int TrackModel::getBlankEnd(int position, int track)
{
READ_LOCK();
// Q_ASSERT(m_playlists[track].is_blank_at(position));
if (!m_playlists[track].is_blank_at(position)) {
return position;
}
int clip_index = m_playlists[track].get_clip_index_at(position);
int count = m_playlists[track].count();
if (clip_index < count) {
int blank_start = m_playlists[track].clip_start(clip_index);
int blank_length = m_playlists[track].clip_length(clip_index);
return blank_start + blank_length;
}
return INT_MAX;
}
int TrackModel::getBlankEnd(int position)
{
READ_LOCK();
int end = INT_MAX;
for (int j = 0; j < 2; j++) {
end = std::min(getBlankEnd(position, j), end);
}
return end;
}
Fun TrackModel::requestCompositionResize_lambda(int compoId, int in, int out, bool logUndo)
{
QWriteLocker locker(&m_lock);
int compo_position = m_allCompositions[compoId]->getPosition();
Q_ASSERT(m_compoPos.count(compo_position) > 0);
Q_ASSERT(m_compoPos[compo_position] == compoId);
int old_in = compo_position;
int old_out = old_in + m_allCompositions[compoId]->getPlaytime() - 1;
qDebug() << "compo resize " << compoId << in << "-" << out << " / " << old_in << "-" << old_out;
if (out == -1) {
out = in + old_out - old_in;
}
auto update_snaps = [compoId, old_in, old_out, logUndo, this](int new_in, int new_out) {
if (auto ptr = m_parent.lock()) {
ptr->m_snaps->removePoint(old_in);
ptr->m_snaps->removePoint(old_out + 1);
ptr->m_snaps->addPoint(new_in);
ptr->m_snaps->addPoint(new_out);
ptr->checkRefresh(old_in, old_out);
ptr->checkRefresh(new_in, new_out);
if (logUndo) {
ptr->invalidateZone(old_in, old_out);
ptr->invalidateZone(new_in, new_out);
}
// ptr->adjustAssetRange(compoId, new_in, new_out);
} else {
qDebug() << "Error : Composition resize failed because parent timeline is not available anymore";
Q_ASSERT(false);
}
};
if (in == compo_position && (out == -1 || out == old_out)) {
return []() {
qDebug() << "//// NO MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!";
return true;
};
}
// temporary remove of current compo to check collisions
qDebug() << "// CURRENT COMPOSITIONS ----\n" << m_compoPos << "\n--------------";
m_compoPos.erase(compo_position);
bool intersecting = hasIntersectingComposition(in, out);
// put it back
m_compoPos[compo_position] = compoId;
if (intersecting) {
return []() {
qDebug() << "//// FALSE MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!";
return false;
};
}
return [in, out, compoId, update_snaps, this]() {
m_compoPos.erase(m_allCompositions[compoId]->getPosition());
m_allCompositions[compoId]->setInOut(in, out);
update_snaps(in, out + 1);
m_compoPos[m_allCompositions[compoId]->getPosition()] = compoId;
return true;
};
}
bool TrackModel::requestCompositionInsertion(int compoId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
if (isLocked()) {
return false;
}
auto operation = requestCompositionInsertion_lambda(compoId, position, updateView, finalMove);
if (operation()) {
auto reverse = requestCompositionDeletion_lambda(compoId, updateView, finalMove);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
return false;
}
bool TrackModel::requestCompositionDeletion(int compoId, bool updateView, bool finalMove, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
if (isLocked()) {
return false;
}
Q_ASSERT(m_allCompositions.count(compoId) > 0);
auto old_composition = m_allCompositions[compoId];
int old_position = old_composition->getPosition();
Q_ASSERT(m_compoPos.count(old_position) > 0);
Q_ASSERT(m_compoPos[old_position] == compoId);
auto operation = requestCompositionDeletion_lambda(compoId, updateView, finalMove);
if (operation()) {
auto reverse = requestCompositionInsertion_lambda(compoId, old_position, updateView, finalMove);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
return false;
}
Fun TrackModel::requestCompositionDeletion_lambda(int compoId, bool updateView, bool finalMove)
{
QWriteLocker locker(&m_lock);
// Find index of clip
int clip_position = m_allCompositions[compoId]->getPosition();
int old_in = clip_position;
int old_out = old_in + m_allCompositions[compoId]->getPlaytime();
return [clip_position, compoId, old_in, old_out, updateView, finalMove, this]() {
int old_clip_index = getRowfromComposition(compoId);
auto ptr = m_parent.lock();
if (updateView) {
ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index);
ptr->_endRemoveRows();
}
m_allCompositions[compoId]->setCurrentTrackId(-1);
m_allCompositions.erase(compoId);
m_compoPos.erase(old_in);
ptr->m_snaps->removePoint(old_in);
ptr->m_snaps->removePoint(old_out);
if (finalMove) {
ptr->invalidateZone(old_in, old_out);
}
return true;
};
}
int TrackModel::getCompositionByRow(int row) const
{
READ_LOCK();
if (row < (int)m_allClips.size()) {
return -1;
}
- Q_ASSERT(row <= (int)m_allClips.size() + m_allCompositions.size());
+ Q_ASSERT(row <= (int)m_allClips.size() + (int)m_allCompositions.size());
auto it = m_allCompositions.cbegin();
std::advance(it, row - (int)m_allClips.size());
return (*it).first;
}
int TrackModel::getCompositionsCount() const
{
READ_LOCK();
return (int)m_allCompositions.size();
}
Fun TrackModel::requestCompositionInsertion_lambda(int compoId, int position, bool updateView, bool finalMove)
{
QWriteLocker locker(&m_lock);
bool intersecting = true;
if (auto ptr = m_parent.lock()) {
intersecting = hasIntersectingComposition(position, position + ptr->getCompositionPlaytime(compoId) - 1);
} else {
qDebug() << "Error : Composition Insertion failed because timeline is not available anymore";
}
if (!intersecting) {
return [compoId, this, position, updateView, finalMove]() {
if (auto ptr = m_parent.lock()) {
std::shared_ptr composition = ptr->getCompositionPtr(compoId);
m_allCompositions[composition->getId()] = composition; // store clip
// update clip position and track
composition->setCurrentTrackId(getId());
int new_in = position;
int new_out = new_in + composition->getPlaytime();
composition->setInOut(new_in, new_out - 1);
if (updateView) {
int composition_index = getRowfromComposition(composition->getId());
ptr->_beginInsertRows(ptr->makeTrackIndexFromID(composition->getCurrentTrackId()), composition_index, composition_index);
ptr->_endInsertRows();
}
ptr->m_snaps->addPoint(new_in);
ptr->m_snaps->addPoint(new_out);
m_compoPos[new_in] = composition->getId();
if (finalMove) {
ptr->invalidateZone(new_in, new_out);
}
return true;
}
qDebug() << "Error : Composition Insertion failed because timeline is not available anymore";
return false;
};
}
return []() { return false; };
}
bool TrackModel::hasIntersectingComposition(int in, int out) const
{
READ_LOCK();
auto it = m_compoPos.lower_bound(in);
if (m_compoPos.empty()) {
return false;
}
if (it != m_compoPos.end() && it->first <= out) {
// compo at it intersects
return true;
}
if (it == m_compoPos.begin()) {
return false;
}
--it;
int end = it->first + m_allCompositions.at(it->second)->getPlaytime() - 1;
return end >= in;
return false;
}
bool TrackModel::addEffect(const QString &effectId)
{
READ_LOCK();
return m_effectStack->appendEffect(effectId);
}
const QString TrackModel::effectNames() const
{
READ_LOCK();
return m_effectStack->effectNames();
}
bool TrackModel::stackEnabled() const
{
READ_LOCK();
return m_effectStack->isStackEnabled();
}
void TrackModel::setEffectStackEnabled(bool enable)
{
m_effectStack->setEffectStackEnabled(enable);
}
int TrackModel::trackDuration()
{
return m_track->get_length();
}
bool TrackModel::isLocked() const
{
READ_LOCK();
return m_track->get_int("kdenlive:locked_track");
}
bool TrackModel::isAudioTrack() const
{
return m_track->get_int("kdenlive:audio_track") == 1;
}
PlaylistState::ClipState TrackModel::trackType() const
{
return (m_track->get_int("kdenlive:audio_track") == 1 ? PlaylistState::AudioOnly : PlaylistState::VideoOnly);
}
bool TrackModel::isHidden() const
{
return m_track->get_int("hide") & 1;
}
bool TrackModel::isMute() const
{
return m_track->get_int("hide") & 2;
}
bool TrackModel::importEffects(std::weak_ptr service)
{
QWriteLocker locker(&m_lock);
m_effectStack->importEffects(service, trackType());
return true;
}
bool TrackModel::copyEffect(std::shared_ptr stackModel, int rowId)
{
QWriteLocker locker(&m_lock);
return m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly);
}
diff --git a/tests/modeltest.cpp b/tests/modeltest.cpp
index c70e4bd74..9d762f964 100644
--- a/tests/modeltest.cpp
+++ b/tests/modeltest.cpp
@@ -1,2127 +1,2130 @@
+#include "logger.hpp"
#include "test_utils.hpp"
using namespace fakeit;
std::default_random_engine g(42);
Mlt::Profile profile_model;
TEST_CASE("Basic creation/deletion of a track", "[TrackModel]")
{
+ Logger::clear();
auto binModel = pCore->projectItemModel();
std::shared_ptr undoStack = std::make_shared(nullptr);
std::shared_ptr guideModel = std::make_shared(undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(new Mlt::Profile(), undoStack);
Mock timMock(tim);
auto timeline = std::shared_ptr(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
Fake(Method(timMock, adjustAssetRange));
// This is faked to allow to count calls
Fake(Method(timMock, _resetView));
int id1 = TrackModel::construct(timeline);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTracksCount() == 1);
REQUIRE(timeline->getTrackPosition(id1) == 0);
// In the current implementation, when a track is added/removed, the model is notified with _resetView
Verify(Method(timMock, _resetView)).Exactly(Once);
RESET(timMock);
int id2 = TrackModel::construct(timeline);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTracksCount() == 2);
REQUIRE(timeline->getTrackPosition(id2) == 1);
Verify(Method(timMock, _resetView)).Exactly(Once);
RESET(timMock);
int id3 = TrackModel::construct(timeline);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTracksCount() == 3);
REQUIRE(timeline->getTrackPosition(id3) == 2);
Verify(Method(timMock, _resetView)).Exactly(Once);
RESET(timMock);
int id4;
REQUIRE(timeline->requestTrackInsertion(1, id4));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTracksCount() == 4);
REQUIRE(timeline->getTrackPosition(id1) == 0);
REQUIRE(timeline->getTrackPosition(id4) == 1);
REQUIRE(timeline->getTrackPosition(id2) == 2);
REQUIRE(timeline->getTrackPosition(id3) == 3);
Verify(Method(timMock, _resetView)).Exactly(Once);
RESET(timMock);
// Test deletion
REQUIRE(timeline->requestTrackDeletion(id3));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTracksCount() == 3);
Verify(Method(timMock, _resetView)).Exactly(Once);
RESET(timMock);
REQUIRE(timeline->requestTrackDeletion(id1));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTracksCount() == 2);
Verify(Method(timMock, _resetView)).Exactly(Once);
RESET(timMock);
REQUIRE(timeline->requestTrackDeletion(id4));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTracksCount() == 1);
Verify(Method(timMock, _resetView)).Exactly(Once);
RESET(timMock);
REQUIRE(timeline->requestTrackDeletion(id2));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTracksCount() == 0);
Verify(Method(timMock, _resetView)).Exactly(Once);
RESET(timMock);
SECTION("Delete a track with groups")
{
int tid1, tid2;
REQUIRE(timeline->requestTrackInsertion(-1, tid1));
REQUIRE(timeline->requestTrackInsertion(-1, tid2));
REQUIRE(timeline->checkConsistency());
QString binId = createProducer(profile_model, "red", binModel);
int length = 20;
int cid1, cid2, cid3, cid4;
REQUIRE(timeline->requestClipInsertion(binId, tid1, 2, cid1));
REQUIRE(timeline->requestClipInsertion(binId, tid2, 0, cid2));
REQUIRE(timeline->requestClipInsertion(binId, tid2, length, cid3));
REQUIRE(timeline->requestClipInsertion(binId, tid2, 2 * length, cid4));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipsCount() == 4);
REQUIRE(timeline->getTracksCount() == 2);
auto g1 = std::unordered_set({cid1, cid3});
auto g2 = std::unordered_set({cid2, cid4});
auto g3 = std::unordered_set({cid1, cid4});
REQUIRE(timeline->requestClipsGroup(g1));
REQUIRE(timeline->requestClipsGroup(g2));
REQUIRE(timeline->requestClipsGroup(g3));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->requestTrackDeletion(tid1));
REQUIRE(timeline->getClipsCount() == 3);
REQUIRE(timeline->getTracksCount() == 1);
REQUIRE(timeline->checkConsistency());
}
binModel->clean();
pCore->m_projectManager = nullptr;
+ Logger::print_trace();
}
TEST_CASE("Basic creation/deletion of a clip", "[ClipModel]")
{
auto binModel = pCore->projectItemModel();
std::shared_ptr undoStack = std::make_shared(nullptr);
std::shared_ptr guideModel = std::make_shared(undoStack);
std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
QString binId = createProducer(profile_model, "red", binModel);
QString binId2 = createProducer(profile_model, "green", binModel);
REQUIRE(timeline->getClipsCount() == 0);
int id1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
REQUIRE(timeline->getClipsCount() == 1);
REQUIRE(timeline->checkConsistency());
int id2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly);
REQUIRE(timeline->getClipsCount() == 2);
REQUIRE(timeline->checkConsistency());
int id3 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
REQUIRE(timeline->getClipsCount() == 3);
REQUIRE(timeline->checkConsistency());
// Test deletion
REQUIRE(timeline->requestItemDeletion(id2));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipsCount() == 2);
REQUIRE(timeline->requestItemDeletion(id3));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipsCount() == 1);
REQUIRE(timeline->requestItemDeletion(id1));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipsCount() == 0);
binModel->clean();
pCore->m_projectManager = nullptr;
}
TEST_CASE("Clip manipulation", "[ClipModel]")
{
auto binModel = pCore->projectItemModel();
binModel->clean();
std::shared_ptr undoStack = std::make_shared(nullptr);
std::shared_ptr guideModel = std::make_shared(undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(new Mlt::Profile(), undoStack);
Mock timMock(tim);
auto timeline = std::shared_ptr(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
Fake(Method(timMock, adjustAssetRange));
// This is faked to allow to count calls
Fake(Method(timMock, _resetView));
Fake(Method(timMock, _beginInsertRows));
Fake(Method(timMock, _beginRemoveRows));
Fake(Method(timMock, _endInsertRows));
Fake(Method(timMock, _endRemoveRows));
QString binId = createProducer(profile_model, "red", binModel);
QString binId2 = createProducer(profile_model, "blue", binModel);
QString binId3 = createProducer(profile_model, "green", binModel);
int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
int tid1 = TrackModel::construct(timeline);
int tid2 = TrackModel::construct(timeline);
int tid3 = TrackModel::construct(timeline);
int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly);
int cid3 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly);
int cid4 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly);
Verify(Method(timMock, _resetView)).Exactly(3_Times);
RESET(timMock);
// for testing purposes, we make sure the clip will behave as regular clips
// (ie their size is fixed, we cannot resize them past their original size)
timeline->m_allClips[cid1]->m_endlessResize = false;
timeline->m_allClips[cid2]->m_endlessResize = false;
timeline->m_allClips[cid3]->m_endlessResize = false;
timeline->m_allClips[cid4]->m_endlessResize = false;
SECTION("Insert a clip in a track and change track")
{
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 0);
REQUIRE(timeline->getTrackClipsCount(tid2) == 0);
REQUIRE(timeline->getClipTrackId(cid1) == -1);
REQUIRE(timeline->getClipPosition(cid1) == -1);
int pos = 10;
REQUIRE(timeline->requestClipMove(cid1, tid1, pos));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == pos);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid2) == 0);
// Check that the model was correctly notified
CHECK_INSERT(Once);
pos = 1;
REQUIRE(timeline->requestClipMove(cid1, tid2, pos));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == pos);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 0);
CHECK_MOVE(Once);
// Check conflicts
int pos2 = binModel->getClipByBinID(binId)->frameDuration();
REQUIRE(timeline->requestClipMove(cid2, tid1, pos2));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid2) == pos2);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
CHECK_INSERT(Once);
REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, pos2 + 2));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getClipTrackId(cid1) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == pos);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid2) == pos2);
CHECK_MOVE(Once);
REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, pos2 - 2));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getClipTrackId(cid1) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == pos);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid2) == pos2);
CHECK_MOVE(Once);
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid2) == 0);
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid2) == pos2);
CHECK_MOVE(Once);
}
int length = binModel->getClipByBinID(binId)->frameDuration();
SECTION("Insert consecutive clips")
{
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
CHECK_INSERT(Once);
REQUIRE(timeline->requestClipMove(cid2, tid1, length));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid2) == length);
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
CHECK_INSERT(Once);
}
SECTION("Resize orphan clip")
{
REQUIRE(timeline->getClipPlaytime(cid2) == length);
REQUIRE(timeline->requestItemResize(cid2, 5, true) == 5);
REQUIRE(timeline->checkConsistency());
REQUIRE(binModel->getClipByBinID(binId)->frameDuration() == length);
auto inOut = std::pair{0, 4};
REQUIRE(timeline->m_allClips[cid2]->getInOut() == inOut);
REQUIRE(timeline->getClipPlaytime(cid2) == 5);
REQUIRE(timeline->requestItemResize(cid2, 10, false) == -1);
REQUIRE(timeline->requestItemResize(cid2, length + 1, true) == -1);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(cid2) == 5);
REQUIRE(timeline->getClipPlaytime(cid2) == 5);
REQUIRE(timeline->requestItemResize(cid2, 2, false) == 2);
REQUIRE(timeline->checkConsistency());
inOut = std::pair{3, 4};
REQUIRE(timeline->m_allClips[cid2]->getInOut() == inOut);
REQUIRE(timeline->getClipPlaytime(cid2) == 2);
REQUIRE(timeline->requestItemResize(cid2, length, true) == -1);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(cid2) == 2);
CAPTURE(timeline->m_allClips[cid2]->m_producer->get_in());
REQUIRE(timeline->requestItemResize(cid2, length - 2, true) == -1);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->requestItemResize(cid2, length - 3, true) == length - 3);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(cid2) == length - 3);
}
SECTION("Resize inserted clips")
{
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
REQUIRE(timeline->checkConsistency());
CHECK_INSERT(Once);
REQUIRE(timeline->requestItemResize(cid1, 5, true) == 5);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(cid1) == 5);
REQUIRE(timeline->getClipPosition(cid1) == 0);
CHECK_RESIZE(Once);
REQUIRE(timeline->requestClipMove(cid2, tid1, 5));
REQUIRE(timeline->checkConsistency());
REQUIRE(binModel->getClipByBinID(binId)->frameDuration() == length);
CHECK_INSERT(Once);
REQUIRE(timeline->requestItemResize(cid1, 6, true) == -1);
REQUIRE(timeline->requestItemResize(cid1, 6, false) == -1);
REQUIRE(timeline->checkConsistency());
NO_OTHERS();
REQUIRE(timeline->requestItemResize(cid2, length - 5, false) == length - 5);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPosition(cid2) == 10);
CHECK_RESIZE(Once);
REQUIRE(timeline->requestItemResize(cid1, 10, true) == 10);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
CHECK_RESIZE(Once);
}
SECTION("Change track of resized clips")
{
// // REQUIRE(timeline->allowClipMove(cid2, tid1, 5));
REQUIRE(timeline->requestClipMove(cid2, tid1, 5));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
// // REQUIRE(timeline->allowClipMove(cid1, tid2, 10));
REQUIRE(timeline->requestClipMove(cid1, tid2, 10));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->requestItemResize(cid1, 5, false) == 5);
REQUIRE(timeline->checkConsistency());
// // REQUIRE(timeline->allowClipMove(cid1, tid1, 0));
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getTrackClipsCount(tid2) == 0);
}
SECTION("Clip Move")
{
REQUIRE(timeline->requestClipMove(cid2, tid1, 5));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid2) == 5);
REQUIRE(timeline->requestClipMove(cid1, tid1, 5 + length));
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == 5 + length);
REQUIRE(timeline->getClipPosition(cid2) == 5);
};
state();
REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 3 + length));
state();
REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0));
state();
REQUIRE(timeline->requestClipMove(cid2, tid1, 0));
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == 5 + length);
REQUIRE(timeline->getClipPosition(cid2) == 0);
};
state2();
REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0));
state2();
REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, length - 5));
state2();
REQUIRE(timeline->requestClipMove(cid1, tid1, length));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == length);
REQUIRE(timeline->getClipPosition(cid2) == 0);
REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == length);
REQUIRE(timeline->getClipPosition(cid2) == 0);
REQUIRE(timeline->requestClipMove(cid1, tid1, length - 5));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == length - 5);
REQUIRE(timeline->getClipPosition(cid2) == 0);
REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10);
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == length - 5);
REQUIRE(timeline->getClipPosition(cid2) == 5);
REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == length - 5);
REQUIRE(timeline->getClipPosition(cid2) == 5);
REQUIRE(timeline->requestClipMove(cid2, tid1, 0));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == length - 5);
REQUIRE(timeline->getClipPosition(cid2) == 0);
}
SECTION("Move and resize")
{
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
REQUIRE(timeline->requestItemResize(cid1, length - 2, false) == length - 2);
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPlaytime(cid1) == length - 2);
};
state();
// try to resize past the left end
REQUIRE(timeline->requestItemResize(cid1, length, false) == -1);
state();
REQUIRE(timeline->requestItemResize(cid1, length - 4, true) == length - 4);
REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4 + 1));
REQUIRE(timeline->requestItemResize(cid2, length - 2, false) == length - 2);
REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4 + 1));
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPlaytime(cid1) == length - 4);
REQUIRE(timeline->getClipPosition(cid2) == length - 4 + 1);
REQUIRE(timeline->getClipPlaytime(cid2) == length - 2);
};
state2();
// the gap between the two clips is 1 frame, we try to resize them by 2 frames
REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == -1);
state2();
REQUIRE(timeline->requestItemResize(cid2, length, false) == -1);
state2();
REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4));
auto state3 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPlaytime(cid1) == length - 4);
REQUIRE(timeline->getClipPosition(cid2) == length - 4);
REQUIRE(timeline->getClipPlaytime(cid2) == length - 2);
};
state3();
// Now the gap is 0 frames, the resize should still fail
REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == -1);
state3();
REQUIRE(timeline->requestItemResize(cid2, length, false) == -1);
state3();
// We move cid1 out of the way
REQUIRE(timeline->requestClipMove(cid1, tid2, 0));
// now resize should work
REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == length - 2);
REQUIRE(timeline->requestItemResize(cid2, length, false) == length);
REQUIRE(timeline->checkConsistency());
}
SECTION("Group move")
{
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
REQUIRE(timeline->requestClipMove(cid2, tid1, length + 3));
REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 5));
REQUIRE(timeline->requestClipMove(cid4, tid2, 4));
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipTrackId(cid3) == tid1);
REQUIRE(timeline->getClipTrackId(cid4) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPosition(cid2) == length + 3);
REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5);
REQUIRE(timeline->getClipPosition(cid4) == 4);
// check that move is possible without groups
REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 3));
REQUIRE(timeline->checkConsistency());
undoStack->undo();
REQUIRE(timeline->checkConsistency());
// check that move is possible without groups
REQUIRE(timeline->requestClipMove(cid4, tid2, 9));
REQUIRE(timeline->checkConsistency());
undoStack->undo();
REQUIRE(timeline->checkConsistency());
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipTrackId(cid3) == tid1);
REQUIRE(timeline->getClipTrackId(cid4) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPosition(cid2) == length + 3);
REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5);
REQUIRE(timeline->getClipPosition(cid4) == 4);
};
state();
// grouping
REQUIRE(timeline->requestClipsGroup({cid1, cid3}));
REQUIRE(timeline->requestClipsGroup({cid1, cid4}));
// move left is now forbidden, because clip1 is at position 0
REQUIRE_FALSE(timeline->requestClipMove(cid3, tid1, 2 * length + 3));
state();
// this move is impossible, because clip1 runs into clip2
REQUIRE_FALSE(timeline->requestClipMove(cid4, tid2, 9));
state();
// this move is possible
REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 8));
auto state1 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getTrackClipsCount(tid3) == 0);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipTrackId(cid3) == tid1);
REQUIRE(timeline->getClipTrackId(cid4) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == 3);
REQUIRE(timeline->getClipPosition(cid2) == length + 3);
REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 8);
REQUIRE(timeline->getClipPosition(cid4) == 7);
};
state1();
// this move is possible
REQUIRE(timeline->requestClipMove(cid1, tid2, 8));
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid2) == 2);
REQUIRE(timeline->getTrackClipsCount(tid3) == 1);
REQUIRE(timeline->getClipTrackId(cid1) == tid2);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipTrackId(cid3) == tid2);
REQUIRE(timeline->getClipTrackId(cid4) == tid3);
REQUIRE(timeline->getClipPosition(cid1) == 8);
REQUIRE(timeline->getClipPosition(cid2) == length + 3);
REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5 + 8);
REQUIRE(timeline->getClipPosition(cid4) == 4 + 8);
};
state2();
undoStack->undo();
state1();
undoStack->redo();
state2();
REQUIRE(timeline->requestClipMove(cid1, tid1, 3));
state1();
}
SECTION("Group move consecutive clips")
{
REQUIRE(timeline->requestClipMove(cid1, tid1, 7));
REQUIRE(timeline->requestClipMove(cid2, tid1, 7 + length));
REQUIRE(timeline->requestClipMove(cid3, tid1, 7 + 2 * length));
REQUIRE(timeline->requestClipMove(cid4, tid1, 7 + 3 * length));
REQUIRE(timeline->requestClipsGroup({cid1, cid2, cid3, cid4}));
auto state = [&](int tid, int start) {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid) == 4);
int i = 0;
for (int cid : std::vector({cid1, cid2, cid3, cid4})) {
REQUIRE(timeline->getClipTrackId(cid) == tid);
REQUIRE(timeline->getClipPosition(cid) == start + i * length);
REQUIRE(timeline->getClipPlaytime(cid) == length);
i++;
}
};
state(tid1, 7);
auto check_undo = [&](int target, int tid, int oldTid) {
state(tid, target);
undoStack->undo();
state(oldTid, 7);
undoStack->redo();
state(tid, target);
undoStack->undo();
state(oldTid, 7);
};
REQUIRE(timeline->requestClipMove(cid1, tid1, 6));
qDebug() << "state1";
state(tid1, 6);
undoStack->undo();
state(tid1, 7);
undoStack->redo();
state(tid1, 6);
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
qDebug() << "state2";
state(tid1, 0);
undoStack->undo();
state(tid1, 6);
undoStack->redo();
state(tid1, 0);
undoStack->undo();
state(tid1, 6);
undoStack->undo();
state(tid1, 7);
REQUIRE(timeline->requestClipMove(cid3, tid1, 1 + 2 * length));
qDebug() << "state3";
check_undo(1, tid1, tid1);
REQUIRE(timeline->requestClipMove(cid4, tid1, 4 + 3 * length));
qDebug() << "state4";
check_undo(4, tid1, tid1);
REQUIRE(timeline->requestClipMove(cid4, tid1, 11 + 3 * length));
qDebug() << "state5";
check_undo(11, tid1, tid1);
REQUIRE(timeline->requestClipMove(cid2, tid1, 13 + length));
qDebug() << "state6";
check_undo(13, tid1, tid1);
REQUIRE(timeline->requestClipMove(cid1, tid1, 20));
qDebug() << "state7";
check_undo(20, tid1, tid1);
REQUIRE(timeline->requestClipMove(cid4, tid1, 7 + 4 * length));
qDebug() << "state8";
check_undo(length + 7, tid1, tid1);
REQUIRE(timeline->requestClipMove(cid2, tid1, 7 + 2 * length));
qDebug() << "state9";
check_undo(length + 7, tid1, tid1);
REQUIRE(timeline->requestClipMove(cid1, tid1, 7 + length));
qDebug() << "state10";
check_undo(length + 7, tid1, tid1);
REQUIRE(timeline->requestClipMove(cid2, tid2, 8 + length));
qDebug() << "state11";
check_undo(8, tid2, tid1);
}
SECTION("Group move to unavailable track")
{
REQUIRE(timeline->requestClipMove(cid1, tid1, 10));
REQUIRE(timeline->requestClipMove(cid2, tid2, 12));
REQUIRE(timeline->requestClipsGroup({cid1, cid2}));
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == 10);
REQUIRE(timeline->getClipPosition(cid2) == 12);
};
state();
REQUIRE_FALSE(timeline->requestClipMove(cid2, tid1, 10));
state();
REQUIRE_FALSE(timeline->requestClipMove(cid2, tid1, 100));
state();
REQUIRE_FALSE(timeline->requestClipMove(cid1, tid3, 100));
state();
}
SECTION("Group move with non-consecutive track ids")
{
int tid5 = TrackModel::construct(timeline);
int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
Q_UNUSED(cid6);
int tid6 = TrackModel::construct(timeline);
REQUIRE(tid5 + 1 != tid6);
REQUIRE(timeline->requestClipMove(cid1, tid5, 10));
REQUIRE(timeline->requestClipMove(cid2, tid5, length + 10));
REQUIRE(timeline->requestClipsGroup({cid1, cid2}));
auto state = [&](int t) {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(t) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == t);
REQUIRE(timeline->getClipTrackId(cid2) == t);
REQUIRE(timeline->getClipPosition(cid1) == 10);
REQUIRE(timeline->getClipPosition(cid2) == 10 + length);
};
state(tid5);
REQUIRE(timeline->requestClipMove(cid1, tid6, 10));
state(tid6);
}
SECTION("Movement of AV groups")
{
int tid6b = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid6 = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid5 = TrackModel::construct(timeline);
int tid5b = TrackModel::construct(timeline);
QString binId3 = createProducerWithSound(profile_model, binModel);
int cid6 = -1;
REQUIRE(timeline->requestClipInsertion(binId3, tid5, 3, cid6, true, true, false));
int cid7 = timeline->m_groups->getSplitPartner(cid6);
auto state = [&](int pos) {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid5) == 1);
REQUIRE(timeline->getTrackClipsCount(tid6) == 1);
REQUIRE(timeline->getClipTrackId(cid6) == tid5);
REQUIRE(timeline->getClipTrackId(cid7) == tid6);
REQUIRE(timeline->getClipPosition(cid6) == pos);
REQUIRE(timeline->getClipPosition(cid7) == pos);
REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly);
};
state(3);
// simple translation on the right
REQUIRE(timeline->requestClipMove(cid6, tid5, 10, true, true, true));
state(10);
undoStack->undo();
state(3);
undoStack->redo();
state(10);
// simple translation on the left, moving the audio clip this time
REQUIRE(timeline->requestClipMove(cid7, tid6, 1, true, true, true));
state(1);
undoStack->undo();
state(10);
undoStack->redo();
state(1);
// change track, moving video
REQUIRE(timeline->requestClipMove(cid6, tid5b, 7, true, true, true));
auto state2 = [&](int pos) {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid5b) == 1);
REQUIRE(timeline->getTrackClipsCount(tid6b) == 1);
REQUIRE(timeline->getClipTrackId(cid6) == tid5b);
REQUIRE(timeline->getClipTrackId(cid7) == tid6b);
REQUIRE(timeline->getClipPosition(cid6) == pos);
REQUIRE(timeline->getClipPosition(cid7) == pos);
REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly);
};
state2(7);
undoStack->undo();
state(1);
undoStack->redo();
state2(7);
// change track, moving audio
REQUIRE(timeline->requestClipMove(cid7, tid6b, 2, true, true, true));
state2(2);
undoStack->undo();
state2(7);
undoStack->redo();
state2(2);
undoStack->undo();
undoStack->undo();
state(1);
// Switching audio and video, going to the extra track
REQUIRE(timeline->requestClipMove(cid7, tid5b, 2, true, true, true) == 0);
// This test is invalid. AV clips cannot be switched between audio and video clips anymore
/*auto state3 = [&](int pos) {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid5b) == 1);
REQUIRE(timeline->getTrackClipsCount(tid6b) == 1);
REQUIRE(timeline->getClipTrackId(cid6) == tid6b);
REQUIRE(timeline->getClipTrackId(cid7) == tid5b);
REQUIRE(timeline->getClipPosition(cid6) == pos);
REQUIRE(timeline->getClipPosition(cid7) == pos);
REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::AudioOnly);
};
state3(2);
undoStack->undo();
state(1);
undoStack->redo();
state3(2);
undoStack->undo();
state(1);*/
// Switching audio and video, switching tracks in place
REQUIRE(timeline->requestClipMove(cid6, tid6, 1, true, true, true) == 0);
/*auto state4 = [&](int pos) {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid5) == 1);
REQUIRE(timeline->getTrackClipsCount(tid6) == 1);
REQUIRE(timeline->getClipTrackId(cid6) == tid6);
REQUIRE(timeline->getClipTrackId(cid7) == tid5);
REQUIRE(timeline->getClipPosition(cid6) == pos);
REQUIRE(timeline->getClipPosition(cid7) == pos);
REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::AudioOnly);
};
state4(1);
undoStack->undo();
state(1);
undoStack->redo();
state4(1);*/
}
SECTION("Clip copy")
{
int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
int l = timeline->getClipPlaytime(cid6);
REQUIRE(timeline->requestItemResize(cid6, l - 3, true, true, -1) == l - 3);
REQUIRE(timeline->requestItemResize(cid6, l - 7, false, true, -1) == l - 7);
int newId;
std::function undo = []() { return true; };
std::function redo = []() { return true; };
REQUIRE(TimelineFunctions::copyClip(timeline, cid6, newId, PlaylistState::VideoOnly, undo, redo));
REQUIRE(timeline->m_allClips[cid6]->binId() == timeline->m_allClips[newId]->binId());
// TODO check effects
}
binModel->clean();
pCore->m_projectManager = nullptr;
}
TEST_CASE("Check id unicity", "[ClipModel]")
{
auto binModel = pCore->projectItemModel();
binModel->clean();
std::shared_ptr undoStack = std::make_shared(nullptr);
std::shared_ptr guideModel = std::make_shared(undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(new Mlt::Profile(), undoStack);
Mock timMock(tim);
auto timeline = std::shared_ptr(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
RESET(timMock);
QString binId = createProducer(profile_model, "red", binModel);
std::vector track_ids;
std::unordered_set all_ids;
std::bernoulli_distribution coin(0.5);
const int nbr = 20;
for (int i = 0; i < nbr; i++) {
if (coin(g)) {
int tid = TrackModel::construct(timeline);
REQUIRE(all_ids.count(tid) == 0);
all_ids.insert(tid);
track_ids.push_back(tid);
REQUIRE(timeline->getTracksCount() == track_ids.size());
} else {
int cid = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
REQUIRE(all_ids.count(cid) == 0);
all_ids.insert(cid);
REQUIRE(timeline->getClipsCount() == all_ids.size() - track_ids.size());
}
}
REQUIRE(timeline->checkConsistency());
REQUIRE(all_ids.size() == nbr);
REQUIRE(all_ids.size() != track_ids.size());
binModel->clean();
pCore->m_projectManager = nullptr;
}
TEST_CASE("Undo and Redo", "[ClipModel]")
{
auto binModel = pCore->projectItemModel();
binModel->clean();
std::shared_ptr undoStack = std::make_shared(nullptr);
std::shared_ptr guideModel = std::make_shared(undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(new Mlt::Profile(), undoStack);
Mock