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(); }