diff --git a/src/effects/effectsrepository.cpp b/src/effects/effectsrepository.cpp
index d7bb0a875..b5cbb63b4 100644
--- a/src/effects/effectsrepository.cpp
+++ b/src/effects/effectsrepository.cpp
@@ -1,211 +1,212 @@
/***************************************************************************
* 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 "effectsrepository.hpp"
#include "core.h"
#include "kdenlivesettings.h"
#include "profiles/profilemodel.hpp"
#include "xml/xml.hpp"
#include
#include
#include
#include
#include
#include
std::unique_ptr EffectsRepository::instance;
std::once_flag EffectsRepository::m_onceFlag;
EffectsRepository::EffectsRepository()
: AbstractAssetsRepository()
{
init();
// Check that our favorite effects are valid
QStringList invalidEffect;
for (const QString &effect : KdenliveSettings::favorite_effects()) {
if (!exists(effect)) {
invalidEffect << effect;
}
}
if (!invalidEffect.isEmpty()) {
pCore->displayMessage(i18n("Some of your favorite effects are invalid and were removed: %1", invalidEffect.join(QLatin1Char(','))), ErrorMessage);
QStringList newFavorites = KdenliveSettings::favorite_effects();
for (const QString &effect : invalidEffect) {
newFavorites.removeAll(effect);
}
KdenliveSettings::setFavorite_effects(newFavorites);
}
}
Mlt::Properties *EffectsRepository::retrieveListFromMlt() const
{
return pCore->getMltRepository()->filters();
}
void EffectsRepository::parseFavorites()
{
m_favorites = KdenliveSettings::favorite_effects().toSet();
}
void EffectsRepository::setFavorite(const QString &id, bool favorite)
{
Q_ASSERT(exists(id));
if (favorite) {
m_favorites << id;
} else {
m_favorites.remove(id);
}
KdenliveSettings::setFavorite_effects(QStringList::fromSet(m_favorites));
}
Mlt::Properties *EffectsRepository::getMetadata(const QString &effectId)
{
return pCore->getMltRepository()->metadata(filter_type, effectId.toLatin1().data());
}
void EffectsRepository::parseCustomAssetFile(const QString &file_name, std::unordered_map &customAssets) const
{
QFile file(file_name);
QDomDocument doc;
doc.setContent(&file, false);
file.close();
QDomElement base = doc.documentElement();
if (base.tagName() == QLatin1String("effectgroup")) {
// in that case we have a custom effect
Info info;
info.xml = base;
info.type = EffectType::Custom;
QString tag = base.attribute(QStringLiteral("tag"), QString());
QString id = base.hasAttribute(QStringLiteral("id")) ? base.attribute(QStringLiteral("id")) : tag;
QString name = base.attribute(QStringLiteral("name"), QString());
info.name = name;
info.id = id;
info.mltId = tag;
if (customAssets.count(id) > 0) {
qDebug() << "Error: conflicting effect name" << id;
} else {
customAssets[id] = info;
}
return;
}
QDomNodeList effects = doc.elementsByTagName(QStringLiteral("effect"));
int nbr_effect = effects.count();
if (nbr_effect == 0) {
qDebug() << "+++++++++++++\nEffect broken: " << file_name << "\n+++++++++++";
return;
}
for (int i = 0; i < nbr_effect; ++i) {
QDomNode currentNode = effects.item(i);
if (currentNode.isNull()) {
continue;
}
QDomElement currentEffect = currentNode.toElement();
Info result;
bool ok = parseInfoFromXml(currentEffect, result);
if (!ok) {
continue;
}
if (customAssets.count(result.id) > 0) {
- qDebug() << "Warning: duplicate custom definition of effect" << result.id << "found. Only last one will be considered";
+ qDebug() << "Warning: duplicate custom definition of effect" << result.id << "found. Only last one will be considered. Duplicate found in"
+ << file_name;
}
result.xml = currentEffect;
// Parse type information.
QString type = currentEffect.attribute(QStringLiteral("type"), QString());
if (type == QLatin1String("audio")) {
result.type = EffectType::Audio;
} else if (type == QLatin1String("custom")) {
result.type = EffectType::Custom;
} else if (type == QLatin1String("hidden")) {
result.type = EffectType::Hidden;
} else {
result.type = EffectType::Video;
}
customAssets[result.id] = result;
}
}
std::unique_ptr &EffectsRepository::get()
{
std::call_once(m_onceFlag, [] { instance.reset(new EffectsRepository()); });
return instance;
}
QStringList EffectsRepository::assetDirs() const
{
return QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("effects"), QStandardPaths::LocateDirectory);
}
void EffectsRepository::parseType(QScopedPointer &metadata, Info &res)
{
res.type = EffectType::Video;
Mlt::Properties tags((mlt_properties)metadata->get_data("tags"));
if (QString(tags.get(0)) == QLatin1String("Audio")) {
res.type = EffectType::Audio;
}
}
QString EffectsRepository::assetBlackListPath() const
{
return QStringLiteral(":data/blacklisted_effects.txt");
}
std::unique_ptr EffectsRepository::getEffect(const QString &effectId) const
{
Q_ASSERT(exists(effectId));
QString service_name = m_assets.at(effectId).mltId;
// We create the Mlt element from its name
auto filter = std::make_unique(pCore->getCurrentProfile()->profile(), service_name.toLatin1().constData(), nullptr);
return filter;
}
bool EffectsRepository::hasInternalEffect(const QString &effectId) const
{
// Retrieve the list of MLT's available assets.
QScopedPointer assets(retrieveListFromMlt());
int max = assets->count();
for (int i = 0; i < max; ++i) {
if (assets->get_name(i) == effectId) {
return true;
}
}
return false;
}
QPair EffectsRepository::reloadCustom(const QString &path)
{
std::unordered_map customAssets;
parseCustomAssetFile(path, customAssets);
QPair result;
// TODO: handle files with several effects
for (const auto &custom : customAssets) {
// Custom assets should override default ones
m_assets[custom.first] = custom.second;
result.first = custom.first;
result.second = custom.second.mltId;
}
return result;
}
diff --git a/src/logger.cpp b/src/logger.cpp
index ece26b838..8e6ece04d 100644
--- a/src/logger.cpp
+++ b/src/logger.cpp
@@ -1,381 +1,387 @@
/***************************************************************************
* 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 "bin/projectitemmodel.h"
+#include "timeline2/model/timelinefunctions.hpp"
#include "timeline2/model/timelinemodel.hpp"
#include
#include
#include
#include
#include
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wfloat-equal"
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wpedantic"
#include
#pragma GCC diagnostic pop
thread_local bool Logger::is_executing = false;
std::mutex Logger::mut;
std::vector Logger::operations;
std::vector Logger::invoks;
std::unordered_map> Logger::constr;
std::unordered_map Logger::translation_table;
std::unordered_map Logger::back_translation_table;
int Logger::dump_count = 0;
thread_local size_t Logger::result_awaiting = INT_MAX;
void Logger::init()
{
std::string cur_ind = "a";
auto incr_ind = [&](auto &&self, size_t i = 0) {
if (i >= cur_ind.size()) {
cur_ind += "a";
return;
}
if (cur_ind[i] == 'z') {
cur_ind[i] = 'A';
} else if (cur_ind[i] == 'Z') {
cur_ind[i] = 'a';
self(self, i + 1);
} else {
cur_ind[i]++;
}
};
for (const auto &o : {"TimelineModel", "TrackModel", "test_producer", "test_producer_sound"}) {
translation_table[std::string("constr_") + o] = cur_ind;
incr_ind(incr_ind);
}
for (const auto &m : rttr::type::get().get_methods()) {
translation_table[m.get_name().to_string()] = cur_ind;
incr_ind(incr_ind);
}
+ for (const auto &m : rttr::type::get().get_methods()) {
+ translation_table[m.get_name().to_string()] = cur_ind;
+ incr_ind(incr_ind);
+ }
+
for (const auto &i : translation_table) {
back_translation_table[i.second] = i.first;
}
}
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(const rttr::variant &ptr)
{
if (ptr.can_convert()) {
return "timeline_" + std::to_string(get_id_from_ptr(ptr.convert()));
} else if (ptr.can_convert()) {
return "binModel";
} 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);
}
void Logger::log_create_producer(const std::string &type, 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();
}
const std::string class_name = a.get_type().get_name().to_string();
}
constr[type].push_back({type, std::move(args)});
operations.emplace_back(ConstrId{type, constr[type].size() - 1});
}
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 &");
}
std::string quoted(const std::string &input)
{
#if __cpp_lib_quoted_string_io
std::stringstream ss;
ss << std::quoted(input);
return ss.str();
#else
// very incomplete implem
return "\"" + input + "\"";
#endif
}
} // namespace
void Logger::print_trace()
{
dump_count++;
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;
i = 0;
} else {
ss << ", ";
++i;
}
if (refs.count(i) > 0) {
ss << "dummy_" << i;
} else if (a.get_type() == rttr::type::get()) {
ss << a.convert();
} else if (a.get_type() == rttr::type::get()) {
ss << (a.convert() ? "true" : "false");
} else if (a.get_type().is_enumeration()) {
auto e = a.get_type().get_enumeration();
ss << e.get_name().to_string() << "::" << a.convert();
} else if (a.can_convert()) {
ss << quoted(a.convert().toStdString());
} else if (a.can_convert()) {
ss << quoted(a.convert());
} else if (a.can_convert>()) {
auto set = a.convert>();
ss << "{";
bool beg = true;
for (int s : set) {
if (beg)
beg = false;
else
ss << ", ";
ss << s;
}
ss << "}";
} 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;
}
}
return ss.str();
};
auto process_args_fuzz = [&](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;
i = 0;
} else {
ss << " ";
++i;
}
if (refs.count(i) > 0) {
continue;
} else if (a.get_type() == rttr::type::get()) {
ss << a.convert();
} else if (a.get_type() == rttr::type::get()) {
ss << (a.convert() ? "1" : "0");
} else if (a.get_type().is_enumeration()) {
ss << a.convert();
} else if (a.can_convert()) {
std::string out = a.convert().toStdString();
if (out.empty()) {
out = "$$";
}
ss << out;
} else if (a.can_convert()) {
std::string out = a.convert();
if (out.empty()) {
out = "$$";
}
ss << out;
} else if (a.can_convert>()) {
auto set = a.convert>();
ss << set.size() << " ";
bool beg = true;
for (int s : set) {
if (beg)
beg = false;
else
ss << " ";
ss << s;
}
} else if (a.get_type().is_pointer()) {
if (a.can_convert()) {
ss << get_id_from_ptr(a.convert());
} else if (a.can_convert()) {
// only one binModel, we skip the parameter since it's unambiguous
} else {
std::cout << "Error: unhandled ptr type " << a.get_type().get_name().to_string() << std::endl;
}
} else {
std::cout << "Error: unhandled arg type " << a.get_type().get_name().to_string() << std::endl;
}
}
return ss.str();
};
std::ofstream fuzz_file;
fuzz_file.open("fuzz_case_" + std::to_string(dump_count) + ".txt");
std::ofstream test_file;
test_file.open("test_case_" + std::to_string(dump_count) + ".cpp");
test_file << "TEST_CASE(\"Regression\") {" << std::endl;
test_file << "auto binModel = pCore->projectItemModel();" << std::endl;
test_file << "binModel->clean();" << 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 << "TimelineModel::next_id = 0;" << std::endl;
test_file << "{" << 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;
size_t nbrConstructedTimelines = 0;
auto check_consistancy = [&]() {
for (size_t i = 0; i < nbrConstructedTimelines; ++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;
std::string invok_name = invok.method;
if (translation_table.count(invok_name) > 0) {
auto args = invok.args;
if (rttr::type::get().get_method(invok_name).is_valid()) {
args.insert(args.begin(), invok.ptr);
// adding an arg just messed up the references
std::unordered_set new_refs;
for (const size_t &r : refs) {
new_refs.insert(r + 1);
}
std::swap(refs, new_refs);
}
fuzz_file << translation_table[invok_name] << " " << process_args_fuzz(args, refs) << std::endl;
} else {
std::cout << "ERROR: unknown method " << invok_name << std::endl;
}
} else if (o.can_convert()) {
ConstrId id = o.convert();
std::string constr_name = std::string("constr_") + id.type;
if (translation_table.count(constr_name) > 0) {
fuzz_file << translation_table[constr_name] << " " << process_args_fuzz(constr[id.type][id.id].second) << std::endl;
} else {
std::cout << "ERROR: unknown constructor " << constr_name << std::endl;
}
if (id.type == "TimelineModel") {
test_file << "TimelineItemModel tim_" << id.id << "(®_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;
nbrConstructedTimelines++;
} else if (id.type == "TrackModel") {
std::string params = process_args(constr[id.type][id.id].second);
test_file << "TrackModel::construct(" << params << ");" << std::endl;
} else if (id.type == "test_producer") {
std::string params = process_args(constr[id.type][id.id].second);
test_file << "createProducer(reg_profile, " << params << ");" << std::endl;
} else if (id.type == "test_producer_sound") {
std::string params = process_args(constr[id.type][id.id].second);
test_file << "createProducerWithSound(reg_profile, " << 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;
test_file << "pCore->m_projectManager = nullptr;" << std::endl;
test_file << "}" << std::endl;
}
void Logger::clear()
{
is_executing = false;
invoks.clear();
operations.clear();
constr.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
index 9e4931b56..db0b47bfb 100644
--- a/src/logger.hpp
+++ b/src/logger.hpp
@@ -1,180 +1,187 @@
/***************************************************************************
* 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
#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 Inits the logger. Must be called at startup
static void init();
/** @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();
/** @brief This logs the construction of an object of type T, whose new instance is passed. The instance will be kept around in case future calls refer to
* it. The arguments should more or less match the constructor arguments. In general, it's better to call the corresponding macro TRACE_CONSTR */
template static void log_constr(T *inst, std::vector args);
/** @brief Logs the call to a member function on a given instance of class T. The string contains the method name, and then the vector contains all the
* parameters. In general, the method should be registered in RTTR. It's better to call the corresponding macro TRACE() if appropriate */
template static void log(T *inst, std::string str, std::vector args);
static void log_create_producer(const std::string &type, std::vector args);
/** @brief When the last function logged has a return value, you can log it through this function, by passing the corresponding value. In general, it's
* better to call the macro TRACE_RES */
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();
/// @brief Resets the current log
static void clear();
static std::unordered_map translation_table;
static std::unordered_map back_translation_table;
protected:
/** @brief Look amongst the known instances to get the name of a given pointer */
static std::string get_ptr_name(const rttr::variant &ptr);
template static size_t get_id_from_ptr(T *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;
static int dump_count;
};
/** @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;
};
/// See Logger::log_constr. Note that the macro fills in the ptr instance for you.
#define TRACE_CONSTR(ptr, ...) \
LogGuard __guard; \
if (__guard.hasGuard()) { \
Logger::log_constr((ptr), {__VA_ARGS__}); \
}
/// See Logger::log. Note that the macro fills the ptr instance and the method name for you.
#define TRACE(...) \
LogGuard __guard; \
if (__guard.hasGuard()) { \
Logger::log(this, __FUNCTION__, {__VA_ARGS__}); \
}
+/// Same as TRACE, but called from a static function
+#define TRACE_STATIC(ptr, ...) \
+ LogGuard __guard; \
+ if (__guard.hasGuard()) { \
+ Logger::log(ptr.get(), __FUNCTION__, {__VA_ARGS__}); \
+ }
+
/// See Logger::log_res
#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.emplace_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.emplace_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/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp
index 659cd86db..162adc3cb 100644
--- a/src/timeline2/model/timelinefunctions.cpp
+++ b/src/timeline2/model/timelinefunctions.cpp
@@ -1,1318 +1,1338 @@
/*
Copyright (C) 2017 Jean-Baptiste Mardelle
This file is part of Kdenlive. See www.kdenlive.org.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License or (at your option) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
by the membership of KDE e.V.), which shall act as a proxy
defined in Section 14 of version 3 of the license.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include "timelinefunctions.hpp"
#include "bin/bin.h"
#include "bin/projectclip.h"
#include "bin/projectfolder.h"
#include "bin/projectitemmodel.h"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/kdenlivedoc.h"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "groupsmodel.hpp"
+#include "logger.hpp"
#include "timelineitemmodel.hpp"
#include "trackmodel.hpp"
#include "transitions/transitionsrepository.hpp"
#include
#include
#include
#include
#include
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wsign-conversion"
+#pragma GCC diagnostic ignored "-Wfloat-equal"
+#pragma GCC diagnostic ignored "-Wshadow"
+#pragma GCC diagnostic ignored "-Wpedantic"
+#include
+#pragma GCC diagnostic pop
+
+RTTR_REGISTRATION
+{
+ using namespace rttr;
+ registration::class_("TimelineFunctions")
+ .method("requestClipCut", select_overload, int, int)>(&TimelineFunctions::requestClipCut))(
+ parameter_names("timeline", "clipId", "position"));
+}
+
bool TimelineFunctions::cloneClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo,
Fun &redo)
{
// Special case: slowmotion clips
double clipSpeed = timeline->m_allClips[clipId]->getSpeed();
bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, clipSpeed, undo, redo);
timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize;
// copy useful timeline properties
timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]);
int duration = timeline->getClipPlaytime(clipId);
int init_duration = timeline->getClipPlaytime(newId);
if (duration != init_duration) {
int in = timeline->m_allClips[clipId]->getIn();
res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo);
res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo);
}
if (!res) {
return false;
}
std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId);
std::shared_ptr destStack = timeline->getClipEffectStackModel(newId);
destStack->importEffects(sourceStack, state);
return res;
}
bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr &timeline, const QStringList &binIds, int trackId, int position,
QList &clipIds, bool logUndo, bool refreshView)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
for (const QString &binId : binIds) {
int clipId;
if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) {
clipIds.append(clipId);
position += timeline->getItemPlaytime(clipId);
} else {
undo();
clipIds.clear();
return false;
}
}
if (logUndo) {
pCore->pushUndo(undo, redo, i18n("Insert Clips"));
}
return true;
}
bool TimelineFunctions::processClipCut(const std::shared_ptr &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo)
{
int trackId = timeline->getClipTrackId(clipId);
int trackDuration = timeline->getTrackById_const(trackId)->trackDuration();
int start = timeline->getClipPosition(clipId);
int duration = timeline->getClipPlaytime(clipId);
if (start > position || (start + duration) < position) {
return false;
}
PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState();
bool res = cloneClip(timeline, clipId, newId, state, undo, redo);
timeline->m_blockRefresh = true;
res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo);
int newDuration = timeline->getClipPlaytime(clipId);
// parse effects
std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId);
sourceStack->cleanFadeEffects(true, undo, redo);
std::shared_ptr destStack = timeline->getClipEffectStackModel(newId);
destStack->cleanFadeEffects(false, undo, redo);
res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo);
// The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now
bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration();
res = res && timeline->requestClipMove(newId, trackId, position, true, false, undo, redo);
if (durationChanged) {
// Track length changed, check project duration
Fun updateDuration = [timeline]() {
timeline->updateDuration();
return true;
};
updateDuration();
PUSH_LAMBDA(updateDuration, redo);
}
timeline->m_blockRefresh = false;
return res;
}
bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
+ TRACE_STATIC(timeline, clipId, position);
bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo);
if (result) {
pCore->pushUndo(undo, redo, i18n("Cut clip"));
}
+ TRACE_RES(result);
return result;
}
bool TimelineFunctions::requestClipCut(const std::shared_ptr &timeline, int clipId, int position, Fun &undo, Fun &redo)
{
const std::unordered_set clipselect = timeline->getGroupElements(clipId);
// Remove locked items
std::unordered_set clips;
for (int cid : clipselect) {
int tk = timeline->getClipTrackId(cid);
if (!timeline->getTrackById_const(tk)->isLocked()) {
clips.insert(cid);
}
}
timeline->requestClearSelection();
std::unordered_set topElements;
std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
// We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support)
timeline->requestClearSelection();
int count = 0;
QList newIds;
int mainId = -1;
QList clipsToCut;
for (int cid : clips) {
int start = timeline->getClipPosition(cid);
int duration = timeline->getClipPlaytime(cid);
if (start < position && (start + duration) > position) {
clipsToCut << cid;
}
}
for (int cid : clipsToCut) {
count++;
int newId;
bool res = processClipCut(timeline, cid, position, newId, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
if (cid == clipId) {
mainId = newId;
}
// splitted elements go temporarily in the same group as original ones.
timeline->m_groups->setInGroupOf(newId, cid, undo, redo);
newIds << newId;
}
if (count > 0 && timeline->m_groups->isInGroup(clipId)) {
// we now split the group hierarchy.
// As a splitting criterion, we compare start point with split position
auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; };
bool res = true;
for (const int topId : topElements) {
res = res && timeline->m_groups->split(topId, criterion, undo, redo);
}
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
}
return count > 0;
}
int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr &timeline, int trackId, int position)
{
std::unordered_set clips = timeline->getItemsInRange(trackId, position, -1);
if (!clips.empty()) {
timeline->requestSetSelection(clips);
return (*clips.cbegin());
}
return -1;
}
bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr &timeline, int itemId, int startPosition, int endPosition)
{
// Move group back to original position
int track = timeline->getItemTrackId(itemId);
bool isClip = timeline->isClip(itemId);
if (isClip) {
timeline->requestClipMove(itemId, track, startPosition, false, false);
} else {
timeline->requestCompositionMove(itemId, track, startPosition, false, false);
}
std::unordered_set clips = timeline->getGroupElements(itemId);
// Start undoable command
std::function undo = []() { return true; };
std::function redo = []() { return true; };
int res = timeline->requestClipsGroup(clips, undo, redo);
bool final = false;
if (res > -1) {
if (clips.size() > 1) {
final = timeline->requestGroupMove(itemId, res, 0, endPosition - startPosition, true, true, undo, redo);
} else {
// only 1 clip to be moved
if (isClip) {
final = timeline->requestClipMove(itemId, track, endPosition, true, true, undo, redo);
} else {
final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo);
}
}
}
timeline->requestClearSelection();
if (final) {
if (startPosition < endPosition) {
pCore->pushUndo(undo, redo, i18n("Insert space"));
} else {
pCore->pushUndo(undo, redo, i18n("Remove space"));
}
return true;
}
return false;
}
bool TimelineFunctions::extractZone(const std::shared_ptr &timeline, QVector tracks, QPoint zone, bool liftOnly)
{
// Start undoable command
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool result = true;
for (int trackId : tracks) {
if (timeline->getTrackById_const(trackId)->isLocked()) {
continue;
}
result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo);
}
if (result && !liftOnly) {
result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo);
}
pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone"));
return result;
}
bool TimelineFunctions::insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone,
bool overwrite)
{
// Start undoable command
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool result = true;
int trackId = trackIds.takeFirst();
if (overwrite) {
// Cut all tracks
auto it = timeline->m_allTracks.cbegin();
while (it != timeline->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (!trackIds.contains(target_track) && !timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
++it;
continue;
}
result = TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
if (!result) {
break;
}
++it;
}
} else {
// Cut all tracks
auto it = timeline->m_allTracks.cbegin();
while (it != timeline->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (!trackIds.contains(target_track) && !timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
++it;
continue;
}
int startClipId = timeline->getClipByPosition(target_track, insertFrame);
if (startClipId > -1) {
// There is a clip, cut it
TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo);
}
++it;
}
result = TimelineFunctions::insertSpace(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
}
if (result) {
int newId = -1;
QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1);
result = timeline->requestClipInsertion(binClipId, trackId, insertFrame, newId, true, true, true, undo, redo);
if (result) {
pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
}
}
if (!result) {
undo();
}
return result;
}
bool TimelineFunctions::liftZone(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
{
// Check if there is a clip at start point
int startClipId = timeline->getClipByPosition(trackId, zone.x());
if (startClipId > -1) {
// There is a clip, cut it
if (timeline->getClipPosition(startClipId) < zone.x()) {
qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId;
TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo);
qDebug() << "/// CUTTING AT START DONE";
}
}
int endClipId = timeline->getClipByPosition(trackId, zone.y());
if (endClipId > -1) {
// There is a clip, cut it
if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) {
qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId;
TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo);
qDebug() << "/// CUTTING AT END DONE";
}
}
std::unordered_set clips = timeline->getItemsInRange(trackId, zone.x(), zone.y());
for (const auto &clipId : clips) {
timeline->requestItemDeletion(clipId, undo, redo);
}
return true;
}
bool TimelineFunctions::removeSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
{
Q_UNUSED(trackId)
std::unordered_set clips = timeline->getItemsInRange(-1, zone.y() - 1, -1, true);
bool result = false;
if (!clips.empty()) {
int clipId = *clips.begin();
if (clips.size() > 1) {
int res = timeline->requestClipsGroup(clips, undo, redo);
if (res > -1) {
result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo);
if (result) {
result = timeline->requestClipUngroup(clipId, undo, redo);
}
if (!result) {
undo();
}
}
} else {
// only 1 clip to be moved
int clipStart = timeline->getItemPosition(clipId);
result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, undo, redo);
}
}
return result;
}
bool TimelineFunctions::insertSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
{
Q_UNUSED(trackId)
std::unordered_set clips = timeline->getItemsInRange(-1, zone.x(), -1, true);
bool result = true;
if (!clips.empty()) {
int clipId = *clips.begin();
if (clips.size() > 1) {
int res = timeline->requestClipsGroup(clips, undo, redo);
if (res > -1) {
result = timeline->requestGroupMove(clipId, res, 0, zone.y() - zone.x(), true, true, undo, redo);
if (result) {
result = timeline->requestClipUngroup(clipId, undo, redo);
} else {
pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage);
}
}
} else {
// only 1 clip to be moved
int clipStart = timeline->getItemPosition(clipId);
result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart + (zone.y() - zone.x()), true, true, undo, redo);
}
}
return result;
}
bool TimelineFunctions::requestItemCopy(const std::shared_ptr &timeline, int clipId, int trackId, int position)
{
Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId));
Fun undo = []() { return true; };
Fun redo = []() { return true; };
int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId));
int deltaPos = position - timeline->getItemPosition(clipId);
std::unordered_set allIds = timeline->getGroupElements(clipId);
std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips
bool res = true;
for (int id : allIds) {
int newId = -1;
if (timeline->isClip(id)) {
PlaylistState::ClipState state = timeline->m_allClips[id]->clipState();
res = cloneClip(timeline, id, newId, state, undo, redo);
res = res && (newId != -1);
}
int target_position = timeline->getItemPosition(id) + deltaPos;
int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack;
if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) {
auto it = timeline->m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
if (timeline->isClip(id)) {
res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo);
} else {
const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
std::unique_ptr transProps(timeline->m_allCompositions[id]->properties());
res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo);
}
} else {
res = false;
}
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
mapping[id] = newId;
}
qDebug() << "Successful copy, coping groups...";
res = timeline->m_groups->copyGroups(mapping, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
return true;
}
void TimelineFunctions::showClipKeyframes(const std::shared_ptr &timeline, int clipId, bool value)
{
timeline->m_allClips[clipId]->setShowKeyframes(value);
QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
}
void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr &timeline, int compoId, bool value)
{
timeline->m_allCompositions[compoId]->setShowKeyframes(value);
QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
}
bool TimelineFunctions::switchEnableState(const std::shared_ptr &timeline, int clipId)
{
PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState();
PlaylistState::ClipState state = PlaylistState::Disabled;
bool disable = true;
if (oldState == PlaylistState::Disabled) {
state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType();
disable = false;
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = changeClipState(timeline, clipId, state, undo, redo);
if (result) {
pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip"));
}
return result;
}
bool TimelineFunctions::changeClipState(const std::shared_ptr &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo)
{
int track = timeline->getClipTrackId(clipId);
int start = -1;
int end = -1;
if (track > -1) {
if (!timeline->getTrackById_const(track)->isAudioTrack()) {
start = timeline->getItemPosition(clipId);
end = start + timeline->getItemPlaytime(clipId);
}
}
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
bool result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo);
Fun local_update = [start, end, timeline]() {
if (start > -1) {
timeline->invalidateZone(start, end);
timeline->checkRefresh(start, end);
}
return true;
};
if (start > -1) {
local_update();
PUSH_LAMBDA(local_update, local_redo);
PUSH_LAMBDA(local_update, local_undo);
}
UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
return result;
}
bool TimelineFunctions::requestSplitAudio(const std::shared_ptr &timeline, int clipId, int audioTarget)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
const std::unordered_set clips = timeline->getGroupElements(clipId);
bool done = false;
// Now clear selection so we don't mess with groups
timeline->requestClearSelection(false, undo, redo);
for (int cid : clips) {
if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) {
// clip without audio or audio only, skip
pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage);
return false;
}
int position = timeline->getClipPosition(cid);
int track = timeline->getClipTrackId(cid);
QList possibleTracks = audioTarget >= 0 ? QList() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack);
if (possibleTracks.isEmpty()) {
// No available audio track for splitting, abort
undo();
pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage);
return false;
}
int newId;
bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
return false;
}
bool success = false;
while (!success && !possibleTracks.isEmpty()) {
int newTrack = possibleTracks.takeFirst();
success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo);
}
TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo);
success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo);
if (!success) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
return false;
}
done = true;
}
if (done) {
timeline->requestSetSelection(clips, undo, redo);
pCore->pushUndo(undo, redo, i18n("Split Audio"));
}
return done;
}
bool TimelineFunctions::requestSplitVideo(const std::shared_ptr &timeline, int clipId, int videoTarget)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
const std::unordered_set clips = timeline->getGroupElements(clipId);
bool done = false;
// Now clear selection so we don't mess with groups
timeline->requestClearSelection();
for (int cid : clips) {
if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) {
// clip without audio or audio only, skip
continue;
}
int position = timeline->getClipPosition(cid);
QList possibleTracks = QList() << videoTarget;
if (possibleTracks.isEmpty()) {
// No available audio track for splitting, abort
undo();
pCore->displayMessage(i18n("No available video track for split operation"), ErrorMessage);
return false;
}
int newId;
bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Video split failed"), ErrorMessage);
return false;
}
bool success = false;
while (!success && !possibleTracks.isEmpty()) {
int newTrack = possibleTracks.takeFirst();
success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo);
}
TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo);
success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo);
if (!success) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Video split failed"), ErrorMessage);
return false;
}
done = true;
}
if (done) {
pCore->pushUndo(undo, redo, i18n("Split Video"));
}
return done;
}
void TimelineFunctions::setCompositionATrack(const std::shared_ptr &timeline, int cid, int aTrack)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
std::shared_ptr compo = timeline->getCompositionPtr(cid);
int previousATrack = compo->getATrack();
int previousAutoTrack = static_cast(compo->getForcedTrack() == -1);
bool autoTrack = aTrack < 0;
if (autoTrack) {
// Automatic track compositing, find lower video track
aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId());
}
int start = timeline->getItemPosition(cid);
int end = start + timeline->getItemPlaytime(cid);
Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() {
QScopedPointer field(timeline->m_tractor->field());
field->lock();
timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack);
timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1));
field->unlock();
QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack});
timeline->invalidateZone(start, end);
timeline->checkRefresh(start, end);
return true;
};
Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() {
QScopedPointer field(timeline->m_tractor->field());
field->lock();
timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0);
timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1));
field->unlock();
QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack});
timeline->invalidateZone(start, end);
timeline->checkRefresh(start, end);
return true;
};
if (local_redo()) {
PUSH_LAMBDA(local_undo, undo);
PUSH_LAMBDA(local_redo, redo);
}
pCore->pushUndo(undo, redo, i18n("Change Composition Track"));
}
void TimelineFunctions::enableMultitrackView(const std::shared_ptr &timeline, bool enable)
{
QList videoTracks;
for (const auto &track : timeline->m_iteratorTable) {
if (timeline->getTrackById_const(track.first)->isAudioTrack() || timeline->getTrackById_const(track.first)->isHidden()) {
continue;
}
videoTracks << track.first;
}
if (videoTracks.size() < 2) {
pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), InformationMessage);
}
// First, dis/enable track compositing
QScopedPointer service(timeline->m_tractor->field());
Mlt::Field *field = timeline->m_tractor->field();
field->lock();
while ((service != nullptr) && service->is_valid()) {
if (service->type() == transition_type) {
Mlt::Transition t((mlt_transition)service->get_service());
QString serviceName = t.get("mlt_service");
int added = t.get_int("internal_added");
if (added == 237 && serviceName != QLatin1String("mix")) {
// remove all compositing transitions
t.set("disable", enable ? "1" : nullptr);
} else if (!enable && added == 200) {
field->disconnect_service(t);
}
}
service.reset(service->producer());
}
if (enable) {
for (int i = 0; i < videoTracks.size(); ++i) {
Mlt::Transition transition(*timeline->m_tractor->profile(), "composite");
transition.set("mlt_service", "composite");
transition.set("a_track", 0);
transition.set("b_track", timeline->getTrackMltIndex(videoTracks.at(i)));
transition.set("distort", 0);
transition.set("aligned", 0);
// 200 is an arbitrary number so we can easily remove these transition later
transition.set("internal_added", 200);
QString geometry;
switch (i) {
case 0:
switch (videoTracks.size()) {
case 2:
geometry = QStringLiteral("0 0 50% 100%");
break;
case 3:
geometry = QStringLiteral("0 0 33% 100%");
break;
case 4:
geometry = QStringLiteral("0 0 50% 50%");
break;
case 5:
case 6:
geometry = QStringLiteral("0 0 33% 50%");
break;
default:
geometry = QStringLiteral("0 0 33% 33%");
break;
}
break;
case 1:
switch (videoTracks.size()) {
case 2:
geometry = QStringLiteral("50% 0 50% 100%");
break;
case 3:
geometry = QStringLiteral("33% 0 33% 100%");
break;
case 4:
geometry = QStringLiteral("50% 0 50% 50%");
break;
case 5:
case 6:
geometry = QStringLiteral("33% 0 33% 50%");
break;
default:
geometry = QStringLiteral("33% 0 33% 33%");
break;
}
break;
case 2:
switch (videoTracks.size()) {
case 3:
geometry = QStringLiteral("66% 0 33% 100%");
break;
case 4:
geometry = QStringLiteral("0 50% 50% 50%");
break;
case 5:
case 6:
geometry = QStringLiteral("66% 0 33% 50%");
break;
default:
geometry = QStringLiteral("66% 0 33% 33%");
break;
}
break;
case 3:
switch (videoTracks.size()) {
case 4:
geometry = QStringLiteral("50% 50% 50% 50%");
break;
case 5:
case 6:
geometry = QStringLiteral("0 50% 33% 50%");
break;
default:
geometry = QStringLiteral("0 33% 33% 33%");
break;
}
break;
case 4:
switch (videoTracks.size()) {
case 5:
case 6:
geometry = QStringLiteral("33% 50% 33% 50%");
break;
default:
geometry = QStringLiteral("33% 33% 33% 33%");
break;
}
break;
case 5:
switch (videoTracks.size()) {
case 6:
geometry = QStringLiteral("66% 50% 33% 50%");
break;
default:
geometry = QStringLiteral("66% 33% 33% 33%");
break;
}
break;
case 6:
geometry = QStringLiteral("0 66% 33% 33%");
break;
case 7:
geometry = QStringLiteral("33% 66% 33% 33%");
break;
default:
geometry = QStringLiteral("66% 66% 33% 33%");
break;
}
// Add transition to track:
transition.set("geometry", geometry.toUtf8().constData());
transition.set("always_active", 1);
field->plant_transition(transition, 0, timeline->getTrackMltIndex(videoTracks.at(i)));
}
}
field->unlock();
timeline->requestMonitorRefresh();
}
void TimelineFunctions::saveTimelineSelection(const std::shared_ptr &timeline, const std::unordered_set &selection,
const QDir &targetDir)
{
bool ok;
QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal,
QString(), &ok);
if (name.isEmpty() || !ok) {
return;
}
if (targetDir.exists(name + QStringLiteral(".mlt"))) {
// TODO: warn and ask for overwrite / rename
}
int offset = -1;
int lowerAudioTrack = -1;
int lowerVideoTrack = -1;
QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt"));
// Build a copy of selected tracks.
QMap sourceTracks;
for (int i : selection) {
int sourceTrack = timeline->getItemTrackId(i);
int clipPos = timeline->getItemPosition(i);
if (offset < 0 || clipPos < offset) {
offset = clipPos;
}
int trackPos = timeline->getTrackMltIndex(sourceTrack);
if (!sourceTracks.contains(trackPos)) {
sourceTracks.insert(trackPos, sourceTrack);
}
}
// Build target timeline
Mlt::Tractor newTractor(*timeline->m_tractor->profile());
QScopedPointer field(newTractor.field());
int ix = 0;
QString composite = TransitionsRepository::get()->getCompositingTransition();
QMapIterator i(sourceTracks);
QList compositions;
while (i.hasNext()) {
i.next();
QScopedPointer newTrackPlaylist(new Mlt::Playlist(*newTractor.profile()));
newTractor.set_track(*newTrackPlaylist, ix);
// QScopedPointer trackProducer(newTractor.track(ix));
int trackId = i.value();
sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix);
std::shared_ptr track = timeline->getTrackById_const(trackId);
bool isAudio = track->isAudioTrack();
if (isAudio) {
newTrackPlaylist->set("hide", 1);
if (lowerAudioTrack < 0) {
lowerAudioTrack = ix;
}
} else {
newTrackPlaylist->set("hide", 2);
if (lowerVideoTrack < 0) {
lowerVideoTrack = ix;
}
}
for (int itemId : selection) {
if (timeline->getItemTrackId(itemId) == trackId) {
// Copy clip on the destination track
if (timeline->isClip(itemId)) {
int clip_position = timeline->m_allClips[itemId]->getPosition();
auto clip_loc = track->getClipIndexAt(clip_position);
int target_clip = clip_loc.second;
QSharedPointer clip = track->getClipProducer(target_clip);
newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1);
} else if (timeline->isComposition(itemId)) {
// Composition
auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get());
QString id(t->get("kdenlive_id"));
QString internal(t->get("internal_added"));
if (internal.isEmpty()) {
compositions << t;
if (id.isEmpty()) {
qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service");
t->set("kdenlive_id", t->get("mlt_service"));
}
}
}
}
}
ix++;
}
// Sort compositions and insert
if (!compositions.isEmpty()) {
std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); });
while (!compositions.isEmpty()) {
QScopedPointer t(compositions.takeFirst());
if (sourceTracks.contains(t->get_a_track()) && sourceTracks.contains(t->get_b_track())) {
Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service"));
Mlt::Properties sourceProps(t->get_properties());
newComposition.inherit(sourceProps);
QString id(t->get("kdenlive_id"));
int in = qMax(0, t->get_in() - offset);
int out = t->get_out() - offset;
newComposition.set_in_and_out(in, out);
int a_track = sourceTracks.value(t->get_a_track());
int b_track = sourceTracks.value(t->get_b_track());
field->plant_transition(newComposition, a_track, b_track);
}
}
}
// Track compositing
i.toFront();
ix = 0;
while (i.hasNext()) {
i.next();
int trackId = i.value();
std::shared_ptr track = timeline->getTrackById_const(trackId);
bool isAudio = track->isAudioTrack();
if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) {
// add track compositing / mix
Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData());
if (isAudio) {
t.set("sum", 1);
}
t.set("always_active", 1);
t.set("internal_added", 237);
field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix);
}
ix++;
}
Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData());
xmlConsumer.set("terminate_on_pause", 1);
xmlConsumer.connect(newTractor);
xmlConsumer.run();
}
int TimelineFunctions::getTrackOffset(const std::shared_ptr &timeline, int startTrack, int destTrack)
{
qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack;
int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
int destTrackMltIndex = timeline->getTrackMltIndex(destTrack);
int offset = 0;
qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex;
if (masterTrackMltIndex == destTrackMltIndex) {
return offset;
}
int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1;
bool isAudio = timeline->isAudioTrack(startTrack);
int track = masterTrackMltIndex;
while (track != destTrackMltIndex) {
track += step;
qDebug() << "+ + +TESTING TRACK: " << track;
int trackId = timeline->getTrackIndexFromPosition(track - 1);
if (isAudio == timeline->isAudioTrack(trackId)) {
offset += step;
}
}
return offset;
}
int TimelineFunctions::getOffsetTrackId(const std::shared_ptr &timeline, int startTrack, int offset, bool audioOffset)
{
int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
bool isAudio = timeline->isAudioTrack(startTrack);
if (isAudio != audioOffset) {
offset = -offset;
}
qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset;
while (offset != 0) {
masterTrackMltIndex += offset > 0 ? 1 : -1;
qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex;
if (masterTrackMltIndex < 0) {
masterTrackMltIndex = 0;
break;
}
if (masterTrackMltIndex > (int)timeline->m_allTracks.size()) {
masterTrackMltIndex = (int)timeline->m_allTracks.size();
break;
}
int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
if (timeline->isAudioTrack(trackId) == isAudio) {
offset += offset > 0 ? -1 : 1;
}
}
return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
}
QPair, QList> TimelineFunctions::getAVTracksIds(const std::shared_ptr &timeline)
{
QList audioTracks;
QList videoTracks;
for (const auto &track : timeline->m_allTracks) {
if (track->isAudioTrack()) {
audioTracks << track->getId();
} else {
videoTracks << track->getId();
}
}
return {audioTracks, videoTracks};
}
QString TimelineFunctions::copyClips(const std::shared_ptr &timeline, const std::unordered_set &itemIds)
{
int clipId = *(itemIds.begin());
timeline->requestClearSelection();
// TODO better guess for master track
int masterTid = timeline->getItemTrackId(clipId);
bool audioCopy = timeline->isAudioTrack(masterTid);
int masterTrack = timeline->getTrackPosition(masterTid);
std::unordered_set groupRoots;
std::transform(itemIds.begin(), itemIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
qDebug() << "==============\n GROUP ROOTS: ";
for (int gp : groupRoots) {
qDebug() << "GROUP: " << gp;
}
qDebug() << "\n=======";
QDomDocument copiedItems;
int offset = -1;
QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene"));
copiedItems.appendChild(container);
QStringList binIds;
for (int id : itemIds) {
if (offset == -1 || timeline->getItemPosition(id) < offset) {
offset = timeline->getItemPosition(id);
}
if (timeline->isClip(id)) {
container.appendChild(timeline->m_allClips[id]->toXml(copiedItems));
const QString bid = timeline->m_allClips[id]->binId();
if (!binIds.contains(bid)) {
binIds << bid;
}
} else if (timeline->isComposition(id)) {
container.appendChild(timeline->m_allCompositions[id]->toXml(copiedItems));
} else {
Q_ASSERT(false);
}
}
QDomElement container2 = copiedItems.createElement(QStringLiteral("bin"));
container.appendChild(container2);
for (const QString &id : binIds) {
std::shared_ptr clip = pCore->projectItemModel()->getClipByBinID(id);
QDomDocument tmp;
container2.appendChild(clip->toXml(tmp));
}
container.setAttribute(QStringLiteral("offset"), offset);
if (audioCopy) {
int masterMirror = timeline->getMirrorVideoTrackId(masterTid);
if (masterMirror == -1) {
QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline);
if (!projectTracks.second.isEmpty()) {
masterTrack = timeline->getTrackPosition(projectTracks.second.first());
}
} else {
masterTrack = timeline->getTrackPosition(masterMirror);
}
}
/* masterTrack contains the reference track over which we want to paste.
this is a video track, unless audioCopy is defined */
container.setAttribute(QStringLiteral("masterTrack"), masterTrack);
container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid")));
QDomElement grp = copiedItems.createElement(QStringLiteral("groups"));
container.appendChild(grp);
grp.appendChild(copiedItems.createTextNode(timeline->m_groups->toJson(groupRoots)));
// TODO: groups
qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------";
return copiedItems.toString();
}
bool TimelineFunctions::pasteClips(const std::shared_ptr &timeline, const QString &pasteString, int trackId, int position)
{
QDomDocument copiedItems;
copiedItems.setContent(pasteString);
if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) {
qDebug() << " / / READING CLIPS FROM CLIPBOARD";
} else {
return false;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid"));
QMap mappedIds;
// Check available tracks
QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline);
int masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack")).toInt();
QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition"));
// find paste tracks
// List of all source audio tracks
QList audioTracks;
// List of all source video tracks
QList videoTracks;
// List of all audio tracks with their corresponding video mirror
QMap audioMirrors;
// List of all source audio tracks that don't have video mirror
QList singleAudioTracks;
for (int i = 0; i < clips.count(); i++) {
QDomElement prod = clips.at(i).toElement();
int trackPos = prod.attribute(QStringLiteral("track")).toInt();
bool audioTrack = prod.hasAttribute(QStringLiteral("audioTrack"));
if (audioTrack) {
if (!audioTracks.contains(trackPos)) {
audioTracks << trackPos;
}
int videoMirror = prod.attribute(QStringLiteral("mirrorTrack")).toInt();
if (videoMirror == -1) {
if (singleAudioTracks.contains(trackPos)) {
continue;
}
singleAudioTracks << trackPos;
continue;
}
audioMirrors.insert(trackPos, videoMirror);
if (videoTracks.contains(videoMirror)) {
continue;
}
videoTracks << videoMirror;
} else {
if (videoTracks.contains(trackPos)) {
continue;
}
videoTracks << trackPos;
}
}
for (int i = 0; i < compositions.count(); i++) {
QDomElement prod = compositions.at(i).toElement();
int trackPos = prod.attribute(QStringLiteral("track")).toInt();
if (!videoTracks.contains(trackPos)) {
videoTracks << trackPos;
}
int atrackPos = prod.attribute(QStringLiteral("a_track")).toInt();
if (atrackPos == 0 || videoTracks.contains(atrackPos)) {
continue;
}
videoTracks << atrackPos;
}
// Now we have a list of all source tracks, check that we have enough target tracks
qSort(videoTracks);
qSort(audioTracks);
qSort(singleAudioTracks);
int requestedVideoTracks = videoTracks.isEmpty() ? 0 : videoTracks.last() - videoTracks.first() + 1;
int requestedAudioTracks = audioTracks.isEmpty() ? 0 : audioTracks.last() - audioTracks.first() + 1;
if (requestedVideoTracks > projectTracks.second.size() || requestedAudioTracks > projectTracks.first.size()) {
pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500);
return false;
}
// Check we have enough tracks above/below
if (requestedVideoTracks > 0) {
qDebug() << "MASTERSTK: " << masterSourceTrack << ", VTKS: " << videoTracks;
int tracksBelow = masterSourceTrack - videoTracks.first();
int tracksAbove = videoTracks.last() - masterSourceTrack;
qDebug() << "// RQST TKS BELOW: " << tracksBelow << " / ABOVE: " << tracksAbove;
qDebug() << "// EXISTING TKS BELOW: " << projectTracks.second.indexOf(trackId) << ", IX: " << trackId;
qDebug() << "// EXISTING TKS ABOVE: " << projectTracks.second.size() << " - " << projectTracks.second.indexOf(trackId);
if (projectTracks.second.indexOf(trackId) < tracksBelow) {
qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow;
// not enough tracks below, try to paste on upper track
trackId = projectTracks.second.at(tracksBelow);
} else if ((projectTracks.second.size() - (projectTracks.second.indexOf(trackId) + 1)) < tracksAbove) {
// not enough tracks above, try to paste on lower track
qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.second.size() - tracksAbove);
trackId = projectTracks.second.at(projectTracks.second.size() - tracksAbove - 1);
}
}
QMap tracksMap;
int masterIx = projectTracks.second.indexOf(trackId);
qDebug() << "/// PROJECT VIDEO TKS: " << projectTracks.second << ", MASTER: " << trackId;
qDebug() << "/// PASTE VIDEO TKS: " << videoTracks << " / MASTER: " << masterSourceTrack;
qDebug() << "/// MASTER PASTE: " << masterIx;
for (int tk : videoTracks) {
tracksMap.insert(tk, projectTracks.second.at(masterIx + tk - masterSourceTrack));
qDebug() << "// TK MAP: " << tk << " => " << projectTracks.second.at(masterIx + tk - masterSourceTrack);
}
QMapIterator it(audioMirrors);
while (it.hasNext()) {
it.next();
int videoIx = tracksMap.value(it.value());
// qDebug()<<"// TK AUDIO MAP: "< "<getMirrorAudioTrackId(videoIx);
tracksMap.insert(it.key(), timeline->getMirrorAudioTrackId(videoIx));
}
for (int i = 0; i < singleAudioTracks.size(); i++) {
tracksMap.insert(singleAudioTracks.at(i), projectTracks.first.at(i));
}
qDebug() << "++++++++++++++++++++++++++\n\n\n// AUDIO MIRRORS: " << audioMirrors << ", RESULT: " << tracksMap;
if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) {
// paste from another document, import bin clips
QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips"));
if (folderId.isEmpty()) {
// Folder doe not exist
const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId();
folderId = QString::number(pCore->projectItemModel()->getFreeFolderId());
pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo);
}
QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer"));
for (int i = 0; i < binClips.count(); ++i) {
QDomElement currentProd = binClips.item(i).toElement();
QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id"));
if (!pCore->projectItemModel()->isIdFree(clipId)) {
QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId());
Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId);
mappedIds.insert(clipId, updatedId);
clipId = updatedId;
}
pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo);
}
}
int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt();
bool res = true;
QLocale locale;
std::unordered_map correspondingIds;
QList waitingIds;
for (int i = 0; i < clips.count(); i++) {
waitingIds << i;
}
for (int i = 0; res && !waitingIds.isEmpty();) {
if (i >= waitingIds.size()) {
i = 0;
}
QDomElement prod = clips.at(waitingIds.at(i)).toElement();
QString originalId = prod.attribute(QStringLiteral("binid"));
if (mappedIds.contains(originalId)) {
// Map id
originalId = mappedIds.value(originalId);
}
int in = prod.attribute(QStringLiteral("in")).toInt();
int out = prod.attribute(QStringLiteral("out")).toInt();
int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
int pos = prod.attribute(QStringLiteral("position")).toInt() - offset;
double speed = locale.toDouble(prod.attribute(QStringLiteral("speed")));
int newId;
bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), speed, undo, redo);
if (created) {
// Master producer is ready
// ids.removeAll(originalId);
waitingIds.removeAt(i);
} else {
i++;
qApp->processEvents();
continue;
}
if (timeline->m_allClips[newId]->m_endlessResize) {
out = out - in;
in = 0;
timeline->m_allClips[newId]->m_producer->set("length", out + 1);
}
timeline->m_allClips[newId]->setInOut(in, out);
int targetId = prod.attribute(QStringLiteral("id")).toInt();
correspondingIds[targetId] = newId;
res = res && timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, undo, redo);
// paste effects
if (res) {
std::shared_ptr destStack = timeline->getClipEffectStackModel(newId);
destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), undo, redo);
}
}
// Compositions
for (int i = 0; res && i < compositions.count(); i++) {
QDomElement prod = compositions.at(i).toElement();
QString originalId = prod.attribute(QStringLiteral("composition"));
int in = prod.attribute(QStringLiteral("in")).toInt();
int out = prod.attribute(QStringLiteral("out")).toInt();
int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
int aTrackId = prod.attribute(QStringLiteral("a_track")).toInt();
if (aTrackId > 0) {
aTrackId = timeline->getTrackPosition(tracksMap.value(aTrackId));
}
int pos = prod.attribute(QStringLiteral("position")).toInt() - offset;
int newId;
auto transProps = std::make_unique();
QDomNodeList props = prod.elementsByTagName(QStringLiteral("property"));
for (int j = 0; j < props.count(); j++) {
transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(),
props.at(j).toElement().text().toUtf8().constData());
}
res = timeline->requestCompositionInsertion(originalId, curTrackId, aTrackId, position + pos, out - in, std::move(transProps), newId, undo, redo);
}
if (!res) {
undo();
return false;
}
const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text();
// Rebuild groups
timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, undo, redo);
pCore->pushUndo(undo, redo, i18n("Paste clips"));
return true;
}
bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr &timeline, int trackId, int position, bool affectAllTracks)
{
// find blank duration
int spaceDuration = timeline->getTrackById_const(trackId)->getBlankSizeAtPos(position);
int cid = requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position);
if (cid == -1) {
return false;
}
int start = timeline->getItemPosition(cid);
requestSpacerEndOperation(timeline, cid, start, start - spaceDuration);
return true;
}
diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp
index 828fa5171..8c3c1ebf1 100644
--- a/src/timeline2/model/timelinemodel.cpp
+++ b/src/timeline2/model/timelinemodel.cpp
@@ -1,2990 +1,3028 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "timelinemodel.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "effects/effectsrepository.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "groupsmodel.hpp"
#include "kdenlivesettings.h"
#include "logger.hpp"
#include "snapmodel.hpp"
#include "timelinefunctions.hpp"
#include "trackmodel.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "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))(
parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
.method("requestCompositionMove", select_overload(&TimelineModel::requestCompositionMove))(
parameter_names("compoId", "trackId", "position", "updateView", "logUndo"))
.method("requestClipInsertion", select_overload(&TimelineModel::requestClipInsertion))(
parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets"))
.method("requestItemDeletion", select_overload(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo"))
.method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove))(
parameter_names("itemId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
.method("requestGroupDeletion", select_overload(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo"))
.method("requestItemResize", select_overload