diff --git a/src/effects/effectstack/model/effectstackmodel.cpp b/src/effects/effectstack/model/effectstackmodel.cpp
index 20ba4d008..759be67ea 100644
--- a/src/effects/effectstack/model/effectstackmodel.cpp
+++ b/src/effects/effectstack/model/effectstackmodel.cpp
@@ -1,1081 +1,1081 @@
/***************************************************************************
* 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());
sub.setAttribute(QStringLiteral("in"), sourceEffect->filter().get_int("in"));
sub.setAttribute(QStringLiteral("out"), sourceEffect->filter().get_int("out"));
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;
}
void EffectStackModel::fromXml(const QDomElement &effectsXml, Fun &undo, Fun &redo)
{
QDomNodeList nodeList = effectsXml.elementsByTagName(QStringLiteral("effect"));
for (int i = 0; i < nodeList.count(); ++i) {
QDomElement node = nodeList.item(i).toElement();
const QString effectId = node.attribute(QStringLiteral("id"));
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);
}
QVector> parameters;
QDomNodeList params = node.elementsByTagName(QStringLiteral("property"));
for (int j = 0; j < params.count(); j++) {
QDomElement pnode = params.item(j).toElement();
parameters.append(QPair(pnode.attribute(QStringLiteral("name")), 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());
} else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
m_fadeOuts.insert(effect->getId());
}
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);
}
}
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());
roles << TimelineModel::FadeInRole;
} else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
m_fadeOuts.insert(effect->getId());
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");
}
qDebug() << "//// SETTING CLIP FADIN: " << duration;
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);
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();
}
}
} 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();
+ 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;
}
}
}
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) {
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->unplant(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/model/compositionmodel.cpp b/src/timeline2/model/compositionmodel.cpp
index 6cd655bdc..a6db8c16b 100644
--- a/src/timeline2/model/compositionmodel.cpp
+++ b/src/timeline2/model/compositionmodel.cpp
@@ -1,285 +1,285 @@
/***************************************************************************
* 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 "compositionmodel.hpp"
#include "assets/keyframes/model/keyframemodellist.hpp"
#include "timelinemodel.hpp"
#include "trackmodel.hpp"
#include "transitions/transitionsrepository.hpp"
#include "undohelper.hpp"
#include
#include
#include
CompositionModel::CompositionModel(std::weak_ptr parent, std::unique_ptr transition, int id, const QDomElement &transitionXml,
const QString &transitionId)
: MoveableItem(std::move(parent), id)
, AssetParameterModel(std::move(transition), transitionXml, transitionId, {ObjectType::TimelineComposition, m_id})
, m_a_track(-1)
, m_duration(0)
{
m_compositionName = TransitionsRepository::get()->getName(transitionId);
}
int CompositionModel::construct(const std::weak_ptr &parent, const QString &transitionId, int id,
std::unique_ptr sourceProperties)
{
std::unique_ptr transition = TransitionsRepository::get()->getTransition(transitionId);
transition->set_in_and_out(0, 0);
auto xml = TransitionsRepository::get()->getXml(transitionId);
if (sourceProperties) {
// Paste parameters from existing source composition
QStringList sourceProps;
for (int i = 0; i < sourceProperties->count(); i++) {
sourceProps << sourceProperties->get_name(i);
}
QDomNodeList params = xml.elementsByTagName(QStringLiteral("parameter"));
for (int i = 0; i < params.count(); ++i) {
QDomElement currentParameter = params.item(i).toElement();
QString paramName = currentParameter.attribute(QStringLiteral("name"));
if (!sourceProps.contains(paramName)) {
continue;
}
QString paramValue = sourceProperties->get(paramName.toUtf8().constData());
currentParameter.setAttribute(QStringLiteral("value"), paramValue);
}
}
std::shared_ptr composition(new CompositionModel(parent, std::move(transition), id, xml, transitionId));
id = composition->m_id;
if (auto ptr = parent.lock()) {
ptr->registerComposition(composition);
} else {
qDebug() << "Error : construction of composition failed because parent timeline is not available anymore";
Q_ASSERT(false);
}
return id;
}
bool CompositionModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo)
{
QWriteLocker locker(&m_lock);
if (size <= 0) {
return false;
}
int delta = getPlaytime() - size;
qDebug() << "compo request resize to " << size << ", ACTUAL SZ: " << getPlaytime() << ", " << right << delta;
int in = getIn();
int out = in + getPlaytime() - 1;
int oldDuration = out - in;
int old_in = in, old_out = out;
if (right) {
out -= delta;
} else {
in += delta;
}
// if the in becomes negative, we add the necessary length in out.
if (in < 0) {
out = out - in;
in = 0;
}
std::function track_operation = []() { return true; };
std::function track_reverse = []() { return true; };
if (m_currentTrackId != -1) {
if (auto ptr = m_parent.lock()) {
track_operation = ptr->getTrackById(m_currentTrackId)->requestCompositionResize_lambda(m_id, in, out, logUndo);
} else {
qDebug() << "Error : Moving composition failed because parent timeline is not available anymore";
Q_ASSERT(false);
}
} else {
// Perform resize only
setInOut(in, out);
}
Fun operation = [track_operation]() {
if (track_operation()) {
return true;
}
return false;
};
if (operation()) {
// Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here
auto ptr = m_parent.lock();
// we send a list of roles to be updated
QVector roles{TimelineModel::DurationRole};
if (!right) {
roles.push_back(TimelineModel::StartRole);
}
if (m_currentTrackId != -1 && ptr) {
QModelIndex ix = ptr->makeCompositionIndexFromID(m_id);
// TODO: integrate in undo
ptr->dataChanged(ix, ix, roles);
track_reverse = ptr->getTrackById(m_currentTrackId)->requestCompositionResize_lambda(m_id, old_in, old_out, logUndo);
}
Fun reverse = [track_reverse]() {
if (track_reverse()) {
return true;
}
return false;
};
auto kfr = getKeyframeModel();
if (kfr) {
// Adjust keyframe length
if (oldDuration > 0) {
kfr->resizeKeyframes(0, oldDuration, 0, out - in, 0, right, undo, redo);
}
Fun refresh = [kfr]() {
kfr->modelChanged();
return true;
};
refresh();
UPDATE_UNDO_REDO(refresh, refresh, undo, redo);
}
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
return false;
}
const QString CompositionModel::getProperty(const QString &name) const
{
READ_LOCK();
return QString::fromUtf8(service()->get(name.toUtf8().constData()));
}
Mlt::Transition *CompositionModel::service() const
{
READ_LOCK();
return static_cast(m_asset.get());
}
Mlt::Properties *CompositionModel::properties()
{
READ_LOCK();
return new Mlt::Properties(m_asset.get()->get_properties());
}
int CompositionModel::getPlaytime() const
{
READ_LOCK();
return m_duration + 1;
}
int CompositionModel::getATrack() const
{
READ_LOCK();
return m_a_track == -1 ? -1 : service()->get_int("a_track");
}
void CompositionModel::setForceTrack(bool force)
{
READ_LOCK();
service()->set("force_track", force ? 1 : 0);
}
int CompositionModel::getForcedTrack() const
{
QWriteLocker locker(&m_lock);
return (service()->get_int("force_track") == 0 || m_a_track == -1) ? -1 : service()->get_int("a_track");
}
void CompositionModel::setATrack(int trackMltPosition, int trackId)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(trackId != getCurrentTrackId()); // can't compose with same track
m_a_track = trackMltPosition;
if (m_a_track >= 0) {
service()->set("a_track", trackMltPosition);
}
if (m_currentTrackId != -1) {
emit compositionTrackChanged();
}
}
KeyframeModel *CompositionModel::getEffectKeyframeModel()
{
prepareKeyframes();
if (getKeyframeModel()) {
return getKeyframeModel()->getKeyModel();
}
return nullptr;
}
bool CompositionModel::showKeyframes() const
{
READ_LOCK();
return !service()->get_int("kdenlive:hide_keyframes");
}
void CompositionModel::setShowKeyframes(bool show)
{
QWriteLocker locker(&m_lock);
service()->set("kdenlive:hide_keyframes", (int)!show);
}
const QString &CompositionModel::displayName() const
{
return m_compositionName;
}
void CompositionModel::setInOut(int in, int out)
{
MoveableItem::setInOut(in, out);
m_duration = out - in;
setPosition(in);
}
void CompositionModel::setCurrentTrackId(int tid, bool finalMove)
{
Q_UNUSED(finalMove);
MoveableItem::setCurrentTrackId(tid);
}
int CompositionModel::getOut() const
{
return getPosition() + m_duration;
}
int CompositionModel::getIn() const
{
return getPosition();
}
QDomElement CompositionModel::toXml(QDomDocument &document)
{
QDomElement container = document.createElement(QStringLiteral("composition"));
container.setAttribute(QStringLiteral("id"), m_id);
container.setAttribute(QStringLiteral("composition"), m_assetId);
container.setAttribute(QStringLiteral("in"), getIn());
container.setAttribute(QStringLiteral("out"), getOut());
container.setAttribute(QStringLiteral("position"), getPosition());
if (auto ptr = m_parent.lock()) {
- int trackId = ptr->getTrackPosition(getCurrentTrackId());
+ int trackId = ptr->getTrackPosition(m_currentTrackId);
container.setAttribute(QStringLiteral("track"), trackId);
}
container.setAttribute(QStringLiteral("a_track"), getATrack());
QScopedPointer props(properties());
for (int i = 0; i < props->count(); i++) {
QString name = props->get_name(i);
if (name.startsWith(QLatin1Char('_'))) {
continue;
}
Xml::setXmlProperty(container, name, props->get(i));
}
return container;
}
diff --git a/src/timeline2/view/qml/Clip.qml b/src/timeline2/view/qml/Clip.qml
index 7fb6685c7..f677e5649 100644
--- a/src/timeline2/view/qml/Clip.qml
+++ b/src/timeline2/view/qml/Clip.qml
@@ -1,763 +1,759 @@
/*
* Copyright (c) 2013-2016 Meltytech, LLC
* Author: Dan Dennedy
*
* 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 3 of the License, or
* (at your option) any later version.
*
* 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 .
*/
import QtQuick 2.6
import QtQuick.Controls 2.2
import Kdenlive.Controls 1.0
import QtQml.Models 2.2
import QtQuick.Window 2.2
import 'Timeline.js' as Logic
import com.enums 1.0
Rectangle {
id: clipRoot
property real timeScale: 1.0
property string clipName: ''
property string clipResource: ''
property string mltService: ''
property string effectNames
property int modelStart
property real scrollX: 0
property int inPoint: 0
property int outPoint: 0
property int clipDuration: 0
property bool isAudio: false
property int audioChannels
property bool showKeyframes: false
property bool isGrabbed: false
property bool grouped: false
property var audioLevels
property var markers
property var keyframeModel
property int clipStatus: 0
property int itemType: 0
property int fadeIn: 0
property int fadeOut: 0
property int binId: 0
property var parentTrack
property int trackIndex //Index in track repeater
property int clipId //Id of the clip in the model
property int trackId: -1 // Id of the parent track in the model
property int fakeTid: -1
property int fakePosition: 0
property int originalTrackId: -1
property int originalX: x
property int originalDuration: clipDuration
property int lastValidDuration: clipDuration
property int draggedX: x
property bool selected: false
property bool isLocked: parentTrack && parentTrack.isLocked == true
property bool hasAudio
property bool canBeAudio
property bool canBeVideo
property string hash: 'ccc' //TODO
property double speed: 1.0
property color borderColor: 'black'
property bool forceReloadThumb: false
width : clipDuration * timeScale;
opacity: dragProxyArea.drag.active && dragProxy.draggedItem == clipId ? 0.8 : 1.0
signal trimmingIn(var clip, real newDuration, var mouse, bool shiftTrim)
signal trimmedIn(var clip, bool shiftTrim)
signal trimmingOut(var clip, real newDuration, var mouse, bool shiftTrim)
signal trimmedOut(var clip, bool shiftTrim)
onIsGrabbedChanged: {
if (clipRoot.isGrabbed) {
clipRoot.forceActiveFocus();
mouseArea.focus = true
}
}
onInPointChanged: {
if (parentTrack && parentTrack.isAudio) {
thumbsLoader.item.reload()
}
}
onClipResourceChanged: {
if (itemType == ProducerType.Color) {
color: Qt.darker(getColor())
}
}
ToolTip {
visible: mouseArea.containsMouse && !dragProxyArea.pressed
font.pixelSize: root.baseUnit
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
text: clipRoot.clipName + ' (' + timeline.timecode(clipRoot.inPoint) + '-' + timeline.timecode(clipRoot.outPoint) + ')'
}
}
onKeyframeModelChanged: {
console.log('keyframe model changed............')
if (effectRow.keyframecanvas) {
effectRow.keyframecanvas.requestPaint()
}
}
onClipDurationChanged: {
width = clipDuration * timeScale;
}
onModelStartChanged: {
x = modelStart * timeScale;
}
onFakePositionChanged: {
x = fakePosition * timeScale;
}
onFakeTidChanged: {
if (clipRoot.fakeTid > -1 && parentTrack) {
if (clipRoot.parent != dragContainer) {
var pos = clipRoot.mapToGlobal(clipRoot.x, clipRoot.y);
clipRoot.parent = dragContainer
pos = clipRoot.mapFromGlobal(pos.x, pos.y)
clipRoot.x = pos.x
clipRoot.y = pos.y
}
clipRoot.y = Logic.getTrackById(clipRoot.fakeTid).y
}
}
onForceReloadThumbChanged: {
// TODO: find a way to force reload of clip thumbs
if (thumbsLoader.item) {
thumbsLoader.item.reload()
}
}
onTimeScaleChanged: {
x = modelStart * timeScale;
width = clipDuration * timeScale;
labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0
if (parentTrack && parentTrack.isAudio) {
thumbsLoader.item.reload();
}
}
onScrollXChanged: {
labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0
}
border.color: selected? activePalette.highlight : grouped ? root.groupColor : borderColor
border.width: isGrabbed ? 8 : 1.5
function updateDrag() {
var itemPos = mapToItem(tracksContainerArea, 0, 0, clipRoot.width, clipRoot.height)
initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false)
}
function getColor() {
if (clipStatus == ClipState.Disabled) {
return 'grey'
}
if (itemType == ProducerType.Color) {
var color = clipResource.substring(clipResource.length - 9)
if (color[0] == '#') {
return color
}
return '#' + color.substring(color.length - 8, color.length - 2)
}
return isAudio? root.audioColor : root.videoColor
}
/* function reparent(track) {
console.log('TrackId: ',trackId)
parent = track
height = track.height
parentTrack = track
trackId = parentTrack.trackId
console.log('Reparenting clip to Track: ', trackId)
//generateWaveform()
}
*/
property bool variableThumbs: (isAudio || itemType == ProducerType.Color || mltService === '')
property bool isImage: itemType == ProducerType.Image
property string baseThumbPath: variableThumbs ? '' : 'image://thumbnail/' + binId + '/' + (isImage ? '#0' : '#')
property string inThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(inPoint * speed)
property string outThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(outPoint * speed)
DropArea { //Drop area for clips
anchors.fill: clipRoot
keys: 'kdenlive/effect'
property string dropData
property string dropSource
property int dropRow: -1
onEntered: {
dropData = drag.getDataAsString('kdenlive/effect')
dropSource = drag.getDataAsString('kdenlive/effectsource')
}
onDropped: {
console.log("Add effect: ", dropData)
if (dropSource == '') {
// drop from effects list
controller.addClipEffect(clipRoot.clipId, dropData);
} else {
controller.copyClipEffect(clipRoot.clipId, dropSource);
}
dropSource = ''
dropRow = -1
drag.acceptProposedAction
}
}
onAudioLevelsChanged: {
if (parentTrack && parentTrack.isAudio && thumbsLoader.item) {
thumbsLoader.item.reload()
}
}
MouseArea {
id: mouseArea
visible: root.activeTool === 0
anchors.fill: clipRoot
acceptedButtons: Qt.RightButton
hoverEnabled: true
cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor
onPressed: {
root.stopScrolling = true
if (mouse.button == Qt.RightButton) {
if (timeline.selection.indexOf(clipRoot.clipId) == -1) {
timeline.addSelection(clipRoot.clipId, true)
}
clipMenu.clipId = clipRoot.clipId
clipMenu.clipStatus = clipRoot.clipStatus
clipMenu.clipFrame = Math.round(mouse.x / timeline.scaleFactor)
clipMenu.grouped = clipRoot.grouped
clipMenu.trackId = clipRoot.trackId
clipMenu.canBeAudio = clipRoot.canBeAudio
clipMenu.canBeVideo = clipRoot.canBeVideo
clipMenu.popup()
}
}
Keys.onShortcutOverride: event.accepted = clipRoot.isGrabbed && (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down)
Keys.onLeftPressed: {
controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart - 1, true, true, true);
}
Keys.onRightPressed: {
controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart + 1, true, true, true);
}
Keys.onUpPressed: {
controller.requestClipMove(clipRoot.clipId, controller.getNextTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true);
}
Keys.onDownPressed: {
controller.requestClipMove(clipRoot.clipId, controller.getPreviousTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true);
}
onPositionChanged: {
var mapped = parentTrack.mapFromItem(clipRoot, mouse.x, mouse.y).x
root.mousePosChanged(Math.round(mapped / timeline.scaleFactor))
if (mouse.modifiers & Qt.ShiftModifier) {
timeline.position = Math.round(mapped / timeline.scaleFactor)
}
}
onEntered: {
var itemPos = mapToItem(tracksContainerArea, 0, 0, width, height)
initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false)
}
onExited: {
endDrag()
}
onWheel: zoomByWheel(wheel)
}
Item {
// Clipping container
id: container
anchors.fill: parent
anchors.margins:1.5
clip: true
Loader {
id: thumbsLoader
anchors.fill: parent
source: parentTrack.isAudio ? (timeline.showAudioThumbnails ? "ClipAudioThumbs.qml" : "") : itemType == ProducerType.Color ? "" : timeline.showThumbnails ? "ClipThumbs.qml" : ""
}
Rectangle {
// text background
id: labelRect
color: clipRoot.selected ? 'darkred' : '#66000000'
width: label.width + 2
height: label.height
visible: clipRoot.width > width / 2
Text {
id: label
text: clipName + (clipRoot.speed != 1.0 ? ' [' + Math.round(clipRoot.speed*100) + '%]': '')
font.pixelSize: root.baseUnit * 1.2
anchors {
top: labelRect.top
left: labelRect.left
topMargin: 1
leftMargin: 1
}
color: 'white'
style: Text.Outline
styleColor: 'black'
}
}
Rectangle {
// effects
id: effectsRect
color: '#555555'
width: effectLabel.width + 2
height: effectLabel.height
x: labelRect.x
anchors.top: labelRect.bottom
visible: labelRect.visible && clipRoot.effectNames != ''
Text {
id: effectLabel
text: clipRoot.effectNames
font.pixelSize: root.baseUnit * 1.2
anchors {
top: effectsRect.top
left: effectsRect.left
topMargin: 1
leftMargin: 1
// + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1
}
color: 'white'
//style: Text.Outline
styleColor: 'black'
}
}
Repeater {
model: markers
delegate:
Item {
anchors.fill: parent
Rectangle {
id: markerBase
width: 1
height: parent.height
x: (model.frame - clipRoot.inPoint) * timeScale;
color: model.color
}
Rectangle {
visible: mlabel.visible
opacity: 0.7
x: markerBase.x
radius: 2
width: mlabel.width + 4
height: mlabel.height
anchors {
bottom: parent.verticalCenter
}
color: model.color
MouseArea {
z: 10
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onDoubleClicked: timeline.editMarker(clipRoot.binId, model.frame)
onClicked: timeline.position = (clipRoot.x + markerBase.x) / timeline.scaleFactor
}
}
Text {
id: mlabel
visible: timeline.showMarkers && parent.width > width * 1.5
text: model.comment
font.pixelSize: root.baseUnit
x: markerBase.x
anchors {
bottom: parent.verticalCenter
topMargin: 2
leftMargin: 2
}
color: 'white'
}
}
}
KeyframeView {
id: effectRow
visible: clipRoot.showKeyframes && clipRoot.keyframeModel
selected: clipRoot.selected
inPoint: clipRoot.inPoint
outPoint: clipRoot.outPoint
masterObject: clipRoot
kfrModel: clipRoot.keyframeModel
}
}
states: [
State {
name: 'locked'
when: isLocked
PropertyChanges {
target: clipRoot
color: root.neutralColor
opacity: 0.8
z: 0
}
},
State {
name: 'normal'
when: clipRoot.selected === false
PropertyChanges {
target: clipRoot
color: getColor()
z: 0
}
},
State {
name: 'selected'
when: clipRoot.selected === true
PropertyChanges {
target: clipRoot
color: Qt.lighter(getColor(), 2)
z: 3
}
}
]
TimelineTriangle {
id: fadeInTriangle
fillColor: 'green'
width: Math.min(clipRoot.fadeIn * timeScale, clipRoot.width)
height: clipRoot.height - clipRoot.border.width * 2
anchors.left: clipRoot.left
anchors.top: clipRoot.top
anchors.margins: clipRoot.border.width
opacity: 0.3
}
Rectangle {
id: fadeInControl
anchors.left: fadeInTriangle.width > radius? undefined : fadeInTriangle.left
anchors.horizontalCenter: fadeInTriangle.width > radius? fadeInTriangle.right : undefined
anchors.top: fadeInTriangle.top
anchors.topMargin: -10
width: root.baseUnit * 2
height: width
radius: width / 2
color: '#FF66FFFF'
border.width: 2
border.color: 'green'
opacity: 0
Drag.active: fadeInMouseArea.drag.active
MouseArea {
id: fadeInMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
drag.target: parent
- drag.minimumX: -root.baseUnit * 2
+ drag.minimumX: -root.baseUnit
drag.maximumX: container.width
drag.axis: Drag.XAxis
+ drag.smoothed: false
property int startX
property int startFadeIn
onEntered: parent.opacity = 0.7
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
- drag.smoothed: false
onPressed: {
root.stopScrolling = true
- startX = parent.x
+ startX = Math.round(parent.x / timeScale)
startFadeIn = clipRoot.fadeIn
parent.anchors.left = undefined
parent.anchors.horizontalCenter = undefined
parent.opacity = 1
fadeInTriangle.opacity = 0.5
// parentTrack.clipSelected(clipRoot, parentTrack) TODO
}
onReleased: {
root.stopScrolling = false
fadeInTriangle.opacity = 0.3
parent.opacity = 0
if (fadeInTriangle.width > parent.radius)
parent.anchors.horizontalCenter = fadeInTriangle.right
else
parent.anchors.left = fadeInTriangle.left
console.log('released fade: ', clipRoot.fadeIn)
- timeline.adjustFade(clipRoot.clipId, 'fadein', clipRoot.fadeIn, startFadeIn)
+ timeline.adjustFade(clipRoot.clipId, 'fadein', clipRoot.fadeIn - 1, startFadeIn)
bubbleHelp.hide()
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
- var delta = Math.round((parent.x - startX) / timeScale)
- if (delta != 0) {
- var duration = Math.max(0, startFadeIn + delta)
- duration = Math.min(duration, clipRoot.clipDuration)
- if (clipRoot.fadeIn - 1 != duration) {
- timeline.adjustFade(clipRoot.clipId, 'fadein', duration, -1)
- }
+ var delta = Math.round(parent.x / timeScale) - startX
+ var duration = Math.max(0, startFadeIn + delta - 1)
+ duration = Math.min(duration, clipRoot.clipDuration)
+ if (duration != clipRoot.fadeIn - 1) {
+ timeline.adjustFade(clipRoot.clipId, 'fadein', duration, -1)
// Show fade duration as time in a "bubble" help.
var s = timeline.timecode(Math.max(duration, 0))
bubbleHelp.show(clipRoot.x, parentTrack.y + clipRoot.height, s)
}
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: fadeInMouseArea.containsMouse && !fadeInMouseArea.pressed
NumberAnimation {
from: 1.0
to: 0.7
duration: 250
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 0.7
to: 1.0
duration: 250
easing.type: Easing.InOutQuad
}
}
}
TimelineTriangle {
id: fadeOutCanvas
fillColor: 'red'
width: Math.min(clipRoot.fadeOut * timeScale, clipRoot.width)
height: clipRoot.height - clipRoot.border.width * 2
anchors.right: clipRoot.right
anchors.top: clipRoot.top
anchors.margins: clipRoot.border.width
opacity: 0.3
transform: Scale { xScale: -1; origin.x: fadeOutCanvas.width / 2}
}
Rectangle {
id: fadeOutControl
anchors.right: fadeOutCanvas.width > radius? undefined : fadeOutCanvas.right
anchors.horizontalCenter: fadeOutCanvas.width > radius? fadeOutCanvas.left : undefined
anchors.top: fadeOutCanvas.top
anchors.topMargin: -10
width: root.baseUnit * 2
height: width
radius: width / 2
color: '#66FFFFFF'
border.width: 2
border.color: 'red'
opacity: 0
Drag.active: fadeOutMouseArea.drag.active
MouseArea {
id: fadeOutMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
drag.target: parent
drag.axis: Drag.XAxis
- drag.minimumX: -root.baseUnit * 2
+ drag.minimumX: -root.baseUnit
drag.maximumX: container.width
property int startX
property int startFadeOut
onEntered: parent.opacity = 0.7
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
drag.smoothed: false
onPressed: {
root.stopScrolling = true
- startX = parent.x
+ startX = Math.round(parent.x / timeScale)
startFadeOut = clipRoot.fadeOut
parent.anchors.right = undefined
parent.anchors.horizontalCenter = undefined
parent.opacity = 1
fadeOutCanvas.opacity = 0.5
}
onReleased: {
fadeOutCanvas.opacity = 0.3
parent.opacity = 0
root.stopScrolling = false
if (fadeOutCanvas.width > parent.radius)
parent.anchors.horizontalCenter = fadeOutCanvas.left
else
parent.anchors.right = fadeOutCanvas.right
timeline.adjustFade(clipRoot.clipId, 'fadeout', clipRoot.fadeOut, startFadeOut)
bubbleHelp.hide()
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
- var delta = Math.round((startX - parent.x) / timeScale)
- if (delta != 0) {
- var duration = Math.max(0, startFadeOut + delta)
- duration = Math.min(duration, clipRoot.clipDuration)
- if (clipRoot.fadeOut - 1 != duration) {
- timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, -1)
- }
+ var delta = startX - Math.round(parent.x / timeScale)
+ var duration = Math.max(0, startFadeOut + delta)
+ duration = Math.min(duration, clipRoot.clipDuration)
+ if (clipRoot.fadeOut != duration) {
+ timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, -1)
// Show fade duration as time in a "bubble" help.
var s = timeline.timecode(Math.max(duration, 0))
bubbleHelp.show(clipRoot.x + clipRoot.width, parentTrack.y + clipRoot.height, s)
}
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: fadeOutMouseArea.containsMouse && !fadeOutMouseArea.pressed
NumberAnimation {
from: 1.0
to: 0.7
duration: 250
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 0.7
to: 1.0
duration: 250
easing.type: Easing.InOutQuad
}
}
}
Rectangle {
id: trimIn
anchors.left: clipRoot.left
anchors.leftMargin: 0
height: parent.height
width: 5
color: isAudio? 'green' : 'lawngreen'
opacity: 0
Drag.active: trimInMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
visible: root.activeTool === 0 && !mouseArea.drag.active
MouseArea {
id: trimInMouseArea
anchors.fill: parent
hoverEnabled: true
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
property bool shiftTrim: false
property bool sizeChanged: false
cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
onPressed: {
root.stopScrolling = true
clipRoot.originalX = clipRoot.x
clipRoot.originalDuration = clipDuration
parent.anchors.left = undefined
shiftTrim = mouse.modifiers & Qt.ShiftModifier
parent.opacity = 0
}
onReleased: {
root.stopScrolling = false
parent.anchors.left = clipRoot.left
if (sizeChanged) {
clipRoot.trimmedIn(clipRoot, shiftTrim)
sizeChanged = false
}
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var delta = Math.round((trimIn.x) / timeScale)
if (delta !== 0) {
if (delta < -modelStart) {
delta = -modelStart
}
var newDuration = clipDuration - delta
sizeChanged = true
clipRoot.trimmingIn(clipRoot, newDuration, mouse, shiftTrim)
}
}
}
onEntered: {
if (!pressed) {
parent.opacity = 0.5
}
}
onExited: {
parent.opacity = 0
}
}
}
Rectangle {
id: trimOut
anchors.right: clipRoot.right
anchors.rightMargin: 0
height: parent.height
width: 5
color: 'red'
opacity: 0
Drag.active: trimOutMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
visible: root.activeTool === 0 && !mouseArea.drag.active
MouseArea {
id: trimOutMouseArea
anchors.fill: parent
hoverEnabled: true
property bool shiftTrim: false
property bool sizeChanged: false
cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
onPressed: {
root.stopScrolling = true
clipRoot.originalDuration = clipDuration
parent.anchors.right = undefined
shiftTrim = mouse.modifiers & Qt.ShiftModifier
parent.opacity = 0
}
onReleased: {
root.stopScrolling = false
parent.anchors.right = clipRoot.right
if (sizeChanged) {
clipRoot.trimmedOut(clipRoot, shiftTrim)
sizeChanged = false
}
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var newDuration = Math.round((parent.x + parent.width) / timeScale)
if (newDuration != clipDuration) {
sizeChanged = true
clipRoot.trimmingOut(clipRoot, newDuration, mouse, shiftTrim)
}
}
}
onEntered: {
if (!pressed) {
parent.opacity = 0.5
}
}
onExited: parent.opacity = 0
}
}
/*MenuItem {
id: mergeItem
text: i18n('Merge with next clip')
onTriggered: timeline.mergeClipWithNext(trackIndex, index, false)
}
MenuItem {
text: i18n('Rebuild Audio Waveform')
onTriggered: timeline.remakeAudioLevels(trackIndex, index)
}*/
/*onPopupVisibleChanged: {
if (visible && application.OS !== 'OS X' && __popupGeometry.height > 0) {
// Try to fix menu running off screen. This only works intermittently.
menu.__yOffset = Math.min(0, Screen.height - (__popupGeometry.y + __popupGeometry.height + 40))
menu.__xOffset = Math.min(0, Screen.width - (__popupGeometry.x + __popupGeometry.width))
}
}*/
}