diff --git a/src/effects/effectstack/model/effectstackmodel.cpp b/src/effects/effectstack/model/effectstackmodel.cpp
index 9fdb88454..b444bc5c5 100644
--- a/src/effects/effectstack/model/effectstackmodel.cpp
+++ b/src/effects/effectstack/model/effectstackmodel.cpp
@@ -1,1117 +1,1135 @@
/***************************************************************************
* 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 "effectstackmodel.hpp"
#include "assets/keyframes/model/keyframemodellist.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "effectgroupmodel.hpp"
#include "effectitemmodel.hpp"
#include "effects/effectsrepository.hpp"
#include "macros.hpp"
#include "timeline2/model/timelinemodel.hpp"
#include
#include
#include
#include
EffectStackModel::EffectStackModel(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack)
: AbstractTreeModel()
, m_effectStackEnabled(true)
, m_ownerId(std::move(ownerId))
, m_undoStack(std::move(undo_stack))
, m_lock(QReadWriteLock::Recursive)
, m_loadingExisting(false)
{
m_masterService = std::move(service);
}
std::shared_ptr EffectStackModel::construct(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack)
{
std::shared_ptr self(new EffectStackModel(std::move(service), ownerId, std::move(undo_stack)));
self->rootItem = EffectGroupModel::construct(QStringLiteral("root"), self, true);
return self;
}
void EffectStackModel::resetService(std::weak_ptr service)
{
QWriteLocker locker(&m_lock);
m_masterService = std::move(service);
m_childServices.clear();
// replant all effects in new service
for (int i = 0; i < rootItem->childCount(); ++i) {
std::static_pointer_cast(rootItem->child(i))->plant(m_masterService);
}
}
void EffectStackModel::addService(std::weak_ptr service)
{
QWriteLocker locker(&m_lock);
m_childServices.emplace_back(std::move(service));
for (int i = 0; i < rootItem->childCount(); ++i) {
std::static_pointer_cast(rootItem->child(i))->plantClone(m_childServices.back());
}
}
void EffectStackModel::loadService(std::weak_ptr service)
{
QWriteLocker locker(&m_lock);
m_childServices.emplace_back(std::move(service));
for (int i = 0; i < rootItem->childCount(); ++i) {
std::static_pointer_cast(rootItem->child(i))->loadClone(m_childServices.back());
}
}
void EffectStackModel::removeService(const std::shared_ptr &service)
{
QWriteLocker locker(&m_lock);
std::vector to_delete;
for (int i = int(m_childServices.size()) - 1; i >= 0; --i) {
auto ptr = m_childServices[uint(i)].lock();
if (service->get_int("_childid") == ptr->get_int("_childid")) {
for (int j = 0; j < rootItem->childCount(); ++j) {
std::static_pointer_cast(rootItem->child(j))->unplantClone(ptr);
}
to_delete.push_back(i);
}
}
for (int i : to_delete) {
m_childServices.erase(m_childServices.begin() + i);
}
}
void EffectStackModel::removeCurrentEffect()
{
int ix = 0;
if (auto ptr = m_masterService.lock()) {
ix = ptr->get_int("kdenlive:activeeffect");
}
if (ix < 0) {
return;
}
std::shared_ptr effect = std::static_pointer_cast(rootItem->child(ix));
if (effect) {
removeEffect(effect);
}
}
void EffectStackModel::removeEffect(const std::shared_ptr &effect)
{
qDebug() << "* * ** REMOVING EFFECT FROM STACK!!!\n!!!!!!!!!";
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allItems.count(effect->getId()) > 0);
int parentId = -1;
if (auto ptr = effect->parentItem().lock()) parentId = ptr->getId();
int current = 0;
if (auto srv = m_masterService.lock()) {
current = srv->get_int("kdenlive:activeeffect");
if (current >= rootItem->childCount() - 1) {
srv->set("kdenlive:activeeffect", --current);
}
}
int currentRow = effect->row();
Fun undo = addItem_lambda(effect, parentId);
if (currentRow != rowCount() - 1) {
Fun move = moveItem_lambda(effect->getId(), currentRow, true);
PUSH_LAMBDA(move, undo);
}
Fun redo = removeItem_lambda(effect->getId());
bool res = redo();
if (res) {
int inFades = int(m_fadeIns.size());
int outFades = int(m_fadeOuts.size());
m_fadeIns.erase(effect->getId());
m_fadeOuts.erase(effect->getId());
inFades = int(m_fadeIns.size()) - inFades;
outFades = int(m_fadeOuts.size()) - outFades;
QString effectName = EffectsRepository::get()->getName(effect->getAssetId());
Fun update = [this, current, inFades, outFades]() {
// Required to build the effect view
if (current < 0 || rowCount() == 0) {
// Stack is now empty
emit dataChanged(QModelIndex(), QModelIndex(), {});
} else {
QVector roles = {TimelineModel::EffectNamesRole};
if (inFades < 0) {
roles << TimelineModel::FadeInRole;
}
if (outFades < 0) {
roles << TimelineModel::FadeOutRole;
}
qDebug() << "// EMITTING UNDO DATA CHANGE: " << roles;
emit dataChanged(QModelIndex(), QModelIndex(), roles);
}
// TODO: only update if effect is fade or keyframe
/*if (inFades < 0) {
pCore->updateItemModel(m_ownerId, QStringLiteral("fadein"));
} else if (outFades < 0) {
pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout"));
}*/
pCore->updateItemKeyframes(m_ownerId);
return true;
};
Fun update2 = [this, inFades, outFades]() {
// Required to build the effect view
QVector roles = {TimelineModel::EffectNamesRole};
// TODO: only update if effect is fade or keyframe
if (inFades < 0) {
roles << TimelineModel::FadeInRole;
} else if (outFades < 0) {
roles << TimelineModel::FadeOutRole;
}
qDebug() << "// EMITTING REDO DATA CHANGE: " << roles;
emit dataChanged(QModelIndex(), QModelIndex(), roles);
pCore->updateItemKeyframes(m_ownerId);
return true;
};
update();
PUSH_LAMBDA(update, redo);
PUSH_LAMBDA(update2, undo);
PUSH_UNDO(undo, redo, i18n("Delete effect %1", effectName));
} else {
qDebug() << "..........FAILED EFFECT DELETION";
}
}
bool EffectStackModel::copyEffect(const std::shared_ptr &sourceItem, PlaylistState::ClipState state, bool logUndo)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool result = copyEffect(sourceItem, state, undo, redo);
if (result && logUndo) {
std::shared_ptr sourceEffect = std::static_pointer_cast(sourceItem);
QString effectName = EffectsRepository::get()->getName(sourceEffect->getAssetId());
PUSH_UNDO(undo, redo, i18n("Copy effect %1", effectName));
}
return result;
}
QDomElement EffectStackModel::toXml(QDomDocument &document)
{
QDomElement container = document.createElement(QStringLiteral("effects"));
for (int i = 0; i < rootItem->childCount(); ++i) {
std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i));
QDomElement sub = document.createElement(QStringLiteral("effect"));
sub.setAttribute(QStringLiteral("id"), sourceEffect->getAssetId());
int filterIn = sourceEffect->filter().get_int("in");
int filterOut = sourceEffect->filter().get_int("out");
if (filterOut > filterIn) {
sub.setAttribute(QStringLiteral("in"), filterIn);
sub.setAttribute(QStringLiteral("out"), filterOut);
}
QVector> params = sourceEffect->getAllParameters();
QLocale locale;
for (const auto ¶m : params) {
if (param.second.type() == QVariant::Double) {
Xml::setXmlProperty(sub, param.first, locale.toString(param.second.toDouble()));
} else {
Xml::setXmlProperty(sub, param.first, param.second.toString());
}
}
container.appendChild(sub);
}
return container;
}
bool EffectStackModel::fromXml(const QDomElement &effectsXml, Fun &undo, Fun &redo)
{
QDomNodeList nodeList = effectsXml.elementsByTagName(QStringLiteral("effect"));
int parentIn = effectsXml.attribute(QStringLiteral("parentIn")).toInt();
int currentIn = pCore->getItemIn(m_ownerId);
PlaylistState::ClipState state = pCore->getItemState(m_ownerId);
for (int i = 0; i < nodeList.count(); ++i) {
QDomElement node = nodeList.item(i).toElement();
const QString effectId = node.attribute(QStringLiteral("id"));
bool isAudioEffect = EffectsRepository::get()->getType(effectId) == EffectType::Audio;
if (isAudioEffect) {
if (state != PlaylistState::AudioOnly) {
continue;
}
} else if (state != PlaylistState::VideoOnly) {
continue;
}
auto effect = EffectItemModel::construct(effectId, shared_from_this());
- int in = node.attribute(QStringLiteral("in")).toInt();
- int out = node.attribute(QStringLiteral("out")).toInt();
- if (out > 0) {
- effect->filter().set("in", in);
- effect->filter().set("out", out);
+ const QString in = node.attribute(QStringLiteral("in"));
+ const QString out = node.attribute(QStringLiteral("out"));
+ if (!out.isEmpty()) {
+ effect->filter().set("in", in.toUtf8().constData());
+ effect->filter().set("out", out.toUtf8().constData());
}
QStringList keyframeParams = effect->getKeyframableParameters();
QVector> parameters;
QDomNodeList params = node.elementsByTagName(QStringLiteral("property"));
for (int j = 0; j < params.count(); j++) {
QDomElement pnode = params.item(j).toElement();
const QString pName = pnode.attribute(QStringLiteral("name"));
+ if (pName == QLatin1String("in") || pName == QLatin1String("out")) {
+ continue;
+ }
if (keyframeParams.contains(pName)) {
// This is a keyframable parameter, fix offest
QString pValue = KeyframeModel::getAnimationStringWithOffset(effect, pnode.text(), currentIn - parentIn);
parameters.append(QPair(pName, QVariant(pValue)));
} else {
parameters.append(QPair(pName, QVariant(pnode.text())));
}
}
effect->setParameters(parameters);
Fun local_undo = removeItem_lambda(effect->getId());
// TODO the parent should probably not always be the root
Fun local_redo = addItem_lambda(effect, rootItem->getId());
effect->prepareKeyframes();
connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
m_fadeIns.insert(effect->getId());
+ int duration = effect->filter().get_length() - 1;
+ effect->filter().set("in", currentIn);
+ effect->filter().set("out", currentIn + duration);
} else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
m_fadeOuts.insert(effect->getId());
+ int duration = effect->filter().get_length() - 1;
+ int filterOut = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1;
+ effect->filter().set("in", filterOut - duration);
+ effect->filter().set("out", filterOut);
}
local_redo();
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
}
if (true) {
Fun update = [this]() {
emit dataChanged(QModelIndex(), QModelIndex(), {});
return true;
};
update();
PUSH_LAMBDA(update, redo);
PUSH_LAMBDA(update, undo);
}
return true;
}
bool EffectStackModel::copyEffect(const std::shared_ptr &sourceItem, PlaylistState::ClipState state, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
if (sourceItem->childCount() > 0) {
// TODO: group
return false;
}
bool audioEffect = sourceItem->isAudio();
if (audioEffect) {
if (state == PlaylistState::VideoOnly) {
// This effect cannot be used
return false;
}
} else if (state == PlaylistState::AudioOnly) {
return false;
}
std::shared_ptr sourceEffect = std::static_pointer_cast(sourceItem);
const QString effectId = sourceEffect->getAssetId();
auto effect = EffectItemModel::construct(effectId, shared_from_this());
effect->setParameters(sourceEffect->getAllParameters());
effect->filter().set("in", sourceEffect->filter().get_int("in"));
effect->filter().set("out", sourceEffect->filter().get_int("out"));
Fun local_undo = removeItem_lambda(effect->getId());
// TODO the parent should probably not always be the root
Fun local_redo = addItem_lambda(effect, rootItem->getId());
effect->prepareKeyframes();
connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
QVector roles = {TimelineModel::EffectNamesRole};
if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
m_fadeIns.insert(effect->getId());
+ int duration = effect->filter().get_length() - 1;
+ int in = pCore->getItemIn(m_ownerId);
+ effect->filter().set("in", in);
+ effect->filter().set("out", in + duration);
roles << TimelineModel::FadeInRole;
} else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
m_fadeOuts.insert(effect->getId());
+ int duration = effect->filter().get_length() - 1;
+ int out = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1;
+ effect->filter().set("in", out - duration);
+ effect->filter().set("out", out);
roles << TimelineModel::FadeOutRole;
}
bool res = local_redo();
if (res) {
Fun update = [this, roles]() {
emit dataChanged(QModelIndex(), QModelIndex(), roles);
return true;
};
update();
PUSH_LAMBDA(update, local_redo);
PUSH_LAMBDA(update, local_undo);
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
}
return res;
}
bool EffectStackModel::appendEffect(const QString &effectId, bool makeCurrent)
{
QWriteLocker locker(&m_lock);
auto effect = EffectItemModel::construct(effectId, shared_from_this());
PlaylistState::ClipState state = pCore->getItemState(m_ownerId);
if (effect->isAudio()) {
if (state == PlaylistState::VideoOnly) {
// Cannot add effect to this clip
return false;
}
} else if (state == PlaylistState::AudioOnly) {
// Cannot add effect to this clip
return false;
}
Fun undo = removeItem_lambda(effect->getId());
// TODO the parent should probably not always be the root
Fun redo = addItem_lambda(effect, rootItem->getId());
effect->prepareKeyframes();
connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
int currentActive = getActiveEffect();
if (makeCurrent) {
if (auto srvPtr = m_masterService.lock()) {
srvPtr->set("kdenlive:activeeffect", rowCount());
}
}
bool res = redo();
if (res) {
int inFades = 0;
int outFades = 0;
if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
inFades++;
} else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
outFades++;
}
QString effectName = EffectsRepository::get()->getName(effectId);
Fun update = [this, inFades, outFades]() {
// TODO: only update if effect is fade or keyframe
QVector roles = {TimelineModel::EffectNamesRole};
if (inFades > 0) {
roles << TimelineModel::FadeInRole;
} else if (outFades > 0) {
roles << TimelineModel::FadeOutRole;
}
pCore->updateItemKeyframes(m_ownerId);
emit dataChanged(QModelIndex(), QModelIndex(), roles);
return true;
};
update();
PUSH_LAMBDA(update, redo);
PUSH_LAMBDA(update, undo);
PUSH_UNDO(undo, redo, i18n("Add effect %1", effectName));
} else if (makeCurrent) {
if (auto srvPtr = m_masterService.lock()) {
srvPtr->set("kdenlive:activeeffect", currentActive);
}
}
return res;
}
bool EffectStackModel::adjustStackLength(bool adjustFromEnd, int oldIn, int oldDuration, int newIn, int duration, int offset, Fun &undo, Fun &redo,
bool logUndo)
{
QWriteLocker locker(&m_lock);
const int fadeInDuration = getFadePosition(true);
const int fadeOutDuration = getFadePosition(false);
int out = newIn + duration;
for (const auto &leaf : rootItem->getLeaves()) {
std::shared_ptr item = std::static_pointer_cast(leaf);
if (item->effectItemType() == EffectItemType::Group) {
// probably an empty group, ignore
continue;
}
std::shared_ptr effect = std::static_pointer_cast(leaf);
if (fadeInDuration > 0 && m_fadeIns.count(leaf->getId()) > 0) {
int oldEffectIn = qMax(0, effect->filter().get_in());
int oldEffectOut = effect->filter().get_out();
qDebug() << "--previous effect: " << oldEffectIn << "-" << oldEffectOut;
int effectDuration = qMin(effect->filter().get_length() - 1, duration);
if (!adjustFromEnd && (oldIn != newIn || duration != oldDuration)) {
// Clip start was resized, adjust effect in / out
Fun operation = [effect, newIn, effectDuration, logUndo]() {
effect->setParameter(QStringLiteral("in"), newIn, false);
effect->setParameter(QStringLiteral("out"), newIn + effectDuration, logUndo);
qDebug() << "--new effect: " << newIn << "-" << newIn + effectDuration;
return true;
};
bool res = operation();
if (!res) {
return false;
}
Fun reverse = [effect, oldEffectIn, oldEffectOut, logUndo]() {
effect->setParameter(QStringLiteral("in"), oldEffectIn, false);
effect->setParameter(QStringLiteral("out"), oldEffectOut, logUndo);
return true;
};
PUSH_LAMBDA(operation, redo);
PUSH_LAMBDA(reverse, undo);
} else if (effectDuration < oldEffectOut - oldEffectIn || (logUndo && effect->filter().get_int("_refout") > 0)) {
// Clip length changed, shorter than effect length so resize
int referenceEffectOut = effect->filter().get_int("_refout");
if (referenceEffectOut <= 0) {
referenceEffectOut = oldEffectOut;
effect->filter().set("_refout", referenceEffectOut);
}
Fun operation = [effect, oldEffectIn, effectDuration, logUndo]() {
effect->setParameter(QStringLiteral("out"), oldEffectIn + effectDuration, logUndo);
return true;
};
bool res = operation();
if (!res) {
return false;
}
if (logUndo) {
Fun reverse = [effect, referenceEffectOut]() {
effect->setParameter(QStringLiteral("out"), referenceEffectOut, true);
effect->filter().set("_refout", (char *)nullptr);
return true;
};
PUSH_LAMBDA(operation, redo);
PUSH_LAMBDA(reverse, undo);
}
}
} else if (fadeOutDuration > 0 && m_fadeOuts.count(leaf->getId()) > 0) {
int effectDuration = qMin(fadeOutDuration, duration);
int newFadeIn = out - effectDuration;
int oldFadeIn = effect->filter().get_int("in");
int oldOut = effect->filter().get_int("out");
int referenceEffectIn = effect->filter().get_int("_refin");
if (referenceEffectIn <= 0) {
referenceEffectIn = oldFadeIn;
effect->filter().set("_refin", referenceEffectIn);
}
Fun operation = [effect, newFadeIn, out, logUndo]() {
effect->setParameter(QStringLiteral("in"), newFadeIn, false);
effect->setParameter(QStringLiteral("out"), out, logUndo);
return true;
};
bool res = operation();
if (!res) {
return false;
}
if (logUndo) {
Fun reverse = [effect, referenceEffectIn, oldOut]() {
effect->setParameter(QStringLiteral("in"), referenceEffectIn, false);
effect->setParameter(QStringLiteral("out"), oldOut, true);
effect->filter().set("_refin", (char *)nullptr);
return true;
};
PUSH_LAMBDA(operation, redo);
PUSH_LAMBDA(reverse, undo);
}
} else {
// Not a fade effect, check for keyframes
std::shared_ptr keyframes = effect->getKeyframeModel();
if (keyframes != nullptr) {
// Effect has keyframes, update these
keyframes->resizeKeyframes(oldIn, oldIn + oldDuration - 1, newIn, out - 1, offset, adjustFromEnd, undo, redo);
QModelIndex index = getIndexFromItem(effect);
Fun refresh = [effect, index]() {
effect->dataChanged(index, index, QVector());
return true;
};
refresh();
PUSH_LAMBDA(refresh, redo);
PUSH_LAMBDA(refresh, undo);
} else {
qDebug() << "// NULL Keyframes---------";
}
}
}
return true;
}
bool EffectStackModel::adjustFadeLength(int duration, bool fromStart, bool audioFade, bool videoFade, bool logUndo)
{
QWriteLocker locker(&m_lock);
if (fromStart) {
// Fade in
if (m_fadeIns.empty()) {
if (audioFade) {
appendEffect(QStringLiteral("fadein"));
}
if (videoFade) {
appendEffect(QStringLiteral("fade_from_black"));
}
}
QList indexes;
auto ptr = m_masterService.lock();
int in = 0;
if (ptr) {
in = ptr->get_int("in");
}
int oldDuration = -1;
for (int i = 0; i < rootItem->childCount(); ++i) {
if (m_fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) {
std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i));
if (oldDuration == -1) {
oldDuration = effect->filter().get_length();
}
effect->filter().set("in", in);
duration = qMin(pCore->getItemDuration(m_ownerId), duration);
effect->filter().set("out", in + duration);
indexes << getIndexFromItem(effect);
}
}
if (!indexes.isEmpty()) {
emit dataChanged(indexes.first(), indexes.last(), QVector());
pCore->updateItemModel(m_ownerId, QStringLiteral("fadein"));
if (videoFade) {
int min = pCore->getItemPosition(m_ownerId);
QSize range(min, min + qMax(duration, oldDuration));
pCore->refreshProjectRange(range);
if (logUndo) {
pCore->invalidateRange(range);
}
}
}
} else {
// Fade out
if (m_fadeOuts.empty()) {
if (audioFade) {
appendEffect(QStringLiteral("fadeout"));
}
if (videoFade) {
appendEffect(QStringLiteral("fade_to_black"));
}
}
int in = 0;
auto ptr = m_masterService.lock();
if (ptr) {
in = ptr->get_int("in");
}
int itemDuration = pCore->getItemDuration(m_ownerId);
int out = in + itemDuration;
int oldDuration = -1;
QList indexes;
for (int i = 0; i < rootItem->childCount(); ++i) {
if (m_fadeOuts.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) {
std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i));
if (oldDuration == -1) {
oldDuration = effect->filter().get_length();
}
effect->filter().set("out", out - 1);
duration = qMin(itemDuration, duration);
effect->filter().set("in", out - duration);
indexes << getIndexFromItem(effect);
}
}
if (!indexes.isEmpty()) {
emit dataChanged(indexes.first(), indexes.last(), QVector());
pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout"));
if (videoFade) {
int min = pCore->getItemPosition(m_ownerId);
QSize range(min + itemDuration - qMax(duration, oldDuration), min + itemDuration);
pCore->refreshProjectRange(range);
if (logUndo) {
pCore->invalidateRange(range);
}
}
}
}
return true;
}
int EffectStackModel::getFadePosition(bool fromStart)
{
QWriteLocker locker(&m_lock);
if (fromStart) {
if (m_fadeIns.empty()) {
return 0;
}
for (int i = 0; i < rootItem->childCount(); ++i) {
if (*(m_fadeIns.begin()) == std::static_pointer_cast(rootItem->child(i))->getId()) {
std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i));
return effect->filter().get_length() - 1;
}
}
} else {
if (m_fadeOuts.empty()) {
return 0;
}
for (int i = 0; i < rootItem->childCount(); ++i) {
if (*(m_fadeOuts.begin()) == std::static_pointer_cast(rootItem->child(i))->getId()) {
std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i));
return effect->filter().get_length() - 1;
}
}
}
return 0;
}
bool EffectStackModel::removeFade(bool fromStart)
{
QWriteLocker locker(&m_lock);
std::vector toRemove;
for (int i = 0; i < rootItem->childCount(); ++i) {
if ((fromStart && m_fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) ||
(!fromStart && m_fadeOuts.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0)) {
toRemove.push_back(i);
}
}
for (int i : toRemove) {
std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i));
removeEffect(effect);
}
return true;
}
void EffectStackModel::moveEffect(int destRow, const std::shared_ptr &item)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allItems.count(item->getId()) > 0);
int oldRow = item->row();
Fun undo = moveItem_lambda(item->getId(), oldRow);
Fun redo = moveItem_lambda(item->getId(), destRow);
bool res = redo();
if (res) {
Fun update = [this]() {
this->dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectNamesRole});
return true;
};
update();
UPDATE_UNDO_REDO(update, update, undo, redo);
auto effectId = std::static_pointer_cast(item)->getAssetId();
QString effectName = EffectsRepository::get()->getName(effectId);
PUSH_UNDO(undo, redo, i18n("Move effect %1", effectName));
}
}
void EffectStackModel::registerItem(const std::shared_ptr &item)
{
QWriteLocker locker(&m_lock);
// qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect";
QModelIndex ix;
if (!item->isRoot()) {
auto effectItem = std::static_pointer_cast(item);
if (!m_loadingExisting) {
// qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect in " << m_childServices.size();
effectItem->plant(m_masterService);
for (const auto &service : m_childServices) {
// qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting CLONE effect in " << (void *)service.lock().get();
effectItem->plantClone(service);
}
}
effectItem->setEffectStackEnabled(m_effectStackEnabled);
const QString &effectId = effectItem->getAssetId();
if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
m_fadeIns.insert(effectItem->getId());
} else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
m_fadeOuts.insert(effectItem->getId());
}
ix = getIndexFromItem(effectItem);
if (!effectItem->isAudio() && !m_loadingExisting) {
pCore->refreshProjectItem(m_ownerId);
pCore->invalidateItem(m_ownerId);
}
}
AbstractTreeModel::registerItem(item);
}
void EffectStackModel::deregisterItem(int id, TreeItem *item)
{
QWriteLocker locker(&m_lock);
if (!item->isRoot()) {
auto effectItem = static_cast(item);
effectItem->unplant(m_masterService);
for (const auto &service : m_childServices) {
effectItem->unplantClone(service);
}
if (!effectItem->isAudio()) {
pCore->refreshProjectItem(m_ownerId);
pCore->invalidateItem(m_ownerId);
}
}
AbstractTreeModel::deregisterItem(id, item);
}
void EffectStackModel::setEffectStackEnabled(bool enabled)
{
QWriteLocker locker(&m_lock);
m_effectStackEnabled = enabled;
// Recursively updates children states
for (int i = 0; i < rootItem->childCount(); ++i) {
std::static_pointer_cast(rootItem->child(i))->setEffectStackEnabled(enabled);
}
emit dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectsEnabledRole});
emit enabledStateChanged();
}
std::shared_ptr EffectStackModel::getEffectStackRow(int row, const std::shared_ptr &parentItem)
{
return std::static_pointer_cast(parentItem ? parentItem->child(row) : rootItem->child(row));
}
bool EffectStackModel::importEffects(const std::shared_ptr &sourceStack, PlaylistState::ClipState state)
{
QWriteLocker locker(&m_lock);
// TODO: manage fades, keyframes if clips don't have same size / in point
bool found = false;
for (int i = 0; i < sourceStack->rowCount(); i++) {
auto item = sourceStack->getEffectStackRow(i);
// NO undo. this should only be used on project opening
if (copyEffect(item, state, false)) {
found = true;
}
}
if (found) {
modelChanged();
}
return found;
}
bool EffectStackModel::importEffects(const std::shared_ptr &sourceStack, PlaylistState::ClipState state, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
// TODO: manage fades, keyframes if clips don't have same size / in point
bool found = false;
for (int i = 0; i < sourceStack->rowCount(); i++) {
auto item = sourceStack->getEffectStackRow(i);
if (copyEffect(item, state, undo, redo)) {
found = true;
}
}
if (found) {
modelChanged();
}
return found;
}
void EffectStackModel::importEffects(const std::weak_ptr &service, PlaylistState::ClipState state, bool alreadyExist)
{
QWriteLocker locker(&m_lock);
m_loadingExisting = alreadyExist;
if (auto ptr = service.lock()) {
for (int i = 0; i < ptr->filter_count(); i++) {
std::unique_ptr filter(ptr->filter(i));
if (filter->get("kdenlive_id") == nullptr) {
// don't consider internal MLT stuff
continue;
}
const QString effectId = qstrdup(filter->get("kdenlive_id"));
// The MLT filter already exists, use it directly to create the effect
std::shared_ptr effect;
if (alreadyExist) {
// effect is already plugged in the service
effect = EffectItemModel::construct(std::move(filter), shared_from_this());
} else {
// duplicate effect
std::unique_ptr asset = EffectsRepository::get()->getEffect(effectId);
asset->inherit(*(filter));
effect = EffectItemModel::construct(std::move(asset), shared_from_this());
}
if (effect->isAudio()) {
if (state == PlaylistState::VideoOnly) {
// Don't import effect
continue;
}
} else if (state == PlaylistState::AudioOnly) {
// Don't import effect
continue;
}
connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
Fun redo = addItem_lambda(effect, rootItem->getId());
effect->prepareKeyframes();
if (redo()) {
if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
m_fadeIns.insert(effect->getId());
} else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
m_fadeOuts.insert(effect->getId());
}
}
}
}
m_loadingExisting = false;
modelChanged();
}
void EffectStackModel::setActiveEffect(int ix)
{
QWriteLocker locker(&m_lock);
if (auto ptr = m_masterService.lock()) {
ptr->set("kdenlive:activeeffect", ix);
}
pCore->updateItemKeyframes(m_ownerId);
}
int EffectStackModel::getActiveEffect() const
{
QWriteLocker locker(&m_lock);
if (auto ptr = m_masterService.lock()) {
return ptr->get_int("kdenlive:activeeffect");
}
return 0;
}
void EffectStackModel::slotCreateGroup(const std::shared_ptr &childEffect)
{
QWriteLocker locker(&m_lock);
auto groupItem = EffectGroupModel::construct(QStringLiteral("group"), shared_from_this());
rootItem->appendChild(groupItem);
groupItem->appendChild(childEffect);
}
ObjectId EffectStackModel::getOwnerId() const
{
return m_ownerId;
}
bool EffectStackModel::checkConsistency()
{
if (!AbstractTreeModel::checkConsistency()) {
return false;
}
std::vector> allFilters;
// We do a DFS on the tree to retrieve all the filters
std::stack> stck;
stck.push(std::static_pointer_cast(rootItem));
while (!stck.empty()) {
auto current = stck.top();
stck.pop();
if (current->effectItemType() == EffectItemType::Effect) {
if (current->childCount() > 0) {
qDebug() << "ERROR: Found an effect with children";
return false;
}
allFilters.push_back(std::static_pointer_cast(current));
continue;
}
for (int i = current->childCount() - 1; i >= 0; --i) {
stck.push(std::static_pointer_cast(current->child(i)));
}
}
for (const auto &service : m_childServices) {
auto ptr = service.lock();
if (!ptr) {
qDebug() << "ERROR: unavailable service";
return false;
}
// MLT inserts some default normalizer filters that are not managed by Kdenlive, which explains why the filter count is not equal
int kdenliveFilterCount = 0;
for (int i = 0; i < ptr->filter_count(); i++) {
std::shared_ptr filt(ptr->filter(i));
if (filt->get("kdenlive_id") != nullptr) {
kdenliveFilterCount++;
}
// qDebug() << "FILTER: "<filter(i)->get("mlt_service");
}
if (kdenliveFilterCount != (int)allFilters.size()) {
qDebug() << "ERROR: Wrong filter count: " << kdenliveFilterCount << " = " << allFilters.size();
return false;
}
int ct = 0;
for (uint i = 0; i < allFilters.size(); ++i) {
while (ptr->filter(ct)->get("kdenlive_id") == nullptr && ct < ptr->filter_count()) {
ct++;
}
auto mltFilter = ptr->filter(ct);
auto currentFilter = allFilters[i]->filter();
if (QString(currentFilter.get("mlt_service")) != QLatin1String(mltFilter->get("mlt_service"))) {
qDebug() << "ERROR: filter " << i << "differ: " << ct << ", " << currentFilter.get("mlt_service") << " = " << mltFilter->get("mlt_service");
return false;
}
QVector> params = allFilters[i]->getAllParameters();
for (const auto &val : params) {
// Check parameters values
if (val.second != QVariant(mltFilter->get(val.first.toUtf8().constData()))) {
qDebug() << "ERROR: filter " << i << "PARAMETER MISMATCH: " << val.first << " = " << val.second
<< " != " << mltFilter->get(val.first.toUtf8().constData());
return false;
}
}
ct++;
}
}
return true;
}
void EffectStackModel::adjust(const QString &effectId, const QString &effectName, double value)
{
QWriteLocker locker(&m_lock);
for (int i = 0; i < rootItem->childCount(); ++i) {
std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i));
if (effectId == sourceEffect->getAssetId()) {
sourceEffect->setParameter(effectName, QString::number(value));
return;
}
}
}
std::shared_ptr EffectStackModel::getAssetModelById(const QString &effectId)
{
QWriteLocker locker(&m_lock);
for (int i = 0; i < rootItem->childCount(); ++i) {
std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i));
if (effectId == sourceEffect->getAssetId()) {
return std::static_pointer_cast(sourceEffect);
}
}
return nullptr;
}
bool EffectStackModel::hasFilter(const QString &effectId) const
{
READ_LOCK();
return rootItem->accumulate_const(false, [effectId](bool b, std::shared_ptr it) {
if (b) return true;
auto item = std::static_pointer_cast(it);
if (item->effectItemType() == EffectItemType::Group) {
return false;
}
auto sourceEffect = std::static_pointer_cast(it);
return effectId == sourceEffect->getAssetId();
});
}
double EffectStackModel::getFilterParam(const QString &effectId, const QString ¶mName)
{
READ_LOCK();
for (int i = 0; i < rootItem->childCount(); ++i) {
std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i));
if (effectId == sourceEffect->getAssetId()) {
return sourceEffect->filter().get_double(paramName.toUtf8().constData());
}
}
return 0.0;
}
KeyframeModel *EffectStackModel::getEffectKeyframeModel()
{
if (rootItem->childCount() == 0) return nullptr;
int ix = 0;
if (auto ptr = m_masterService.lock()) {
ix = ptr->get_int("kdenlive:activeeffect");
}
if (ix < 0 || ix >= rootItem->childCount()) {
return nullptr;
}
std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix));
std::shared_ptr listModel = sourceEffect->getKeyframeModel();
if (listModel) {
return listModel->getKeyModel();
}
return nullptr;
}
void EffectStackModel::replugEffect(const std::shared_ptr &asset)
{
QWriteLocker locker(&m_lock);
auto effectItem = std::static_pointer_cast(asset);
int oldRow = effectItem->row();
int count = rowCount();
for (int ix = oldRow; ix < count; ix++) {
auto item = std::static_pointer_cast(rootItem->child(ix));
item->unplant(m_masterService);
for (const auto &service : m_childServices) {
item->unplantClone(service);
}
}
std::unique_ptr effect = EffectsRepository::get()->getEffect(effectItem->getAssetId());
effect->inherit(effectItem->filter());
effectItem->resetAsset(std::move(effect));
for (int ix = oldRow; ix < count; ix++) {
auto item = std::static_pointer_cast(rootItem->child(ix));
item->plant(m_masterService);
for (const auto &service : m_childServices) {
item->plantClone(service);
}
}
}
void EffectStackModel::cleanFadeEffects(bool outEffects, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
const auto &toDelete = outEffects ? m_fadeOuts : m_fadeIns;
for (int id : toDelete) {
auto effect = std::static_pointer_cast(getItemById(id));
Fun operation = removeItem_lambda(id);
if (operation()) {
Fun reverse = addItem_lambda(effect, rootItem->getId());
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
}
}
if (!toDelete.empty()) {
Fun updateRedo = [this, toDelete, outEffects]() {
for (int id : toDelete) {
if (outEffects) {
m_fadeOuts.erase(id);
} else {
m_fadeIns.erase(id);
}
}
QVector roles = {TimelineModel::EffectNamesRole};
roles << (outEffects ? TimelineModel::FadeOutRole : TimelineModel::FadeInRole);
emit dataChanged(QModelIndex(), QModelIndex(), roles);
pCore->updateItemKeyframes(m_ownerId);
return true;
};
updateRedo();
PUSH_LAMBDA(updateRedo, redo);
}
}
const QString EffectStackModel::effectNames() const
{
QStringList effects;
for (int i = 0; i < rootItem->childCount(); ++i) {
effects.append(EffectsRepository::get()->getName(std::static_pointer_cast(rootItem->child(i))->getAssetId()));
}
return effects.join(QLatin1Char('/'));
}
bool EffectStackModel::isStackEnabled() const
{
return m_effectStackEnabled;
}
bool EffectStackModel::addEffectKeyFrame(int frame, double normalisedVal)
{
if (rootItem->childCount() == 0) return false;
int ix = 0;
if (auto ptr = m_masterService.lock()) {
ix = ptr->get_int("kdenlive:activeeffect");
}
if (ix < 0) {
return false;
}
std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix));
std::shared_ptr listModel = sourceEffect->getKeyframeModel();
return listModel->addKeyframe(frame, normalisedVal);
}
bool EffectStackModel::removeKeyFrame(int frame)
{
if (rootItem->childCount() == 0) return false;
int ix = 0;
if (auto ptr = m_masterService.lock()) {
ix = ptr->get_int("kdenlive:activeeffect");
}
if (ix < 0) {
return false;
}
std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix));
std::shared_ptr listModel = sourceEffect->getKeyframeModel();
return listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps()));
}
bool EffectStackModel::updateKeyFrame(int oldFrame, int newFrame, QVariant normalisedVal)
{
if (rootItem->childCount() == 0) return false;
int ix = 0;
if (auto ptr = m_masterService.lock()) {
ix = ptr->get_int("kdenlive:activeeffect");
}
if (ix < 0) {
return false;
}
std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix));
std::shared_ptr listModel = sourceEffect->getKeyframeModel();
return listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), std::move(normalisedVal));
}
diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp
index 8b04dced9..5563701f7 100644
--- a/src/timeline2/view/timelinecontroller.cpp
+++ b/src/timeline2/view/timelinecontroller.cpp
@@ -1,2412 +1,2412 @@
/***************************************************************************
* Copyright (C) 2017 by 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 "timelinecontroller.h"
#include "../model/timelinefunctions.hpp"
#include "assets/keyframes/model/keyframemodellist.hpp"
#include "bin/bin.h"
#include "bin/clipcreator.hpp"
#include "bin/model/markerlistmodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectfolder.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "dialogs/spacerdialog.h"
#include "dialogs/speeddialog.h"
#include "doc/kdenlivedoc.h"
#include "effects/effectsrepository.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "kdenlivesettings.h"
#include "lib/audio/audioEnvelope.h"
#include "mainwindow.h"
#include "monitor/monitormanager.h"
#include "previewmanager.h"
#include "project/projectmanager.h"
#include "timeline2/model/clipmodel.hpp"
#include "timeline2/model/compositionmodel.hpp"
#include "timeline2/model/groupsmodel.hpp"
#include "timeline2/model/timelineitemmodel.hpp"
#include "timeline2/model/trackmodel.hpp"
#include "timeline2/view/dialogs/clipdurationdialog.h"
#include "timeline2/view/dialogs/trackdialog.h"
#include "transitions/transitionsrepository.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
int TimelineController::m_duration = 0;
TimelineController::TimelineController(QObject *parent)
: QObject(parent)
, m_root(nullptr)
, m_usePreview(false)
, m_position(0)
, m_seekPosition(-1)
, m_activeTrack(0)
, m_audioRef(-1)
, m_zone(-1, -1)
, m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250)
, m_timelinePreview(nullptr)
{
m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview"));
connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview);
connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
m_disablePreview->setEnabled(false);
connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording);
}
TimelineController::~TimelineController()
{
delete m_timelinePreview;
m_timelinePreview = nullptr;
}
void TimelineController::setModel(std::shared_ptr model)
{
delete m_timelinePreview;
m_zone = QPoint(-1, -1);
m_timelinePreview = nullptr;
m_model = std::move(model);
connect(m_model.get(), &TimelineItemModel::requestClearAssetView, [&](int id) { pCore->clearAssetPanel(id); });
connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); });
connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection);
connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration);
connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
}
void TimelineController::setTargetTracks(QPair targets)
{
setVideoTarget(targets.first >= 0 && targets.first < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.first) : -1);
setAudioTarget(targets.second >= 0 && targets.second < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.second) : -1);
}
std::shared_ptr TimelineController::getModel() const
{
return m_model;
}
void TimelineController::setRoot(QQuickItem *root)
{
m_root = root;
}
Mlt::Tractor *TimelineController::tractor()
{
return m_model->tractor();
}
int TimelineController::getCurrentItem()
{
// TODO: if selection is empty, return topmost clip under timeline cursor
auto selection = m_model->getCurrentSelection();
if (selection.empty()) {
return -1;
}
// TODO: if selection contains more than 1 clip, return topmost clip under timeline cursor in selection
return *(selection.begin());
}
double TimelineController::scaleFactor() const
{
return m_scale;
}
const QString TimelineController::getTrackNameFromMltIndex(int trackPos)
{
if (trackPos == -1) {
return i18n("unknown");
}
if (trackPos == 0) {
return i18n("Black");
}
return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1));
}
const QString TimelineController::getTrackNameFromIndex(int trackIndex)
{
QString trackName = m_model->getTrackFullName(trackIndex);
return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName;
}
QMap TimelineController::getTrackNames(bool videoOnly)
{
QMap names;
for (const auto &track : m_model->m_iteratorTable) {
if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) {
continue;
}
QString trackName = m_model->getTrackFullName(track.first);
names[m_model->getTrackMltIndex(track.first)] = trackName;
}
return names;
}
void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse)
{
if (m_root) {
m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMin(getMousePos(), duration()) : -1);
m_scale = scale;
emit scaleFactorChanged();
} else {
qWarning("Timeline root not created, impossible to zoom in");
}
}
void TimelineController::setScaleFactor(double scale)
{
m_scale = scale;
// Update mainwindow's zoom slider
emit updateZoom(scale);
// inform qml
emit scaleFactorChanged();
}
int TimelineController::duration() const
{
return m_duration;
}
int TimelineController::fullDuration() const
{
return m_duration + TimelineModel::seekDuration;
}
void TimelineController::checkDuration()
{
int currentLength = m_model->duration();
if (currentLength != m_duration) {
m_duration = currentLength;
emit durationChanged();
}
}
int TimelineController::selectedTrack() const
{
std::unordered_set sel = m_model->getCurrentSelection();
if (sel.empty()) return -1;
std::vector> selected_tracks; // contains pairs of (track position, track id) for each selected item
for (int s : sel) {
int tid = m_model->getItemTrackId(s);
selected_tracks.push_back({m_model->getTrackPosition(tid), tid});
}
// sort by track position
std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; });
return selected_tracks.front().second;
}
void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent)
{
QList toSelect;
int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, timelinePosition())
: m_model->getCompositionByPosition(m_activeTrack, timelinePosition());
if (currentClip == -1) {
pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500);
return;
}
if (!select) {
m_model->requestRemoveFromSelection(currentClip);
} else {
m_model->requestAddToSelection(currentClip, !addToCurrent);
}
}
QList TimelineController::selection() const
{
if (!m_root) return QList();
QList items;
for (int id : m_model->getCurrentSelection()) {
items << id;
}
return items;
}
void TimelineController::selectItems(const QList &ids)
{
std::unordered_set ids_s(ids.begin(), ids.end());
m_model->requestSetSelection(ids_s);
}
void TimelineController::setScrollPos(int pos)
{
if (pos > 0 && m_root) {
QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos));
}
}
void TimelineController::resetView()
{
m_model->_resetView();
if (m_root) {
QMetaObject::invokeMethod(m_root, "updatePalette");
}
emit colorsChanged();
}
bool TimelineController::snap()
{
return KdenliveSettings::snaptopoints();
}
bool TimelineController::ripple()
{
return false;
}
bool TimelineController::scrub()
{
return false;
}
int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets)
{
int id;
if (tid == -1) {
tid = m_activeTrack;
}
if (position == -1) {
position = timelinePosition();
}
if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) {
id = -1;
}
return id;
}
QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView)
{
QList clipIds;
if (tid == -1) {
tid = m_activeTrack;
}
if (position == -1) {
position = timelinePosition();
}
TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView);
// we don't need to check the return value of the above function, in case of failure it will return an empty list of ids.
return clipIds;
}
int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo)
{
int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
if (clipId > 0) {
int minimum = m_model->getClipPosition(clipId);
return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo);
}
return insertComposition(tid, position, transitionId, logUndo);
}
int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo)
{
int id;
int minimum = m_model->getClipPosition(clipId);
int clip_duration = m_model->getClipPlaytime(clipId);
int position = minimum;
if (offset > clip_duration / 2) {
position += offset;
} else {
// Check if we have a composition at beginning
std::unordered_set existing = m_model->getTrackById_const(tid)->getCompositionsInRange(minimum, minimum + offset);
if (existing.size() > 0) {
position += offset;
}
}
position = qMin(minimum + clip_duration - 1, position);
int duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position);
int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid);
bool revert = offset > clip_duration / 2;
if (lowerVideoTrackId > 0) {
int bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position);
if (bottomId > 0) {
QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime());
if (bottom.first > minimum && position > bottom.first) {
int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first);
if (test_duration > 0) {
position = bottom.first;
duration = test_duration;
revert = position > minimum;
}
}
}
int duration2 = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position);
if (duration2 > 0) {
duration = (duration > 0) ? qMin(duration, duration2) : duration2;
}
}
if (duration < 0) {
duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration());
} else if (duration <= 1) {
// if suggested composition duration is lower than 4 frames, use default
duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration());
if (minimum + clip_duration - position < 3) {
position = minimum + clip_duration - duration;
}
}
std::unique_ptr props(nullptr);
if (revert) {
props = std::make_unique();
if (transitionId == QLatin1String("dissolve")) {
props->set("reverse", 1);
} else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) {
props->set("invert", 1);
} else if (transitionId == QLatin1String("wipe")) {
props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0");
}
}
if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
id = -1;
pCore->displayMessage(i18n("Could not add composition at selected position"), InformationMessage, 500);
}
return id;
}
int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo)
{
int id;
int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration());
if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) {
id = -1;
}
return id;
}
void TimelineController::deleteSelectedClips()
{
auto sel = m_model->getCurrentSelection();
if (sel.empty()) {
return;
}
// only need to delete the first item, the others will be deleted in cascade
m_model->requestItemDeletion(*sel.begin());
}
int TimelineController::getMainSelectedClip()
{
auto sel = m_model->getCurrentSelection();
if (sel.empty() || sel.size() > 2) {
return -1;
}
int itemId = *(sel.begin());
if (sel.size() == 2) {
int parentGroup = m_model->m_groups->getRootId(itemId);
if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) {
return -1;
}
}
if (m_model->isClip(itemId)) {
int position = timelinePosition();
int start = m_model->getClipPosition(itemId);
int end = start + m_model->getClipPlaytime(itemId);
if (position >= start && position <= end) {
return itemId;
}
}
return -1;
}
void TimelineController::copyItem()
{
std::unordered_set selectedIds = m_model->getCurrentSelection();
if (selectedIds.empty()) {
return;
}
int clipId = *(selectedIds.begin());
QString copyString = TimelineFunctions::copyClips(m_model, selectedIds);
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(copyString);
m_root->setProperty("copiedClip", clipId);
m_model->requestSetSelection(selectedIds);
}
bool TimelineController::pasteItem()
{
QClipboard *clipboard = QApplication::clipboard();
QString txt = clipboard->text();
int tid = getMouseTrack();
int position = getMousePos();
if (tid == -1) {
tid = m_activeTrack;
}
if (position == -1) {
position = timelinePosition();
}
return TimelineFunctions::pasteClips(m_model, txt, tid, position);
}
void TimelineController::triggerAction(const QString &name)
{
pCore->triggerAction(name);
}
QString TimelineController::timecode(int frames)
{
return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
}
bool TimelineController::showThumbnails() const
{
return KdenliveSettings::videothumbnails();
}
bool TimelineController::showAudioThumbnails() const
{
return KdenliveSettings::audiothumbnails();
}
bool TimelineController::showMarkers() const
{
return KdenliveSettings::showmarkers();
}
bool TimelineController::audioThumbFormat() const
{
return KdenliveSettings::displayallchannels();
}
bool TimelineController::showWaveforms() const
{
return KdenliveSettings::audiothumbnails();
}
void TimelineController::addTrack(int tid)
{
if (tid == -1) {
tid = m_activeTrack;
}
QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow());
if (d->exec() == QDialog::Accepted) {
int newTid;
bool audioRecTrack = d->addRecTrack();
bool addAVTrack = d->addAVTrack();
m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack());
if (addAVTrack) {
int newTid2;
int mirrorPos = 0;
int mirrorId = m_model->getMirrorAudioTrackId(newTid);
if (mirrorId > -1) {
mirrorPos = m_model->getTrackMltIndex(mirrorId);
}
m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true);
}
m_model->buildTrackCompositing(true);
if (audioRecTrack) {
m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1"));
}
}
}
void TimelineController::deleteTrack(int tid)
{
if (tid == -1) {
tid = m_activeTrack;
}
QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true);
if (d->exec() == QDialog::Accepted) {
int selectedTrackIx = d->selectedTrackId();
if (m_activeTrack == selectedTrackIx) {
// Make sure we don't keep an index on a deleted track
m_activeTrack = -1;
}
if (m_model->m_audioTarget == selectedTrackIx) {
setAudioTarget(-1);
}
if (m_model->m_videoTarget == selectedTrackIx) {
setVideoTarget(-1);
}
m_model->requestTrackDeletion(selectedTrackIx);
m_model->buildTrackCompositing(true);
if (m_activeTrack == -1) {
setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1));
}
}
}
void TimelineController::showConfig(int page, int tab)
{
pCore->showConfigDialog(page, tab);
}
void TimelineController::gotoNextSnap()
{
setPosition(m_model->getNextSnapPos(timelinePosition()));
}
void TimelineController::gotoPreviousSnap()
{
setPosition(m_model->getPreviousSnapPos(timelinePosition()));
}
void TimelineController::groupSelection()
{
const auto selection = m_model->getCurrentSelection();
if (selection.size() < 2) {
pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500);
return;
}
m_model->requestClearSelection();
m_model->requestClipsGroup(selection);
m_model->requestSetSelection(selection);
}
void TimelineController::unGroupSelection(int cid)
{
auto ids = m_model->getCurrentSelection();
// ask to unselect if needed
m_model->requestClearSelection();
if (cid > -1) {
ids.insert(cid);
}
if (!ids.empty()) {
m_model->requestClipsUngroup(ids);
}
}
bool TimelineController::dragOperationRunning()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue));
return returnedValue.toBool();
}
void TimelineController::setInPoint()
{
if (dragOperationRunning()) {
// Don't allow timeline operation while drag in progress
qDebug()<<"Cannot operate while dragging";
return;
}
int cursorPos = timelinePosition();
const auto selection = m_model->getCurrentSelection();
if (!selection.empty()) {
for (int id : selection) {
int start = m_model->getItemPosition(id);
if (start == cursorPos) {
continue;
}
int size = start + m_model->getItemPlaytime(id) - cursorPos;
m_model->requestItemResize(id, size, false, true, 0, false);
}
}
}
int TimelineController::timelinePosition() const
{
return m_seekPosition >= 0 ? m_seekPosition : m_position;
}
void TimelineController::setOutPoint()
{
if (dragOperationRunning()) {
// Don't allow timeline operation while drag in progress
qDebug()<<"Cannot operate while dragging";
return;
}
int cursorPos = timelinePosition();
const auto selection = m_model->getCurrentSelection();
if (!selection.empty()) {
for (int id : selection) {
int start = m_model->getItemPosition(id);
if (start + m_model->getItemPlaytime(id) == cursorPos) {
continue;
}
int size = cursorPos - start;
m_model->requestItemResize(id, size, true, true, 0, false);
}
}
}
void TimelineController::editMarker(int cid, int position)
{
Q_ASSERT(m_model->isClip(cid));
if (position == -1) {
position = timelinePosition();
}
if (position < m_model->getClipPosition(cid) || position > (m_model->getClipPosition(cid) + m_model->getClipPlaytime(cid))) {
pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500);
return;
}
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
GenTime pos(position - m_model->getClipPosition(cid) + m_model->getClipIn(cid), pCore->getCurrentFps());
clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get());
}
void TimelineController::addMarker(int cid, int position)
{
Q_ASSERT(m_model->isClip(cid));
if (position == -1) {
position = timelinePosition();
}
if (position < m_model->getClipPosition(cid) || position > (m_model->getClipPosition(cid) + m_model->getClipPlaytime(cid))) {
pCore->displayMessage(i18n("Cannot find clip to add marker"), InformationMessage, 500);
return;
}
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
GenTime pos(position - m_model->getClipPosition(cid) + m_model->getClipIn(cid), pCore->getCurrentFps());
clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get());
}
void TimelineController::addQuickMarker(int cid, int position)
{
Q_ASSERT(m_model->isClip(cid));
if (position == -1) {
position = timelinePosition();
}
if (position < m_model->getClipPosition(cid) || position > (m_model->getClipPosition(cid) + m_model->getClipPlaytime(cid))) {
pCore->displayMessage(i18n("Cannot find clip to add marker"), InformationMessage, 500);
return;
}
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
GenTime pos(position - m_model->getClipPosition(cid) + m_model->getClipIn(cid), pCore->getCurrentFps());
CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type());
clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType());
}
void TimelineController::deleteMarker(int cid, int position)
{
Q_ASSERT(m_model->isClip(cid));
if (position == -1) {
position = timelinePosition();
}
if (position < m_model->getClipPosition(cid) || position > (m_model->getClipPosition(cid) + m_model->getClipPlaytime(cid))) {
pCore->displayMessage(i18n("Cannot find clip to remove marker"), InformationMessage, 500);
return;
}
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
GenTime pos(position - m_model->getClipPosition(cid) + m_model->getClipIn(cid), pCore->getCurrentFps());
clip->getMarkerModel()->removeMarker(pos);
}
void TimelineController::deleteAllMarkers(int cid)
{
Q_ASSERT(m_model->isClip(cid));
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
clip->getMarkerModel()->removeAllMarkers();
}
void TimelineController::editGuide(int frame)
{
if (frame == -1) {
frame = timelinePosition();
}
auto guideModel = pCore->projectManager()->current()->getGuideModel();
GenTime pos(frame, pCore->getCurrentFps());
guideModel->editMarkerGui(pos, qApp->activeWindow(), false);
}
void TimelineController::moveGuide(int frame, int newFrame)
{
auto guideModel = pCore->projectManager()->current()->getGuideModel();
GenTime pos(frame, pCore->getCurrentFps());
GenTime newPos(newFrame, pCore->getCurrentFps());
guideModel->editMarker(pos, newPos);
}
void TimelineController::switchGuide(int frame, bool deleteOnly)
{
bool markerFound = false;
if (frame == -1) {
frame = timelinePosition();
}
CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound);
if (!markerFound) {
if (deleteOnly) {
pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500);
return;
}
GenTime pos(frame, pCore->getCurrentFps());
pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide"));
} else {
pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time());
}
}
void TimelineController::addAsset(const QVariantMap &data)
{
QString effect = data.value(QStringLiteral("kdenlive/effect")).toString();
const auto selection = m_model->getCurrentSelection();
if (!selection.empty()) {
QList effectSelection;
for (int id : selection) {
if (m_model->isClip(id)) {
effectSelection << id;
}
}
bool foundMatch = false;
for (int id : effectSelection) {
if (m_model->addClipEffect(id, effect, false)) {
foundMatch = true;
}
}
if (!foundMatch) {
QString effectName = EffectsRepository::get()->getName(effect);
pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500);
}
} else {
pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500);
}
}
void TimelineController::requestRefresh()
{
pCore->requestMonitorRefresh();
}
void TimelineController::showAsset(int id)
{
if (m_model->isComposition(id)) {
emit showTransitionModel(id, m_model->getCompositionParameterModel(id));
} else if (m_model->isClip(id)) {
QModelIndex clipIx = m_model->makeClipIndexFromID(id);
QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString();
bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt();
qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes;
emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes);
}
}
void TimelineController::showTrackAsset(int trackId)
{
emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false);
}
void TimelineController::adjustAllTrackHeight(int trackId, int height)
{
bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack();
auto it = m_model->m_allTracks.cbegin();
while (it != m_model->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) {
m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height));
}
++it;
}
int tracksCount = m_model->getTracksCount();
QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
}
void TimelineController::setPosition(int position)
{
setSeekPosition(position);
emit seeked(position);
}
void TimelineController::setAudioTarget(int track)
{
m_model->m_audioTarget = track;
emit audioTargetChanged();
}
void TimelineController::setVideoTarget(int track)
{
m_model->m_videoTarget = track;
emit videoTargetChanged();
}
void TimelineController::setActiveTrack(int track)
{
m_activeTrack = track;
emit activeTrackChanged();
}
void TimelineController::setSeekPosition(int position)
{
m_seekPosition = position;
emit seekPositionChanged();
}
void TimelineController::onSeeked(int position)
{
m_position = position;
emit positionChanged();
if (m_seekPosition > -1 && position == m_seekPosition) {
m_seekPosition = -1;
emit seekPositionChanged();
}
}
void TimelineController::setZone(const QPoint &zone)
{
if (m_zone.x() > 0) {
m_model->removeSnap(m_zone.x());
}
if (m_zone.y() > 0) {
m_model->removeSnap(m_zone.y() - 1);
}
if (zone.x() > 0) {
m_model->addSnap(zone.x());
}
if (zone.y() > 0) {
m_model->addSnap(zone.y() - 1);
}
m_zone = zone;
emit zoneChanged();
}
void TimelineController::setZoneIn(int inPoint)
{
if (m_zone.x() > 0) {
m_model->removeSnap(m_zone.x());
}
if (inPoint > 0) {
m_model->addSnap(inPoint);
}
m_zone.setX(inPoint);
emit zoneMoved(m_zone);
}
void TimelineController::setZoneOut(int outPoint)
{
if (m_zone.y() > 0) {
m_model->removeSnap(m_zone.y() - 1);
}
if (outPoint > 0) {
m_model->addSnap(outPoint - 1);
}
m_zone.setY(outPoint);
emit zoneMoved(m_zone);
}
void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect)
{
std::unordered_set itemsToSelect;
if (addToSelect) {
itemsToSelect = m_model->getCurrentSelection();
}
for (int i = 0; i < tracks.count(); i++) {
if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) {
continue;
}
auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, true);
itemsToSelect.insert(currentClips.begin(), currentClips.end());
}
m_model->requestSetSelection(itemsToSelect);
}
void TimelineController::requestClipCut(int clipId, int position)
{
if (position == -1) {
position = timelinePosition();
}
TimelineFunctions::requestClipCut(m_model, clipId, position);
}
void TimelineController::cutClipUnderCursor(int position, int track)
{
if (position == -1) {
position = timelinePosition();
}
QMutexLocker lk(&m_metaMutex);
bool foundClip = false;
const auto selection = m_model->getCurrentSelection();
for (int cid : selection) {
if (m_model->isClip(cid)) {
if (TimelineFunctions::requestClipCut(m_model, cid, position)) {
foundClip = true;
// Cutting clips in the selection group is handled in TimelineFunctions
break;
}
} else {
qDebug() << "//// TODO: COMPOSITION CUT!!!";
}
}
if (!foundClip) {
if (track == -1) {
track = m_activeTrack;
}
if (track >= 0) {
int cid = m_model->getClipByPosition(track, position);
if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) {
foundClip = true;
}
}
}
if (!foundClip) {
pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500);
}
}
int TimelineController::requestSpacerStartOperation(int trackId, int position)
{
return TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position);
}
bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition)
{
return TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition);
}
void TimelineController::seekCurrentClip(bool seekToEnd)
{
const auto selection = m_model->getCurrentSelection();
for (int cid : selection) {
int start = m_model->getItemPosition(cid);
if (seekToEnd) {
start += m_model->getItemPlaytime(cid);
}
setPosition(start);
break;
}
}
void TimelineController::seekToClip(int cid, bool seekToEnd)
{
int start = m_model->getItemPosition(cid);
if (seekToEnd) {
start += m_model->getItemPlaytime(cid);
}
setPosition(start);
}
void TimelineController::seekToMouse()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue));
int mousePos = returnedValue.toInt();
setPosition(mousePos);
}
int TimelineController::getMousePos()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue));
return returnedValue.toInt();
}
int TimelineController::getMouseTrack()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue));
return returnedValue.toInt();
}
void TimelineController::refreshItem(int id)
{
int in = m_model->getItemPosition(id);
if (in > m_position || (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly())) {
return;
}
if (m_position <= in + m_model->getItemPlaytime(id)) {
pCore->requestMonitorRefresh();
}
}
QPoint TimelineController::getTracksCount() const
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue));
QVariantList tracks = returnedValue.toList();
QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt());
return p;
}
QStringList TimelineController::extractCompositionLumas() const
{
return m_model->extractCompositionLumas();
}
void TimelineController::addEffectToCurrentClip(const QStringList &effectData)
{
QList activeClips;
for (int track = m_model->getTracksCount() - 1; track >= 0; track--) {
int trackIx = m_model->getTrackIndexFromPosition(track);
int cid = m_model->getClipByPosition(trackIx, timelinePosition());
if (cid > -1) {
activeClips << cid;
}
}
if (!activeClips.isEmpty()) {
if (effectData.count() == 4) {
QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3);
m_model->copyClipEffect(activeClips.first(), effectString);
} else {
m_model->addClipEffect(activeClips.first(), effectData.constFirst());
}
}
}
void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration)
{
if (duration <= 0) {
// remove fade
m_model->removeFade(cid, effectId == QLatin1String("fadein"));
} else {
m_model->adjustEffectLength(cid, effectId, duration, initialDuration);
}
}
QPair TimelineController::getCompositionATrack(int cid) const
{
QPair result;
std::shared_ptr compo = m_model->getCompositionPtr(cid);
if (compo) {
result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId()));
}
return result;
}
void TimelineController::setCompositionATrack(int cid, int aTrack)
{
TimelineFunctions::setCompositionATrack(m_model, cid, aTrack);
}
bool TimelineController::compositionAutoTrack(int cid) const
{
std::shared_ptr compo = m_model->getCompositionPtr(cid);
return compo && compo->getForcedTrack() == -1;
}
const QString TimelineController::getClipBinId(int clipId) const
{
return m_model->getClipBinId(clipId);
}
void TimelineController::focusItem(int itemId)
{
int start = m_model->getItemPosition(itemId);
setPosition(start);
}
int TimelineController::headerWidth() const
{
return qMax(10, KdenliveSettings::headerwidth());
}
void TimelineController::setHeaderWidth(int width)
{
KdenliveSettings::setHeaderwidth(width);
}
bool TimelineController::createSplitOverlay(Mlt::Filter *filter)
{
if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) {
return true;
}
int clipId = getCurrentItem();
if (clipId == -1) {
pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500);
return false;
}
std::shared_ptr clip = m_model->getClipPtr(clipId);
const QString binId = clip->binId();
// Get clean bin copy of the clip
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId);
std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut()));
// Get copy of timeline producer
std::shared_ptr clipProducer(new Mlt::Producer(*clip));
// Built tractor and compositing
Mlt::Tractor trac(*m_model->m_tractor->profile());
Mlt::Playlist play(*m_model->m_tractor->profile());
Mlt::Playlist play2(*m_model->m_tractor->profile());
play.append(*clipProducer.get());
play2.append(*binProd);
trac.set_track(play, 0);
trac.set_track(play2, 1);
play2.attach(*filter);
QString splitTransition = TransitionsRepository::get()->getCompositingTransition();
Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData());
t.set("always_active", 1);
trac.plant_transition(t, 0, 1);
int startPos = m_model->getClipPosition(clipId);
// plug in overlay playlist
auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile());
overlay->insert_blank(0, startPos);
Mlt::Producer split(trac.get_producer());
overlay->insert_at(startPos, &split, 1);
// insert in tractor
if (!m_timelinePreview) {
initializePreview();
}
m_timelinePreview->setOverlayTrack(overlay);
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
return true;
}
void TimelineController::removeSplitOverlay()
{
if (m_timelinePreview && !m_timelinePreview->hasOverlayTrack()) {
return;
}
// disconnect
m_timelinePreview->removeOverlayTrack();
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
void TimelineController::addPreviewRange(bool add)
{
if (m_zone.isNull()) {
return;
}
if (!m_timelinePreview) {
initializePreview();
}
if (m_timelinePreview) {
m_timelinePreview->addPreviewRange(m_zone, add);
}
}
void TimelineController::clearPreviewRange(bool resetZones)
{
if (m_timelinePreview) {
m_timelinePreview->clearPreviewRange(resetZones);
}
}
void TimelineController::startPreviewRender()
{
// Timeline preview stuff
if (!m_timelinePreview) {
initializePreview();
} else if (m_disablePreview->isChecked()) {
m_disablePreview->setChecked(false);
disablePreview(false);
}
if (m_timelinePreview) {
if (!m_usePreview) {
m_timelinePreview->buildPreviewTrack();
m_usePreview = true;
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
m_timelinePreview->startPreviewRender();
}
}
void TimelineController::stopPreviewRender()
{
if (m_timelinePreview) {
m_timelinePreview->abortRendering();
}
}
void TimelineController::initializePreview()
{
if (m_timelinePreview) {
// Update parameters
if (!m_timelinePreview->loadParams()) {
if (m_usePreview) {
// Disconnect preview track
m_timelinePreview->disconnectTrack();
m_usePreview = false;
}
delete m_timelinePreview;
m_timelinePreview = nullptr;
}
} else {
m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get());
if (!m_timelinePreview->initialize()) {
// TODO warn user
delete m_timelinePreview;
m_timelinePreview = nullptr;
} else {
}
}
QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone"));
if (previewRender) {
previewRender->setEnabled(m_timelinePreview != nullptr);
}
m_disablePreview->setEnabled(m_timelinePreview != nullptr);
m_disablePreview->blockSignals(true);
m_disablePreview->setChecked(false);
m_disablePreview->blockSignals(false);
}
void TimelineController::disablePreview(bool disable)
{
if (disable) {
m_timelinePreview->deletePreviewTrack();
m_usePreview = false;
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
} else {
if (!m_usePreview) {
if (!m_timelinePreview->buildPreviewTrack()) {
// preview track already exists, reconnect
m_model->m_tractor->lock();
m_timelinePreview->reconnectTrack();
m_model->m_tractor->unlock();
}
m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime());
m_usePreview = true;
}
}
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
QVariantList TimelineController::dirtyChunks() const
{
return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList();
}
QVariantList TimelineController::renderedChunks() const
{
return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList();
}
int TimelineController::workingPreview() const
{
return m_timelinePreview ? m_timelinePreview->workingPreview : -1;
}
bool TimelineController::useRuler() const
{
return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1;
}
void TimelineController::resetPreview()
{
if (m_timelinePreview) {
m_timelinePreview->clearPreviewRange(true);
initializePreview();
}
}
void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable)
{
if (chunks.isEmpty() && dirty.isEmpty()) {
return;
}
if (!m_timelinePreview) {
initializePreview();
}
QVariantList renderedChunks;
QVariantList dirtyChunks;
QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts);
QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts);
for (const QString &frame : chunksList) {
renderedChunks << frame.toInt();
}
for (const QString &frame : dirtyList) {
dirtyChunks << frame.toInt();
}
m_disablePreview->blockSignals(true);
m_disablePreview->setChecked(enable);
m_disablePreview->blockSignals(false);
if (!enable) {
m_timelinePreview->buildPreviewTrack();
m_usePreview = true;
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate);
}
QMap TimelineController::documentProperties()
{
QMap props = pCore->currentDoc()->documentProperties();
int audioTarget = m_model->m_audioTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_audioTarget);
int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget);
int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack);
props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget));
props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget));
props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack));
props.insert(QStringLiteral("position"), QString::number(timelinePosition()));
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue));
int scrollPos = returnedValue.toInt();
props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos));
props.insert(QStringLiteral("zonein"), QString::number(m_zone.x()));
props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y()));
if (m_timelinePreview) {
QPair chunks = m_timelinePreview->previewChunks();
props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(',')));
props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(',')));
}
props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked()));
return props;
}
void TimelineController::insertSpace(int trackId, int frame)
{
if (frame == -1) {
frame = timelinePosition();
}
if (trackId == -1) {
trackId = m_activeTrack;
}
QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow());
if (d->exec() != QDialog::Accepted) {
delete d;
return;
}
int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame);
int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps());
delete d;
if (cid == -1) {
pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500);
return;
}
int start = m_model->getItemPosition(cid);
requestSpacerEndOperation(cid, start, start + spaceDuration);
}
void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks)
{
if (frame == -1) {
frame = timelinePosition();
}
if (trackId == -1) {
trackId = m_activeTrack;
}
bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks);
if (!res) {
pCore->displayMessage(i18n("Cannot remove space at given position"), InformationMessage, 500);
}
}
void TimelineController::invalidateItem(int cid)
{
if (!m_timelinePreview || !m_model->isItem(cid) || m_model->getItemTrackId(cid) == -1) {
return;
}
int start = m_model->getItemPosition(cid);
int end = start + m_model->getItemPlaytime(cid);
m_timelinePreview->invalidatePreview(start, end);
}
void TimelineController::invalidateZone(int in, int out)
{
if (!m_timelinePreview) {
return;
}
m_timelinePreview->invalidatePreview(in, out);
}
void TimelineController::changeItemSpeed(int clipId, double speed)
{
if (qFuzzyCompare(speed, -1)) {
speed = 100 * m_model->getClipSpeed(clipId);
double duration = m_model->getItemPlaytime(clipId);
// this is the max speed so that the clip is at least one frame long
double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId));
// this is the min speed so that the clip doesn't bump into the next one on track
double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true)) - 1);
// if there is a split partner, we must also take it into account
int partner = m_model->getClipSplitPartner(clipId);
if (partner != -1) {
double duration2 = m_model->getItemPlaytime(partner);
double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner));
double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true)) - 1);
minSpeed = std::max(minSpeed, minSpeed2);
maxSpeed = std::min(maxSpeed, maxSpeed2);
}
// speed = QInputDialog::getDouble(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), speed, minSpeed, maxSpeed, 2, &ok);
QScopedPointer d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), minSpeed, maxSpeed, speed < 0));
if (d->exec() != QDialog::Accepted) {
return;
}
speed = d->getValue();
qDebug() << "requesting speed " << speed;
}
m_model->requestClipTimeWarp(clipId, speed);
}
void TimelineController::switchCompositing(int mode)
{
// m_model->m_tractor->lock();
QScopedPointer service(m_model->m_tractor->field());
Mlt::Field *field = m_model->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");
if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) {
// remove all compositing transitions
field->disconnect_service(t);
}
}
service.reset(service->producer());
}
if (mode > 0) {
const QString compositeGeometry =
QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height());
// Loop through tracks
for (int track = 1; track < m_model->getTracksCount(); track++) {
if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) {
// This is a video track
Mlt::Transition t(*m_model->m_tractor->profile(),
mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData());
t.set("always_active", 1);
t.set("a_track", 0);
t.set("b_track", track + 1);
if (mode == 1) {
t.set("valign", "middle");
t.set("halign", "centre");
t.set("fill", 1);
t.set("aligned", 0);
t.set("geometry", compositeGeometry.toUtf8().constData());
}
t.set("internal_added", 237);
field->plant_transition(t, 0, track + 1);
}
}
}
field->unlock();
delete field;
pCore->requestMonitorRefresh();
}
void TimelineController::extractZone(QPoint zone, bool liftOnly)
{
QVector tracks;
auto it = m_model->m_allTracks.cbegin();
while (it != m_model->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (target_track != audioTarget() && target_track != videoTarget() && !m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
++it;
continue;
}
tracks << target_track;
++it;
}
if (m_zone == QPoint()) {
// Use current timeline position and clip zone length
zone.setY(timelinePosition() + zone.y() - zone.x());
zone.setX(timelinePosition());
}
TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly);
}
void TimelineController::extract(int clipId)
{
// TODO: grouped clips?
int in = m_model->getClipPosition(clipId);
QPoint zone(in, in + m_model->getClipPlaytime(clipId));
int track = m_model->getClipTrackId(clipId);
TimelineFunctions::extractZone(m_model, QVector() << track, zone, false);
}
bool TimelineController::insertClipZone(const QString &binId, int tid, int position)
{
QStringList binIdData = binId.split(QLatin1Char('/'));
int in = 0;
int out = -1;
if (binIdData.size() >= 3) {
in = binIdData.at(1).toInt();
out = binIdData.at(2).toInt();
}
QString bid = binIdData.first();
// 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);
}
int aTrack = -1;
int vTrack = -1;
std::shared_ptr clip = pCore->bin()->getBinClip(bid);
if (out <= in) {
out = clip->frameDuration() - 1;
}
if (dropType == PlaylistState::VideoOnly) {
vTrack = tid;
} else if (dropType == PlaylistState::AudioOnly) {
aTrack = tid;
} else {
if (m_model->getTrackById_const(tid)->isAudioTrack()) {
aTrack = tid;
vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(aTrack) : -1;
} else {
vTrack = tid;
aTrack = clip->hasAudioAndVideo() ? m_model->getMirrorAudioTrackId(vTrack) : -1;
}
}
QList target_tracks;
if (vTrack > -1) {
target_tracks << vTrack;
}
if (aTrack > -1) {
target_tracks << aTrack;
}
return TimelineFunctions::insertZone(m_model, target_tracks, binId, position, QPoint(in, out), m_model->m_editMode == TimelineMode::OverwriteEdit);
}
int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite)
{
std::shared_ptr clip = pCore->bin()->getBinClip(binId);
int aTrack = -1;
int vTrack = -1;
if (clip->hasAudio()) {
aTrack = audioTarget();
}
if (clip->hasVideo()) {
vTrack = videoTarget();
}
/*if (aTrack == -1 && vTrack == -1) {
// No target tracks defined, use active track
if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) {
aTrack = m_activeTrack;
vTrack = m_model->getMirrorVideoTrackId(aTrack);
} else {
vTrack = m_activeTrack;
aTrack = m_model->getMirrorAudioTrackId(vTrack);
}
}*/
int insertPoint;
QPoint sourceZone;
if (useRuler() && m_zone != QPoint()) {
// We want to use timeline zone for in/out insert points
insertPoint = m_zone.x();
sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x());
} else {
// Use current timeline pos and clip zone for in/out
insertPoint = timelinePosition();
sourceZone = zone;
}
QList target_tracks;
if (vTrack > -1) {
target_tracks << vTrack;
}
if (aTrack > -1) {
target_tracks << aTrack;
}
return TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite) ? insertPoint + (sourceZone.y() - sourceZone.x())
: -1;
}
void TimelineController::updateClip(int clipId, const QVector &roles)
{
QModelIndex ix = m_model->makeClipIndexFromID(clipId);
if (ix.isValid()) {
m_model->dataChanged(ix, ix, roles);
}
}
void TimelineController::showClipKeyframes(int clipId, bool value)
{
TimelineFunctions::showClipKeyframes(m_model, clipId, value);
}
void TimelineController::showCompositionKeyframes(int clipId, bool value)
{
TimelineFunctions::showCompositionKeyframes(m_model, clipId, value);
}
void TimelineController::switchEnableState(int clipId)
{
TimelineFunctions::switchEnableState(m_model, clipId);
}
void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset)
{
int track = m_model->getClipTrackId(clipId);
int compoId = -1;
if (assetId.isEmpty()) {
QStringList compositions = KdenliveSettings::favorite_transitions();
if (compositions.isEmpty()) {
pCore->displayMessage(i18n("Select a favorite composition"), InformationMessage, 500);
return;
}
compoId = insertNewComposition(track, clipId, offset, compositions.first(), true);
} else {
compoId = insertNewComposition(track, clipId, offset, assetId, true);
}
if (compoId > 0) {
m_model->requestSetSelection({compoId});
}
}
void TimelineController::addEffectToClip(const QString &assetId, int clipId)
{
qDebug() << "/// ADDING ASSET: " << assetId;
m_model->addClipEffect(clipId, assetId);
}
bool TimelineController::splitAV()
{
int cid = *m_model->getCurrentSelection().begin();
if (m_model->isClip(cid)) {
std::shared_ptr clip = m_model->getClipPtr(cid);
if (clip->clipState() == PlaylistState::AudioOnly) {
return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget());
} else {
return TimelineFunctions::requestSplitAudio(m_model, cid, audioTarget());
}
}
pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500);
return false;
}
void TimelineController::splitAudio(int clipId)
{
TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget());
}
void TimelineController::splitVideo(int clipId)
{
TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget());
}
void TimelineController::setAudioRef(int clipId)
{
m_audioRef = clipId;
std::unique_ptr envelope(new AudioEnvelope(getClipBinId(clipId), clipId));
m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope)));
connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, [&](int cid, int shift) {
int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef);
bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true);
if (!result) {
pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), InformationMessage, 500);
}
});
connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage);
}
void TimelineController::alignAudio(int clipId)
{
// find other clip
if (m_audioRef == -1 || m_audioRef == clipId) {
pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500);
return;
}
const QString masterBinClipId = getClipBinId(m_audioRef);
if (m_model->m_groups->isInGroup(clipId)) {
std::unordered_set groupIds = m_model->getGroupElements(clipId);
// Check that no item is grouped with our audioRef item
// TODO
m_model->requestClearSelection();
}
const QString otherBinId = getClipBinId(clipId);
if (otherBinId == masterBinClipId) {
// easy, same clip.
int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(clipId);
if (newPos) {
bool result = m_model->requestClipMove(clipId, m_model->getClipTrackId(clipId), newPos, true, true);
if (!result) {
pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500);
}
return;
}
}
// Perform audio calculation
AudioEnvelope *envelope = new AudioEnvelope(otherBinId, clipId, (size_t)m_model->getClipIn(clipId), (size_t)m_model->getClipPlaytime(clipId),
(size_t)m_model->getClipPosition(clipId));
m_audioCorrelator->addChild(envelope);
}
void TimelineController::switchTrackActive(int trackId)
{
if (trackId == -1) {
trackId = m_activeTrack;
}
bool active = m_model->getTrackById_const(trackId)->isTimelineActive();
m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1"));
}
void TimelineController::switchTrackLock(bool applyToAll)
{
if (!applyToAll) {
// apply to active track only
bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked();
m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:locked_track"), locked ? QStringLiteral("0") : QStringLiteral("1"));
} else {
// Invert track lock
// Get track states first
QMap trackLockState;
int unlockedTracksCount = 0;
int tracksCount = m_model->getTracksCount();
for (int track = tracksCount - 1; track >= 0; track--) {
int trackIx = m_model->getTrackIndexFromPosition(track);
bool isLocked = m_model->getTrackById_const(trackIx)->getProperty("kdenlive:locked_track").toInt() == 1;
if (!isLocked) {
unlockedTracksCount++;
}
trackLockState.insert(trackIx, isLocked);
}
if (unlockedTracksCount == tracksCount) {
// do not lock all tracks, leave active track unlocked
trackLockState.insert(m_activeTrack, true);
}
QMapIterator i(trackLockState);
while (i.hasNext()) {
i.next();
m_model->setTrackProperty(i.key(), QStringLiteral("kdenlive:locked_track"), i.value() ? QStringLiteral("0") : QStringLiteral("1"));
}
}
}
void TimelineController::switchTargetTrack()
{
bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1;
if (isAudio) {
setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack);
} else {
setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack);
}
}
int TimelineController::audioTarget() const
{
return m_model->m_audioTarget;
}
int TimelineController::videoTarget() const
{
return m_model->m_videoTarget;
}
void TimelineController::resetTrackHeight()
{
int tracksCount = m_model->getTracksCount();
for (int track = tracksCount - 1; track >= 0; track--) {
int trackIx = m_model->getTrackIndexFromPosition(track);
m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
}
QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
}
void TimelineController::selectAll()
{
std::unordered_set ids;
for (auto clp : m_model->m_allClips) {
ids.insert(clp.first);
}
for (auto clp : m_model->m_allCompositions) {
ids.insert(clp.first);
}
m_model->requestSetSelection(ids);
}
void TimelineController::selectCurrentTrack()
{
std::unordered_set ids;
for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) {
ids.insert(clp.first);
}
for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) {
ids.insert(clp.first);
}
m_model->requestSetSelection(ids);
}
void TimelineController::pasteEffects(int targetId)
{
std::unordered_set targetIds;
if (targetId == -1) {
std::unordered_set sel = m_model->getCurrentSelection();
if (sel.empty()) {
pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500);
}
for (int s : sel) {
if (m_model->isGroup(s)) {
std::unordered_set sub = m_model->m_groups->getLeaves(s);
for (int current_id : sub) {
if (m_model->isClip(current_id)) {
targetIds.insert(current_id);
}
}
} else if (m_model->isClip(s)) {
targetIds.insert(s);
}
}
} else {
if (m_model->m_groups->isInGroup(targetId)) {
targetId = m_model->m_groups->getRootId(targetId);
}
if (m_model->isGroup(targetId)) {
std::unordered_set sub = m_model->m_groups->getLeaves(targetId);
for (int current_id : sub) {
if (m_model->isClip(current_id)) {
targetIds.insert(current_id);
}
}
} else if (m_model->isClip(targetId)) {
targetIds.insert(targetId);
}
}
if (targetIds.empty()) {
pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500);
}
QClipboard *clipboard = QApplication::clipboard();
QString txt = clipboard->text();
if (txt.isEmpty()) {
pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500);
return;
}
QDomDocument copiedItems;
copiedItems.setContent(txt);
if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) {
pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500);
return;
}
QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
if (clips.isEmpty()) {
pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500);
return;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects"));
effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in")));
for (int i = 1; i < clips.size(); i++) {
QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects"));
QDomNodeList subs = subeffects.childNodes();
- for (int j = 0; j < subs.size(); j++) {
- subs.at(j).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in")));
- effects.appendChild(subs.at(j));
+ while (!subs.isEmpty()) {
+ subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in")));
+ effects.appendChild(subs.at(0));
}
}
bool result = true;
for (int target : targetIds) {
std::shared_ptr destStack = m_model->getClipEffectStackModel(target);
result = result && destStack->fromXml(effects, undo, redo);
if (!result) {
break;
}
}
if (result) {
pCore->pushUndo(undo, redo, i18n("Paste effects"));
} else {
pCore->displayMessage(i18n("Cannot paste effect on selected clip"), InformationMessage, 500);
undo();
}
}
double TimelineController::fps() const
{
return pCore->getCurrentFps();
}
void TimelineController::editItemDuration(int id)
{
int start = m_model->getItemPosition(id);
int in = 0;
int duration = m_model->getItemPlaytime(id);
int maxLength = -1;
bool isComposition = false;
if (m_model->isClip(id)) {
in = m_model->getClipIn(id);
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id));
if (clip && clip->hasLimitedDuration()) {
maxLength = clip->getProducerDuration();
}
} else if (m_model->isComposition(id)) {
// nothing to do
isComposition = true;
} else {
pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500);
return;
}
int trackId = m_model->getItemTrackId(id);
int maxFrame = qMax(0, start + duration +
(isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true)
: m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true)));
int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false)
: m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false)));
int partner = isComposition ? -1 : m_model->getClipSplitPartner(id);
QPointer dialog =
new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow());
if (dialog->exec() == QDialog::Accepted) {
std::function undo = []() { return true; };
std::function redo = []() { return true; };
int newPos = dialog->startPos().frames(pCore->getCurrentFps());
int newIn = dialog->cropStart().frames(pCore->getCurrentFps());
int newDuration = dialog->duration().frames(pCore->getCurrentFps());
bool result = true;
if (newPos < start) {
if (!isComposition) {
result = m_model->requestClipMove(id, trackId, newPos, true, true, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, undo, redo);
}
} else {
result = m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo);
}
if (result && newIn != in) {
m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo);
}
}
if (newDuration != duration + (in - newIn)) {
result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo);
}
}
} else {
// perform resize first
if (newIn != in) {
result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo);
}
}
if (newDuration != duration + (in - newIn)) {
result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo);
}
}
if (start != newPos || newIn != in) {
if (!isComposition) {
result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, undo, redo);
}
} else {
result = result &&
m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo);
}
}
}
if (result) {
pCore->pushUndo(undo, redo, i18n("Edit item"));
} else {
undo();
}
}
}
void TimelineController::updateClipActions()
{
if (m_model->getCurrentSelection().empty()) {
for (QAction *act : clipActions) {
act->setEnabled(false);
}
emit timelineClipSelected(false);
return;
}
std::shared_ptr clip(nullptr);
int item = *m_model->getCurrentSelection().begin();
if (m_model->isClip(item)) {
clip = m_model->getClipPtr(item);
}
for (QAction *act : clipActions) {
bool enableAction = true;
const QChar actionData = act->data().toChar();
if (actionData == QLatin1Char('G')) {
enableAction = isInSelection(item);
} else if (actionData == QLatin1Char('U')) {
enableAction = isInSelection(item) || (m_model->m_groups->isInGroup(item) && !isInSelection(item));
} else if (actionData == QLatin1Char('A')) {
enableAction = clip && clip->clipState() == PlaylistState::AudioOnly;
} else if (actionData == QLatin1Char('V')) {
enableAction = clip && clip->clipState() == PlaylistState::VideoOnly;
} else if (actionData == QLatin1Char('D')) {
enableAction = clip && clip->clipState() == PlaylistState::Disabled;
} else if (actionData == QLatin1Char('E')) {
enableAction = clip && clip->clipState() != PlaylistState::Disabled;
} else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) {
enableAction = clip && clip->canBeVideo() && clip->canBeAudio();
if (enableAction && actionData == QLatin1Char('S')) {
act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio"));
}
} else if (actionData == QLatin1Char('C') && clip == nullptr) {
enableAction = false;
}
act->setEnabled(enableAction);
}
emit timelineClipSelected(clip != nullptr);
}
const QString TimelineController::getAssetName(const QString &assetId, bool isTransition)
{
return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId);
}
void TimelineController::grabCurrent()
{
if (m_model->getCurrentSelection().empty()) {
// TODO: error displayMessage
return;
}
int id = *m_model->getCurrentSelection().begin();
if (m_model->isClip(id)) {
std::shared_ptr clip = m_model->getClipPtr(id);
clip->setGrab(!clip->isGrabbed());
QModelIndex ix = m_model->makeClipIndexFromID(id);
if (ix.isValid()) {
m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole});
}
} else if (m_model->isComposition(id)) {
std::shared_ptr clip = m_model->getCompositionPtr(id);
clip->setGrab(!clip->isGrabbed());
QModelIndex ix = m_model->makeCompositionIndexFromID(id);
if (ix.isValid()) {
m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole});
}
}
}
int TimelineController::getItemMovingTrack(int itemId) const
{
if (m_model->isClip(itemId)) {
int trackId = m_model->m_allClips[itemId]->getFakeTrackId();
return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId;
}
return m_model->m_allCompositions[itemId]->getCurrentTrackId();
}
bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
Q_ASSERT(m_model->m_allClips.count(clipId) > 0);
int trackId = m_model->m_allClips[clipId]->getFakeTrackId();
if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) {
qDebug() << "* * ** END FAKE; NO MOVE RQSTED";
return true;
}
if (m_model->m_groups->isInGroup(clipId)) {
// element is in a group.
int groupId = m_model->m_groups->getRootId(clipId);
int current_trackId = m_model->getClipTrackId(clipId);
int track_pos1 = m_model->getTrackPosition(trackId);
int track_pos2 = m_model->getTrackPosition(current_trackId);
int delta_track = track_pos1 - track_pos2;
int delta_pos = position - m_model->m_allClips[clipId]->getPosition();
return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
}
qDebug() << "//////\n//////\nENDING FAKE MNOVE: " << trackId << ", POS: " << position;
std::function undo = []() { return true; };
std::function redo = []() { return true; };
int duration = m_model->getClipPlaytime(clipId);
int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId();
bool res = true;
if (currentTrack > -1) {
res = res && m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo);
}
if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo);
} else if (m_model->m_editMode == TimelineMode::InsertEdit) {
int startClipId = m_model->getClipByPosition(trackId, position);
if (startClipId > -1) {
// There is a clip, cut
res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo);
}
res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo, false);
}
res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo);
if (res) {
if (logUndo) {
pCore->pushUndo(undo, redo, i18n("Move item"));
}
} else {
qDebug() << "//// FAKE FAILED";
undo();
}
return res;
}
bool TimelineController::endFakeGroupMove(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 = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
if (res && logUndo) {
pCore->pushUndo(undo, redo, i18n("Move group"));
}
return res;
}
bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo)
{
Q_ASSERT(m_model->m_allGroups.count(groupId) > 0);
bool ok = true;
auto all_items = m_model->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 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition();
int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->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
// First, remove clips
int audio_delta, video_delta;
audio_delta = video_delta = delta_track;
int master_trackId = m_model->getItemTrackId(clipId);
if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) {
// Master clip is audio, so reverse delta for video clips
video_delta = -delta_track;
} else {
audio_delta = -delta_track;
}
int min = -1;
int max = -1;
std::unordered_map old_track_ids, old_position, old_forced_track, new_track_ids;
for (int item : sorted_clips) {
int old_trackId = m_model->getItemTrackId(item);
old_track_ids[item] = old_trackId;
if (old_trackId != -1) {
bool updateThisView = true;
if (m_model->isClip(item)) {
int current_track_position = m_model->getTrackPosition(old_trackId);
int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta;
int target_track_position = current_track_position + d;
auto it = m_model->m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
new_track_ids[item] = target_track;
old_position[item] = m_model->m_allClips[item]->getPosition();
int duration = m_model->m_allClips[item]->getPlaytime();
min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos);
max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration);
ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo);
} else {
// ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo);
old_position[item] = m_model->m_allCompositions[item]->getPosition();
old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack();
}
if (!ok) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
}
}
bool res = true;
if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
for (int item : sorted_clips) {
if (m_model->isClip(item) && new_track_ids.count(item) > 0) {
int target_track = new_track_ids[item];
int target_position = old_position[item] + delta_pos;
int duration = m_model->m_allClips[item]->getPlaytime();
res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo);
}
}
} else if (m_model->m_editMode == TimelineMode::InsertEdit) {
QList processedTracks;
for (int item : sorted_clips) {
int target_track = new_track_ids[item];
if (processedTracks.contains(target_track)) {
// already processed
continue;
}
processedTracks << target_track;
int target_position = min;
int startClipId = m_model->getClipByPosition(target_track, target_position);
if (startClipId > -1) {
// There is a clip, cut
res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo);
}
}
res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo, false);
}
for (int item : sorted_clips) {
if (m_model->isClip(item)) {
int target_track = new_track_ids[item];
int target_position = old_position[item] + delta_pos;
ok = ok && m_model->requestClipMove(item, target_track, target_position, updateView, finalMove, finalMove, undo, redo);
} else {
// ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo);
}
if (!ok) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
}
return true;
}
QStringList TimelineController::getThumbKeys()
{
QStringList result;
for (const auto &clp : m_model->m_allClips) {
const QString binId = getClipBinId(clp.first);
std::shared_ptr binClip = pCore->bin()->getBinClip(binId);
result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".png");
result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".png");
}
result.removeDuplicates();
return result;
}
bool TimelineController::isInSelection(int itemId)
{
return m_model->getCurrentSelection().count(itemId) > 0;
}
bool TimelineController::exists(int itemId)
{
return m_model->isClip(itemId) || m_model->isComposition(itemId);
}
void TimelineController::slotMultitrackView(bool enable)
{
TimelineFunctions::enableMultitrackView(m_model, enable);
}
void TimelineController::saveTimelineSelection(const QDir &targetDir)
{
TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir);
}
void TimelineController::addEffectKeyframe(int cid, int frame, double val)
{
if (m_model->isClip(cid)) {
std::shared_ptr destStack = m_model->getClipEffectStackModel(cid);
destStack->addEffectKeyFrame(frame, val);
} else if (m_model->isComposition(cid)) {
std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
listModel->addKeyframe(frame, val);
}
}
void TimelineController::removeEffectKeyframe(int cid, int frame)
{
if (m_model->isClip(cid)) {
std::shared_ptr destStack = m_model->getClipEffectStackModel(cid);
destStack->removeKeyFrame(frame);
} else if (m_model->isComposition(cid)) {
std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps()));
}
}
void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue)
{
if (m_model->isClip(cid)) {
std::shared_ptr destStack = m_model->getClipEffectStackModel(cid);
destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue);
} else if (m_model->isComposition(cid)) {
std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue);
}
}
bool TimelineController::darkBackground() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup());
return scheme.background(KColorScheme::NormalBackground).color().value() < 0.5;
}
QColor TimelineController::videoColor() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup());
return scheme.foreground(KColorScheme::LinkText).color();
}
QColor TimelineController::audioColor() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary);
return scheme.foreground(KColorScheme::ActiveText).color();
}
QColor TimelineController::lockedColor() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup());
return scheme.foreground(KColorScheme::NegativeText).color();
}
QColor TimelineController::groupColor() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup());
return scheme.foreground(KColorScheme::ActiveText).color();
}
QColor TimelineController::selectionColor() const
{
KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary);
return scheme.foreground(KColorScheme::NeutralText).color();
}
void TimelineController::switchRecording(int trackId)
{
if (!pCore->isMediaCapturing()) {
qDebug() << "start recording" << trackId;
if (!m_model->isTrack(trackId)) {
qDebug() << "ERROR: Starting to capture on invalid track " << trackId;
}
if (m_model->getTrackById_const(trackId)->isLocked()) {
pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500);
return;
}
m_recordStart.first = timelinePosition();
m_recordTrack = trackId;
int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first);
if (maximumSpace == INT_MAX) {
m_recordStart.second = 0;
} else {
m_recordStart.second = maximumSpace - m_recordStart.first;
if (m_recordStart.second < 8) {
pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or "
"choose a different track"),
ErrorMessage, 500);
return;
}
}
pCore->monitorManager()->slotSwitchMonitors(false);
pCore->startMediaCapture(true, false);
pCore->monitorManager()->slotPlay();
} else {
pCore->stopMediaCapture(true, false);
pCore->monitorManager()->slotPause();
}
}
void TimelineController::finishRecording(const QString &recordedFile)
{
if (recordedFile.isEmpty()) {
return;
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
std::function callBack = [this](const QString &binId) {
int id = -1;
if (m_recordTrack == -1) {
return;
}
qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second;
bool res = false;
if (m_recordStart.second > 0) {
// Limited space on track
QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(m_recordStart.second - 1);
res = m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false);
} else {
res = m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false);
}
};
QString binId = ClipCreator::createClipFromFile(recordedFile, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel(), undo,
redo, callBack);
if (binId != QStringLiteral("-1")) {
pCore->pushUndo(undo, redo, i18n("Record audio"));
}
}