diff --git a/src/assets/keyframes/model/keyframemodel.cpp b/src/assets/keyframes/model/keyframemodel.cpp index df6658833..1cb6499ea 100644 --- a/src/assets/keyframes/model/keyframemodel.cpp +++ b/src/assets/keyframes/model/keyframemodel.cpp @@ -1,1217 +1,1219 @@ /*************************************************************************** * 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; - if (oldPos != pos && hasKeyframe(pos)) return false; 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; // 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; 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); } 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); 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))); 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; if (auto ptr = m_model.lock()) { in = ptr->data(m_index, AssetParameterModel::ParentInRole).toInt(); ptr->passProperties(mlt_prop); } 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); 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))); 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; if (auto ptr = m_model.lock()) { ptr->passProperties(prop); } 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 (vals.count() > 4) { 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 (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); const QString res = QStringLiteral("%1 %2 %3 %4 %5").arg((int)rect.x).arg((int)rect.y).arg((int)rect.w).arg((int)rect.h).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); qreal relPos = (p - prev->first.frames(pCore->getCurrentFps())) / (qreal)(((next->first - prev->first).frames(pCore->getCurrentFps())) + 1); QList p2 = RotoHelper::getPoints(next->second.second, frame); 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; } 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/assets/keyframes/model/keyframemodel.hpp b/src/assets/keyframes/model/keyframemodel.hpp index 2dbbc22a2..e98632d58 100644 --- a/src/assets/keyframes/model/keyframemodel.hpp +++ b/src/assets/keyframes/model/keyframemodel.hpp @@ -1,221 +1,221 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef KEYFRAMELISTMODEL_H #define KEYFRAMELISTMODEL_H #include "assets/model/assetparametermodel.hpp" #include "definitions.h" #include "gentime.h" #include "undohelper.hpp" #include #include #include #include class AssetParameterModel; class DocUndoStack; class EffectItemModel; /* @brief This class is the model for a list of keyframes. A keyframe is defined by a time, a type and a value We store them in a sorted fashion using a std::map */ enum class KeyframeType { Linear = mlt_keyframe_linear, Discrete = mlt_keyframe_discrete, Curve = mlt_keyframe_smooth }; Q_DECLARE_METATYPE(KeyframeType) using Keyframe = std::pair; class KeyframeModel : public QAbstractListModel { Q_OBJECT public: /* @brief Construct a keyframe list bound to the given effect @param init_value is the value taken by the param at time 0. @param model is the asset this parameter belong to @param index is the index of this parameter in its model */ explicit KeyframeModel(std::weak_ptr model, const QModelIndex &index, std::weak_ptr undo_stack, QObject *parent = nullptr); enum { TypeRole = Qt::UserRole + 1, PosRole, FrameRole, ValueRole, NormalizedValueRole }; friend class KeyframeModelList; friend class KeyframeWidget; friend class KeyframeImport; protected: /** @brief These methods should ONLY be called by keyframemodellist to ensure synchronisation * with keyframes from other parameters */ /* @brief Adds a keyframe at the given position. If there is already one then we update it. @param pos defines the position of the keyframe, relative to the clip @param type is the type of the keyframe. */ bool addKeyframe(GenTime pos, KeyframeType type, QVariant value); bool addKeyframe(int frame, double normalizedValue); /* @brief Same function but accumulates undo/redo @param notify: if true, send a signal to model */ bool addKeyframe(GenTime pos, KeyframeType type, QVariant value, bool notify, Fun &undo, Fun &redo); /* @brief Removes the keyframe at the given position. */ bool removeKeyframe(int frame); bool moveKeyframe(int oldPos, int pos, QVariant newVal); bool removeKeyframe(GenTime pos); /* @brief Delete all the keyframes of the model */ bool removeAllKeyframes(); bool removeAllKeyframes(Fun &undo, Fun &redo); bool removeNextKeyframes(GenTime pos, Fun &undo, Fun &redo); QList getKeyframePos() const; protected: /* @brief Same function but accumulates undo/redo */ bool removeKeyframe(GenTime pos, Fun &undo, Fun &redo, bool notify = true); public: /* @brief moves a keyframe @param oldPos is the old position of the keyframe @param pos defines the new position of the keyframe, relative to the clip @param logUndo if true, then an undo object is created */ - Q_INVOKABLE bool moveKeyframe(int oldPos, int pos, bool logUndo); - Q_INVOKABLE bool offsetKeyframes(int oldPos, int pos, bool logUndo); + bool moveKeyframe(int oldPos, int pos, bool logUndo); + bool offsetKeyframes(int oldPos, int pos, bool logUndo); bool moveKeyframe(GenTime oldPos, GenTime pos, QVariant newVal, bool logUndo); bool moveKeyframe(GenTime oldPos, GenTime pos, QVariant newVal, Fun &undo, Fun &redo); /* @brief updates the value of a keyframe @param old is the position of the keyframe @param value is the new value of the param */ Q_INVOKABLE bool updateKeyframe(int pos, double newVal); bool updateKeyframe(GenTime pos, QVariant value); bool updateKeyframeType(GenTime pos, int type, Fun &undo, Fun &redo); bool updateKeyframe(GenTime pos, const QVariant &value, Fun &undo, Fun &redo, bool update = true); /* @brief updates the value of a keyframe, without any management of undo/redo @param pos is the position of the keyframe @param value is the new value of the param */ bool directUpdateKeyframe(GenTime pos, QVariant value); /* @brief Returns a keyframe data at given pos ok is a return parameter, set to true if everything went good */ Keyframe getKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns true if we only have 1 keyframe */ bool singleKeyframe() const; /* @brief Returns the keyframe located after given position. If there is a keyframe at given position it is ignored. @param ok is a return parameter to tell if a keyframe was found. */ Keyframe getNextKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns the keyframe located before given position. If there is a keyframe at given position it is ignored. @param ok is a return parameter to tell if a keyframe was found. */ Keyframe getPrevKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns the closest keyframe from given position. @param ok is a return parameter to tell if a keyframe was found. */ Keyframe getClosestKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns true if a keyframe exists at given pos Notice that add/remove queries are done in real time (gentime), but this request is made in frame */ Q_INVOKABLE bool hasKeyframe(int frame) const; Q_INVOKABLE bool hasKeyframe(const GenTime &pos) const; /* @brief Read the value from the model and update itself accordingly */ void refresh(); /* @brief Reset all values to their default */ void reset(); /* @brief Return the interpolated value at given pos */ QVariant getInterpolatedValue(int pos) const; QVariant getInterpolatedValue(const GenTime &pos) const; QVariant updateInterpolated(const QVariant &interpValue, double val); /* @brief Return the real value from a normalized one */ QVariant getNormalizedValue(double newVal) const; // Mandatory overloads Q_INVOKABLE QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; static QList getRanges(const QString &animData, const std::shared_ptr &model); static std::shared_ptr getAnimation(std::shared_ptr model, const QString &animData, int duration = 0); protected: /** @brief Helper function that generate a lambda to change type / value of given keyframe */ Fun updateKeyframe_lambda(GenTime pos, KeyframeType type, const QVariant &value, bool notify); /** @brief Helper function that generate a lambda to add given keyframe */ Fun addKeyframe_lambda(GenTime pos, KeyframeType type, const QVariant &value, bool notify); /** @brief Helper function that generate a lambda to remove given keyframe */ Fun deleteKeyframe_lambda(GenTime pos, bool notify); /* @brief Connects the signals of this object */ void setup(); /* @brief Commit the modification to the model */ void sendModification(); /** @brief returns the keyframes as a Mlt Anim Property string. It is defined as pairs of frame and value, separated by ; Example : "0|=50; 50|=100; 100=200; 200~=60;" Spaces are ignored by Mlt. |= represents a discrete keyframe, = a linear one and ~= a Catmull-Rom spline */ QString getAnimProperty() const; QString getRotoProperty() const; /* @brief this function clears all existing keyframes, and reloads its data from the string passed */ void resetAnimProperty(const QString &prop); /* @brief this function does the opposite of getAnimProperty: given a MLT representation of an animation, build the corresponding model */ void parseAnimProperty(const QString &prop); void parseRotoProperty(const QString &prop); private: std::weak_ptr m_model; std::weak_ptr m_undoStack; QPersistentModelIndex m_index; QString m_lastData; ParamType m_paramType; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access std::map> m_keyframeList; signals: void modelChanged(); public: // this is to enable for range loops auto begin() -> decltype(m_keyframeList.begin()) { return m_keyframeList.begin(); } auto end() -> decltype(m_keyframeList.end()) { return m_keyframeList.end(); } }; // Q_DECLARE_METATYPE(KeyframeModel *) #endif diff --git a/src/core.cpp b/src/core.cpp index f2a7de595..251a87c2d 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,739 +1,740 @@ /* 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; } Core::~Core() { if (m_monitorManager) { delete m_monitorManager; } // delete m_binWidget; if (m_projectManager) { delete m_projectManager; } ClipController::mediaUnavailable.reset(); } void Core::build(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"); // 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(); } 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); } 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() { 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(); } const QString Core::stopMediaCapture(bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(false); } else if (checkAudio) { m_capture->recordAudio(false); } return m_capture->getCaptureOutputLocation().toLocalFile(); } 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(); }