diff --git a/src/assets/keyframes/model/keyframemodel.cpp b/src/assets/keyframes/model/keyframemodel.cpp
index cdb53e7d4..f32ad9ee2 100644
--- a/src/assets/keyframes/model/keyframemodel.cpp
+++ b/src/assets/keyframes/model/keyframemodel.cpp
@@ -1,1279 +1,1281 @@
/***************************************************************************
* 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 "keyframemodel.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "macros.hpp"
#include "profiles/profilemodel.hpp"
#include "rotoscoping/bpoint.h"
#include "rotoscoping/rotohelper.hpp"
#include
#include
#include
#include
KeyframeModel::KeyframeModel(std::weak_ptr model, const QModelIndex &index, std::weak_ptr undo_stack, QObject *parent)
: QAbstractListModel(parent)
, m_model(std::move(model))
, m_undoStack(std::move(undo_stack))
, m_index(index)
, m_lastData()
, m_lock(QReadWriteLock::Recursive)
{
qDebug() << "Construct keyframemodel. Checking model:" << m_model.expired();
if (auto ptr = m_model.lock()) {
m_paramType = ptr->data(m_index, AssetParameterModel::TypeRole).value();
}
setup();
refresh();
}
void KeyframeModel::setup()
{
// We connect the signals of the abstractitemmodel to a more generic one.
connect(this, &KeyframeModel::columnsMoved, this, &KeyframeModel::modelChanged);
connect(this, &KeyframeModel::columnsRemoved, this, &KeyframeModel::modelChanged);
connect(this, &KeyframeModel::columnsInserted, this, &KeyframeModel::modelChanged);
connect(this, &KeyframeModel::rowsMoved, this, &KeyframeModel::modelChanged);
connect(this, &KeyframeModel::rowsRemoved, this, &KeyframeModel::modelChanged);
connect(this, &KeyframeModel::rowsInserted, this, &KeyframeModel::modelChanged);
connect(this, &KeyframeModel::modelReset, this, &KeyframeModel::modelChanged);
connect(this, &KeyframeModel::dataChanged, this, &KeyframeModel::modelChanged);
connect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification);
}
bool KeyframeModel::addKeyframe(GenTime pos, KeyframeType type, QVariant value, bool notify, Fun &undo, Fun &redo)
{
qDebug() << "ADD keyframe" << pos.frames(pCore->getCurrentFps()) << value << notify;
QWriteLocker locker(&m_lock);
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
if (m_keyframeList.count(pos) > 0) {
qDebug() << "already there";
if (std::pair({type, value}) == m_keyframeList.at(pos)) {
qDebug() << "nothing to do";
return true; // nothing to do
}
// In this case we simply change the type and value
KeyframeType oldType = m_keyframeList[pos].first;
QVariant oldValue = m_keyframeList[pos].second;
local_undo = updateKeyframe_lambda(pos, oldType, oldValue, notify);
local_redo = updateKeyframe_lambda(pos, type, value, notify);
} else {
local_redo = addKeyframe_lambda(pos, type, value, notify);
local_undo = deleteKeyframe_lambda(pos, notify);
}
if (local_redo()) {
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
return false;
}
bool KeyframeModel::addKeyframe(int frame, double normalizedValue)
{
if (auto ptr = m_model.lock()) {
Q_ASSERT(m_index.isValid());
double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble();
double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble();
double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble();
double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble();
int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt();
double realValue;
if (logRole == -1) {
// Logarythmic scale for lower than norm values
if (normalizedValue >= 0.5) {
realValue = norm + (2 * (normalizedValue - 0.5) * (max / factor - norm));
} else {
realValue = norm - pow(2 * (0.5 - normalizedValue), 10.0 / 6) * (norm - min / factor);
}
} else {
realValue = (normalizedValue * (max - min) + min) / factor;
}
// TODO: Use default configurable kf type
return addKeyframe(GenTime(frame, pCore->getCurrentFps()), KeyframeType::Linear, realValue);
}
return false;
}
bool KeyframeModel::addKeyframe(GenTime pos, KeyframeType type, QVariant value)
{
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool update = (m_keyframeList.count(pos) > 0);
bool res = addKeyframe(pos, type, std::move(value), true, undo, redo);
if (res) {
PUSH_UNDO(undo, redo, update ? i18n("Change keyframe type") : i18n("Add keyframe"));
}
return res;
}
bool KeyframeModel::removeKeyframe(GenTime pos, Fun &undo, Fun &redo, bool notify)
{
qDebug() << "Going to remove keyframe at " << pos.frames(pCore->getCurrentFps()) << " NOTIFY: " << notify;
qDebug() << "before" << getAnimProperty();
QWriteLocker locker(&m_lock);
Q_ASSERT(m_keyframeList.count(pos) > 0);
KeyframeType oldType = m_keyframeList[pos].first;
QVariant oldValue = m_keyframeList[pos].second;
Fun local_undo = addKeyframe_lambda(pos, oldType, oldValue, notify);
Fun local_redo = deleteKeyframe_lambda(pos, notify);
if (local_redo()) {
qDebug() << "after" << getAnimProperty();
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
return false;
}
bool KeyframeModel::removeKeyframe(int frame)
{
GenTime pos(frame, pCore->getCurrentFps());
return removeKeyframe(pos);
}
bool KeyframeModel::removeKeyframe(GenTime pos)
{
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
if (m_keyframeList.count(pos) > 0 && m_keyframeList.find(pos) == m_keyframeList.begin()) {
return false; // initial point must stay
}
bool res = removeKeyframe(pos, undo, redo);
if (res) {
PUSH_UNDO(undo, redo, i18n("Delete keyframe"));
}
return res;
}
bool KeyframeModel::moveKeyframe(GenTime oldPos, GenTime pos, QVariant newVal, Fun &undo, Fun &redo)
{
qDebug() << "starting to move keyframe" << oldPos.frames(pCore->getCurrentFps()) << pos.frames(pCore->getCurrentFps());
QWriteLocker locker(&m_lock);
Q_ASSERT(m_keyframeList.count(oldPos) > 0);
if (oldPos == pos) {
if (!newVal.isValid()) {
// no change
return true;
}
if (m_paramType == ParamType::AnimatedRect) {
return updateKeyframe(pos, newVal);
}
double realValue = newVal.toDouble();
// Calculate real value from normalized
if (auto ptr = m_model.lock()) {
double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble();
double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble();
double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble();
double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble();
int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt();
if (logRole == -1) {
// Logarythmic scale for lower than norm values
if (realValue >= 0.5) {
realValue = norm + (2 * (realValue - 0.5) * (max / factor - norm));
} else {
realValue = norm - pow(2 * (0.5 - realValue), 10.0 / 6) * (norm - min / factor);
}
} else {
realValue = (realValue * (max - min) + min) / factor;
}
}
return updateKeyframe(pos, realValue);
}
if (oldPos != pos && hasKeyframe(pos)) {
return false;
}
KeyframeType oldType = m_keyframeList[oldPos].first;
QVariant oldValue = m_keyframeList[oldPos].second;
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
qDebug() << getAnimProperty();
// TODO: use the new Animation::key_set_frame to move a keyframe
bool res = removeKeyframe(oldPos, local_undo, local_redo);
qDebug() << "Move keyframe finished deletion:" << res;
qDebug() << getAnimProperty();
if (res) {
if (m_paramType == ParamType::AnimatedRect) {
if (!newVal.isValid()) {
newVal = oldValue;
}
res = addKeyframe(pos, oldType, newVal, true, local_undo, local_redo);
} else if (newVal.isValid()) {
if (auto ptr = m_model.lock()) {
double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble();
double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble();
double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble();
double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble();
int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt();
double realValue = newVal.toDouble();
if (logRole == -1) {
// Logarythmic scale for lower than norm values
if (newVal >= 0.5) {
realValue = norm + (2 * (realValue - 0.5) * (max / factor - norm));
} else {
realValue = norm - pow(2 * (0.5 - realValue), 10.0 / 6) * (norm - min / factor);
}
} else {
realValue = (realValue * (max - min) + min) / factor;
}
res = addKeyframe(pos, oldType, realValue, true, local_undo, local_redo);
}
} else {
res = addKeyframe(pos, oldType, oldValue, true, local_undo, local_redo);
}
qDebug() << "Move keyframe finished insertion:" << res;
qDebug() << getAnimProperty();
}
if (res) {
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
} else {
bool undone = local_undo();
Q_ASSERT(undone);
}
return res;
}
bool KeyframeModel::moveKeyframe(int oldPos, int pos, bool logUndo)
{
GenTime oPos(oldPos, pCore->getCurrentFps());
GenTime nPos(pos, pCore->getCurrentFps());
return moveKeyframe(oPos, nPos, QVariant(), logUndo);
}
bool KeyframeModel::offsetKeyframes(int oldPos, int pos, bool logUndo)
{
if (oldPos == pos) return true;
GenTime oldFrame(oldPos, pCore->getCurrentFps());
Q_ASSERT(m_keyframeList.count(oldFrame) > 0);
GenTime diff(pos - oldPos, pCore->getCurrentFps());
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
QList times;
for (const auto &m : m_keyframeList) {
if (m.first < oldFrame) continue;
times << m.first;
}
bool res = true;
for (const auto &t : times) {
res &= moveKeyframe(t, t + diff, QVariant(), undo, redo);
}
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move keyframes"));
}
return res;
}
bool KeyframeModel::moveKeyframe(int oldPos, int pos, QVariant newVal)
{
GenTime oPos(oldPos, pCore->getCurrentFps());
GenTime nPos(pos, pCore->getCurrentFps());
return moveKeyframe(oPos, nPos, std::move(newVal), true);
}
bool KeyframeModel::moveKeyframe(GenTime oldPos, GenTime pos, QVariant newVal, bool logUndo)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_keyframeList.count(oldPos) > 0);
if (oldPos == pos) return true;
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = moveKeyframe(oldPos, pos, std::move(newVal), undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move keyframe"));
}
return res;
}
bool KeyframeModel::directUpdateKeyframe(GenTime pos, QVariant value)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_keyframeList.count(pos) > 0);
KeyframeType type = m_keyframeList[pos].first;
auto operation = updateKeyframe_lambda(pos, type, std::move(value), true);
return operation();
}
bool KeyframeModel::updateKeyframe(GenTime pos, const QVariant &value, Fun &undo, Fun &redo, bool update)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_keyframeList.count(pos) > 0);
KeyframeType type = m_keyframeList[pos].first;
QVariant oldValue = m_keyframeList[pos].second;
// Check if keyframe is different
if (m_paramType == ParamType::KeyframeParam) {
if (qFuzzyCompare(oldValue.toDouble(), value.toDouble())) return true;
}
auto operation = updateKeyframe_lambda(pos, type, value, update);
auto reverse = updateKeyframe_lambda(pos, type, oldValue, update);
bool res = operation();
if (res) {
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
}
return res;
}
bool KeyframeModel::updateKeyframe(int pos, double newVal)
{
GenTime Pos(pos, pCore->getCurrentFps());
if (auto ptr = m_model.lock()) {
double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble();
double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble();
double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble();
double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble();
int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt();
double realValue;
if (logRole == -1) {
// Logarythmic scale for lower than norm values
if (newVal >= 0.5) {
realValue = norm + (2 * (newVal - 0.5) * (max / factor - norm));
} else {
realValue = norm - pow(2 * (0.5 - newVal), 10.0 / 6) * (norm - min / factor);
}
} else {
realValue = (newVal * (max - min) + min) / factor;
}
return updateKeyframe(Pos, realValue);
}
return false;
}
bool KeyframeModel::updateKeyframe(GenTime pos, QVariant value)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_keyframeList.count(pos) > 0);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = updateKeyframe(pos, std::move(value), undo, redo);
if (res) {
PUSH_UNDO(undo, redo, i18n("Update keyframe"));
}
return res;
}
KeyframeType convertFromMltType(mlt_keyframe_type type)
{
switch (type) {
case mlt_keyframe_linear:
return KeyframeType::Linear;
case mlt_keyframe_discrete:
return KeyframeType::Discrete;
case mlt_keyframe_smooth:
return KeyframeType::Curve;
}
return KeyframeType::Linear;
}
bool KeyframeModel::updateKeyframeType(GenTime pos, int type, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_keyframeList.count(pos) > 0);
KeyframeType oldType = m_keyframeList[pos].first;
KeyframeType newType = convertFromMltType((mlt_keyframe_type)type);
QVariant value = m_keyframeList[pos].second;
// Check if keyframe is different
if (m_paramType == ParamType::KeyframeParam) {
if (oldType == newType) return true;
}
auto operation = updateKeyframe_lambda(pos, newType, value, true);
auto reverse = updateKeyframe_lambda(pos, oldType, value, true);
bool res = operation();
if (res) {
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
}
return res;
}
Fun KeyframeModel::updateKeyframe_lambda(GenTime pos, KeyframeType type, const QVariant &value, bool notify)
{
QWriteLocker locker(&m_lock);
return [this, pos, type, value, notify]() {
qDebug() << "update lambda" << pos.frames(pCore->getCurrentFps()) << value << notify;
Q_ASSERT(m_keyframeList.count(pos) > 0);
int row = static_cast(std::distance(m_keyframeList.begin(), m_keyframeList.find(pos)));
m_keyframeList[pos].first = type;
m_keyframeList[pos].second = value;
if (notify) emit dataChanged(index(row), index(row), {ValueRole, NormalizedValueRole, TypeRole});
return true;
};
}
Fun KeyframeModel::addKeyframe_lambda(GenTime pos, KeyframeType type, const QVariant &value, bool notify)
{
QWriteLocker locker(&m_lock);
return [this, notify, pos, type, value]() {
qDebug() << "add lambda" << pos.frames(pCore->getCurrentFps()) << value << notify;
Q_ASSERT(m_keyframeList.count(pos) == 0);
// We determine the row of the newly added marker
auto insertionIt = m_keyframeList.lower_bound(pos);
int insertionRow = static_cast(m_keyframeList.size());
if (insertionIt != m_keyframeList.end()) {
insertionRow = static_cast(std::distance(m_keyframeList.begin(), insertionIt));
}
if (notify) beginInsertRows(QModelIndex(), insertionRow, insertionRow);
m_keyframeList[pos].first = type;
m_keyframeList[pos].second = value;
if (notify) endInsertRows();
return true;
};
}
Fun KeyframeModel::deleteKeyframe_lambda(GenTime pos, bool notify)
{
QWriteLocker locker(&m_lock);
return [this, pos, notify]() {
qDebug() << "delete lambda" << pos.frames(pCore->getCurrentFps()) << notify;
qDebug() << "before" << getAnimProperty();
Q_ASSERT(m_keyframeList.count(pos) > 0);
Q_ASSERT(pos != GenTime()); // cannot delete initial point
int row = static_cast(std::distance(m_keyframeList.begin(), m_keyframeList.find(pos)));
if (notify) beginRemoveRows(QModelIndex(), row, row);
m_keyframeList.erase(pos);
if (notify) endRemoveRows();
qDebug() << "after" << getAnimProperty();
return true;
};
}
QHash KeyframeModel::roleNames() const
{
QHash roles;
roles[PosRole] = "position";
roles[FrameRole] = "frame";
roles[TypeRole] = "type";
roles[ValueRole] = "value";
roles[NormalizedValueRole] = "normalizedValue";
return roles;
}
QVariant KeyframeModel::data(const QModelIndex &index, int role) const
{
READ_LOCK();
if (index.row() < 0 || index.row() >= static_cast(m_keyframeList.size()) || !index.isValid()) {
return QVariant();
}
auto it = m_keyframeList.begin();
std::advance(it, index.row());
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
case ValueRole:
return it->second.second;
case NormalizedValueRole: {
if (m_paramType == ParamType::AnimatedRect) {
const QString &data = it->second.second.toString();
QLocale locale;
return locale.toDouble(data.section(QLatin1Char(' '), -1));
}
double val = it->second.second.toDouble();
if (auto ptr = m_model.lock()) {
Q_ASSERT(m_index.isValid());
double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble();
double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble();
double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble();
double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble();
int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt();
double linear = val * factor;
if (logRole == -1) {
// Logarythmic scale for lower than norm values
if (linear >= norm) {
return 0.5 + (linear - norm) / (max * factor - norm) * 0.5;
}
// transform current value to 0..1 scale
double scaled = (linear - norm) / (min * factor - norm);
// Log scale
return 0.5 - pow(scaled, 0.6) * 0.5;
}
return (linear - min) / (max - min);
} else {
qDebug() << "// CANNOT LOCK effect MODEL";
}
return 1;
}
case PosRole:
return it->first.seconds();
case FrameRole:
case Qt::UserRole:
return it->first.frames(pCore->getCurrentFps());
case TypeRole:
return QVariant::fromValue(it->second.first);
}
return QVariant();
}
int KeyframeModel::rowCount(const QModelIndex &parent) const
{
READ_LOCK();
if (parent.isValid()) return 0;
return static_cast(m_keyframeList.size());
}
bool KeyframeModel::singleKeyframe() const
{
READ_LOCK();
return m_keyframeList.size() <= 1;
}
Keyframe KeyframeModel::getKeyframe(const GenTime &pos, bool *ok) const
{
READ_LOCK();
if (m_keyframeList.count(pos) <= 0) {
// return empty marker
*ok = false;
return {GenTime(), KeyframeType::Linear};
}
*ok = true;
return {pos, m_keyframeList.at(pos).first};
}
Keyframe KeyframeModel::getNextKeyframe(const GenTime &pos, bool *ok) const
{
auto it = m_keyframeList.upper_bound(pos);
if (it == m_keyframeList.end()) {
// return empty marker
*ok = false;
return {GenTime(), KeyframeType::Linear};
}
*ok = true;
return {(*it).first, (*it).second.first};
}
Keyframe KeyframeModel::getPrevKeyframe(const GenTime &pos, bool *ok) const
{
auto it = m_keyframeList.lower_bound(pos);
if (it == m_keyframeList.begin()) {
// return empty marker
*ok = false;
return {GenTime(), KeyframeType::Linear};
}
--it;
*ok = true;
return {(*it).first, (*it).second.first};
}
Keyframe KeyframeModel::getClosestKeyframe(const GenTime &pos, bool *ok) const
{
if (m_keyframeList.count(pos) > 0) {
return getKeyframe(pos, ok);
}
bool ok1, ok2;
auto next = getNextKeyframe(pos, &ok1);
auto prev = getPrevKeyframe(pos, &ok2);
*ok = ok1 || ok2;
if (ok1 && ok2) {
double fps = pCore->getCurrentFps();
if (qAbs(next.first.frames(fps) - pos.frames(fps)) < qAbs(prev.first.frames(fps) - pos.frames(fps))) {
return next;
}
return prev;
} else if (ok1) {
return next;
} else if (ok2) {
return prev;
}
// return empty marker
return {GenTime(), KeyframeType::Linear};
}
bool KeyframeModel::hasKeyframe(int frame) const
{
return hasKeyframe(GenTime(frame, pCore->getCurrentFps()));
}
bool KeyframeModel::hasKeyframe(const GenTime &pos) const
{
READ_LOCK();
return m_keyframeList.count(pos) > 0;
}
bool KeyframeModel::removeAllKeyframes(Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
std::vector all_pos;
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
int kfrCount = (int)m_keyframeList.size() - 1;
if (kfrCount <= 0) {
// Nothing to do
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
// we trigger only one global remove/insertrow event
Fun update_redo_start = [this, kfrCount]() {
beginRemoveRows(QModelIndex(), 1, kfrCount);
return true;
};
Fun update_redo_end = [this]() {
endRemoveRows();
return true;
};
Fun update_undo_start = [this, kfrCount]() {
beginInsertRows(QModelIndex(), 1, kfrCount);
return true;
};
Fun update_undo_end = [this]() {
endInsertRows();
return true;
};
PUSH_LAMBDA(update_redo_start, local_redo);
PUSH_LAMBDA(update_undo_start, local_undo);
for (const auto &m : m_keyframeList) {
all_pos.push_back(m.first);
}
update_redo_start();
bool res = true;
bool first = true;
for (const auto &p : all_pos) {
if (first) { // skip first point
first = false;
continue;
}
res = removeKeyframe(p, local_undo, local_redo, false);
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
update_redo_end();
PUSH_LAMBDA(update_redo_end, local_redo);
PUSH_LAMBDA(update_undo_end, local_undo);
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool KeyframeModel::removeAllKeyframes()
{
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = removeAllKeyframes(undo, redo);
if (res) {
PUSH_UNDO(undo, redo, i18n("Delete all keyframes"));
}
return res;
}
mlt_keyframe_type convertToMltType(KeyframeType type)
{
switch (type) {
case KeyframeType::Linear:
return mlt_keyframe_linear;
case KeyframeType::Discrete:
return mlt_keyframe_discrete;
case KeyframeType::Curve:
return mlt_keyframe_smooth;
}
return mlt_keyframe_linear;
}
QString KeyframeModel::getAnimProperty() const
{
if (m_paramType == ParamType::Roto_spline) {
return getRotoProperty();
}
Mlt::Properties mlt_prop;
if (auto ptr = m_model.lock()) {
ptr->passProperties(mlt_prop);
}
int ix = 0;
bool first = true;
std::shared_ptr anim;
for (const auto &keyframe : m_keyframeList) {
if (first) {
switch (m_paramType) {
case ParamType::AnimatedRect:
mlt_prop.anim_set("key", keyframe.second.second.toString().toUtf8().constData(), keyframe.first.frames(pCore->getCurrentFps()));
break;
default:
mlt_prop.anim_set("key", keyframe.second.second.toDouble(), keyframe.first.frames(pCore->getCurrentFps()));
break;
}
anim.reset(mlt_prop.get_anim("key"));
anim->key_set_type(ix, convertToMltType(keyframe.second.first));
first = false;
ix++;
continue;
}
switch (m_paramType) {
case ParamType::AnimatedRect:
mlt_prop.anim_set("key", keyframe.second.second.toString().toUtf8().constData(), keyframe.first.frames(pCore->getCurrentFps()));
break;
default:
mlt_prop.anim_set("key", keyframe.second.second.toDouble(), keyframe.first.frames(pCore->getCurrentFps()));
break;
}
anim->key_set_type(ix, convertToMltType(keyframe.second.first));
ix++;
}
char *cut = anim->serialize_cut();
QString ret(cut);
free(cut);
return ret;
}
QString KeyframeModel::getRotoProperty() const
{
QJsonDocument doc;
if (auto ptr = m_model.lock()) {
int in = 0; // ptr->data(m_index, AssetParameterModel::ParentInRole).toInt();
int out = ptr->data(m_index, AssetParameterModel::ParentDurationRole).toInt();
QMap map;
for (const auto &keyframe : m_keyframeList) {
map.insert(QString::number(in + keyframe.first.frames(pCore->getCurrentFps())).rightJustified(log10((double)out) + 1, '0'), keyframe.second.second);
}
doc = QJsonDocument::fromVariant(QVariant(map));
}
return doc.toJson();
}
void KeyframeModel::parseAnimProperty(const QString &prop)
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
QLocale locale;
disconnect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification);
removeAllKeyframes(undo, redo);
int in = 0;
int out = 0;
bool useOpacity = true;
Mlt::Properties mlt_prop;
if (auto ptr = m_model.lock()) {
in = ptr->data(m_index, AssetParameterModel::ParentInRole).toInt();
out = ptr->data(m_index, AssetParameterModel::ParentDurationRole).toInt();
ptr->passProperties(mlt_prop);
useOpacity = ptr->data(m_index, AssetParameterModel::OpacityRole).toBool();
+ } else {
+ qDebug()<<"###################\n\n/// ERROR LOCKING MODEL!!! ";
}
mlt_prop.set("key", prop.toUtf8().constData());
// This is a fake query to force the animation to be parsed
(void)mlt_prop.anim_get_double("key", 0, out);
Mlt::Animation anim = mlt_prop.get_animation("key");
qDebug() << "Found" << anim.key_count() << ", OUT: " << out << ", animation properties: " << prop;
bool useDefaultType = !prop.contains(QLatin1Char('='));
for (int i = 0; i < anim.key_count(); ++i) {
int frame;
mlt_keyframe_type type;
anim.key_get(i, frame, type);
if (useDefaultType) {
// TODO: use a default user defined type
type = mlt_keyframe_linear;
}
QVariant value;
switch (m_paramType) {
case ParamType::AnimatedRect: {
mlt_rect rect = mlt_prop.anim_get_rect("key", frame);
if (useOpacity) {
value = QVariant(QStringLiteral("%1 %2 %3 %4 %5").arg(rect.x).arg(rect.y).arg(rect.w).arg(rect.h).arg(locale.toString(rect.o)));
} else {
value = QVariant(QStringLiteral("%1 %2 %3 %4").arg(rect.x).arg(rect.y).arg(rect.w).arg(rect.h));
}
break;
}
default:
value = QVariant(mlt_prop.anim_get_double("key", frame));
break;
}
if (i == 0 && frame > in) {
// Always add a keyframe at start pos
addKeyframe(GenTime(in, pCore->getCurrentFps()), convertFromMltType(type), value, true, undo, redo);
} else if (frame == in && hasKeyframe(GenTime(in))) {
// First keyframe already exists, adjust its value
updateKeyframe(GenTime(frame, pCore->getCurrentFps()), value, undo, redo, true);
continue;
}
addKeyframe(GenTime(frame, pCore->getCurrentFps()), convertFromMltType(type), value, true, undo, redo);
}
connect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification);
}
void KeyframeModel::resetAnimProperty(const QString &prop)
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
// Delete all existing keyframes
disconnect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification);
removeAllKeyframes(undo, redo);
Mlt::Properties mlt_prop;
QLocale locale;
int in = 0;
bool useOpacity = true;
if (auto ptr = m_model.lock()) {
in = ptr->data(m_index, AssetParameterModel::ParentInRole).toInt();
ptr->passProperties(mlt_prop);
if (m_paramType == ParamType::AnimatedRect) {
useOpacity = ptr->data(m_index, AssetParameterModel::OpacityRole).toBool();
}
}
mlt_prop.set("key", prop.toUtf8().constData());
// This is a fake query to force the animation to be parsed
(void)mlt_prop.anim_get_int("key", 0, 0);
Mlt::Animation anim = mlt_prop.get_animation("key");
qDebug() << "Found" << anim.key_count() << "animation properties";
for (int i = 0; i < anim.key_count(); ++i) {
int frame;
mlt_keyframe_type type;
anim.key_get(i, frame, type);
if (!prop.contains(QLatin1Char('='))) {
// TODO: use a default user defined type
type = mlt_keyframe_linear;
}
QVariant value;
switch (m_paramType) {
case ParamType::AnimatedRect: {
mlt_rect rect = mlt_prop.anim_get_rect("key", frame);
if (useOpacity) {
value = QVariant(QStringLiteral("%1 %2 %3 %4 %5").arg(rect.x).arg(rect.y).arg(rect.w).arg(rect.h).arg(locale.toString(rect.o)));
} else {
value = QVariant(QStringLiteral("%1 %2 %3 %4").arg(rect.x).arg(rect.y).arg(rect.w).arg(rect.h));
}
break;
}
default:
value = QVariant(mlt_prop.anim_get_double("key", frame));
break;
}
if (i == 0 && frame > in) {
// Always add a keyframe at start pos
addKeyframe(GenTime(in, pCore->getCurrentFps()), convertFromMltType(type), value, false, undo, redo);
} else if (frame == in && hasKeyframe(GenTime(in))) {
// First keyframe already exists, adjust its value
updateKeyframe(GenTime(frame, pCore->getCurrentFps()), value, undo, redo, false);
continue;
}
addKeyframe(GenTime(frame, pCore->getCurrentFps()), convertFromMltType(type), value, false, undo, redo);
}
QString effectName;
if (auto ptr = m_model.lock()) {
effectName = ptr->data(m_index, Qt::DisplayRole).toString();
} else {
effectName = i18n("effect");
}
Fun update_local = [this]() {
emit dataChanged(index(0), index((int)m_keyframeList.size()), {});
return true;
};
update_local();
PUSH_LAMBDA(update_local, undo);
PUSH_LAMBDA(update_local, redo);
PUSH_UNDO(undo, redo, i18n("Reset %1", effectName));
connect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification);
}
void KeyframeModel::parseRotoProperty(const QString &prop)
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(prop.toLatin1(), &jsonError);
QVariant data = doc.toVariant();
if (data.canConvert(QVariant::Map)) {
QList keyframes;
QMap map = data.toMap();
QMap::const_iterator i = map.constBegin();
while (i != map.constEnd()) {
addKeyframe(GenTime(i.key().toInt(), pCore->getCurrentFps()), KeyframeType::Linear, i.value(), false, undo, redo);
++i;
}
}
}
QVariant KeyframeModel::getInterpolatedValue(int p) const
{
auto pos = GenTime(p, pCore->getCurrentFps());
return getInterpolatedValue(pos);
}
QVariant KeyframeModel::updateInterpolated(const QVariant &interpValue, double val)
{
QStringList vals = interpValue.toString().split(QLatin1Char(' '));
QLocale locale;
if (!vals.isEmpty()) {
vals[vals.size() - 1] = locale.toString(val);
}
return vals.join(QLatin1Char(' '));
}
QVariant KeyframeModel::getNormalizedValue(double newVal) const
{
if (auto ptr = m_model.lock()) {
double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble();
double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble();
double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble();
double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble();
int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt();
double realValue;
if (logRole == -1) {
// Logarythmic scale for lower than norm values
if (newVal >= 0.5) {
realValue = norm + (2 * (newVal - 0.5) * (max / factor - norm));
} else {
realValue = norm - pow(2 * (0.5 - newVal), 10.0 / 6) * (norm - min / factor);
}
} else {
realValue = (newVal * (max - min) + min) / factor;
}
return QVariant(realValue);
}
return QVariant();
}
QVariant KeyframeModel::getInterpolatedValue(const GenTime &pos) const
{
if (m_keyframeList.count(pos) > 0) {
return m_keyframeList.at(pos).second;
}
if (m_keyframeList.size() == 0) {
return QVariant();
}
auto next = m_keyframeList.upper_bound(pos);
if (next == m_keyframeList.cbegin()) {
return (m_keyframeList.cbegin())->second.second;
} else if (next == m_keyframeList.cend()) {
auto it = m_keyframeList.cend();
--it;
return it->second.second;
}
auto prev = next;
--prev;
// We now have surrounding keyframes, we use mlt to compute the value
Mlt::Properties prop;
bool useOpacity = true;
if (auto ptr = m_model.lock()) {
ptr->passProperties(prop);
if (m_paramType == ParamType::AnimatedRect) {
useOpacity = ptr->data(m_index, AssetParameterModel::OpacityRole).toBool();
}
}
QLocale locale;
int p = pos.frames(pCore->getCurrentFps());
if (m_paramType == ParamType::KeyframeParam) {
prop.anim_set("keyframe", prev->second.second.toDouble(), prev->first.frames(pCore->getCurrentFps()), next->first.frames(pCore->getCurrentFps()),
convertToMltType(prev->second.first));
prop.anim_set("keyframe", next->second.second.toDouble(), next->first.frames(pCore->getCurrentFps()), next->first.frames(pCore->getCurrentFps()),
convertToMltType(next->second.first));
return QVariant(prop.anim_get_double("keyframe", p));
} else if (m_paramType == ParamType::AnimatedRect) {
QStringList vals = prev->second.second.toString().split(QLatin1Char(' '));
if (vals.count() >= 4) {
mlt_rect rect;
rect.x = vals.at(0).toInt();
rect.y = vals.at(1).toInt();
rect.w = vals.at(2).toInt();
rect.h = vals.at(3).toInt();
if (useOpacity) {
if (vals.count()) {
rect.o = locale.toDouble(vals.at(4));
} else {
rect.o = 1;
}
}
prop.anim_set("keyframe", rect, prev->first.frames(pCore->getCurrentFps()), next->first.frames(pCore->getCurrentFps()),
convertToMltType(prev->second.first));
}
vals = next->second.second.toString().split(QLatin1Char(' '));
if (vals.count() >= 4) {
mlt_rect rect;
rect.x = vals.at(0).toInt();
rect.y = vals.at(1).toInt();
rect.w = vals.at(2).toInt();
rect.h = vals.at(3).toInt();
if (useOpacity) {
if (vals.count() > 4) {
rect.o = locale.toDouble(vals.at(4));
} else {
rect.o = 1;
}
}
prop.anim_set("keyframe", rect, next->first.frames(pCore->getCurrentFps()), next->first.frames(pCore->getCurrentFps()),
convertToMltType(next->second.first));
}
mlt_rect rect = prop.anim_get_rect("keyframe", p);
QString res = QStringLiteral("%1 %2 %3 %4").arg((int)rect.x).arg((int)rect.y).arg((int)rect.w).arg((int)rect.h);
if (useOpacity) {
res.append(QStringLiteral(" %1").arg(locale.toString(rect.o)));
}
return QVariant(res);
} else if (m_paramType == ParamType::Roto_spline) {
// interpolate
QSize frame = pCore->getCurrentFrameSize();
QList p1 = RotoHelper::getPoints(prev->second.second, frame);
QList p2 = RotoHelper::getPoints(next->second.second, frame);
// relPos should be in [0,1]:
// - equal to 0 on prev keyframe
// - equal to 1 on next keyframe
qreal relPos = 0;
if (next->first != prev->first) {
relPos = (p - prev->first.frames(pCore->getCurrentFps())) / (qreal)(((next->first - prev->first).frames(pCore->getCurrentFps())));
}
int count = qMin(p1.count(), p2.count());
QList vlist;
for (int i = 0; i < count; ++i) {
BPoint bp;
QList pl;
for (int j = 0; j < 3; ++j) {
if (p1.at(i)[j] != p2.at(i)[j]) {
bp[j] = QLineF(p1.at(i)[j], p2.at(i)[j]).pointAt(relPos);
} else {
bp[j] = p1.at(i)[j];
}
pl << QVariant(QList() << QVariant(bp[j].x() / frame.width()) << QVariant(bp[j].y() / frame.height()));
}
vlist << QVariant(pl);
}
return vlist;
}
return QVariant();
}
void KeyframeModel::sendModification()
{
if (auto ptr = m_model.lock()) {
Q_ASSERT(m_index.isValid());
QString name = ptr->data(m_index, AssetParameterModel::NameRole).toString();
if (m_paramType == ParamType::KeyframeParam || m_paramType == ParamType::AnimatedRect || m_paramType == ParamType::Roto_spline) {
m_lastData = getAnimProperty();
ptr->setParameter(name, m_lastData, false);
} else {
Q_ASSERT(false); // Not implemented, TODO
}
}
}
void KeyframeModel::refresh()
{
Q_ASSERT(m_index.isValid());
QString animData;
if (auto ptr = m_model.lock()) {
animData = ptr->data(m_index, AssetParameterModel::ValueRole).toString();
} else {
qDebug() << "WARNING : unable to access keyframe's model";
return;
}
if (animData == m_lastData) {
// nothing to do
qDebug() << "// DATA WAS ALREADY PARSED, ABORTING REFRESH\n_________________";
return;
}
if (m_paramType == ParamType::KeyframeParam || m_paramType == ParamType::AnimatedRect) {
parseAnimProperty(animData);
} else if (m_paramType == ParamType::Roto_spline) {
parseRotoProperty(animData);
} else {
// first, try to convert to double
bool ok = false;
double value = animData.toDouble(&ok);
if (ok) {
Fun undo = []() { return true; };
Fun redo = []() { return true; };
addKeyframe(GenTime(), KeyframeType::Linear, QVariant(value), false, undo, redo);
} else {
Q_ASSERT(false); // Not implemented, TODO
}
}
m_lastData = animData;
}
void KeyframeModel::reset()
{
Q_ASSERT(m_index.isValid());
QString animData;
if (auto ptr = m_model.lock()) {
animData = ptr->data(m_index, AssetParameterModel::ValueRole).toString();
} else {
qDebug() << "WARNING : unable to access keyframe's model";
return;
}
if (animData == m_lastData) {
// nothing to do
qDebug() << "// DATA WAS ALREADY PARSED, ABORTING\n_________________";
return;
}
if (m_paramType == ParamType::KeyframeParam || m_paramType == ParamType::AnimatedRect) {
qDebug() << "parsing keyframe" << animData;
resetAnimProperty(animData);
} else if (m_paramType == ParamType::Roto_spline) {
// TODO: resetRotoProperty(animData);
} else {
// first, try to convert to double
bool ok = false;
double value = animData.toDouble(&ok);
if (ok) {
Fun undo = []() { return true; };
Fun redo = []() { return true; };
addKeyframe(GenTime(), KeyframeType::Linear, QVariant(value), false, undo, redo);
PUSH_UNDO(undo, redo, i18n("Reset effect"));
qDebug() << "KEYFRAME ADDED" << value;
} else {
Q_ASSERT(false); // Not implemented, TODO
}
}
m_lastData = animData;
}
QList KeyframeModel::getRanges(const QString &animData, const std::shared_ptr &model)
{
Mlt::Properties mlt_prop;
model->passProperties(mlt_prop);
QLocale locale;
mlt_prop.set("key", animData.toUtf8().constData());
// This is a fake query to force the animation to be parsed
(void)mlt_prop.anim_get_int("key", 0, 0);
Mlt::Animation anim = mlt_prop.get_animation("key");
int frame;
mlt_keyframe_type type;
anim.key_get(0, frame, type);
mlt_rect rect = mlt_prop.anim_get_rect("key", frame);
QPoint pX(rect.x, rect.x);
QPoint pY(rect.y, rect.y);
QPoint pW(rect.w, rect.w);
QPoint pH(rect.h, rect.h);
QPoint pO(rect.o, rect.o);
for (int i = 1; i < anim.key_count(); ++i) {
anim.key_get(i, frame, type);
if (!animData.contains(QLatin1Char('='))) {
// TODO: use a default user defined type
type = mlt_keyframe_linear;
}
rect = mlt_prop.anim_get_rect("key", frame);
pX.setX(qMin((int)rect.x, pX.x()));
pX.setY(qMax((int)rect.x, pX.y()));
pY.setX(qMin((int)rect.y, pY.x()));
pY.setY(qMax((int)rect.y, pY.y()));
pW.setX(qMin((int)rect.w, pW.x()));
pW.setY(qMax((int)rect.w, pW.y()));
pH.setX(qMin((int)rect.h, pH.x()));
pH.setY(qMax((int)rect.h, pH.y()));
pO.setX(qMin((int)rect.o, pO.x()));
pO.setY(qMax((int)rect.o, pO.y()));
// value = QVariant(QStringLiteral("%1 %2 %3 %4 %5").arg(rect.x).arg(rect.y).arg(rect.w).arg(rect.h).arg(locale.toString(rect.o)));
}
QList result{pX, pY, pW, pH, pO};
return result;
}
std::shared_ptr KeyframeModel::getAnimation(std::shared_ptr model, const QString &animData, int duration)
{
std::shared_ptr mlt_prop(new Mlt::Properties());
model->passProperties(*mlt_prop.get());
mlt_prop->set("key", animData.toUtf8().constData());
// This is a fake query to force the animation to be parsed
(void)mlt_prop->anim_get_rect("key", 0, duration);
return mlt_prop;
}
const QString KeyframeModel::getAnimationStringWithOffset(std::shared_ptr model, const QString &animData, int offset)
{
Mlt::Properties mlt_prop;
model->passProperties(mlt_prop);
mlt_prop.set("key", animData.toUtf8().constData());
// This is a fake query to force the animation to be parsed
(void)mlt_prop.anim_get_rect("key", 0);
Mlt::Animation anim = mlt_prop.get_animation("key");
if (offset > 0) {
for (int i = anim.key_count() - 1; i >= 0; --i) {
int pos = anim.key_get_frame(i) + offset;
anim.key_set_frame(i, pos);
}
} else {
for (int i = 0; i < anim.key_count(); ++i) {
int pos = anim.key_get_frame(i) + offset;
if (pos > 0) {
anim.key_set_frame(i, pos);
}
}
}
return qstrdup(anim.serialize_cut());
}
QList KeyframeModel::getKeyframePos() const
{
QList all_pos;
for (const auto &m : m_keyframeList) {
all_pos.push_back(m.first);
}
return all_pos;
}
bool KeyframeModel::removeNextKeyframes(GenTime pos, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
std::vector all_pos;
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
int firstPos = 0;
for (const auto &m : m_keyframeList) {
if (m.first <= pos) {
firstPos++;
continue;
}
all_pos.push_back(m.first);
}
int kfrCount = (int)all_pos.size();
// we trigger only one global remove/insertrow event
Fun update_redo_start = [this, firstPos, kfrCount]() {
beginRemoveRows(QModelIndex(), firstPos, kfrCount);
return true;
};
Fun update_redo_end = [this]() {
endRemoveRows();
return true;
};
Fun update_undo_start = [this, firstPos, kfrCount]() {
beginInsertRows(QModelIndex(), firstPos, kfrCount);
return true;
};
Fun update_undo_end = [this]() {
endInsertRows();
return true;
};
PUSH_LAMBDA(update_redo_start, local_redo);
PUSH_LAMBDA(update_undo_start, local_undo);
update_redo_start();
bool res = true;
for (const auto &p : all_pos) {
res = removeKeyframe(p, local_undo, local_redo, false);
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
update_redo_end();
PUSH_LAMBDA(update_redo_end, local_redo);
PUSH_LAMBDA(update_undo_end, local_undo);
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
diff --git a/src/core.cpp b/src/core.cpp
index 0f1f5d282..767ecd736 100644
--- a/src/core.cpp
+++ b/src/core.cpp
@@ -1,752 +1,754 @@
/*
Copyright (C) 2014 Till Theato
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 3 of the License, or
(at your option) any later version.
*/
#include "core.h"
#include "bin/bin.h"
#include "bin/projectitemmodel.h"
#include "capture/mediacapture.h"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
#include "jobs/jobmanager.h"
#include "kdenlive_debug.h"
#include "kdenlivesettings.h"
#include "library/librarywidget.h"
#include "mainwindow.h"
#include "mltconnection.h"
#include "mltcontroller/clipcontroller.h"
#include "monitor/monitormanager.h"
#include "profiles/profilemodel.hpp"
#include "profiles/profilerepository.hpp"
#include "project/projectmanager.h"
#include "timeline2/model/timelineitemmodel.hpp"
#include "timeline2/view/timelinecontroller.h"
#include "timeline2/view/timelinewidget.h"
#include
#include
#include
#include
#include
#include
#include
#ifdef Q_OS_MAC
#include
#endif
std::unique_ptr Core::m_self;
Core::Core()
: m_thumbProfile(nullptr)
, m_capture(new MediaCapture(this))
{
}
void Core::prepareShutdown()
{
m_guiConstructed = false;
QThreadPool::globalInstance()->clear();
}
Core::~Core()
{
qDebug() << "deleting core";
if (m_monitorManager) {
delete m_monitorManager;
}
// delete m_binWidget;
if (m_projectManager) {
delete m_projectManager;
}
ClipController::mediaUnavailable.reset();
}
void Core::build(bool isAppImage, const QString &MltPath)
{
if (m_self) {
return;
}
m_self.reset(new Core());
m_self->initLocale();
qRegisterMetaType("audioShortVector");
qRegisterMetaType>("QVector");
qRegisterMetaType("MessageType");
qRegisterMetaType("stringMap");
qRegisterMetaType("audioByteArray");
qRegisterMetaType>("QList");
qRegisterMetaType>("std::shared_ptr");
qRegisterMetaType>();
qRegisterMetaType("QDomElement");
qRegisterMetaType("requestClipInfo");
if (isAppImage) {
QString appPath = qApp->applicationDirPath();
KdenliveSettings::setFfmpegpath(QDir::cleanPath(appPath + QStringLiteral("/ffmpeg")));
KdenliveSettings::setFfplaypath(QDir::cleanPath(appPath + QStringLiteral("/ffplay")));
KdenliveSettings::setFfprobepath(QDir::cleanPath(appPath + QStringLiteral("/ffprobe")));
KdenliveSettings::setRendererpath(QDir::cleanPath(appPath + QStringLiteral("/melt")));
MltConnection::construct(QDir::cleanPath(appPath + QStringLiteral("/../share/mlt/profiles")));
} else {
// Open connection with Mlt
MltConnection::construct(MltPath);
}
// load the profile from disk
ProfileRepository::get()->refresh();
// load default profile
m_self->m_profile = KdenliveSettings::default_profile();
if (m_self->m_profile.isEmpty()) {
m_self->m_profile = ProjectManager::getDefaultProjectFormat();
KdenliveSettings::setDefault_profile(m_self->m_profile);
}
// Init producer shown for unavailable media
// TODO make it a more proper image, it currently causes a crash on exit
ClipController::mediaUnavailable = std::make_shared(ProfileRepository::get()->getProfile(m_self->m_profile)->profile(), "color:blue");
ClipController::mediaUnavailable->set("length", 99999999);
m_self->m_projectItemModel = ProjectItemModel::construct();
// Job manager must be created before bin to correctly connect
m_self->m_jobManager.reset(new JobManager(m_self.get()));
}
void Core::initGUI(const QUrl &Url)
{
m_guiConstructed = true;
m_profile = KdenliveSettings::default_profile();
m_currentProfile = m_profile;
profileChanged();
m_mainWindow = new MainWindow();
connect(this, &Core::showConfigDialog, m_mainWindow, &MainWindow::slotPreferences);
// load default profile and ask user to select one if not found.
if (m_profile.isEmpty()) {
m_profile = ProjectManager::getDefaultProjectFormat();
profileChanged();
KdenliveSettings::setDefault_profile(m_profile);
}
if (!ProfileRepository::get()->profileExists(m_profile)) {
KMessageBox::sorry(m_mainWindow, i18n("The default profile of Kdenlive is not set or invalid, press OK to set it to a correct value."));
// TODO this simple widget should be improved and probably use profileWidget
// we get the list of profiles
QVector> all_profiles = ProfileRepository::get()->getAllProfiles();
QStringList all_descriptions;
for (const auto &profile : all_profiles) {
all_descriptions << profile.first;
}
// ask the user
bool ok;
QString item = QInputDialog::getItem(m_mainWindow, i18n("Select Default Profile"), i18n("Profile:"), all_descriptions, 0, false, &ok);
if (ok) {
ok = false;
for (const auto &profile : all_profiles) {
if (profile.first == item) {
m_profile = profile.second;
ok = true;
}
}
}
if (!ok) {
KMessageBox::error(
m_mainWindow,
i18n("The given profile is invalid. We default to the profile \"dv_pal\", but you can change this from Kdenlive's settings panel"));
m_profile = QStringLiteral("dv_pal");
}
KdenliveSettings::setDefault_profile(m_profile);
profileChanged();
}
m_projectManager = new ProjectManager(this);
m_binWidget = new Bin(m_projectItemModel, m_mainWindow);
m_library = new LibraryWidget(m_projectManager, m_mainWindow);
connect(m_library, SIGNAL(addProjectClips(QList)), m_binWidget, SLOT(droppedUrls(QList)));
connect(this, &Core::updateLibraryPath, m_library, &LibraryWidget::slotUpdateLibraryPath);
m_monitorManager = new MonitorManager(this);
// Producer queue, creating MLT::Producers on request
/*
m_producerQueue = new ProducerQueue(m_binController);
connect(m_producerQueue, &ProducerQueue::gotFileProperties, m_binWidget, &Bin::slotProducerReady);
connect(m_producerQueue, &ProducerQueue::replyGetImage, m_binWidget, &Bin::slotThumbnailReady);
connect(m_producerQueue, &ProducerQueue::requestProxy,
[this](const QString &id){ m_binWidget->startJob(id, AbstractClipJob::PROXYJOB);});
connect(m_producerQueue, &ProducerQueue::removeInvalidClip, m_binWidget, &Bin::slotRemoveInvalidClip, Qt::DirectConnection);
connect(m_producerQueue, SIGNAL(addClip(QString, QMap)), m_binWidget, SLOT(slotAddUrl(QString, QMap)));
connect(m_binController.get(), SIGNAL(createThumb(QDomElement, QString, int)), m_producerQueue, SLOT(getFileProperties(QDomElement, QString, int)));
connect(m_binWidget, &Bin::producerReady, m_producerQueue, &ProducerQueue::slotProcessingDone, Qt::DirectConnection);
// TODO
connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/
m_mainWindow->init();
projectManager()->init(Url, QString());
if (qApp->isSessionRestored()) {
// NOTE: we are restoring only one window, because Kdenlive only uses one MainWindow
m_mainWindow->restore(1, false);
}
QMetaObject::invokeMethod(pCore->projectManager(), "slotLoadOnOpen", Qt::QueuedConnection);
m_mainWindow->show();
QThreadPool::globalInstance()->setMaxThreadCount(qMin(4, QThreadPool::globalInstance()->maxThreadCount()));
}
std::unique_ptr &Core::self()
{
if (!m_self) {
qDebug() << "Error : Core has not been created";
}
return m_self;
}
MainWindow *Core::window()
{
return m_mainWindow;
}
ProjectManager *Core::projectManager()
{
return m_projectManager;
}
MonitorManager *Core::monitorManager()
{
return m_monitorManager;
}
Monitor *Core::getMonitor(int id)
{
if (id == Kdenlive::ClipMonitor) {
return m_monitorManager->clipMonitor();
}
return m_monitorManager->projectMonitor();
}
Bin *Core::bin()
{
return m_binWidget;
}
void Core::selectBinClip(const QString &clipId, int frame, const QPoint &zone)
{
m_binWidget->selectClipById(clipId, frame, zone);
}
std::shared_ptr Core::jobManager()
{
return m_jobManager;
}
LibraryWidget *Core::library()
{
return m_library;
}
void Core::initLocale()
{
QLocale systemLocale = QLocale();
#ifndef Q_OS_MAC
setlocale(LC_NUMERIC, nullptr);
#else
setlocale(LC_NUMERIC_MASK, nullptr);
#endif
// localeconv()->decimal_point does not give reliable results on Windows
#ifndef Q_OS_WIN
char *separator = localeconv()->decimal_point;
if (QString::fromUtf8(separator) != QChar(systemLocale.decimalPoint())) {
// qCDebug(KDENLIVE_LOG)<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------";
// HACK: There is a locale conflict, so set locale to C
// Make sure to override exported values or it won't work
qputenv("LANG", "C");
#ifndef Q_OS_MAC
setlocale(LC_NUMERIC, "C");
#else
setlocale(LC_NUMERIC_MASK, "C");
#endif
systemLocale = QLocale::c();
}
#endif
systemLocale.setNumberOptions(QLocale::OmitGroupSeparator);
QLocale::setDefault(systemLocale);
}
std::unique_ptr &Core::getMltRepository()
{
return MltConnection::self()->getMltRepository();
}
std::unique_ptr &Core::getCurrentProfile() const
{
return ProfileRepository::get()->getProfile(m_currentProfile);
}
const QString &Core::getCurrentProfilePath() const
{
return m_currentProfile;
}
bool Core::setCurrentProfile(const QString &profilePath)
{
if (m_currentProfile == profilePath) {
// no change required
return true;
}
if (ProfileRepository::get()->profileExists(profilePath)) {
m_currentProfile = profilePath;
m_thumbProfile.reset();
// inform render widget
profileChanged();
m_mainWindow->updateRenderWidgetProfile();
if (m_guiConstructed && m_mainWindow->getCurrentTimeline()->controller()->getModel()) {
m_mainWindow->getCurrentTimeline()->controller()->getModel()->updateProfile(&getCurrentProfile()->profile());
checkProfileValidity();
}
return true;
}
return false;
}
void Core::checkProfileValidity()
{
int offset = (getCurrentProfile()->profile().width() % 8) + (getCurrentProfile()->profile().height() % 2);
if (offset > 0) {
// Profile is broken, warn user
if (m_binWidget) {
m_binWidget->displayBinMessage(i18n("Your project profile is invalid, rendering might fail."), KMessageWidget::Warning);
}
}
}
double Core::getCurrentSar() const
{
return getCurrentProfile()->sar();
}
double Core::getCurrentDar() const
{
return getCurrentProfile()->dar();
}
double Core::getCurrentFps() const
{
return getCurrentProfile()->fps();
}
QSize Core::getCurrentFrameDisplaySize() const
{
return {(int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()};
}
QSize Core::getCurrentFrameSize() const
{
return {getCurrentProfile()->width(), getCurrentProfile()->height()};
}
void Core::requestMonitorRefresh()
{
if (!m_guiConstructed) return;
m_monitorManager->refreshProjectMonitor();
}
void Core::refreshProjectRange(QSize range)
{
if (!m_guiConstructed) return;
m_monitorManager->refreshProjectRange(range);
}
int Core::getItemPosition(const ObjectId &id)
{
if (!m_guiConstructed) return 0;
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPosition(id.second);
}
break;
case ObjectType::TimelineComposition:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPosition(id.second);
}
break;
case ObjectType::BinClip:
return 0;
break;
default:
qDebug() << "ERROR: unhandled object type";
}
return 0;
}
int Core::getItemIn(const ObjectId &id)
{
if (!m_guiConstructed || !m_mainWindow->getCurrentTimeline() || !m_mainWindow->getCurrentTimeline()->controller()->getModel()) {
qDebug() << "/ / // QUERYING ITEM IN BUT GUI NOT BUILD!!";
return 0;
}
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipIn(id.second);
+ } else {
+ qDebug()<<"// ERROR QUERYING NON CLIP PROPERTIES\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!";
}
break;
case ObjectType::TimelineComposition:
return 0;
break;
case ObjectType::BinClip:
return 0;
break;
default:
qDebug() << "ERROR: unhandled object type";
}
return 0;
}
PlaylistState::ClipState Core::getItemState(const ObjectId &id)
{
if (!m_guiConstructed) return PlaylistState::Disabled;
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipState(id.second);
}
break;
case ObjectType::TimelineComposition:
return PlaylistState::VideoOnly;
break;
case ObjectType::BinClip:
return m_binWidget->getClipState(id.second);
break;
case ObjectType::TimelineTrack:
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->isAudioTrack(id.second) ? PlaylistState::AudioOnly : PlaylistState::VideoOnly;
default:
qDebug() << "ERROR: unhandled object type";
break;
}
return PlaylistState::Disabled;
}
int Core::getItemDuration(const ObjectId &id)
{
if (!m_guiConstructed) return 0;
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPlaytime(id.second);
}
break;
case ObjectType::TimelineComposition:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPlaytime(id.second);
}
break;
case ObjectType::BinClip:
return (int)m_binWidget->getClipDuration(id.second);
break;
default:
qDebug() << "ERROR: unhandled object type";
}
return 0;
}
int Core::getItemTrack(const ObjectId &id)
{
if (!m_guiConstructed) return 0;
switch (id.first) {
case ObjectType::TimelineClip:
case ObjectType::TimelineComposition:
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getItemTrackId(id.second);
break;
default:
qDebug() << "ERROR: unhandled object type";
}
return 0;
}
void Core::refreshProjectItem(const ObjectId &id)
{
if (!m_guiConstructed || m_mainWindow->getCurrentTimeline()->loading) return;
switch (id.first) {
case ObjectType::TimelineClip:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) {
m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second);
}
break;
case ObjectType::TimelineComposition:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) {
m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second);
}
break;
case ObjectType::TimelineTrack:
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) {
requestMonitorRefresh();
}
break;
case ObjectType::BinClip:
m_monitorManager->refreshClipMonitor();
break;
default:
qDebug() << "ERROR: unhandled object type";
}
}
bool Core::hasTimelinePreview() const
{
if (!m_guiConstructed) {
return false;
}
return m_mainWindow->getCurrentTimeline()->controller()->renderedChunks().size() > 0;
}
KdenliveDoc *Core::currentDoc()
{
return m_projectManager->current();
}
int Core::projectDuration() const
{
if (!m_guiConstructed) {
return 0;
}
return m_mainWindow->getCurrentTimeline()->controller()->duration();
}
void Core::profileChanged()
{
GenTime::setFps(getCurrentFps());
}
void Core::pushUndo(const Fun &undo, const Fun &redo, const QString &text)
{
undoStack()->push(new FunctionalUndoCommand(undo, redo, text));
}
void Core::pushUndo(QUndoCommand *command)
{
undoStack()->push(command);
}
void Core::displayMessage(const QString &message, MessageType type, int timeout)
{
if (m_mainWindow) {
m_mainWindow->displayMessage(message, type, timeout);
} else {
qDebug() << message;
}
}
void Core::displayBinMessage(const QString &text, int type, const QList &actions)
{
m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, actions);
}
void Core::displayBinLogMessage(const QString &text, int type, const QString &logInfo)
{
m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, logInfo);
}
void Core::clearAssetPanel(int itemId)
{
if (m_guiConstructed) m_mainWindow->clearAssetPanel(itemId);
}
std::shared_ptr Core::getItemEffectStack(int itemType, int itemId)
{
if (!m_guiConstructed) return nullptr;
switch (itemType) {
case (int)ObjectType::TimelineClip:
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId);
case (int)ObjectType::TimelineTrack:
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getTrackEffectStackModel(itemId);
break;
case (int)ObjectType::BinClip:
return m_binWidget->getClipEffectStack(itemId);
default:
return nullptr;
}
}
std::shared_ptr Core::undoStack()
{
return projectManager()->undoStack();
}
QMap Core::getVideoTrackNames()
{
if (!m_guiConstructed) return QMap();
return m_mainWindow->getCurrentTimeline()->controller()->getTrackNames(true);
}
QPair Core::getCompositionATrack(int cid) const
{
if (!m_guiConstructed) return {};
return m_mainWindow->getCurrentTimeline()->controller()->getCompositionATrack(cid);
}
bool Core::compositionAutoTrack(int cid) const
{
return m_mainWindow->getCurrentTimeline()->controller()->compositionAutoTrack(cid);
}
void Core::setCompositionATrack(int cid, int aTrack)
{
if (!m_guiConstructed) return;
m_mainWindow->getCurrentTimeline()->controller()->setCompositionATrack(cid, aTrack);
}
std::shared_ptr Core::projectItemModel()
{
return m_projectItemModel;
}
void Core::invalidateRange(QSize range)
{
if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return;
m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(range.width(), range.height());
}
void Core::invalidateItem(ObjectId itemId)
{
if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return;
switch (itemId.first) {
case ObjectType::TimelineClip:
case ObjectType::TimelineComposition:
m_mainWindow->getCurrentTimeline()->controller()->invalidateItem(itemId.second);
break;
case ObjectType::TimelineTrack:
// TODO: invalidate all clips in track
break;
default:
// bin clip should automatically be reloaded, compositions should not have effects
break;
}
}
double Core::getClipSpeed(int id) const
{
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipSpeed(id);
}
void Core::updateItemKeyframes(ObjectId id)
{
if (id.first == ObjectType::TimelineClip && m_mainWindow) {
m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {TimelineModel::KeyframesRole});
}
}
void Core::updateItemModel(ObjectId id, const QString &service)
{
if (m_mainWindow && !m_mainWindow->getCurrentTimeline()->loading && service.startsWith(QLatin1String("fade")) && id.first == ObjectType::TimelineClip) {
bool startFade = service == QLatin1String("fadein") || service == QLatin1String("fade_from_black");
m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {startFade ? TimelineModel::FadeInRole : TimelineModel::FadeOutRole});
}
}
void Core::showClipKeyframes(ObjectId id, bool enable)
{
if (id.first == ObjectType::TimelineClip) {
m_mainWindow->getCurrentTimeline()->controller()->showClipKeyframes(id.second, enable);
} else if (id.first == ObjectType::TimelineComposition) {
m_mainWindow->getCurrentTimeline()->controller()->showCompositionKeyframes(id.second, enable);
}
}
Mlt::Profile *Core::thumbProfile()
{
QMutexLocker lck(&m_thumbProfileMutex);
if (!m_thumbProfile) {
m_thumbProfile = std::make_unique(m_currentProfile.toStdString().c_str());
m_thumbProfile->set_height(200);
int width = 200 * m_thumbProfile->dar();
if (width % 8 > 0) {
width += 8 - width % 8;
}
m_thumbProfile->set_width(width);
}
return m_thumbProfile.get();
}
int Core::getTimelinePosition() const
{
if (m_mainWindow && m_guiConstructed) {
return m_mainWindow->getCurrentTimeline()->controller()->timelinePosition();
}
return 0;
}
void Core::triggerAction(const QString &name)
{
QAction *action = m_mainWindow->actionCollection()->action(name);
if (action) {
action->trigger();
}
}
void Core::clean()
{
m_self.reset();
}
void Core::startMediaCapture(bool checkAudio, bool checkVideo)
{
if (checkAudio && checkVideo) {
m_capture->recordVideo(true);
} else if (checkAudio) {
m_capture->recordAudio(true);
}
m_mediaCaptureFile = m_capture->getCaptureOutputLocation();
}
void Core::stopMediaCapture(bool checkAudio, bool checkVideo)
{
if (checkAudio && checkVideo) {
m_capture->recordVideo(false);
} else if (checkAudio) {
m_capture->recordAudio(false);
}
}
QStringList Core::getAudioCaptureDevices()
{
return m_capture->getAudioCaptureDevices();
}
int Core::getMediaCaptureState()
{
return m_capture->getState();
}
bool Core::isMediaCapturing()
{
return m_capture->isRecording();
}
MediaCapture *Core::getAudioDevice()
{
return m_capture.get();
}
QString Core::getProjectFolderName()
{
return m_monitorManager->getProjectFolder();
}
QString Core::getTimelineClipBinId(int cid)
{
if (!m_guiConstructed) {
return QString();
}
if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(cid)) {
return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipBinId(cid);
}
return QString();
}
diff --git a/src/timeline2/model/clipmodel.cpp b/src/timeline2/model/clipmodel.cpp
index d1d1af162..c20fe8cb8 100644
--- a/src/timeline2/model/clipmodel.cpp
+++ b/src/timeline2/model/clipmodel.cpp
@@ -1,802 +1,802 @@
/***************************************************************************
* 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 "clipmodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "clipsnapmodel.hpp"
#include "core.h"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "logger.hpp"
#include "macros.hpp"
#include "timelinemodel.hpp"
#include "trackmodel.hpp"
#include
#include
#include
#include
ClipModel::ClipModel(const std::shared_ptr &parent, std::shared_ptr prod, const QString &binClipId, int id,
PlaylistState::ClipState state, double speed)
: MoveableItem(parent, id)
, m_producer(std::move(prod))
, m_effectStack(EffectStackModel::construct(m_producer, {ObjectType::TimelineClip, m_id}, parent->m_undoStack))
, m_clipMarkerModel(new ClipSnapModel())
, m_binClipId(binClipId)
, forceThumbReload(false)
, m_currentState(state)
, m_speed(speed)
, m_fakeTrack(-1)
, m_positionOffset(0)
{
m_producer->set("kdenlive:id", binClipId.toUtf8().constData());
m_producer->set("_kdenlive_cid", m_id);
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
m_canBeVideo = binClip->hasVideo();
m_canBeAudio = binClip->hasAudio();
m_clipType = binClip->clipType();
if (binClip) {
m_endlessResize = !binClip->hasLimitedDuration();
} else {
m_endlessResize = false;
}
QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector roles) {
qDebug() << "// GOT CLIP STACK DATA CHANGE: " << roles;
if (m_currentTrackId != -1) {
if (auto ptr = m_parent.lock()) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
qDebug() << "// GOT CLIP STACK DATA CHANGE DONE: " << ix << " = " << roles;
ptr->dataChanged(ix, ix, roles);
}
}
});
}
int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState::ClipState state, double speed)
{
id = (id == -1 ? TimelineModel::getNextId() : id);
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId);
// We refine the state according to what the clip can actually produce
std::pair videoAudio = stateToBool(state);
videoAudio.first = videoAudio.first && binClip->hasVideo();
videoAudio.second = videoAudio.second && binClip->hasAudio();
state = stateFromBool(videoAudio);
std::shared_ptr cutProducer = binClip->getTimelineProducer(-1, id, state, speed);
std::shared_ptr clip(new ClipModel(parent, cutProducer, binClipId, id, state, speed));
TRACE_CONSTR(clip.get(), parent, binClipId, id, state, speed);
clip->setClipState_lambda(state)();
parent->registerClip(clip);
clip->m_clipMarkerModel->setReferenceModel(binClip->getMarkerModel(), speed);
return id;
}
int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, const std::shared_ptr &producer,
PlaylistState::ClipState state)
{
// we hand the producer to the bin clip, and in return we get a cut to a good master producer
// We might not be able to use directly the producer that we receive as an argument, because it cannot share the same master producer with any other
// clipModel (due to a mlt limitation, see ProjectClip doc)
int id = TimelineModel::getNextId();
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId);
// We refine the state according to what the clip can actually produce
std::pair videoAudio = stateToBool(state);
videoAudio.first = videoAudio.first && binClip->hasVideo();
videoAudio.second = videoAudio.second && binClip->hasAudio();
state = stateFromBool(videoAudio);
double speed = 1.0;
if (QString::fromUtf8(producer->parent().get("mlt_service")) == QLatin1String("timewarp")) {
speed = producer->parent().get_double("warp_speed");
}
auto result = binClip->giveMasterAndGetTimelineProducer(id, producer, state);
std::shared_ptr clip(new ClipModel(parent, result.first, binClipId, id, state, speed));
- clip->setClipState_lambda(state)();
- clip->m_effectStack->importEffects(producer, state, result.second);
+ clip->setClipState_lambda(state);
parent->registerClip(clip);
+ clip->m_effectStack->importEffects(producer, state, result.second);
clip->m_clipMarkerModel->setReferenceModel(binClip->getMarkerModel(), speed);
return id;
}
void ClipModel::registerClipToBin(std::shared_ptr service, bool registerProducer)
{
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
if (!binClip) {
qDebug() << "Error : Bin clip for id: " << m_binClipId << " NOT AVAILABLE!!!";
}
qDebug() << "REGISTRATION " << m_id << "ptr count" << m_parent.use_count();
binClip->registerService(m_parent, m_id, std::move(service), registerProducer);
}
void ClipModel::deregisterClipToBin()
{
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
binClip->deregisterTimelineClip(m_id);
}
ClipModel::~ClipModel() = default;
bool ClipModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo)
{
QWriteLocker locker(&m_lock);
// qDebug() << "RESIZE CLIP" << m_id << "target size=" << size << "right=" << right << "endless=" << m_endlessResize << "length" <<
// m_producer->get_length();
if (!m_endlessResize && (size <= 0 || size > m_producer->get_length())) {
return false;
}
int delta = getPlaytime() - size;
if (delta == 0) {
return true;
}
int in = m_producer->get_in();
int out = m_producer->get_out();
int old_in = in, old_out = out;
// check if there is enough space on the chosen side
if (!m_endlessResize) {
if (!right && in + delta < 0) {
return false;
}
if (right && (out - delta >= m_producer->get_length())) {
return false;
}
}
if (right) {
out -= delta;
} else {
in += delta;
}
// qDebug() << "Resize facts delta =" << delta << "old in" << old_in << "old_out" << old_out << "in" << in << "out" << out;
std::function track_operation = []() { return true; };
std::function track_reverse = []() { return true; };
int outPoint = out;
int inPoint = in;
int offset = 0;
if (m_endlessResize) {
offset = inPoint;
outPoint = out - in;
inPoint = 0;
}
if (m_currentTrackId != -1) {
if (auto ptr = m_parent.lock()) {
if (ptr->getTrackById(m_currentTrackId)->isLocked()) {
return false;
}
track_operation = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, inPoint, outPoint, right);
} else {
qDebug() << "Error : Moving clip failed because parent timeline is not available anymore";
Q_ASSERT(false);
}
} else {
// Ensure producer is long enough
if (m_endlessResize && outPoint > m_producer->parent().get_length()) {
m_producer->set("length", outPoint + 1);
}
}
QVector roles{TimelineModel::DurationRole};
if (!right) {
roles.push_back(TimelineModel::StartRole);
roles.push_back(TimelineModel::InPointRole);
} else {
roles.push_back(TimelineModel::OutPointRole);
}
Fun operation = [this, inPoint, outPoint, roles, track_operation]() {
if (track_operation()) {
setInOut(inPoint, outPoint);
if (m_currentTrackId > -1) {
if (auto ptr = m_parent.lock()) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
ptr->notifyChange(ix, ix, roles);
}
}
return true;
}
return false;
};
if (operation()) {
Fun reverse = []() { return true; };
if (logUndo) {
// 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
if (m_currentTrackId != -1) {
if (auto ptr = m_parent.lock()) {
track_reverse = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, old_in, old_out, right);
}
}
reverse = [this, old_in, old_out, track_reverse, roles]() {
if (track_reverse()) {
setInOut(old_in, old_out);
if (m_currentTrackId > -1) {
if (auto ptr = m_parent.lock()) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
ptr->notifyChange(ix, ix, roles);
}
}
return true;
}
return false;
};
qDebug() << "----------\n-----------\n// ADJUSTING EFFECT LENGTH, LOGUNDO " << logUndo << ", " << old_in << "/" << inPoint << ", "
<< m_producer->get_playtime();
adjustEffectLength(right, old_in, inPoint, old_out - old_in, m_producer->get_playtime(), offset, reverse, operation, logUndo);
}
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
return false;
}
const QString ClipModel::getProperty(const QString &name) const
{
READ_LOCK();
if (service()->parent().is_valid()) {
return QString::fromUtf8(service()->parent().get(name.toUtf8().constData()));
}
return QString::fromUtf8(service()->get(name.toUtf8().constData()));
}
int ClipModel::getIntProperty(const QString &name) const
{
READ_LOCK();
if (service()->parent().is_valid()) {
return service()->parent().get_int(name.toUtf8().constData());
}
return service()->get_int(name.toUtf8().constData());
}
QSize ClipModel::getFrameSize() const
{
READ_LOCK();
if (service()->parent().is_valid()) {
return QSize(service()->parent().get_int("meta.media.width"), service()->parent().get_int("meta.media.height"));
}
return {service()->get_int("meta.media.width"), service()->get_int("meta.media.height")};
}
double ClipModel::getDoubleProperty(const QString &name) const
{
READ_LOCK();
if (service()->parent().is_valid()) {
return service()->parent().get_double(name.toUtf8().constData());
}
return service()->get_double(name.toUtf8().constData());
}
Mlt::Producer *ClipModel::service() const
{
READ_LOCK();
return m_producer.get();
}
std::shared_ptr ClipModel::getProducer()
{
READ_LOCK();
return m_producer;
}
int ClipModel::getPlaytime() const
{
READ_LOCK();
return m_producer->get_playtime();
}
void ClipModel::setTimelineEffectsEnabled(bool enabled)
{
QWriteLocker locker(&m_lock);
m_effectStack->setEffectStackEnabled(enabled);
}
bool ClipModel::addEffect(const QString &effectId)
{
QWriteLocker locker(&m_lock);
if (EffectsRepository::get()->getType(effectId) == EffectType::Audio) {
if (m_currentState == PlaylistState::VideoOnly) {
return false;
}
} else if (m_currentState == PlaylistState::AudioOnly) {
return false;
}
m_effectStack->appendEffect(effectId);
return true;
}
bool ClipModel::copyEffect(const std::shared_ptr &stackModel, int rowId)
{
QWriteLocker locker(&m_lock);
QDomDocument doc;
m_effectStack->copyXmlEffect(stackModel->rowToXml(rowId, doc));
return true;
}
bool ClipModel::importEffects(std::shared_ptr stackModel)
{
QWriteLocker locker(&m_lock);
m_effectStack->importEffects(std::move(stackModel), m_currentState);
return true;
}
bool ClipModel::importEffects(std::weak_ptr service)
{
QWriteLocker locker(&m_lock);
m_effectStack->importEffects(std::move(service), m_currentState);
return true;
}
bool ClipModel::removeFade(bool fromStart)
{
QWriteLocker locker(&m_lock);
m_effectStack->removeFade(fromStart);
return true;
}
bool ClipModel::adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, int offset, Fun &undo, Fun &redo, bool logUndo)
{
QWriteLocker locker(&m_lock);
return m_effectStack->adjustStackLength(adjustFromEnd, oldIn, oldDuration, newIn, duration, offset, undo, redo, logUndo);
}
bool ClipModel::adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
qDebug() << ".... ADJUSTING FADE LENGTH: " << duration << " / " << effectName;
Fun operation = [this, duration, effectName, originalDuration]() {
return m_effectStack->adjustFadeLength(duration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(),
!isAudioOnly(), originalDuration > 0);
};
if (operation() && originalDuration > 0) {
Fun reverse = [this, originalDuration, effectName]() {
return m_effectStack->adjustFadeLength(originalDuration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"),
audioEnabled(), !isAudioOnly(), true);
};
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
}
return true;
}
bool ClipModel::audioEnabled() const
{
READ_LOCK();
return stateToBool(m_currentState).second;
}
bool ClipModel::isAudioOnly() const
{
READ_LOCK();
return m_currentState == PlaylistState::AudioOnly;
}
void ClipModel::refreshProducerFromBin(PlaylistState::ClipState state, double speed)
{
// We require that the producer is not in the track when we refresh the producer, because otherwise the modification will not be propagated. Remove the clip
// first, refresh, and then replant.
QWriteLocker locker(&m_lock);
int in = getIn();
int out = getOut();
if (!qFuzzyCompare(speed, m_speed) && !qFuzzyCompare(speed, 0.)) {
in = in * std::abs(m_speed / speed);
out = in + getPlaytime() - 1;
// prevent going out of the clip's range
out = std::min(out, int(double(m_producer->get_length()) * std::abs(m_speed / speed)) - 1);
m_speed = speed;
qDebug() << "changing speed" << in << out << m_speed;
}
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
std::shared_ptr binProducer = binClip->getTimelineProducer(m_currentTrackId, m_id, state, m_speed);
m_producer = std::move(binProducer);
m_producer->set_in_and_out(in, out);
// replant effect stack in updated service
m_effectStack->resetService(m_producer);
m_producer->set("kdenlive:id", binClip->clipId().toUtf8().constData());
m_producer->set("_kdenlive_cid", m_id);
m_endlessResize = !binClip->hasLimitedDuration();
}
void ClipModel::refreshProducerFromBin()
{
refreshProducerFromBin(m_currentState);
}
bool ClipModel::useTimewarpProducer(double speed, bool changeDuration, Fun &undo, Fun &redo)
{
if (m_endlessResize) {
// no timewarp for endless producers
return false;
}
if (qFuzzyCompare(speed, m_speed)) {
// nothing to do
return true;
}
std::function local_undo = []() { return true; };
std::function local_redo = []() { return true; };
double previousSpeed = getSpeed();
int oldDuration = getPlaytime();
int newDuration = int(double(oldDuration) * std::abs(previousSpeed / speed) + 0.5);
int oldOut = getOut();
int oldIn = getIn();
auto operation = useTimewarpProducer_lambda(speed);
auto reverse = useTimewarpProducer_lambda(previousSpeed);
if (changeDuration && oldOut >= newDuration) {
// in that case, we are going to shrink the clip when changing the producer. We must undo that when reloading the old producer
reverse = [reverse, oldIn, oldOut, this]() {
bool res = reverse();
if (res) {
setInOut(oldIn, oldOut);
}
return res;
};
}
if (operation()) {
UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
// When calculating duration, result can be a few frames longer than possible duration so adjust
if (changeDuration) {
bool res = requestResize(qMin(newDuration, getMaxDuration()), true, local_undo, local_redo, true);
if (!res) {
local_undo();
return false;
}
}
adjustEffectLength(false, oldIn, getIn(), oldOut - oldIn, m_producer->get_playtime(), 0, local_undo, local_redo, true);
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
qDebug() << "tw: operation fail";
return false;
}
Fun ClipModel::useTimewarpProducer_lambda(double speed)
{
QWriteLocker locker(&m_lock);
return [speed, this]() {
qDebug() << "timeWarp producer" << speed;
refreshProducerFromBin(m_currentState, speed);
if (auto ptr = m_parent.lock()) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
ptr->notifyChange(ix, ix, TimelineModel::SpeedRole);
}
return true;
};
}
QVariant ClipModel::getAudioWaveform()
{
READ_LOCK();
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
if (binClip) {
return QVariant::fromValue(binClip->audioFrameCache);
}
return QVariant();
}
const QString &ClipModel::binId() const
{
return m_binClipId;
}
std::shared_ptr ClipModel::getMarkerModel() const
{
READ_LOCK();
return pCore->projectItemModel()->getClipByBinID(m_binClipId)->getMarkerModel();
}
int ClipModel::audioChannels() const
{
READ_LOCK();
return pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioChannels();
}
int ClipModel::fadeIn() const
{
return m_effectStack->getFadePosition(true);
}
int ClipModel::fadeOut() const
{
return m_effectStack->getFadePosition(false);
}
double ClipModel::getSpeed() const
{
return m_speed;
}
KeyframeModel *ClipModel::getKeyframeModel()
{
return m_effectStack->getEffectKeyframeModel();
}
bool ClipModel::showKeyframes() const
{
READ_LOCK();
return !service()->get_int("kdenlive:hide_keyframes");
}
void ClipModel::setShowKeyframes(bool show)
{
QWriteLocker locker(&m_lock);
service()->set("kdenlive:hide_keyframes", (int)!show);
}
void ClipModel::setPosition(int pos)
{
MoveableItem::setPosition(pos);
m_clipMarkerModel->updateSnapModelPos(pos);
}
void ClipModel::setInOut(int in, int out)
{
MoveableItem::setInOut(in, out);
m_clipMarkerModel->updateSnapModelInOut(std::pair(in, out));
}
void ClipModel::setCurrentTrackId(int tid, bool finalMove)
{
if (tid == m_currentTrackId) {
return;
}
bool registerSnap = m_currentTrackId == -1 && tid > -1;
if (m_currentTrackId > -1 && tid == -1) {
// Removing clip
m_clipMarkerModel->deregisterSnapModel();
}
MoveableItem::setCurrentTrackId(tid, finalMove);
if (registerSnap) {
if (auto ptr = m_parent.lock()) {
m_clipMarkerModel->registerSnapModel(ptr->m_snaps, getPosition(), getIn(), getOut(), m_speed);
}
}
if (finalMove && tid != -1 && m_lastTrackId != m_currentTrackId) {
refreshProducerFromBin(m_currentState);
m_lastTrackId = m_currentTrackId;
}
}
Fun ClipModel::setClipState_lambda(PlaylistState::ClipState state)
{
QWriteLocker locker(&m_lock);
return [this, state]() {
if (auto ptr = m_parent.lock()) {
m_currentState = state;
// Enforce producer reload
m_lastTrackId = -1;
if (m_currentTrackId != -1 && ptr->isClip(m_id)) { // if this is false, the clip is being created. Don't update model in that case
refreshProducerFromBin(m_currentState);
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
ptr->dataChanged(ix, ix, {TimelineModel::StatusRole});
}
return true;
}
return false;
};
}
bool ClipModel::setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo)
{
if (state == PlaylistState::VideoOnly && !canBeVideo()) {
return false;
}
if (state == PlaylistState::AudioOnly && !canBeAudio()) {
return false;
}
if (state == m_currentState) {
return true;
}
auto old_state = m_currentState;
auto operation = setClipState_lambda(state);
if (operation()) {
auto reverse = setClipState_lambda(old_state);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
return false;
}
PlaylistState::ClipState ClipModel::clipState() const
{
READ_LOCK();
return m_currentState;
}
ClipType::ProducerType ClipModel::clipType() const
{
READ_LOCK();
return m_clipType;
}
void ClipModel::passTimelineProperties(const std::shared_ptr &other)
{
READ_LOCK();
Mlt::Properties source(m_producer->get_properties());
Mlt::Properties dest(other->service()->get_properties());
dest.pass_list(source, "kdenlive:hide_keyframes,kdenlive:activeeffect");
}
bool ClipModel::canBeVideo() const
{
return m_canBeVideo;
}
bool ClipModel::canBeAudio() const
{
return m_canBeAudio;
}
const QString ClipModel::effectNames() const
{
READ_LOCK();
return m_effectStack->effectNames();
}
int ClipModel::getFakeTrackId() const
{
return m_fakeTrack;
}
void ClipModel::setFakeTrackId(int fid)
{
m_fakeTrack = fid;
}
int ClipModel::getFakePosition() const
{
return m_fakePosition;
}
void ClipModel::setFakePosition(int fid)
{
m_fakePosition = fid;
}
QDomElement ClipModel::toXml(QDomDocument &document)
{
QDomElement container = document.createElement(QStringLiteral("clip"));
container.setAttribute(QStringLiteral("binid"), m_binClipId);
container.setAttribute(QStringLiteral("id"), m_id);
container.setAttribute(QStringLiteral("in"), getIn());
container.setAttribute(QStringLiteral("out"), getOut());
container.setAttribute(QStringLiteral("position"), getPosition());
container.setAttribute(QStringLiteral("state"), (int)m_currentState);
if (auto ptr = m_parent.lock()) {
int trackId = ptr->getTrackPosition(m_currentTrackId);
container.setAttribute(QStringLiteral("track"), trackId);
if (ptr->isAudioTrack(getCurrentTrackId())) {
container.setAttribute(QStringLiteral("audioTrack"), 1);
int partner = ptr->getClipSplitPartner(m_id);
if (partner != -1) {
int mirrorId = ptr->getMirrorVideoTrackId(m_currentTrackId);
if (mirrorId > -1) {
mirrorId = ptr->getTrackPosition(mirrorId);
}
container.setAttribute(QStringLiteral("mirrorTrack"), mirrorId);
} else {
container.setAttribute(QStringLiteral("mirrorTrack"), QStringLiteral("-1"));
}
}
}
container.setAttribute(QStringLiteral("speed"), m_speed);
container.appendChild(m_effectStack->toXml(document));
return container;
}
bool ClipModel::checkConsistency()
{
if (!m_effectStack->checkConsistency()) {
qDebug() << "Consistency check failed for effecstack";
return false;
}
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
auto instances = binClip->timelineInstances();
bool found = false;
for (const auto &i : instances) {
if (i == m_id) {
found = true;
break;
}
}
if (!found) {
qDebug() << "ERROR: binClip doesn't acknowledge timeline clip existence";
return false;
}
if (m_currentState == PlaylistState::VideoOnly && !m_canBeVideo) {
qDebug() << "ERROR: clip is in video state but doesn't have video";
return false;
}
if (m_currentState == PlaylistState::AudioOnly && !m_canBeAudio) {
qDebug() << "ERROR: clip is in video state but doesn't have video";
return false;
}
// TODO: check speed
return true;
}
int ClipModel::getSubPlaylistIndex() const
{
return m_subPlaylistIndex;
}
void ClipModel::setSubPlaylistIndex(int index)
{
m_subPlaylistIndex = index;
}
void ClipModel::setOffset(int offset)
{
m_positionOffset = offset;
if (auto ptr = m_parent.lock()) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
ptr->dataChanged(ix, ix, {TimelineModel::PositionOffsetRole});
}
}
void ClipModel::setGrab(bool grab)
{
QWriteLocker locker(&m_lock);
if (grab == m_grabbed) {
return;
}
m_grabbed = grab;
if (auto ptr = m_parent.lock()) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
ptr->dataChanged(ix, ix, {TimelineModel::GrabbedRole});
}
}
void ClipModel::setSelected(bool sel)
{
QWriteLocker locker(&m_lock);
if (sel == selected) {
return;
}
selected = sel;
if (auto ptr = m_parent.lock()) {
if (m_currentTrackId != -1) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
ptr->dataChanged(ix, ix, {TimelineModel::SelectedRole});
}
}
}
void ClipModel::clearOffset()
{
if (m_positionOffset != 0) {
setOffset(0);
}
}
int ClipModel::getOffset() const
{
return m_positionOffset;
}
int ClipModel::getMaxDuration() const
{
READ_LOCK();
if (m_endlessResize) {
return -1;
}
return m_producer->get_length();
}