diff --git a/src/assets/keyframes/model/keyframemodel.cpp b/src/assets/keyframes/model/keyframemodel.cpp index 78c7a9de4..f4a74627f 100644 --- a/src/assets/keyframes/model/keyframemodel.cpp +++ b/src/assets/keyframes/model/keyframemodel.cpp @@ -1,1238 +1,1238 @@ /*************************************************************************** * 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 #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) { QVariant result = getNormalizedValue(normalizedValue); if (result.isValid()) { // TODO: Use default configurable kf type return addKeyframe(GenTime(frame, pCore->getCurrentFps()), KeyframeType::Linear, result); } 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); } // Calculate real value from normalized QVariant result = getNormalizedValue(newVal.toDouble()); return updateKeyframe(pos, result); } 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()) { QVariant result = getNormalizedValue(newVal.toDouble()); if (result.isValid()) { res = addKeyframe(pos, oldType, result, 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(nullptr); 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++; } QString ret; if (anim) { char *cut = anim->serialize_cut(); ret = QString(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; + locale.setNumberOptions(QLocale::OmitGroupSeparator); 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/assets/model/assetcommand.cpp b/src/assets/model/assetcommand.cpp index 983a100e9..c3e80e864 100644 --- a/src/assets/model/assetcommand.cpp +++ b/src/assets/model/assetcommand.cpp @@ -1,212 +1,214 @@ /*************************************************************************** * Copyright (C) 2017 by by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "assetcommand.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "effects/effectsrepository.hpp" #include "transitions/transitionsrepository.hpp" #include #include AssetCommand::AssetCommand(const std::shared_ptr &model, const QModelIndex &index, QString value, QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) , m_index(index) , m_value(std::move(value)) , m_updateView(false) , m_stamp(QTime::currentTime()) { QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); m_name = m_model->data(index, AssetParameterModel::NameRole).toString(); const QString id = model->getAssetId(); if (EffectsRepository::get()->exists(id)) { setText(i18n("Edit %1", EffectsRepository::get()->getName(id))); } else if (TransitionsRepository::get()->exists(id)) { setText(i18n("Edit %1", TransitionsRepository::get()->getName(id))); } QVariant previousVal = m_model->data(index, AssetParameterModel::ValueRole); m_oldValue = previousVal.type() == QVariant::Double ? locale.toString(previousVal.toDouble()) : previousVal.toString(); } void AssetCommand::undo() { m_model->setParameter(m_name, m_oldValue, true, m_index); } // virtual void AssetCommand::redo() { m_model->setParameter(m_name, m_value, m_updateView, m_index); m_updateView = true; } // virtual int AssetCommand::id() const { return 1; } // virtual bool AssetCommand::mergeWith(const QUndoCommand *other) { if (other->id() != id() || static_cast(other)->m_index != m_index || m_stamp.msecsTo(static_cast(other)->m_stamp) > 3000) { return false; } m_value = static_cast(other)->m_value; m_stamp = static_cast(other)->m_stamp; return true; } AssetMultiCommand::AssetMultiCommand(const std::shared_ptr &model, const QList indexes, const QStringList values, QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) , m_indexes(indexes) , m_values(values) , m_updateView(false) , m_stamp(QTime::currentTime()) { QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); qDebug()<<"CREATING MULTIPLE COMMAND!!!\nVALUES: "<data(indexes.first(), AssetParameterModel::NameRole).toString(); const QString id = model->getAssetId(); if (EffectsRepository::get()->exists(id)) { setText(i18n("Edit %1", EffectsRepository::get()->getName(id))); } else if (TransitionsRepository::get()->exists(id)) { setText(i18n("Edit %1", TransitionsRepository::get()->getName(id))); } for (QModelIndex ix : m_indexes) { QVariant previousVal = m_model->data(ix, AssetParameterModel::ValueRole); m_oldValues << (previousVal.type() == QVariant::Double ? locale.toString(previousVal.toDouble()) : previousVal.toString()); } } void AssetMultiCommand::undo() { int indx = 0; int max = m_indexes.size() - 1; for (const QModelIndex &ix : m_indexes) { m_model->setParameter(m_model->data(ix, AssetParameterModel::NameRole).toString(), m_oldValues.at(indx), indx == max, ix); indx++; } } // virtual void AssetMultiCommand::redo() { int indx = 0; int max = m_indexes.size() - 1; for (const QModelIndex &ix : m_indexes) { m_model->setParameter(m_model->data(ix, AssetParameterModel::NameRole).toString(), m_values.at(indx), m_updateView && indx == max, ix); indx++; } m_updateView = true; } // virtual int AssetMultiCommand::id() const { return 1; } // virtual bool AssetMultiCommand::mergeWith(const QUndoCommand *other) { if (other->id() != id() || static_cast(other)->m_indexes != m_indexes || m_stamp.msecsTo(static_cast(other)->m_stamp) > 3000) { return false; } m_values = static_cast(other)->m_values; m_stamp = static_cast(other)->m_stamp; return true; } AssetKeyframeCommand::AssetKeyframeCommand(const std::shared_ptr &model, const QModelIndex &index, QVariant value, GenTime pos, QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) , m_index(index) , m_value(std::move(value)) , m_pos(pos) , m_updateView(false) , m_stamp(QTime::currentTime()) { const QString id = model->getAssetId(); if (EffectsRepository::get()->exists(id)) { setText(i18n("Edit %1 keyframe", EffectsRepository::get()->getName(id))); } else if (TransitionsRepository::get()->exists(id)) { setText(i18n("Edit %1 keyframe", TransitionsRepository::get()->getName(id))); } m_oldValue = m_model->getKeyframeModel()->getKeyModel(m_index)->getInterpolatedValue(m_pos); } void AssetKeyframeCommand::undo() { m_model->getKeyframeModel()->getKeyModel(m_index)->directUpdateKeyframe(m_pos, m_oldValue); } // virtual void AssetKeyframeCommand::redo() { m_model->getKeyframeModel()->getKeyModel(m_index)->directUpdateKeyframe(m_pos, m_value); m_updateView = true; } // virtual int AssetKeyframeCommand::id() const { return 2; } // virtual bool AssetKeyframeCommand::mergeWith(const QUndoCommand *other) { if (other->id() != id() || static_cast(other)->m_index != m_index || m_stamp.msecsTo(static_cast(other)->m_stamp) > 1000) { return false; } m_value = static_cast(other)->m_value; m_stamp = static_cast(other)->m_stamp; return true; } AssetUpdateCommand::AssetUpdateCommand(const std::shared_ptr &model, QVector> parameters, QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) , m_value(std::move(parameters)) { const QString id = model->getAssetId(); if (EffectsRepository::get()->exists(id)) { setText(i18n("Update %1", EffectsRepository::get()->getName(id))); } else if (TransitionsRepository::get()->exists(id)) { setText(i18n("Update %1", TransitionsRepository::get()->getName(id))); } m_oldValue = m_model->getAllParameters(); } void AssetUpdateCommand::undo() { m_model->setParameters(m_oldValue); } // virtual void AssetUpdateCommand::redo() { m_model->setParameters(m_value); } // virtual int AssetUpdateCommand::id() const { return 3; } diff --git a/src/assets/model/assetparametermodel.cpp b/src/assets/model/assetparametermodel.cpp index 1922f35e4..b8f04c059 100644 --- a/src/assets/model/assetparametermodel.cpp +++ b/src/assets/model/assetparametermodel.cpp @@ -1,870 +1,871 @@ /*************************************************************************** * 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 "assetparametermodel.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "core.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "profiles/profilemodel.hpp" #include #include #include #include #include #include AssetParameterModel::AssetParameterModel(std::unique_ptr asset, const QDomElement &assetXml, const QString &assetId, ObjectId ownerId, QObject *parent) : QAbstractListModel(parent) , monitorId(ownerId.first == ObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor) , m_assetId(assetId) , m_ownerId(ownerId) , m_asset(std::move(asset)) , m_keyframes(nullptr) { Q_ASSERT(m_asset->is_valid()); QDomNodeList nodeList = assetXml.elementsByTagName(QStringLiteral("parameter")); m_hideKeyframesByDefault = assetXml.hasAttribute(QStringLiteral("hideKeyframes")); m_isAudio = assetXml.attribute(QStringLiteral("type")) == QLatin1String("audio"); bool needsLocaleConversion = false; QChar separator, oldSeparator; // Check locale, default effects xml has no LC_NUMERIC defined and always uses the C locale QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); if (assetXml.hasAttribute(QStringLiteral("LC_NUMERIC"))) { QLocale effectLocale = QLocale(assetXml.attribute(QStringLiteral("LC_NUMERIC"))); if (QLocale::c().decimalPoint() != effectLocale.decimalPoint()) { needsLocaleConversion = true; separator = QLocale::c().decimalPoint(); oldSeparator = effectLocale.decimalPoint(); } } qDebug() << "XML parsing of " << assetId << ". found : " << nodeList.count(); for (int i = 0; i < nodeList.count(); ++i) { QDomElement currentParameter = nodeList.item(i).toElement(); // Convert parameters if we need to if (needsLocaleConversion) { QDomNamedNodeMap attrs = currentParameter.attributes(); for (int k = 0; k < attrs.count(); ++k) { QString nodeName = attrs.item(k).nodeName(); if (nodeName != QLatin1String("type") && nodeName != QLatin1String("name")) { QString val = attrs.item(k).nodeValue(); if (val.contains(oldSeparator)) { QString newVal = val.replace(oldSeparator, separator); attrs.item(k).setNodeValue(newVal); } } } } // Parse the basic attributes of the parameter QString name = currentParameter.attribute(QStringLiteral("name")); QString type = currentParameter.attribute(QStringLiteral("type")); QString value = currentParameter.attribute(QStringLiteral("value")); ParamRow currentRow; currentRow.type = paramTypeFromStr(type); currentRow.xml = currentParameter; if (value.isEmpty()) { QVariant defaultValue = parseAttribute(m_ownerId, QStringLiteral("default"), currentParameter); value = defaultValue.type() == QVariant::Double ? locale.toString(defaultValue.toDouble()) : defaultValue.toString(); } bool isFixed = (type == QLatin1String("fixed")); if (isFixed) { m_fixedParams[name] = value; } else if (currentRow.type == ParamType::Position) { int val = value.toInt(); if (val < 0) { int in = pCore->getItemIn(m_ownerId); int out = in + pCore->getItemDuration(m_ownerId) - 1; val += out; value = QString::number(val); } } else if (currentRow.type == ParamType::KeyframeParam || currentRow.type == ParamType::AnimatedRect) { if (!value.contains(QLatin1Char('='))) { value.prepend(QStringLiteral("%1=").arg(pCore->getItemIn(m_ownerId))); } } if (!name.isEmpty()) { internalSetParameter(name, value); // Keep track of param order m_paramOrder.push_back(name); } if (isFixed) { // fixed parameters are not displayed so we don't store them. continue; } currentRow.value = value; QString title = i18n(currentParameter.firstChildElement(QStringLiteral("name")).text().toUtf8().data()); currentRow.name = title.isEmpty() ? name : title; m_params[name] = currentRow; m_rows.push_back(name); } if (m_assetId.startsWith(QStringLiteral("sox_"))) { // Sox effects need to have a special "Effect" value set QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)}; for (const QString &pName : m_paramOrder) { effectParam << m_asset->get(pName.toUtf8().constData()); } m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData()); } qDebug() << "END parsing of " << assetId << ". Number of found parameters" << m_rows.size(); emit modelChanged(); } void AssetParameterModel::prepareKeyframes() { if (m_keyframes) return; int ix = 0; for (const auto &name : m_rows) { if (m_params.at(name).type == ParamType::KeyframeParam || m_params.at(name).type == ParamType::AnimatedRect || m_params.at(name).type == ParamType::Roto_spline) { addKeyframeParam(index(ix, 0)); } ix++; } if (m_keyframes) { // Make sure we have keyframes at same position for all parameters m_keyframes->checkConsistency(); } } QStringList AssetParameterModel::getKeyframableParameters() const { QStringList paramNames; int ix = 0; for (const auto &name : m_rows) { if (m_params.at(name).type == ParamType::KeyframeParam || m_params.at(name).type == ParamType::AnimatedRect) { //addKeyframeParam(index(ix, 0)); paramNames << name; } ix++; } return paramNames; } void AssetParameterModel::setParameter(const QString &name, int value, bool update) { Q_ASSERT(m_asset->is_valid()); m_asset->set(name.toLatin1().constData(), value); if (m_fixedParams.count(name) == 0) { m_params[name].value = value; } else { m_fixedParams[name] = value; } if (m_assetId.startsWith(QStringLiteral("sox_"))) { // Warning, SOX effect, need unplug/replug qDebug() << "// Warning, SOX effect, need unplug/replug"; QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)}; for (const QString &pName : m_paramOrder) { effectParam << m_asset->get(pName.toUtf8().constData()); } m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData()); emit replugEffect(shared_from_this()); } else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) { // these effects don't understand param change and need to be rebuild emit replugEffect(shared_from_this()); } if (update) { emit modelChanged(); emit dataChanged(index(0, 0), index(m_rows.count() - 1, 0), {}); // Update fades in timeline pCore->updateItemModel(m_ownerId, m_assetId); if (!m_isAudio) { // Trigger monitor refresh pCore->refreshProjectItem(m_ownerId); // Invalidate timeline preview pCore->invalidateItem(m_ownerId); } } } void AssetParameterModel::internalSetParameter(const QString &name, const QString ¶mValue, const QModelIndex ¶mIndex) { Q_ASSERT(m_asset->is_valid()); QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); // TODO: this does not really belong here, but I don't see another way to do it so that undo works if (data(paramIndex, AssetParameterModel::TypeRole).value() == ParamType::Curve) { QStringList vals = paramValue.split(QLatin1Char(';'), QString::SkipEmptyParts); int points = vals.size(); m_asset->set("3", points / 10.); // for the curve, inpoints are numbered: 6, 8, 10, 12, 14 // outpoints, 7, 9, 11, 13,15 so we need to deduce these enums for (int i = 0; i < points; i++) { const QString &pointVal = vals.at(i); int idx = 2 * i + 6; m_asset->set(QString::number(idx).toLatin1().constData(), pointVal.section(QLatin1Char('/'), 0, 0).toDouble()); idx++; m_asset->set(QString::number(idx).toLatin1().constData(), pointVal.section(QLatin1Char('/'), 1, 1).toDouble()); } } bool conversionSuccess; double doubleValue = locale.toDouble(paramValue, &conversionSuccess); if (conversionSuccess) { m_asset->set(name.toLatin1().constData(), doubleValue); if (m_fixedParams.count(name) == 0) { m_params[name].value = doubleValue; } else { m_fixedParams[name] = doubleValue; } } else { m_asset->set(name.toLatin1().constData(), paramValue.toUtf8().constData()); qDebug() << " = = SET EFFECT PARAM: " << name << " = " << paramValue; if (m_fixedParams.count(name) == 0) { m_params[name].value = paramValue; if (m_keyframes) { KeyframeModel *km = m_keyframes->getKeyModel(paramIndex); if (km) { km->refresh(); } //m_keyframes->refresh(); } } else { m_fixedParams[name] = paramValue; } } } void AssetParameterModel::setParameter(const QString &name, const QString ¶mValue, bool update, const QModelIndex ¶mIndex) { //qDebug() << "// PROCESSING PARAM CHANGE: " << name << ", UPDATE: " << update << ", VAL: " << paramValue; internalSetParameter(name, paramValue, paramIndex); bool updateChildRequired = true; if (m_assetId.startsWith(QStringLiteral("sox_"))) { // Warning, SOX effect, need unplug/replug QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)}; for (const QString &pName : m_paramOrder) { effectParam << m_asset->get(pName.toUtf8().constData()); } m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData()); emit replugEffect(shared_from_this()); updateChildRequired = false; } else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) { // these effects don't understand param change and need to be rebuild emit replugEffect(shared_from_this()); updateChildRequired = false; } else if (update) { qDebug() << "// SENDING DATA CHANGE...."; if (paramIndex.isValid()) { emit dataChanged(paramIndex, paramIndex); } else { QModelIndex ix = index(m_rows.indexOf(name), 0); emit dataChanged(ix, ix); } emit modelChanged(); } if (updateChildRequired) { emit updateChildren(name); } // Update timeline view if necessary if (m_ownerId.first == ObjectType::NoItem) { // Used for generator clips if (!update) emit modelChanged(); } else { // Update fades in timeline pCore->updateItemModel(m_ownerId, m_assetId); if (!m_isAudio) { // Trigger monitor refresh pCore->refreshProjectItem(m_ownerId); // Invalidate timeline preview pCore->invalidateItem(m_ownerId); } } } AssetParameterModel::~AssetParameterModel() = default; QVariant AssetParameterModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= m_rows.size() || !index.isValid()) { return QVariant(); } QString paramName = m_rows[index.row()]; Q_ASSERT(m_params.count(paramName) > 0); const QDomElement &element = m_params.at(paramName).xml; switch (role) { case Qt::DisplayRole: case Qt::EditRole: return m_params.at(paramName).name; case NameRole: return paramName; case TypeRole: return QVariant::fromValue(m_params.at(paramName).type); case CommentRole: { QDomElement commentElem = element.firstChildElement(QStringLiteral("comment")); QString comment; if (!commentElem.isNull()) { comment = i18n(commentElem.text().toUtf8().data()); } return comment; } case InRole: return m_asset->get_int("in"); case OutRole: return m_asset->get_int("out"); case ParentInRole: return pCore->getItemIn(m_ownerId); case ParentDurationRole: return pCore->getItemDuration(m_ownerId); case ParentPositionRole: return pCore->getItemPosition(m_ownerId); case HideKeyframesFirstRole: return m_hideKeyframesByDefault; case MinRole: return parseAttribute(m_ownerId, QStringLiteral("min"), element); case MaxRole: return parseAttribute(m_ownerId, QStringLiteral("max"), element); case FactorRole: return parseAttribute(m_ownerId, QStringLiteral("factor"), element, 1); case ScaleRole: return parseAttribute(m_ownerId, QStringLiteral("scale"), element, 0); case DecimalsRole: return parseAttribute(m_ownerId, QStringLiteral("decimals"), element); case OddRole: return element.attribute(QStringLiteral("odd")) == QLatin1String("1"); case DefaultRole: return parseAttribute(m_ownerId, QStringLiteral("default"), element); case FilterRole: return parseAttribute(m_ownerId, QStringLiteral("filter"), element); case FilterParamsRole: return parseAttribute(m_ownerId, QStringLiteral("filterparams"), element); case FilterJobParamsRole: return parseSubAttributes(QStringLiteral("jobparam"), element); case AlternateNameRole: { QDomNode child = element.firstChildElement(QStringLiteral("name")); if (child.toElement().hasAttribute(QStringLiteral("conditional"))) { return child.toElement().attribute(QStringLiteral("conditional")); } return m_params.at(paramName).name; } case SuffixRole: return element.attribute(QStringLiteral("suffix")); case OpacityRole: return element.attribute(QStringLiteral("opacity")) != QLatin1String("false"); case RelativePosRole: return element.attribute(QStringLiteral("relative")) == QLatin1String("true"); case ShowInTimelineRole: return !element.hasAttribute(QStringLiteral("notintimeline")); case AlphaRole: return element.attribute(QStringLiteral("alpha")) == QLatin1String("1"); case ValueRole: { QString value(m_asset->get(paramName.toUtf8().constData())); return value.isEmpty() ? (element.attribute(QStringLiteral("value")).isNull() ? parseAttribute(m_ownerId, QStringLiteral("default"), element) : element.attribute(QStringLiteral("value"))) : value; } case ListValuesRole: return element.attribute(QStringLiteral("paramlist")).split(QLatin1Char(';')); case ListNamesRole: { QDomElement namesElem = element.firstChildElement(QStringLiteral("paramlistdisplay")); return i18n(namesElem.text().toUtf8().data()).split(QLatin1Char(',')); } case List1Role: return parseAttribute(m_ownerId, QStringLiteral("list1"), element); case List2Role: return parseAttribute(m_ownerId, QStringLiteral("list2"), element); case Enum1Role: return m_asset->get_double("1"); case Enum2Role: return m_asset->get_double("2"); case Enum3Role: return m_asset->get_double("3"); case Enum4Role: return m_asset->get_double("4"); case Enum5Role: return m_asset->get_double("5"); case Enum6Role: return m_asset->get_double("6"); case Enum7Role: return m_asset->get_double("7"); case Enum8Role: return m_asset->get_double("8"); case Enum9Role: return m_asset->get_double("9"); case Enum10Role: return m_asset->get_double("10"); case Enum11Role: return m_asset->get_double("11"); case Enum12Role: return m_asset->get_double("12"); case Enum13Role: return m_asset->get_double("13"); case Enum14Role: return m_asset->get_double("14"); case Enum15Role: return m_asset->get_double("15"); } return QVariant(); } int AssetParameterModel::rowCount(const QModelIndex &parent) const { qDebug() << "===================================================== Requested rowCount" << parent << m_rows.size(); if (parent.isValid()) return 0; return m_rows.size(); } // static ParamType AssetParameterModel::paramTypeFromStr(const QString &type) { if (type == QLatin1String("double") || type == QLatin1String("float") || type == QLatin1String("constant")) { return ParamType::Double; } if (type == QLatin1String("list")) { return ParamType::List; } if (type == QLatin1String("bool")) { return ParamType::Bool; } if (type == QLatin1String("switch")) { return ParamType::Switch; } else if (type == QLatin1String("simplekeyframe")) { return ParamType::KeyframeParam; } else if (type == QLatin1String("animatedrect")) { return ParamType::AnimatedRect; } else if (type == QLatin1String("geometry")) { return ParamType::Geometry; } else if (type == QLatin1String("addedgeometry")) { return ParamType::Addedgeometry; } else if (type == QLatin1String("keyframe") || type == QLatin1String("animated")) { return ParamType::KeyframeParam; } else if (type == QLatin1String("color")) { return ParamType::Color; } else if (type == QLatin1String("colorwheel")) { return ParamType::ColorWheel; } else if (type == QLatin1String("position")) { return ParamType::Position; } else if (type == QLatin1String("curve")) { return ParamType::Curve; } else if (type == QLatin1String("bezier_spline")) { return ParamType::Bezier_spline; } else if (type == QLatin1String("roto-spline")) { return ParamType::Roto_spline; } else if (type == QLatin1String("wipe")) { return ParamType::Wipe; } else if (type == QLatin1String("url")) { return ParamType::Url; } else if (type == QLatin1String("keywords")) { return ParamType::Keywords; } else if (type == QLatin1String("fontfamily")) { return ParamType::Fontfamily; } else if (type == QLatin1String("filterjob")) { return ParamType::Filterjob; } else if (type == QLatin1String("readonly")) { return ParamType::Readonly; } else if (type == QLatin1String("hidden")) { return ParamType::Hidden; } qDebug() << "WARNING: Unknown type :" << type; return ParamType::Double; } // static QString AssetParameterModel::getDefaultKeyframes(int start, const QString &defaultValue, bool linearOnly) { QString keyframes = QString::number(start); if (linearOnly) { keyframes.append(QLatin1Char('=')); } else { switch (KdenliveSettings::defaultkeyframeinterp()) { case mlt_keyframe_discrete: keyframes.append(QStringLiteral("|=")); break; case mlt_keyframe_smooth: keyframes.append(QStringLiteral("~=")); break; default: keyframes.append(QLatin1Char('=')); break; } } keyframes.append(defaultValue); return keyframes; } // static QVariant AssetParameterModel::parseAttribute(const ObjectId &owner, const QString &attribute, const QDomElement &element, QVariant defaultValue) { if (!element.hasAttribute(attribute) && !defaultValue.isNull()) { return defaultValue; } ParamType type = paramTypeFromStr(element.attribute(QStringLiteral("type"))); QString content = element.attribute(attribute); if (content.contains(QLatin1Char('%'))) { std::unique_ptr &profile = pCore->getCurrentProfile(); int width = profile->width(); int height = profile->height(); int in = pCore->getItemIn(owner); int out = in + pCore->getItemDuration(owner); int frame_duration = pCore->getDurationFromString(KdenliveSettings::fade_duration()); // replace symbols in the double parameter content.replace(QLatin1String("%maxWidth"), QString::number(width)) .replace(QLatin1String("%maxHeight"), QString::number(height)) .replace(QLatin1String("%width"), QString::number(width)) .replace(QLatin1String("%height"), QString::number(height)) .replace(QLatin1String("%out"), QString::number(out)) .replace(QLatin1String("%fade"), QString::number(frame_duration)); if (type == ParamType::Double || type == ParamType::Hidden) { // Use a Mlt::Properties to parse mathematical operators Mlt::Properties p; p.set("eval", content.prepend(QLatin1Char('@')).toLatin1().constData()); return p.get_double("eval"); } } else if (type == ParamType::Double || type == ParamType::Hidden) { if (attribute == QLatin1String("default")) { return content.toDouble(); } QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); return locale.toDouble(content); } if (attribute == QLatin1String("default")) { if (type == ParamType::RestrictedAnim) { content = getDefaultKeyframes(0, content, true); } else if (type == ParamType::KeyframeParam) { return content.toDouble(); } else if (type == ParamType::List) { bool ok; double res = content.toDouble(&ok); if (ok) { return res; } return defaultValue.isNull() ? content : defaultValue; } else if (type == ParamType::Bezier_spline) { QLocale locale; if (locale.decimalPoint() != QLocale::c().decimalPoint()) { return content.replace(QLocale::c().decimalPoint(), locale.decimalPoint()); } } } return content; } QVariant AssetParameterModel::parseSubAttributes(const QString &attribute, const QDomElement &element) const { QDomNodeList nodeList = element.elementsByTagName(attribute); if (nodeList.isEmpty()) { return QVariant(); } QVariantList jobDataList; for (int i = 0; i < nodeList.count(); ++i) { QDomElement currentParameter = nodeList.item(i).toElement(); QStringList jobData {currentParameter.attribute(QStringLiteral("name")), currentParameter.text()}; jobDataList << jobData; } return jobDataList; } QString AssetParameterModel::getAssetId() const { return m_assetId; } QVector> AssetParameterModel::getAllParameters() const { QVector> res; res.reserve((int)m_fixedParams.size() + (int)m_params.size()); for (const auto &fixed : m_fixedParams) { res.push_back(QPair(fixed.first, fixed.second)); } for (const auto ¶m : m_params) { res.push_back(QPair(param.first, param.second.value)); } return res; } QJsonDocument AssetParameterModel::toJson(bool includeFixed) const { QJsonArray list; - QLocale locale; if (includeFixed) { for (const auto &fixed : m_fixedParams) { QJsonObject currentParam; QModelIndex ix = index(m_rows.indexOf(fixed.first), 0); currentParam.insert(QLatin1String("name"), QJsonValue(fixed.first)); currentParam.insert(QLatin1String("value"), fixed.second.type() == QVariant::Double ? QJsonValue(fixed.second.toDouble()) : QJsonValue(fixed.second.toString())); int type = data(ix, AssetParameterModel::TypeRole).toInt(); double min = data(ix, AssetParameterModel::MinRole).toDouble(); double max = data(ix, AssetParameterModel::MaxRole).toDouble(); double factor = data(ix, AssetParameterModel::FactorRole).toDouble(); int in = data(ix, AssetParameterModel::ParentInRole).toInt(); int out = in + data(ix, AssetParameterModel::ParentDurationRole).toInt(); if (factor > 0) { min /= factor; max /= factor; } currentParam.insert(QLatin1String("type"), QJsonValue(type)); currentParam.insert(QLatin1String("min"), QJsonValue(min)); currentParam.insert(QLatin1String("max"), QJsonValue(max)); currentParam.insert(QLatin1String("in"), QJsonValue(in)); currentParam.insert(QLatin1String("out"), QJsonValue(out)); list.push_back(currentParam); } } for (const auto ¶m : m_params) { if (!includeFixed && param.second.type != ParamType::KeyframeParam && param.second.type != ParamType::AnimatedRect) { continue; } QJsonObject currentParam; QModelIndex ix = index(m_rows.indexOf(param.first), 0); currentParam.insert(QLatin1String("name"), QJsonValue(param.first)); currentParam.insert(QLatin1String("value"), param.second.value.type() == QVariant::Double ? QJsonValue(param.second.value.toDouble()) : QJsonValue(param.second.value.toString())); int type = data(ix, AssetParameterModel::TypeRole).toInt(); double min = data(ix, AssetParameterModel::MinRole).toDouble(); double max = data(ix, AssetParameterModel::MaxRole).toDouble(); double factor = data(ix, AssetParameterModel::FactorRole).toDouble(); int in = data(ix, AssetParameterModel::ParentInRole).toInt(); int out = in + data(ix, AssetParameterModel::ParentDurationRole).toInt(); if (factor > 0) { min /= factor; max /= factor; } currentParam.insert(QLatin1String("type"), QJsonValue(type)); currentParam.insert(QLatin1String("min"), QJsonValue(min)); currentParam.insert(QLatin1String("max"), QJsonValue(max)); currentParam.insert(QLatin1String("in"), QJsonValue(in)); currentParam.insert(QLatin1String("out"), QJsonValue(out)); list.push_back(currentParam); } return QJsonDocument(list); } void AssetParameterModel::deletePreset(const QString &presetFile, const QString &presetName) { QJsonObject object; QJsonArray array; QFile loadFile(presetFile); if (loadFile.exists()) { if (loadFile.open(QIODevice::ReadOnly)) { QByteArray saveData = loadFile.readAll(); QJsonDocument loadDoc(QJsonDocument::fromJson(saveData)); if (loadDoc.isArray()) { array = loadDoc.array(); QList toDelete; for (int i = 0; i < array.size(); i++) { QJsonValue val = array.at(i); if (val.isObject() && val.toObject().keys().contains(presetName)) { toDelete << i; } } for (int i : toDelete) { array.removeAt(i); } } else if (loadDoc.isObject()) { QJsonObject obj = loadDoc.object(); qDebug() << " * * ** JSON IS AN OBJECT, DELETING: " << presetName; if (obj.keys().contains(presetName)) { obj.remove(presetName); } else { qDebug() << " * * ** JSON DOES NOT CONTAIN: " << obj.keys(); } array.append(obj); } loadFile.close(); } } if (!loadFile.open(QIODevice::WriteOnly)) { pCore->displayMessage(i18n("Cannot open preset file %1", presetFile), ErrorMessage); return; } if (array.isEmpty()) { QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); if (dir.exists(presetFile)) { // Ensure we don't delete an unwanted file loadFile.remove(); } } else { loadFile.write(QJsonDocument(array).toJson()); } } void AssetParameterModel::savePreset(const QString &presetFile, const QString &presetName) { QJsonObject object; QJsonArray array; QJsonDocument doc = toJson(); QFile loadFile(presetFile); if (loadFile.exists()) { if (loadFile.open(QIODevice::ReadOnly)) { QByteArray saveData = loadFile.readAll(); QJsonDocument loadDoc(QJsonDocument::fromJson(saveData)); if (loadDoc.isArray()) { array = loadDoc.array(); QList toDelete; for (int i = 0; i < array.size(); i++) { QJsonValue val = array.at(i); if (val.isObject() && val.toObject().keys().contains(presetName)) { toDelete << i; } } for (int i : toDelete) { array.removeAt(i); } } else if (loadDoc.isObject()) { QJsonObject obj = loadDoc.object(); if (obj.keys().contains(presetName)) { obj.remove(presetName); } array.append(obj); } loadFile.close(); } } if (!loadFile.open(QIODevice::WriteOnly)) { pCore->displayMessage(i18n("Cannot open preset file %1", presetFile), ErrorMessage); return; } object[presetName] = doc.array(); array.append(object); loadFile.write(QJsonDocument(array).toJson()); } const QStringList AssetParameterModel::getPresetList(const QString &presetFile) const { QFile loadFile(presetFile); if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) { QByteArray saveData = loadFile.readAll(); QJsonDocument loadDoc(QJsonDocument::fromJson(saveData)); if (loadDoc.isObject()) { qDebug() << "// PRESET LIST IS AN OBJECT!!!"; return loadDoc.object().keys(); } else if (loadDoc.isArray()) { qDebug() << "// PRESET LIST IS AN ARRAY!!!"; QStringList result; QJsonArray array = loadDoc.array(); for (auto &&i : array) { QJsonValue val = i; if (val.isObject()) { result << val.toObject().keys(); } } return result; } } return QStringList(); } const QVector> AssetParameterModel::loadPreset(const QString &presetFile, const QString &presetName) { QFile loadFile(presetFile); QVector> params; if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) { QByteArray saveData = loadFile.readAll(); QJsonDocument loadDoc(QJsonDocument::fromJson(saveData)); if (loadDoc.isObject() && loadDoc.object().contains(presetName)) { qDebug() << "..........\n..........\nLOADING OBJECT JSON"; QJsonValue val = loadDoc.object().value(presetName); if (val.isObject()) { QVariantMap map = val.toObject().toVariantMap(); QMap::const_iterator i = map.constBegin(); while (i != map.constEnd()) { params.append({i.key(), i.value()}); ++i; } } } else if (loadDoc.isArray()) { QJsonArray array = loadDoc.array(); for (auto &&i : array) { QJsonValue val = i; if (val.isObject() && val.toObject().contains(presetName)) { QJsonValue preset = val.toObject().value(presetName); if (preset.isArray()) { QJsonArray paramArray = preset.toArray(); for (auto &&j : paramArray) { QJsonValue v1 = j; if (v1.isObject()) { QJsonObject ob = v1.toObject(); params.append({ob.value("name").toString(), ob.value("value").toVariant()}); } } } qDebug() << "// LOADED PRESET: " << presetName << "\n" << params; break; } } } } return params; } void AssetParameterModel::setParameters(const QVector> ¶ms) { QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); for (const auto ¶m : params) { if (param.second.type() == QVariant::Double) { setParameter(param.first, locale.toString(param.second.toDouble()), false); } else { setParameter(param.first, param.second.toString(), false); } } if (m_keyframes) { m_keyframes->refresh(); } emit dataChanged(index(0), index(m_rows.count()), {}); } ObjectId AssetParameterModel::getOwnerId() const { return m_ownerId; } void AssetParameterModel::addKeyframeParam(const QModelIndex &index) { if (m_keyframes) { m_keyframes->addParameter(index); } else { m_keyframes.reset(new KeyframeModelList(shared_from_this(), index, pCore->undoStack())); } } std::shared_ptr AssetParameterModel::getKeyframeModel() { return m_keyframes; } void AssetParameterModel::resetAsset(std::unique_ptr asset) { m_asset = std::move(asset); } bool AssetParameterModel::hasMoreThanOneKeyframe() const { if (m_keyframes) { return (!m_keyframes->isEmpty() && !m_keyframes->singleKeyframe()); } return false; } int AssetParameterModel::time_to_frames(const QString &time) { return m_asset->time_to_frames(time.toUtf8().constData()); } void AssetParameterModel::passProperties(Mlt::Properties &target) { target.set("_profile", pCore->getCurrentProfile()->get_profile(), 0); target.set_lcnumeric(m_asset->get_lcnumeric()); } diff --git a/src/assets/view/assetparameterview.cpp b/src/assets/view/assetparameterview.cpp index 5ceef648b..65073b6b4 100644 --- a/src/assets/view/assetparameterview.cpp +++ b/src/assets/view/assetparameterview.cpp @@ -1,385 +1,386 @@ /*************************************************************************** * 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 "assetparameterview.hpp" #include "assets/model/assetcommand.hpp" #include "assets/model/assetparametermodel.hpp" #include "assets/view/widgets/abstractparamwidget.hpp" #include "assets/view/widgets/keyframewidget.hpp" #include "core.h" #include #include #include #include #include #include #include #include #include AssetParameterView::AssetParameterView(QWidget *parent) : QWidget(parent) { m_lay = new QVBoxLayout(this); m_lay->setContentsMargins(0, 0, 0, 2); m_lay->setSpacing(0); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); // Presets Combo m_presetMenu = new QMenu(this); } void AssetParameterView::setModel(const std::shared_ptr &model, QSize frameSize, bool addSpacer) { unsetModel(); QMutexLocker lock(&m_lock); m_model = model; setSizePolicy(QSizePolicy::Preferred, addSpacer ? QSizePolicy::Preferred : QSizePolicy::Fixed); const QString paramTag = model->getAssetId(); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(paramTag)); connect(this, &AssetParameterView::updatePresets, [this, presetFile](const QString &presetName) { m_presetMenu->clear(); m_presetGroup.reset(new QActionGroup(this)); m_presetGroup->setExclusive(true); m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reset Effect"), this, SLOT(resetValues())); // Save preset m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as-template")), i18n("Save preset"), this, SLOT(slotSavePreset())); QAction *updatePreset = m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as-template")), i18n("Update current preset"), this, SLOT(slotUpdatePreset())); QAction *deletePreset = m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete preset"), this, SLOT(slotDeletePreset())); m_presetMenu->addSeparator(); QStringList presets = m_model->getPresetList(presetFile); if (presetName.isEmpty() || presets.isEmpty()) { updatePreset->setEnabled(false); deletePreset->setEnabled(false); } for (const QString &pName : presets) { QAction *ac = m_presetMenu->addAction(pName, this, SLOT(slotLoadPreset())); m_presetGroup->addAction(ac); ac->setData(pName); ac->setCheckable(true); if (pName == presetName) { ac->setChecked(true); } } }); emit updatePresets(); connect(m_model.get(), &AssetParameterModel::dataChanged, this, &AssetParameterView::refresh); if (paramTag.endsWith(QStringLiteral("lift_gamma_gain"))) { // Special case, the colorwheel widget manages several parameters QModelIndex index = model->index(0, 0); auto w = AbstractParamWidget::construct(model, index, frameSize, this); connect(w, &AbstractParamWidget::valuesChanged, this, &AssetParameterView::commitMultipleChanges); connect(w, &AbstractParamWidget::valueChanged, this, &AssetParameterView::commitChanges); m_lay->addWidget(w); connect(w, &AbstractParamWidget::updateHeight, [&, w](int h) { setFixedHeight(h + m_lay->contentsMargins().bottom()); emit updateHeight(); }); m_widgets.push_back(w); } else { int minHeight = 0; for (int i = 0; i < model->rowCount(); ++i) { QModelIndex index = model->index(i, 0); auto type = model->data(index, AssetParameterModel::TypeRole).value(); if (m_mainKeyframeWidget && (type == ParamType::Geometry || type == ParamType::Animated || type == ParamType::RestrictedAnim || type == ParamType::KeyframeParam)) { // Keyframe widget can have some extra params that shouldn't build a new widget qDebug() << "// FOUND ADDED PARAM"; m_mainKeyframeWidget->addParameter(index); } else { auto w = AbstractParamWidget::construct(model, index, frameSize, this); connect(this, &AssetParameterView::initKeyframeView, w, &AbstractParamWidget::slotInitMonitor); connect(w, &AbstractParamWidget::valueChanged, this, &AssetParameterView::commitChanges); connect(w, &AbstractParamWidget::seekToPos, this, &AssetParameterView::seekToPos); connect(w, &AbstractParamWidget::updateHeight, [&, w]() { setFixedHeight(contentHeight()); emit updateHeight(); }); m_lay->addWidget(w); if (type == ParamType::KeyframeParam || type == ParamType::AnimatedRect || type == ParamType::Roto_spline) { m_mainKeyframeWidget = static_cast(w); } else { minHeight += w->minimumHeight(); } m_widgets.push_back(w); } } setMinimumHeight(m_mainKeyframeWidget ? m_mainKeyframeWidget->minimumHeight() + minHeight : minHeight); } if (addSpacer) { m_lay->addStretch(); } } QVector> AssetParameterView::getDefaultValues() const { QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); QVector> values; for (int i = 0; i < m_model->rowCount(); ++i) { QModelIndex index = m_model->index(i, 0); QString name = m_model->data(index, AssetParameterModel::NameRole).toString(); auto type = m_model->data(index, AssetParameterModel::TypeRole).value(); QVariant defaultValue = m_model->data(index, AssetParameterModel::DefaultRole); if (type == ParamType::KeyframeParam || type == ParamType::AnimatedRect) { QString val = type == ParamType::KeyframeParam ? locale.toString(defaultValue.toDouble()) : defaultValue.toString(); if (!val.contains(QLatin1Char('='))) { val.prepend(QStringLiteral("%1=").arg(m_model->data(index, AssetParameterModel::ParentInRole).toInt())); defaultValue = QVariant(val); } } values.append({name, defaultValue}); } return values; } void AssetParameterView::resetValues() { const QVector> values = getDefaultValues(); auto *command = new AssetUpdateCommand(m_model, values); if (m_model->getOwnerId().second != -1) { pCore->pushUndo(command); } else { command->redo(); delete command; } // Unselect preset if any QAction *ac = m_presetGroup->checkedAction(); if (ac) { ac->setChecked(false);; } } void AssetParameterView::commitChanges(const QModelIndex &index, const QString &value, bool storeUndo) { // Warning: please note that some widgets (for example keyframes) do NOT send the valueChanged signal and do modifications on their own auto *command = new AssetCommand(m_model, index, value); if (storeUndo && m_model->getOwnerId().second != -1) { pCore->pushUndo(command); } else { command->redo(); delete command; } } void AssetParameterView::commitMultipleChanges(const QList indexes, const QStringList &values, bool storeUndo) { // Warning: please note that some widgets (for example keyframes) do NOT send the valueChanged signal and do modifications on their own auto *command = new AssetMultiCommand(m_model, indexes, values); if (storeUndo) { pCore->pushUndo(command); } else { command->redo(); delete command; } } void AssetParameterView::unsetModel() { QMutexLocker lock(&m_lock); if (m_model) { // if a model is already there, we have to disconnect signals first disconnect(m_model.get(), &AssetParameterModel::dataChanged, this, &AssetParameterView::refresh); } m_mainKeyframeWidget = nullptr; // clear layout m_widgets.clear(); QLayoutItem *child; while ((child = m_lay->takeAt(0)) != nullptr) { if (child->layout()) { QLayoutItem *subchild; while ((subchild = child->layout()->takeAt(0)) != nullptr) { delete subchild->widget(); delete subchild->spacerItem(); } } delete child->widget(); delete child->spacerItem(); } // Release ownership of smart pointer m_model.reset(); } void AssetParameterView::refresh(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { QMutexLocker lock(&m_lock); if (m_widgets.size() == 0) { // no visible param for this asset, abort return; } Q_UNUSED(roles); // We are expecting indexes that are children of the root index, which is "invalid" Q_ASSERT(!topLeft.parent().isValid()); // We make sure the range is valid if (m_mainKeyframeWidget) { m_mainKeyframeWidget->slotRefresh(); } else { auto type = m_model->data(m_model->index(topLeft.row(), 0), AssetParameterModel::TypeRole).value(); if (type == ParamType::ColorWheel) { // Some special widgets, like colorwheel handle multiple params so we can have cases where param index row is greater than the number of widgets. // Should be better managed m_widgets[0]->slotRefresh(); return; } size_t max; if (!bottomRight.isValid()) { max = m_widgets.size() - 1; } else { max = (size_t)bottomRight.row(); } Q_ASSERT(max < m_widgets.size()); for (size_t i = (size_t)topLeft.row(); i <= max; ++i) { m_widgets[i]->slotRefresh(); } } } int AssetParameterView::contentHeight() const { return m_lay->minimumSize().height(); } MonitorSceneType AssetParameterView::needsMonitorEffectScene() const { if (m_mainKeyframeWidget) { return m_mainKeyframeWidget->requiredScene(); } for (int i = 0; i < m_model->rowCount(); ++i) { QModelIndex index = m_model->index(i, 0); auto type = m_model->data(index, AssetParameterModel::TypeRole).value(); if (type == ParamType::Geometry) { return MonitorSceneGeometry; } } return MonitorSceneDefault; } /*void AssetParameterView::initKeyframeView() { if (m_mainKeyframeWidget) { m_mainKeyframeWidget->initMonitor(); } else { for (int i = 0; i < m_model->rowCount(); ++i) { QModelIndex index = m_model->index(i, 0); auto type = m_model->data(index, AssetParameterModel::TypeRole).value(); if (type == ParamType::Geometry) { return MonitorSceneGeometry; } } } }*/ void AssetParameterView::slotRefresh() { refresh(m_model->index(0, 0), m_model->index(m_model->rowCount() - 1, 0), {}); } bool AssetParameterView::keyframesAllowed() const { return m_mainKeyframeWidget != nullptr; } bool AssetParameterView::modelHideKeyframes() const { return m_mainKeyframeWidget != nullptr && !m_mainKeyframeWidget->keyframesVisible(); } void AssetParameterView::toggleKeyframes(bool enable) { if (m_mainKeyframeWidget) { m_mainKeyframeWidget->showKeyframes(enable); setFixedHeight(contentHeight()); emit updateHeight(); } } void AssetParameterView::slotDeletePreset() { QAction *ac = m_presetGroup->checkedAction(); if (!ac) { return; } slotDeletePreset(ac->data().toString()); } void AssetParameterView::slotDeletePreset(const QString &presetName) { if (presetName.isEmpty()) { return; } QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); if (dir.exists()) { const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId())); m_model->deletePreset(presetFile, presetName); emit updatePresets(); } } void AssetParameterView::slotUpdatePreset() { QAction *ac = m_presetGroup->checkedAction(); if (!ac) { return; } slotSavePreset(ac->data().toString()); } void AssetParameterView::slotSavePreset(QString presetName) { if (presetName.isEmpty()) { bool ok; presetName = QInputDialog::getText(this, i18n("Enter preset name"), i18n("Enter the name of this preset"), QLineEdit::Normal, QString(), &ok); if (!ok) return; } QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId())); m_model->savePreset(presetFile, presetName); emit updatePresets(presetName); } void AssetParameterView::slotLoadPreset() { auto *action = qobject_cast(sender()); if (!action) { return; } const QString presetName = action->data().toString(); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId())); const QVector> params = m_model->loadPreset(presetFile, presetName); auto *command = new AssetUpdateCommand(m_model, params); pCore->pushUndo(command); emit updatePresets(presetName); } QMenu *AssetParameterView::presetMenu() { return m_presetMenu; } diff --git a/src/assets/view/widgets/animationwidget.cpp b/src/assets/view/widgets/animationwidget.cpp index 39c6e1aaf..85cfb6adc 100644 --- a/src/assets/view/widgets/animationwidget.cpp +++ b/src/assets/view/widgets/animationwidget.cpp @@ -1,1687 +1,1686 @@ /*************************************************************************** * Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mlt++/MltAnimation.h" #include "mlt++/MltProfile.h" #include "animationwidget.h" #include "assets/model/assetparametermodel.hpp" #include "core.h" #include "kdenlivesettings.h" #include "mltcontroller/effectscontroller.h" #include "monitor/monitor.h" #include "timecodedisplay.h" #include "widgets/doublewidget.h" #include "widgets/dragvalue.h" AnimationWidget::AnimationWidget(std::shared_ptr model, QModelIndex index, QPair range, QWidget *parent) : AbstractParamWidget(std::move(model), index, parent) , m_active(false) //, m_clipPos(clipPos) , m_editedKeyframe(-1) , m_attachedToEnd(-2) //, m_xml(xml) //, m_effectId(effectId) , m_spinX(nullptr) , m_spinY(nullptr) , m_spinWidth(nullptr) , m_spinHeight(nullptr) , m_spinSize(nullptr) , m_spinOpacity(nullptr) { setAcceptDrops(true); auto *vbox2 = new QVBoxLayout(this); // Keyframe ruler if (range.second >= 0) { m_inPoint = range.first; m_outPoint = range.second; m_offset = m_model->data(m_index, AssetParameterModel::InRole).toInt(); } else { m_offset = 0; m_inPoint = m_model->data(m_index, AssetParameterModel::InRole).toInt(); m_outPoint = m_model->data(m_index, AssetParameterModel::OutRole).toInt() + 1; } m_monitorSize = pCore->getCurrentFrameSize(); m_monitor = pCore->getMonitor(m_model->monitorId); m_timePos = new TimecodeDisplay(m_monitor->timecode(), this); m_ruler = new AnimKeyframeRuler(0, m_outPoint - m_inPoint, this); connect(m_ruler, &AnimKeyframeRuler::addKeyframe, this, &AnimationWidget::slotAddKeyframe); connect(m_ruler, &AnimKeyframeRuler::removeKeyframe, this, &AnimationWidget::slotDeleteKeyframe); vbox2->addWidget(m_ruler); vbox2->setContentsMargins(0, 0, 0, 0); auto *tb = new QToolBar(this); vbox2->addWidget(tb); setLayout(vbox2); connect(m_ruler, &AnimKeyframeRuler::requestSeek, this, &AnimationWidget::requestSeek); connect(m_ruler, &AnimKeyframeRuler::moveKeyframe, this, &AnimationWidget::moveKeyframe); connect(m_timePos, SIGNAL(timeCodeEditingFinished()), this, SLOT(slotPositionChanged())); connect(m_monitor, &Monitor::seekPosition, this, &AnimationWidget::monitorSeek, Qt::UniqueConnection); if (m_frameSize.isNull() || m_frameSize.width() == 0 || m_frameSize.height() == 0) { m_frameSize = m_monitorSize; } // seek to previous m_previous = tb->addAction(QIcon::fromTheme(QStringLiteral("media-skip-backward")), i18n("Previous keyframe"), this, SLOT(slotPrevious())); // Add/remove keyframe m_addKeyframe = new KDualAction(i18n("Add keyframe"), i18n("Remove keyframe"), this); m_addKeyframe->setInactiveIcon(QIcon::fromTheme(QStringLiteral("list-add"))); m_addKeyframe->setActiveIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); connect(m_addKeyframe, SIGNAL(activeChangedByUser(bool)), this, SLOT(slotAddDeleteKeyframe(bool))); tb->addAction(m_addKeyframe); // seek to next m_next = tb->addAction(QIcon::fromTheme(QStringLiteral("media-skip-forward")), i18n("Next keyframe"), this, SLOT(slotNext())); // Preset combo m_presetCombo = new QComboBox(this); m_presetCombo->setToolTip(i18n("Presets")); connect(m_presetCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(applyPreset(int))); tb->addWidget(m_presetCombo); // Keyframe type widget m_selectType = new KSelectAction(QIcon::fromTheme(QStringLiteral("keyframes")), i18n("Keyframe interpolation"), this); QAction *discrete = new QAction(QIcon::fromTheme(QStringLiteral("discrete")), i18n("Discrete"), this); discrete->setData((int)mlt_keyframe_discrete); discrete->setCheckable(true); m_selectType->addAction(discrete); QAction *linear = new QAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Linear"), this); linear->setData((int)mlt_keyframe_linear); linear->setCheckable(true); m_selectType->addAction(linear); QAction *curve = new QAction(QIcon::fromTheme(QStringLiteral("smooth")), i18n("Smooth"), this); curve->setData((int)mlt_keyframe_smooth); curve->setCheckable(true); m_selectType->addAction(curve); m_selectType->setCurrentAction(linear); connect(m_selectType, SIGNAL(triggered(QAction *)), this, SLOT(slotEditKeyframeType(QAction *))); KSelectAction *defaultInterp = new KSelectAction(QIcon::fromTheme(QStringLiteral("keyframes")), i18n("Default interpolation"), this); discrete = new QAction(QIcon::fromTheme(QStringLiteral("discrete")), i18n("Discrete"), this); discrete->setData((int)mlt_keyframe_discrete); discrete->setCheckable(true); defaultInterp->addAction(discrete); linear = new QAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Linear"), this); linear->setData((int)mlt_keyframe_linear); linear->setCheckable(true); defaultInterp->addAction(linear); curve = new QAction(QIcon::fromTheme(QStringLiteral("smooth")), i18n("Smooth"), this); curve->setData((int)mlt_keyframe_smooth); curve->setCheckable(true); defaultInterp->addAction(curve); switch (KdenliveSettings::defaultkeyframeinterp()) { case mlt_keyframe_discrete: defaultInterp->setCurrentAction(discrete); break; case mlt_keyframe_smooth: defaultInterp->setCurrentAction(curve); break; default: defaultInterp->setCurrentAction(linear); break; } connect(defaultInterp, SIGNAL(triggered(QAction *)), this, SLOT(slotSetDefaultInterp(QAction *))); m_selectType->setToolBarMode(KSelectAction::ComboBoxMode); m_endAttach = new QAction(i18n("Attach keyframe to end"), this); m_endAttach->setCheckable(true); connect(m_endAttach, &QAction::toggled, this, &AnimationWidget::slotReverseKeyframeType); // copy/paste keyframes from clipboard QAction *copy = new QAction(i18n("Copy keyframes to clipboard"), this); connect(copy, &QAction::triggered, this, &AnimationWidget::slotCopyKeyframes); QAction *paste = new QAction(i18n("Import keyframes from clipboard"), this); connect(paste, &QAction::triggered, this, &AnimationWidget::slotImportKeyframes); QAction *removeNext = new QAction(i18n("Remove all keyframes after cursor"), this); connect(removeNext, &QAction::triggered, this, &AnimationWidget::slotRemoveNext); // save preset QAction *savePreset = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save preset"), this); connect(savePreset, &QAction::triggered, this, &AnimationWidget::savePreset); // delete preset QAction *delPreset = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete preset"), this); connect(delPreset, &QAction::triggered, this, &AnimationWidget::deletePreset); auto *container = new QMenu; tb->addAction(m_selectType); container->addAction(m_endAttach); container->addSeparator(); container->addAction(copy); container->addAction(paste); container->addAction(removeNext); container->addSeparator(); container->addAction(savePreset); container->addAction(delPreset); container->addAction(defaultInterp); auto *menuButton = new QToolButton; menuButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); menuButton->setToolTip(i18n("Options")); menuButton->setMenu(container); menuButton->setPopupMode(QToolButton::InstantPopup); tb->addWidget(menuButton); // Spacer QWidget *empty = new QWidget(); empty->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); tb->addWidget(empty); // Timecode tb->addWidget(m_timePos); m_timePos->setFrame(false); m_timePos->setRange(0, m_outPoint - m_inPoint - 1); // Prepare property mlt_profile profile = m_monitor->profile()->get_profile(); m_animProperties.set("_profile", profile, 0, nullptr, nullptr); // Display keyframe parameter addParameter(m_index); finishSetup(); // Update displayed values monitorSeek(m_monitor->position()); } AnimationWidget::~AnimationWidget() {} void AnimationWidget::finishSetup() { // Load effect presets loadPresets(); } // static QString AnimationWidget::getDefaultKeyframes(int start, const QString &defaultValue, bool linearOnly) { QString keyframes = QString::number(start); if (linearOnly) { keyframes.append(QLatin1Char('=')); } else { switch (KdenliveSettings::defaultkeyframeinterp()) { case mlt_keyframe_discrete: keyframes.append(QStringLiteral("|=")); break; case mlt_keyframe_smooth: keyframes.append(QStringLiteral("~=")); break; default: keyframes.append(QLatin1Char('=')); break; } } keyframes.append(defaultValue); return keyframes; } void AnimationWidget::updateTimecodeFormat() { m_timePos->slotUpdateTimeCodeFormat(); } void AnimationWidget::slotPrevious() { int previous = qMax(-m_offset, m_animController.previous_key(m_timePos->getValue() - m_offset - 1)) + m_offset; if (previous == m_timePos->getValue() && previous > 0) { // Make sure we can seek to effect start even if there is no keyframe previous = 0; } m_ruler->setActiveKeyframe(previous); slotPositionChanged(previous, true); } void AnimationWidget::slotNext() { int next = m_animController.next_key(m_timePos->getValue() - m_offset + 1) + m_offset; if (!m_animController.is_key(next - m_offset)) { // No keyframe after current pos, return end position next = m_timePos->maximum(); } else { m_ruler->setActiveKeyframe(next - m_offset); } slotPositionChanged(next, true); } void AnimationWidget::slotAddKeyframe(int pos) { slotAddDeleteKeyframe(true, pos); } void AnimationWidget::doAddKeyframe(int pos, QString paramName, bool directUpdate) { if (paramName.isEmpty()) { paramName = m_inTimeline; } if (pos == -1) { pos = m_timePos->getValue(); } pos -= m_offset; // Try to get previous key's type mlt_keyframe_type type; if (m_selectType->isVisible()) { type = (mlt_keyframe_type)KdenliveSettings::defaultkeyframeinterp(); if (m_animController.key_count() > 1) { int previous = m_animController.previous_key(pos); if (m_animController.is_key(previous)) { type = m_animController.keyframe_type(previous); } else { int next = m_animController.next_key(pos); if (m_animController.is_key(next)) { type = m_animController.keyframe_type(next); } } } } else { type = mlt_keyframe_linear; } if (paramName == m_rectParameter) { mlt_rect rect = m_animProperties.anim_get_rect(paramName.toUtf8().constData(), pos, m_outPoint); m_animProperties.anim_set(paramName.toUtf8().constData(), rect, pos, m_outPoint, type); } else { double val = m_animProperties.anim_get_double(paramName.toUtf8().constData(), pos, m_outPoint); m_animProperties.anim_set(paramName.toUtf8().constData(), val, pos, m_outPoint, type); } slotPositionChanged(-1, false); if (directUpdate) { m_ruler->setActiveKeyframe(pos); rebuildKeyframes(); emit valueChanged(m_index, QString(m_animController.serialize_cut()), true); } } void AnimationWidget::slotDeleteKeyframe(int pos) { slotAddDeleteKeyframe(false, pos); } void AnimationWidget::slotAddDeleteKeyframe(bool add, int pos) { if (pos == -1) { pos = m_timePos->getValue(); } QStringList paramNames = m_doubleWidgets.keys(); if (!m_rectParameter.isEmpty()) { paramNames << m_rectParameter; } if (!add) { // Delete keyframe in all animations at current pos for (int i = 0; i < paramNames.count(); i++) { m_animController = m_animProperties.get_animation(paramNames.at(i).toUtf8().constData()); if (m_animController.is_key(pos - m_offset)) { m_animController.remove(pos - m_offset); } } m_selectType->setEnabled(false); m_addKeyframe->setActive(false); slotPositionChanged(-1, false); } else { // Add keyframe in all animations for (int i = 0; i < paramNames.count(); i++) { m_animController = m_animProperties.get_animation(paramNames.at(i).toUtf8().constData()); if (!m_animController.is_key(pos - m_offset)) { doAddKeyframe(pos, paramNames.at(i), false); } } m_ruler->setActiveKeyframe(pos); } // Rebuild rebuildKeyframes(); // Send updates for (int i = 0; i < m_parameters.count(); i++) { m_animController = m_animProperties.get_animation(m_parameters.at(i).second.toUtf8().constData()); emit valueChanged(m_parameters.at(i).first, QString(m_animController.serialize_cut()), true); } // Restore default controller m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); } void AnimationWidget::slotRemoveNext() { int pos = m_timePos->getValue(); // Delete keyframe in all animations at current pos QStringList paramNames = m_doubleWidgets.keys(); if (!m_rectParameter.isEmpty()) { paramNames << m_rectParameter; } int kfrPos; for (int i = 0; i < paramNames.count(); i++) { m_animController = m_animProperties.get_animation(paramNames.at(i).toUtf8().constData()); int j = 0; while (j < m_animController.key_count()) { kfrPos = m_animController.key_get_frame(j); if (kfrPos > (pos - m_offset)) { m_animController.remove(kfrPos); } else { j++; } } } m_selectType->setEnabled(false); m_addKeyframe->setActive(false); slotPositionChanged(-1, false); // Send updates for (int i = 0; i < m_parameters.count(); i++) { m_animController = m_animProperties.get_animation(m_parameters.at(i).second.toUtf8().constData()); emit valueChanged(m_parameters.at(i).first, QString(m_animController.serialize_cut()), true); } // Restore default controller m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); // Rebuild rebuildKeyframes(); } void AnimationWidget::slotSyncPosition(int relTimelinePos) { // do only sync if this effect is keyframable if (m_timePos->maximum() > 0) { relTimelinePos = qBound(0, relTimelinePos, m_timePos->maximum()); slotPositionChanged(relTimelinePos, false); } } void AnimationWidget::moveKeyframe(int oldPos, int newPos) { bool isKey; mlt_keyframe_type type; if (m_animController.get_item(oldPos - m_offset, isKey, type) != 0) { qCDebug(KDENLIVE_LOG) << "////////ERROR NO KFR"; return; } if (!m_rectParameter.isEmpty()) { m_animController = m_animProperties.get_animation(m_rectParameter.toUtf8().constData()); mlt_rect rect = m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), oldPos - m_offset, m_outPoint); m_animController.remove(oldPos - m_offset); m_animProperties.anim_set(m_rectParameter.toUtf8().constData(), rect, newPos - m_offset, m_outPoint, type); } QStringList paramNames = m_doubleWidgets.keys(); for (int i = 0; i < paramNames.count(); i++) { const QString ¶m = paramNames.at(i); m_animController = m_animProperties.get_animation(param.toUtf8().constData()); double val = m_animProperties.anim_get_double(param.toUtf8().constData(), oldPos - m_offset, m_outPoint); m_animController.remove(oldPos - m_offset); m_animProperties.anim_set(param.toUtf8().constData(), val, newPos - m_offset, m_outPoint, type); } m_ruler->setActiveKeyframe(newPos); if (m_attachedToEnd == oldPos) { m_attachedToEnd = newPos; } rebuildKeyframes(); slotPositionChanged(m_ruler->position(), false); // Send updates for (int i = 0; i < m_parameters.count(); i++) { m_animController = m_animProperties.get_animation(m_parameters.at(i).second.toUtf8().constData()); emit valueChanged(m_parameters.at(i).first, QString(m_animController.serialize_cut()), true); } // Restore default controller m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); } void AnimationWidget::rebuildKeyframes() { // Fetch keyframes QVector keyframes; QVector types; int frame; mlt_keyframe_type type; int count = m_animController.key_count(); for (int i = 0; i < count; i++) { if (m_animController.key_get(i, frame, type) == 0) { frame += m_offset; if (frame >= 0) { keyframes << frame; types << (count > 1 ? (int)type : mlt_keyframe_linear); } } } m_ruler->updateKeyframes(keyframes, types, m_attachedToEnd); } void AnimationWidget::updateToolbar() { int pos = m_timePos->getValue(); QMapIterator i(m_doubleWidgets); while (i.hasNext()) { i.next(); double val = m_animProperties.anim_get_double(i.key().toUtf8().constData(), pos, m_outPoint); i.value()->setValue(val * i.value()->factor); } if (m_animController.is_key(pos) && m_selectType->isVisible()) { QList types = m_selectType->actions(); for (int j = 0; j < types.count(); j++) { if (types.at(j)->data().toInt() == (int)m_animController.keyframe_type(pos)) { m_selectType->setCurrentAction(types.at(j)); break; } } m_selectType->setEnabled(m_animController.key_count() > 1); m_addKeyframe->setActive(true); m_addKeyframe->setEnabled(m_animController.key_count() > 1); if (m_doubleWidgets.value(m_inTimeline) != nullptr) { m_doubleWidgets.value(m_inTimeline)->enableEdit(true); } } else { m_selectType->setEnabled(false); m_addKeyframe->setActive(false); m_addKeyframe->setEnabled(true); if (m_doubleWidgets.value(m_inTimeline) != nullptr) { m_doubleWidgets.value(m_inTimeline)->enableEdit(false); } } } void AnimationWidget::slotPositionChanged(int pos, bool seek) { if (pos == -1) { pos = m_timePos->getValue(); } else { m_timePos->setValue(pos); } m_ruler->setValue(pos); if (m_spinX) { updateRect(pos); } updateSlider(pos); m_previous->setEnabled(pos > 0); m_next->setEnabled(pos < (m_outPoint - 1)); // scene ratio lock if ((m_spinWidth != nullptr) && m_spinWidth->isEnabled()) { double ratio = m_originalSize->isChecked() ? (double)m_frameSize.width() / m_frameSize.height() : (double)m_monitorSize.width() / m_monitorSize.height(); bool lockRatio = m_spinHeight->value() == (int)(m_spinWidth->value() / ratio + 0.5); m_lockRatio->blockSignals(true); m_lockRatio->setChecked(lockRatio); m_lockRatio->blockSignals(false); m_monitor->setEffectSceneProperty(QStringLiteral("lockratio"), m_lockRatio->isChecked() ? ratio : -1); } if (seek) { m_monitor->requestSeek(pos + m_inPoint); } } void AnimationWidget::requestSeek(int pos) { m_monitor->requestSeek(pos + m_inPoint); } void AnimationWidget::updateSlider(int pos) { m_endAttach->blockSignals(true); QMapIterator i(m_doubleWidgets); while (i.hasNext()) { i.next(); m_animController = m_animProperties.get_animation(i.key().toUtf8().constData()); double val = m_animProperties.anim_get_double(i.key().toUtf8().constData(), pos, m_outPoint); if (!m_animController.is_key(pos)) { // no keyframe m_addKeyframe->setEnabled(true); if (m_animController.key_count() <= 1) { // Special case: only one keyframe, allow adjusting whatever the position is i.value()->enableEdit(true); if (!i.value()->hasEditFocus()) { i.value()->setValue(val * i.value()->factor); } if (i.key() == m_inTimeline) { if (m_active) { m_monitor->setEffectKeyframe(true); } m_endAttach->setEnabled(true); m_endAttach->setChecked(m_attachedToEnd > -2 && m_animController.key_get_frame(0) >= m_attachedToEnd); } } else { i.value()->enableEdit(false); i.value()->setValue(val * i.value()->factor); if (i.key() == m_inTimeline) { if (m_active) { m_monitor->setEffectKeyframe(false); } m_endAttach->setEnabled(false); } } if (i.key() == m_inTimeline) { m_selectType->setEnabled(false); m_addKeyframe->setActive(false); } } else { // keyframe i.value()->setValue(val * i.value()->factor); if (i.key() == m_inTimeline) { if (m_active) { m_monitor->setEffectKeyframe(true); } m_addKeyframe->setActive(true); m_addKeyframe->setEnabled(m_animController.key_count() > 1); m_selectType->setEnabled(m_animController.key_count() > 1); m_endAttach->setEnabled(true); m_endAttach->setChecked(m_attachedToEnd > -2 && pos >= m_attachedToEnd); if (m_selectType->isVisible()) { mlt_keyframe_type currentType = m_animController.keyframe_type(pos); QList types = m_selectType->actions(); for (int j = 0; j < types.count(); j++) { if ((mlt_keyframe_type)types.at(j)->data().toInt() == currentType) { m_selectType->setCurrentAction(types.at(j)); break; } } } } i.value()->enableEdit(true); } } m_endAttach->blockSignals(false); // Restore default controller m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); } void AnimationWidget::updateRect(int pos) { m_endAttach->blockSignals(true); m_animController = m_animProperties.get_animation(m_rectParameter.toUtf8().constData()); mlt_rect rect = m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), pos, m_outPoint); m_spinX->blockSignals(true); m_spinY->blockSignals(true); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinSize->blockSignals(true); m_spinX->setValue(rect.x); m_spinY->setValue(rect.y); m_spinWidth->setValue(rect.w); m_spinHeight->setValue(rect.h); double size; if (rect.w / pCore->getCurrentDar() > rect.h) { if (m_originalSize->isChecked()) { size = rect.w * 100.0 / m_frameSize.width(); } else { size = rect.w * 100.0 / m_monitorSize.width(); } } else { if (m_originalSize->isChecked()) { size = rect.h * 100.0 / m_frameSize.height(); } else { size = rect.h * 100.0 / m_monitorSize.height(); } } m_spinSize->setValue(size); if (m_spinOpacity) { m_spinOpacity->blockSignals(true); m_spinOpacity->setValue(100.0 * rect.o); m_spinOpacity->blockSignals(false); } bool enableEdit = false; if (!m_animController.is_key(pos)) { // no keyframe m_addKeyframe->setEnabled(true); if (m_animController.key_count() <= 1) { // Special case: only one keyframe, allow adjusting whatever the position is enableEdit = true; if (m_active) { m_monitor->setEffectKeyframe(true); } m_endAttach->setEnabled(true); m_endAttach->setChecked(m_attachedToEnd > -2 && m_animController.key_get_frame(0) >= m_attachedToEnd); } else { enableEdit = false; if (m_active) { m_monitor->setEffectKeyframe(false); } m_endAttach->setEnabled(false); } m_selectType->setEnabled(false); m_addKeyframe->setActive(false); } else { // keyframe enableEdit = true; if (m_active) { m_monitor->setEffectKeyframe(true); } m_addKeyframe->setActive(true); m_addKeyframe->setEnabled(m_animController.key_count() > 1); m_selectType->setEnabled(m_animController.key_count() > 1); m_endAttach->setEnabled(true); m_endAttach->setChecked(m_attachedToEnd > -2 && pos >= m_attachedToEnd); if (m_selectType->isVisible()) { mlt_keyframe_type currentType = m_animController.keyframe_type(pos); QList types = m_selectType->actions(); for (int i = 0; i < types.count(); i++) { if ((mlt_keyframe_type)types.at(i)->data().toInt() == currentType) { m_selectType->setCurrentAction(types.at(i)); break; } } } } m_spinSize->setEnabled(enableEdit); m_spinX->setEnabled(enableEdit); m_spinY->setEnabled(enableEdit); m_spinWidth->setEnabled(enableEdit); m_spinHeight->setEnabled(enableEdit); m_spinSize->setEnabled(enableEdit); if (m_spinOpacity) { m_spinOpacity->setEnabled(enableEdit); } m_spinX->blockSignals(false); m_spinY->blockSignals(false); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); m_spinSize->blockSignals(false); m_endAttach->blockSignals(false); setupMonitor(QRect(rect.x, rect.y, rect.w, rect.h)); // Restore default controller m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); } void AnimationWidget::slotEditKeyframeType(QAction *action) { int pos = m_timePos->getValue() - m_offset; if (m_animController.is_key(pos)) { for (int i = 0; i < m_parameters.count(); i++) { m_animController = m_animProperties.get_animation(m_parameters.at(i).second.toUtf8().constData()); if (m_parameters.at(i).second == m_rectParameter) { mlt_rect rect = m_animProperties.anim_get_rect(m_parameters.at(i).second.toUtf8().constData(), pos, m_outPoint); m_animProperties.anim_set(m_parameters.at(i).second.toUtf8().constData(), rect, pos, m_outPoint, (mlt_keyframe_type)action->data().toInt()); } else { double val = m_animProperties.anim_get_double(m_parameters.at(i).second.toUtf8().constData(), pos, m_outPoint); m_animProperties.anim_set(m_parameters.at(i).second.toUtf8().constData(), val, pos, m_outPoint, (mlt_keyframe_type)action->data().toInt()); } emit valueChanged(m_parameters.at(i).first, QString(m_animController.serialize_cut()), true); } rebuildKeyframes(); setupMonitor(); // Restore default controller m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); } } void AnimationWidget::slotSetDefaultInterp(QAction *action) { KdenliveSettings::setDefaultkeyframeinterp(action->data().toInt()); } void AnimationWidget::addParameter(QModelIndex ix) { // Anim properties might at some point require some more infos like profile ParamType type = (ParamType)m_model->data(ix, AssetParameterModel::TypeRole).toInt(); QString keyframes = m_model->data(ix, AssetParameterModel::ValueRole).toString(); QString paramTag = m_model->data(ix, AssetParameterModel::NameRole).toString(); m_animProperties.set(paramTag.toUtf8().constData(), keyframes.toUtf8().constData()); m_attachedToEnd = KeyframeView::checkNegatives(keyframes, m_outPoint); if (type == ParamType::Animated || type == ParamType::RestrictedAnim) { // one dimension parameter // Required to initialize anim property m_inTimeline = paramTag; m_animProperties.anim_get_int(paramTag.toUtf8().constData(), 0, m_outPoint); buildSliderWidget(paramTag, ix); } else { // one dimension parameter // TODO: multiple rect parameters in effect ? m_rectParameter = paramTag; m_inTimeline = paramTag; // Required to initialize anim property m_animProperties.anim_get_rect(paramTag.toUtf8().constData(), 0, m_outPoint); buildRectWidget(paramTag, ix); } if (type == ParamType::RestrictedAnim) { // This param only support linear keyframes m_selectType->setVisible(false); m_selectType->setCurrentItem(0); } m_parameters << QPair(ix, paramTag); // Load keyframes rebuildKeyframes(); } void AnimationWidget::buildSliderWidget(const QString ¶mTag, QModelIndex ix) { - QLocale locale; QString paramName = i18n(m_model->data(ix, Qt::DisplayRole).toString().toUtf8().data()); QString comment = m_model->data(ix, AssetParameterModel::CommentRole).toString(); if (!comment.isEmpty()) { comment = i18n(comment.toUtf8().data()); } int index = m_params.count() - 1; DoubleWidget *doubleparam = new DoubleWidget( paramName, 0, m_model->data(ix, AssetParameterModel::MinRole).toDouble(), m_model->data(ix, AssetParameterModel::MaxRole).toDouble(), m_model->data(ix, AssetParameterModel::FactorRole).toDouble() m_model->data(ix, AssetParameterModel::DefaultRole).toDouble(), comment, index, m_model->data(ix, AssetParameterModel::SuffixRole).toString(), m_model->data(ix, AssetParameterModel::DecimalsRole).toInt(), m_model->data(ix, AssetParameterModel::OddRole).toBool(), this); doubleparam->setObjectName(paramTag); doubleparam->setProperty("index", ix); connect(doubleparam, &DoubleWidget::valueChanged, this, &AnimationWidget::slotAdjustKeyframeValue); layout()->addWidget(doubleparam); // TODO: in timeline /*if ((!e.hasAttribute(QStringLiteral("intimeline")) || e.attribute(QStringLiteral("intimeline")) == QLatin1String("1")) && !e.hasAttribute(QStringLiteral("notintimeline"))) {*/ { m_inTimeline = paramTag; m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); } m_doubleWidgets.insert(paramTag, doubleparam); } void AnimationWidget::buildRectWidget(const QString ¶mTag, QModelIndex ix) { QString paramName = i18n(paramTag.toUtf8().data()); QString comment = m_model->data(ix, AssetParameterModel::CommentRole).toString(); if (!comment.isEmpty()) { comment = i18n(comment.toUtf8().data()); } auto *horLayout = new QHBoxLayout; m_spinX = new DragValue(i18nc("x axis position", "X"), 0, 0, -99000, 99000, -1, QString(), false, false, this); connect(m_spinX, &DragValue::valueChanged, this, &AnimationWidget::slotAdjustRectKeyframeValue); horLayout->addWidget(m_spinX); m_spinX->setProperty("index", ix); m_spinY = new DragValue(i18nc("y axis position", "Y"), 0, 0, -99000, 99000, -1, QString(), false, false, this); connect(m_spinY, &DragValue::valueChanged, this, &AnimationWidget::slotAdjustRectKeyframeValue); horLayout->addWidget(m_spinY); m_spinWidth = new DragValue(i18nc("Frame width", "W"), m_monitorSize.width(), 0, 1, 99000, -1, QString(), false, false, this); connect(m_spinWidth, &DragValue::valueChanged, this, &AnimationWidget::slotAdjustRectWidth); horLayout->addWidget(m_spinWidth); // Lock ratio stuff m_lockRatio = new QAction(QIcon::fromTheme(QStringLiteral("link")), i18n("Lock aspect ratio"), this); m_lockRatio->setCheckable(true); connect(m_lockRatio, &QAction::triggered, this, &AnimationWidget::slotLockRatio); auto *ratioButton = new QToolButton; ratioButton->setDefaultAction(m_lockRatio); horLayout->addWidget(ratioButton); m_spinHeight = new DragValue(i18nc("Frame height", "H"), m_monitorSize.height(), 0, 1, 99000, -1, QString(), false, false, this); connect(m_spinHeight, &DragValue::valueChanged, this, &AnimationWidget::slotAdjustRectHeight); horLayout->addWidget(m_spinHeight); horLayout->addStretch(10); auto *horLayout2 = new QHBoxLayout; m_spinSize = new DragValue(i18n("Size"), 100, 2, 1, 99000, -1, i18n("%"), false, false, this); m_spinSize->setStep(10); connect(m_spinSize, &DragValue::valueChanged, this, &AnimationWidget::slotResize); horLayout2->addWidget(m_spinSize); if (m_model->data(ix, AssetParameterModel::OpacityRole).toBool()) { m_spinOpacity = new DragValue(i18n("Opacity"), 100, 0, 0, 100, -1, i18n("%"), true, false, this); connect(m_spinOpacity, &DragValue::valueChanged, this, &AnimationWidget::slotAdjustRectKeyframeValue); horLayout2->addWidget(m_spinOpacity); } // Build buttons m_originalSize = new QAction(QIcon::fromTheme(QStringLiteral("zoom-original")), i18n("Adjust to original size"), this); connect(m_originalSize, &QAction::triggered, this, &AnimationWidget::slotAdjustToSource); m_originalSize->setCheckable(true); QAction *adjustSize = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Adjust and center in frame"), this); connect(adjustSize, &QAction::triggered, this, &AnimationWidget::slotAdjustToFrameSize); QAction *fitToWidth = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Fit to width"), this); connect(fitToWidth, &QAction::triggered, this, &AnimationWidget::slotFitToWidth); QAction *fitToHeight = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-height")), i18n("Fit to height"), this); connect(fitToHeight, &QAction::triggered, this, &AnimationWidget::slotFitToHeight); QAction *alignleft = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-left")), i18n("Align left"), this); connect(alignleft, &QAction::triggered, this, &AnimationWidget::slotMoveLeft); QAction *alignhcenter = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-hor")), i18n("Center horizontally"), this); connect(alignhcenter, &QAction::triggered, this, &AnimationWidget::slotCenterH); QAction *alignright = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-right")), i18n("Align right"), this); connect(alignright, &QAction::triggered, this, &AnimationWidget::slotMoveRight); QAction *aligntop = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-top")), i18n("Align top"), this); connect(aligntop, &QAction::triggered, this, &AnimationWidget::slotMoveTop); QAction *alignvcenter = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-vert")), i18n("Center vertically"), this); connect(alignvcenter, &QAction::triggered, this, &AnimationWidget::slotCenterV); QAction *alignbottom = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-bottom")), i18n("Align bottom"), this); connect(alignbottom, &QAction::triggered, this, &AnimationWidget::slotMoveBottom); auto *alignLayout = new QHBoxLayout; alignLayout->setSpacing(0); auto *alignButton = new QToolButton; alignButton->setDefaultAction(alignleft); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(alignhcenter); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(alignright); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(aligntop); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(alignvcenter); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(alignbottom); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(m_originalSize); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(adjustSize); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(fitToWidth); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(fitToHeight); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignLayout->addStretch(10); static_cast(layout())->addLayout(horLayout); static_cast(layout())->addLayout(alignLayout); static_cast(layout())->addLayout(horLayout2); m_animController = m_animProperties.get_animation(paramTag.toUtf8().constData()); } void AnimationWidget::slotUpdateVisibleParameter(bool display) { if (!display) { return; } DoubleWidget *slider = qobject_cast(QObject::sender()); if (slider) { if (slider->objectName() == m_inTimeline) { return; } m_inTimeline = slider->objectName(); m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); rebuildKeyframes(); emit valueChanged(m_index, QString(m_animController.serialize_cut()), true); } } void AnimationWidget::slotAdjustKeyframeValue(double value) { DoubleWidget *slider = qobject_cast(QObject::sender()); if (!slider) { return; } m_inTimeline = slider->objectName(); QModelIndex ix = slider->property("index").toModelIndex(); m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); int pos = m_ruler->position() - m_offset; mlt_keyframe_type type = m_selectType->isVisible() ? (m_selectType->isEnabled() ? (mlt_keyframe_type)m_selectType->currentAction()->data().toInt() : (mlt_keyframe_type)KdenliveSettings::defaultkeyframeinterp()) : mlt_keyframe_linear; if (m_animController.is_key(pos)) { // This is a keyframe type = m_animController.keyframe_type(pos); m_animProperties.anim_set(m_inTimeline.toUtf8().constData(), value / slider->factor, pos, m_outPoint, type); emit valueChanged(ix, QString(m_animController.serialize_cut()), true); } else if (m_animController.key_count() <= 1) { pos = m_animController.key_get_frame(0); if (pos >= 0) { if (m_animController.is_key(pos)) { type = m_animController.keyframe_type(pos); } m_animProperties.anim_set(m_inTimeline.toUtf8().constData(), value / slider->factor, pos, m_outPoint, type); emit valueChanged(ix, QString(m_animController.serialize_cut()), true); } } } void AnimationWidget::slotAdjustRectKeyframeValue() { m_animController = m_animProperties.get_animation(m_rectParameter.toUtf8().constData()); m_inTimeline = m_rectParameter; int pos = m_ruler->position() - m_offset; mlt_rect rect; rect.x = m_spinX->value(); rect.y = m_spinY->value(); rect.w = m_spinWidth->value(); rect.h = m_spinHeight->value(); rect.o = m_spinOpacity ? m_spinOpacity->value() / 100.0 : DBL_MIN; double size; if (m_spinWidth->value() / pCore->getCurrentDar() > m_spinHeight->value()) { if (m_originalSize->isChecked()) { size = m_spinWidth->value() * 100.0 / m_frameSize.width(); } else { size = m_spinWidth->value() * 100.0 / m_monitorSize.width(); } } else { if (m_originalSize->isChecked()) { size = m_spinHeight->value() * 100.0 / m_frameSize.height(); } else { size = m_spinHeight->value() * 100.0 / m_monitorSize.height(); } } m_spinSize->blockSignals(true); m_spinSize->setValue(size); m_spinSize->blockSignals(false); if (m_animController.is_key(pos)) { // This is a keyframe m_animProperties.anim_set(m_rectParameter.toUtf8().constData(), rect, pos, m_outPoint, (mlt_keyframe_type)m_selectType->currentAction()->data().toInt()); emit valueChanged(m_index, QString(m_animController.serialize_cut()), true); setupMonitor(QRect(rect.x, rect.y, rect.w, rect.h)); } else if (m_animController.key_count() <= 1) { pos = m_animController.key_get_frame(0); if (pos >= 0) { m_animProperties.anim_set(m_rectParameter.toUtf8().constData(), rect, pos, m_outPoint, (mlt_keyframe_type)m_selectType->currentAction()->data().toInt()); emit valueChanged(m_index, QString(m_animController.serialize_cut()), true); setupMonitor(QRect(rect.x, rect.y, rect.w, rect.h)); } } } void AnimationWidget::slotResize(double value) { m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); int w = m_originalSize->isChecked() ? m_frameSize.width() : m_monitorSize.width(); int h = m_originalSize->isChecked() ? m_frameSize.height() : m_monitorSize.height(); m_spinWidth->setValue(w * value / 100.0); m_spinHeight->setValue(h * value / 100.0); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); setupMonitor(); } bool AnimationWidget::isActive(const QString &name) const { return name == m_inTimeline; } const QMap AnimationWidget::getAnimation() { QMap animationResults; if (m_spinX) { m_animController = m_animProperties.get_animation(m_rectParameter.toUtf8().constData()); // TODO: keyframes attached to end animationResults.insert(m_rectParameter, m_animController.serialize_cut()); } QMapIterator i(m_doubleWidgets); while (i.hasNext()) { i.next(); m_animController = m_animProperties.get_animation(i.key().toUtf8().constData()); // no negative keyframe trick, return simple serialize if (m_attachedToEnd == -2) { animationResults.insert(i.key(), m_animController.serialize_cut()); } else { // Do processing ourselves to include negative values for keyframes relative to end int pos; mlt_keyframe_type type; QString key; QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); QStringList result; int duration = m_outPoint; for (int j = 0; j < m_animController.key_count(); ++j) { m_animController.key_get(j, pos, type); double val = m_animProperties.anim_get_double(i.key().toUtf8().constData(), pos, duration); if (pos >= m_attachedToEnd) { pos = qMin(pos - duration, -1); } key = QString::number(pos); switch (type) { case mlt_keyframe_discrete: key.append(QStringLiteral("|=")); break; case mlt_keyframe_smooth: key.append(QStringLiteral("~=")); break; default: key.append(QStringLiteral("=")); break; } key.append(locale.toString(val)); result << key; } animationResults.insert(i.key(), result.join(QLatin1Char(';'))); } } // restore original controller m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); return animationResults; } void AnimationWidget::slotReverseKeyframeType(bool reverse) { int pos = m_timePos->getValue(); if (m_animController.is_key(pos)) { if (reverse) { m_attachedToEnd = pos; } else { m_attachedToEnd = -2; } rebuildKeyframes(); emit valueChanged(m_index, QString(m_animController.serialize_cut()), true); } } void AnimationWidget::loadPresets(QString currentText) { m_presetCombo->blockSignals(true); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); if (currentText.isEmpty()) { currentText = m_presetCombo->currentText(); } while (m_presetCombo->count() > 0) { m_presetCombo->removeItem(0); } m_presetCombo->removeItem(0); QMap defaultEntry; QStringList paramNames = m_doubleWidgets.keys(); for (int i = 0; i < paramNames.count(); i++) { defaultEntry.insert(paramNames.at(i), getDefaultKeyframes(m_offset, m_model->data(m_index, AssetParameterModel::DefaultRole).toString())); } m_presetCombo->addItem(i18n("Default"), defaultEntry); loadPreset(dir.absoluteFilePath(m_model->data(m_index, AssetParameterModel::TypeRole).toString())); loadPreset(dir.absoluteFilePath(m_effectId)); if (!currentText.isEmpty()) { int ix = m_presetCombo->findText(currentText); if (ix >= 0) { m_presetCombo->setCurrentIndex(ix); } } m_presetCombo->blockSignals(false); } void AnimationWidget::loadPreset(const QString &path) { KConfig confFile(path, KConfig::SimpleConfig); const QStringList groups = confFile.groupList(); for (const QString &grp : groups) { QMap entries = KConfigGroup(&confFile, grp).entryMap(); QMap variantEntries; QMapIterator i(entries); while (i.hasNext()) { i.next(); variantEntries.insert(i.key(), i.value()); } m_presetCombo->addItem(grp, variantEntries); } } void AnimationWidget::applyPreset(int ix) { QMap entries = m_presetCombo->itemData(ix).toMap(); QStringList presetNames = entries.keys(); QStringList paramNames = m_doubleWidgets.keys(); for (int i = 0; i < paramNames.count() && i < presetNames.count(); i++) { QString keyframes = entries.value(paramNames.at(i)).toString(); if (!keyframes.isEmpty()) { m_animProperties.set(paramNames.at(i).toUtf8().constData(), keyframes.toUtf8().constData()); // Required to initialize anim property m_animProperties.anim_get_int(m_inTimeline.toUtf8().constData(), 0, m_outPoint); } } if (!m_rectParameter.isEmpty()) { QString keyframes = entries.value(m_rectParameter).toString(); if (!keyframes.isEmpty()) { m_animProperties.set(m_rectParameter.toUtf8().constData(), keyframes.toUtf8().constData()); m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), 0, m_outPoint); } } m_animController = m_animProperties.get_animation(m_inTimeline.toUtf8().constData()); rebuildKeyframes(); emit valueChanged(m_index, QString(m_animController.serialize_cut()), true); } void AnimationWidget::savePreset() { QDialog d(this); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Save); auto *l = new QVBoxLayout; d.setLayout(l); QLineEdit effectName(&d); effectName.setPlaceholderText(i18n("Enter preset name")); QCheckBox cb(i18n("Save as global preset (available to all effects)"), &d); l->addWidget(&effectName); l->addWidget(&cb); l->addWidget(buttonBox); d.connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::reject); d.connect(buttonBox, &QDialogButtonBox::accepted, &d, &QDialog::accept); if (d.exec() != QDialog::Accepted) { return; } QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } QString fileName = cb.isChecked() ? dir.absoluteFilePath(m_xml.attribute(QStringLiteral("type"))) : m_effectId; KConfig confFile(dir.absoluteFilePath(fileName), KConfig::SimpleConfig); KConfigGroup grp(&confFile, effectName.text()); // Parse keyframes QMap currentKeyframes = getAnimation(); QMapIterator i(currentKeyframes); while (i.hasNext()) { i.next(); grp.writeEntry(i.key(), i.value()); } confFile.sync(); loadPresets(effectName.text()); } void AnimationWidget::deletePreset() { QString effectName = m_presetCombo->currentText(); // try deleting as effect preset first QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); KConfig confFile(dir.absoluteFilePath(m_effectId), KConfig::SimpleConfig); KConfigGroup grp(&confFile, effectName); if (grp.exists()) { } else { // try global preset grp = KConfigGroup(&confFile, m_xml.attribute(QStringLiteral("type"))); } grp.deleteGroup(); confFile.sync(); loadPresets(); } void AnimationWidget::setActiveKeyframe(int frame) { m_ruler->setActiveKeyframe(frame); } void AnimationWidget::slotUpdateGeometryRect(const QRect r) { int pos = m_timePos->getValue(); if (!m_animController.is_key(pos)) { // no keyframe if (m_animController.key_count() <= 1) { // Special case: only one keyframe, allow adjusting whatever the position is pos = m_animController.key_get_frame(0); if (pos < 0) { // error qCDebug(KDENLIVE_LOG) << "* * *NOT on a keyframe, something is wrong"; return; } } } m_spinX->blockSignals(true); m_spinY->blockSignals(true); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinX->setValue(r.x()); m_spinY->setValue(r.y()); m_spinWidth->setValue(r.width()); m_spinHeight->setValue(r.height()); m_spinX->blockSignals(false); m_spinY->blockSignals(false); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); setupMonitor(); } void AnimationWidget::slotUpdateCenters(const QVariantList ¢ers) { if (centers.count() != m_animController.key_count()) { qCDebug(KDENLIVE_LOG) << "* * * *CENTER POINTS MISMATCH, aborting edit"; } int pos; mlt_keyframe_type type; for (int i = 0; i < m_animController.key_count(); ++i) { m_animController.key_get(i, pos, type); mlt_rect rect = m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), pos, m_outPoint); // Center rect to new pos QPointF offset = centers.at(i).toPointF() - QPointF(rect.x + rect.w / 2, rect.y + rect.h / 2); rect.x += offset.x(); rect.y += offset.y(); m_animProperties.anim_set(m_rectParameter.toUtf8().constData(), rect, pos, m_outPoint, type); } slotAdjustRectKeyframeValue(); } void AnimationWidget::setupMonitor(QRect r) { QVariantList points; QVariantList types; int pos; mlt_keyframe_type type; for (int j = 0; j < m_animController.key_count(); ++j) { m_animController.key_get(j, pos, type); if (m_animController.key_get_type(j) == mlt_keyframe_smooth) { types << 1; } else { types << 0; } mlt_rect rect = m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), pos, m_outPoint); QRectF frameRect(rect.x, rect.y, rect.w, rect.h); points.append(QVariant(frameRect.center())); } if (m_active) { m_monitor->setUpEffectGeometry(r, points, types); } } void AnimationWidget::slotSeekToKeyframe(int ix) { int pos = m_animController.key_get_frame(ix); slotPositionChanged(pos, true); } void AnimationWidget::connectMonitor(bool activate) { if (m_active == activate) { return; } m_active = activate; if (!m_spinX) { // No animated rect displayed in monitor, return return; } if (activate) { connect(m_monitor, &Monitor::effectChanged, this, &AnimationWidget::slotUpdateGeometryRect, Qt::UniqueConnection); connect(m_monitor, &Monitor::effectPointsChanged, this, &AnimationWidget::slotUpdateCenters, Qt::UniqueConnection); connect(m_monitor, &Monitor::seekToKeyframe, this, &AnimationWidget::slotSeekToKeyframe, Qt::UniqueConnection); connect(m_monitor, &Monitor::seekToNextKeyframe, this, &AnimationWidget::slotNext, Qt::UniqueConnection); connect(m_monitor, &Monitor::seekToPreviousKeyframe, this, &AnimationWidget::slotPrevious, Qt::UniqueConnection); connect(m_monitor, SIGNAL(addKeyframe()), this, SLOT(slotAddKeyframe()), Qt::UniqueConnection); connect(m_monitor, SIGNAL(deleteKeyframe()), this, SLOT(slotDeleteKeyframe()), Qt::UniqueConnection); int framePos = qBound(0, m_monitor->position() - m_inPoint, m_timePos->maximum()); slotPositionChanged(framePos, false); double ratio = (double)m_spinWidth->value() / m_spinHeight->value(); if (m_frameSize.width() != m_monitorSize.width() || m_frameSize.height() != m_monitorSize.height()) { // Source frame size different than project frame size, enable original size option accordingly bool isOriginalSize = qAbs((double)m_frameSize.width() / m_frameSize.height() - ratio) < qAbs((double)m_monitorSize.width() / m_monitorSize.height() - ratio); if (isOriginalSize) { m_originalSize->blockSignals(true); m_originalSize->setChecked(true); m_originalSize->blockSignals(false); } } } else { disconnect(m_monitor, &Monitor::effectChanged, this, &AnimationWidget::slotUpdateGeometryRect); disconnect(m_monitor, &Monitor::effectPointsChanged, this, &AnimationWidget::slotUpdateCenters); disconnect(m_monitor, SIGNAL(addKeyframe()), this, SLOT(slotAddKeyframe())); disconnect(m_monitor, SIGNAL(deleteKeyframe()), this, SLOT(slotDeleteKeyframe())); disconnect(m_monitor, &Monitor::seekToNextKeyframe, this, &AnimationWidget::slotNext); disconnect(m_monitor, &Monitor::seekToPreviousKeyframe, this, &AnimationWidget::slotPrevious); disconnect(m_monitor, &Monitor::seekToKeyframe, this, &AnimationWidget::slotSeekToKeyframe); } } void AnimationWidget::offsetAnimation(int offset) { int pos = 0; mlt_keyframe_type type; QString offsetAnimation = QStringLiteral("kdenliveOffset"); if (m_spinX) { m_animController = m_animProperties.get_animation(m_rectParameter.toUtf8().constData()); for (int j = 0; j < m_animController.key_count(); ++j) { m_animController.key_get(j, pos, type); mlt_rect rect = m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), pos, m_outPoint); m_animProperties.anim_set(offsetAnimation.toUtf8().constData(), rect, pos + offset, m_outPoint, type); } Mlt::Animation offsetAnim = m_animProperties.get_animation(offsetAnimation.toUtf8().constData()); m_animProperties.set(m_rectParameter.toUtf8().constData(), offsetAnim.serialize_cut()); // Required to initialize anim property m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), 0, m_outPoint); m_animProperties.set(offsetAnimation.toUtf8().constData(), ""); } QMapIterator i(m_doubleWidgets); while (i.hasNext()) { i.next(); m_animController = m_animProperties.get_animation(i.key().toUtf8().constData()); for (int j = 0; j < m_animController.key_count(); ++j) { m_animController.key_get(j, pos, type); double val = m_animProperties.anim_get_double(i.key().toUtf8().constData(), pos, m_outPoint); m_animProperties.anim_set(offsetAnimation.toUtf8().constData(), val, pos + offset, m_outPoint, type); } Mlt::Animation offsetAnim = m_animProperties.get_animation(offsetAnimation.toUtf8().constData()); m_animProperties.set(i.key().toUtf8().constData(), offsetAnim.serialize_cut()); // Required to initialize anim property m_animProperties.anim_get_int(i.key().toUtf8().constData(), 0, m_outPoint); m_animProperties.set(offsetAnimation.toUtf8().constData(), ""); } m_offset -= offset; } void AnimationWidget::reload(const QString &tag, const QString &data) { m_animProperties.set(tag.toUtf8().constData(), data.toUtf8().constData()); if (tag == m_rectParameter) { m_animProperties.anim_get_rect(tag.toUtf8().constData(), 0, m_outPoint); } else { m_animProperties.anim_get_int(tag.toUtf8().constData(), 0, m_outPoint); m_attachedToEnd = KeyframeView::checkNegatives(data, m_outPoint); m_inTimeline = tag; } // Also add keyframes positions in other parameters QStringList paramNames = m_doubleWidgets.keys(); - QLocale locale; m_animController = m_animProperties.get_animation(tag.toUtf8().constData()); for (int i = 0; i < paramNames.count(); i++) { const QString ¤tParam = paramNames.at(i); if (currentParam == tag) { continue; } // simple anim parameter, get default value double def = m_model->data(m_index, AssetParameterModel::DefaultRole).toDouble(); // Clear current keyframes m_animProperties.set(currentParam.toUtf8().constData(), ""); // Add default keyframes int pos; mlt_keyframe_type type; for (int j = 0; j < m_animController.key_count(); ++j) { m_animController.key_get(j, pos, type); m_animProperties.anim_set(currentParam.toUtf8().constData(), def, pos, m_outPoint, type); } m_animProperties.anim_get_int(currentParam.toUtf8().constData(), 0, m_outPoint); } if (!m_rectParameter.isEmpty() && tag != m_rectParameter) { // reset geometry keyframes // simple anim parameter, get default value QString def = m_model->data(m_index, AssetParameterModel::DefaultRole).toString(); // Clear current keyframes m_animProperties.set(m_rectParameter.toUtf8().constData(), def.toUtf8().constData()); // Add default keyframes int pos; mlt_keyframe_type type; m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), 0, m_outPoint); mlt_rect rect = m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), 0, m_outPoint); for (int j = 0; j < m_animController.key_count(); ++j) { m_animController.key_get(j, pos, type); m_animProperties.anim_set(m_rectParameter.toUtf8().constData(), rect, pos, m_outPoint, type); } m_animProperties.anim_get_rect(m_rectParameter.toUtf8().constData(), 0, m_outPoint); } rebuildKeyframes(); emit valueChanged(m_index, QString(m_animController.serialize_cut()), true); } QString AnimationWidget::defaultValue(const QString ¶mName) { QStringList paramNames = m_doubleWidgets.keys(); for (int i = 0; i < paramNames.count(); i++) { if (m_params.at(i).attribute(QStringLiteral("name")) == paramName) { QString def = m_params.at(i).attribute(QStringLiteral("default")); if (def.contains(QLatin1Char('%'))) { def = EffectsController::getStringRectEval(def); } return def; } } return QString(); } void AnimationWidget::slotAdjustToSource() { m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinWidth->setValue((int)(m_frameSize.width() / pCore->getCurrentSar() + 0.5), false); m_spinHeight->setValue(m_frameSize.height(), false); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); if (m_lockRatio->isChecked()) { m_monitor->setEffectSceneProperty(QStringLiteral("lockratio"), m_originalSize->isChecked() ? (double)m_frameSize.width() / m_frameSize.height() : (double)m_monitorSize.width() / m_monitorSize.height()); } } void AnimationWidget::slotAdjustToFrameSize() { double monitorDar = pCore->getCurrentDar(); double sourceDar = m_frameSize.width() / m_frameSize.height(); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); if (sourceDar > monitorDar) { // Fit to width double factor = (double)m_monitorSize.width() / m_frameSize.width() * pCore->getCurrentSar(); m_spinHeight->setValue((int)(m_frameSize.height() * factor + 0.5)); m_spinWidth->setValue(m_monitorSize.width()); // Center m_spinY->blockSignals(true); m_spinY->setValue((m_monitorSize.height() - m_spinHeight->value()) / 2); m_spinY->blockSignals(false); } else { // Fit to height double factor = (double)m_monitorSize.height() / m_frameSize.height(); m_spinHeight->setValue(m_monitorSize.height()); m_spinWidth->setValue((int)(m_frameSize.width() / pCore->getCurrentSar() * factor + 0.5)); // Center m_spinX->blockSignals(true); m_spinX->setValue((m_monitorSize.width() - m_spinWidth->value()) / 2); m_spinX->blockSignals(false); } m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); } void AnimationWidget::slotFitToWidth() { double factor = (double)m_monitorSize.width() / m_frameSize.width() * pCore->getCurrentSar(); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinHeight->setValue((int)(m_frameSize.height() * factor + 0.5)); m_spinWidth->setValue(m_monitorSize.width()); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); } void AnimationWidget::slotFitToHeight() { double factor = (double)m_monitorSize.height() / m_frameSize.height(); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinHeight->setValue(m_monitorSize.height()); m_spinWidth->setValue((int)(m_frameSize.width() / pCore->getCurrentSar() * factor + 0.5)); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); } void AnimationWidget::slotMoveLeft() { m_spinX->setValue(0); } void AnimationWidget::slotCenterH() { m_spinX->setValue((m_monitorSize.width() - m_spinWidth->value()) / 2); } void AnimationWidget::slotMoveRight() { m_spinX->setValue(m_monitorSize.width() - m_spinWidth->value()); } void AnimationWidget::slotMoveTop() { m_spinY->setValue(0); } void AnimationWidget::slotCenterV() { m_spinY->setValue((m_monitorSize.height() - m_spinHeight->value()) / 2); } void AnimationWidget::slotMoveBottom() { m_spinY->setValue(m_monitorSize.height() - m_spinHeight->value()); } void AnimationWidget::slotCopyKeyframes() { const QMap anims = getAnimation(); if (anims.isEmpty()) { return; } QString result; QMapIterator i(anims); while (i.hasNext()) { i.next(); result.append(i.key() + QLatin1Char('=') + i.value() + QLatin1Char('\n')); } QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(result); } void AnimationWidget::slotImportKeyframes() { QClipboard *clipboard = QApplication::clipboard(); QString values = clipboard->text(); emit setKeyframes(values); } void AnimationWidget::slotLockRatio() { QAction *lockRatio = qobject_cast(QObject::sender()); if (lockRatio->isChecked()) { m_monitor->setEffectSceneProperty(QStringLiteral("lockratio"), m_originalSize->isChecked() ? (double)m_frameSize.width() / m_frameSize.height() : (double)m_monitorSize.width() / m_monitorSize.height()); } else { m_monitor->setEffectSceneProperty(QStringLiteral("lockratio"), -1); } } void AnimationWidget::slotAdjustRectWidth() { if (m_lockRatio->isChecked()) { m_spinHeight->blockSignals(true); if (m_originalSize->isChecked()) { m_spinHeight->setValue((int)(m_spinWidth->value() * m_frameSize.height() / m_frameSize.width() + 0.5)); } else { m_spinHeight->setValue((int)(m_spinWidth->value() * m_monitorSize.height() / m_monitorSize.width() + 0.5)); } m_spinHeight->blockSignals(false); } slotAdjustRectKeyframeValue(); } void AnimationWidget::slotAdjustRectHeight() { if (m_lockRatio->isChecked()) { m_spinWidth->blockSignals(true); if (m_originalSize->isChecked()) { m_spinWidth->setValue((int)(m_spinHeight->value() * m_frameSize.width() / m_frameSize.height() + 0.5)); } else { m_spinWidth->setValue((int)(m_spinHeight->value() * m_monitorSize.width() / m_monitorSize.height() + 0.5)); } m_spinWidth->blockSignals(false); } slotAdjustRectKeyframeValue(); } void AnimationWidget::monitorSeek(int pos) { slotPositionChanged(pos - m_inPoint, false); if (m_spinX) { // Update monitor scene for geometry params if (pos > m_inPoint && pos < m_outPoint) { if (!m_active) { connectMonitor(true); m_monitor->slotShowEffectScene(MonitorSceneGeometry); } } else { if (m_active) { connectMonitor(false); m_monitor->slotShowEffectScene(MonitorSceneDefault); } } } } void AnimationWidget::slotShowComment(bool show) {} void AnimationWidget::slotRefresh() { for (int i = 0; i < m_parameters.count(); i++) { QModelIndex ix = m_parameters.at(i).first; ParamType type = (ParamType)m_model->data(ix, AssetParameterModel::TypeRole).toInt(); QString keyframes = m_model->data(ix, AssetParameterModel::ValueRole).toString(); QString paramTag = m_model->data(ix, AssetParameterModel::NameRole).toString(); m_animProperties.set(paramTag.toUtf8().constData(), keyframes.toUtf8().constData()); m_attachedToEnd = KeyframeView::checkNegatives(keyframes, m_outPoint); if (type == ParamType::Animated || type == ParamType::RestrictedAnim) { // one dimension parameter // Required to initialize anim property m_animProperties.anim_get_int(paramTag.toUtf8().constData(), 0, m_outPoint); } else { // rect parameter // TODO: multiple rect parameters in effect ? m_rectParameter = paramTag; m_inTimeline = paramTag; // Required to initialize anim property m_animProperties.anim_get_rect(paramTag.toUtf8().constData(), 0, m_outPoint); } } rebuildKeyframes(); monitorSeek(m_monitor->position()); } void AnimationWidget::slotSetRange(QPair range) { m_inPoint = range.first; m_outPoint = range.second; m_offset = m_model->data(m_index, AssetParameterModel::InRole).toInt(); m_ruler->setRange(0, m_outPoint - m_inPoint); m_timePos->setRange(0, m_outPoint - m_inPoint - 1); } diff --git a/src/assets/view/widgets/keyframeimport.cpp b/src/assets/view/widgets/keyframeimport.cpp index 61a9eefd9..f8fdff149 100644 --- a/src/assets/view/widgets/keyframeimport.cpp +++ b/src/assets/view/widgets/keyframeimport.cpp @@ -1,757 +1,758 @@ /*************************************************************************** * Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "klocalizedstring.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "assets/keyframes/view/keyframeview.hpp" #include "core.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "keyframeimport.h" #include "profiles/profilemodel.hpp" #include "widgets/positionwidget.h" #include #include "mlt++/MltAnimation.h" #include "mlt++/MltProperties.h" KeyframeImport::KeyframeImport(const QString &animData, std::shared_ptr model, QList indexes, QWidget *parent) : QDialog(parent) , m_model(std::move(model)) , m_indexes(indexes) , m_supportsAnim(false) , m_previewLabel(nullptr) , m_isReady(false) { auto *lay = new QVBoxLayout(this); auto *l1 = new QHBoxLayout; QLabel *lab = new QLabel(i18n("Data to import:"), this); l1->addWidget(lab); m_dataCombo = new QComboBox(this); l1->addWidget(m_dataCombo); l1->addStretch(10); lay->addLayout(l1); int in = -1; int out = -1; // Set up data auto json = QJsonDocument::fromJson(animData.toUtf8()); if (!json.isArray()) { qDebug() << "Error : Json file should be an array"; // Try to build data from a single value QJsonArray list; QJsonObject currentParam; currentParam.insert(QLatin1String("name"), QStringLiteral("data")); currentParam.insert(QLatin1String("value"), animData); bool ok; QString firstFrame = animData.section(QLatin1Char('='), 0, 0); in = firstFrame.toInt(&ok); if (!ok) { firstFrame.chop(1); in = firstFrame.toInt(&ok); } QString first = animData.section(QLatin1Char('='), 1, 1); if (!first.isEmpty()) { int spaces = first.count(QLatin1Char(' ')); switch (spaces) { case 0: currentParam.insert(QLatin1String("type"), QJsonValue((int)ParamType::Animated)); break; default: currentParam.insert(QLatin1String("type"), QJsonValue((int)ParamType::AnimatedRect)); break; } //currentParam.insert(QLatin1String("min"), QJsonValue(min)); //currentParam.insert(QLatin1String("max"), QJsonValue(max)); list.push_back(currentParam); json = QJsonDocument(list); } } if (!json.isArray()) { qDebug() << "Error : Json file should be an array"; return; } auto list = json.array(); int ix = 0; for (const auto &entry : list) { if (!entry.isObject()) { qDebug() << "Warning : Skipping invalid marker data"; continue; } auto entryObj = entry.toObject(); if (!entryObj.contains(QLatin1String("name"))) { qDebug() << "Warning : Skipping invalid marker data (does not contain name)"; continue; } QString name = entryObj[QLatin1String("name")].toString(); QString value = entryObj[QLatin1String("value")].toString(); int type = entryObj[QLatin1String("type")].toInt(0); double min = entryObj[QLatin1String("min")].toDouble(0); double max = entryObj[QLatin1String("max")].toDouble(0); if (in == -1) { in = entryObj[QLatin1String("in")].toInt(0); } if (out == -1) { out = entryObj[QLatin1String("out")].toInt(0); } m_dataCombo->insertItem(ix, name); m_dataCombo->setItemData(ix, value, Qt::UserRole); m_dataCombo->setItemData(ix, type, Qt::UserRole + 1); m_dataCombo->setItemData(ix, min, Qt::UserRole + 2); m_dataCombo->setItemData(ix, max, Qt::UserRole + 3); ix++; } m_previewLabel = new QLabel(this); m_previewLabel->setMinimumSize(100, 150); m_previewLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_previewLabel->setScaledContents(true); lay->addWidget(m_previewLabel); // Zone in / out in = qMax(0, in); if (out <= 0) { out = in + m_model->data(indexes.first(), AssetParameterModel::ParentDurationRole).toInt(); } m_inPoint = new PositionWidget(i18n("In"), in, 0, out, pCore->currentDoc()->timecode(), QString(), this); connect(m_inPoint, &PositionWidget::valueChanged, this, &KeyframeImport::updateDisplay); lay->addWidget(m_inPoint); m_outPoint = new PositionWidget(i18n("Out"), out, in, out, pCore->currentDoc()->timecode(), QString(), this); connect(m_outPoint, &PositionWidget::valueChanged, this, &KeyframeImport::updateDisplay); lay->addWidget(m_outPoint); // Check what kind of parameters are in our target for (const QPersistentModelIndex &idx : indexes) { auto type = m_model->data(idx, AssetParameterModel::TypeRole).value(); if (type == ParamType::KeyframeParam) { m_simpleTargets.insert(m_model->data(idx, Qt::DisplayRole).toString(), idx); } else if (type == ParamType::AnimatedRect) { m_geometryTargets.insert(m_model->data(idx, Qt::DisplayRole).toString(), idx); } } l1 = new QHBoxLayout; m_targetCombo = new QComboBox(this); m_sourceCombo = new QComboBox(this); ix = 0; /*if (!m_geometryTargets.isEmpty()) { m_sourceCombo->insertItem(ix, i18n("Geometry")); m_sourceCombo->setItemData(ix, QString::number(10), Qt::UserRole); ix++; m_sourceCombo->insertItem(ix, i18n("Position")); m_sourceCombo->setItemData(ix, QString::number(11), Qt::UserRole); ix++; } if (!m_simpleTargets.isEmpty()) { m_sourceCombo->insertItem(ix, i18n("X")); m_sourceCombo->setItemData(ix, QString::number(0), Qt::UserRole); ix++; m_sourceCombo->insertItem(ix, i18n("Y")); m_sourceCombo->setItemData(ix, QString::number(1), Qt::UserRole); ix++; m_sourceCombo->insertItem(ix, i18n("Width")); m_sourceCombo->setItemData(ix, QString::number(2), Qt::UserRole); ix++; m_sourceCombo->insertItem(ix, i18n("Height")); m_sourceCombo->setItemData(ix, QString::number(3), Qt::UserRole); ix++; }*/ connect(m_sourceCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateRange())); m_alignCombo = new QComboBox(this); m_alignCombo->addItems(QStringList() << i18n("Align top left") << i18n("Align center") << i18n("Align bottom right")); lab = new QLabel(i18n("Map "), this); QLabel *lab2 = new QLabel(i18n(" to "), this); l1->addWidget(lab); l1->addWidget(m_sourceCombo); l1->addWidget(lab2); l1->addWidget(m_targetCombo); l1->addWidget(m_alignCombo); l1->addStretch(10); ix = 0; QMap::const_iterator j = m_geometryTargets.constBegin(); while (j != m_geometryTargets.constEnd()) { m_targetCombo->insertItem(ix, j.key()); m_targetCombo->setItemData(ix, j.value(), Qt::UserRole); ++j; ix++; } ix = 0; j = m_simpleTargets.constBegin(); while (j != m_simpleTargets.constEnd()) { m_targetCombo->insertItem(ix, j.key()); m_targetCombo->setItemData(ix, j.value(), Qt::UserRole); ++j; ix++; } if (m_simpleTargets.count() + m_geometryTargets.count() > 1) { // Target contains several animatable parameters, propose choice } lay->addLayout(l1); // Output offset m_offsetPoint = new PositionWidget(i18n("Offset"), 0, 0, out, pCore->currentDoc()->timecode(), "", this); lay->addWidget(m_offsetPoint); // Source range m_sourceRangeLabel = new QLabel(i18n("Source range %1 to %2", 0, 100), this); lay->addWidget(m_sourceRangeLabel); // update range info connect(m_targetCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateDestinationRange())); // Destination range l1 = new QHBoxLayout; lab = new QLabel(i18n("Destination range"), this); l1->addWidget(lab); l1->addWidget(&m_destMin); l1->addWidget(&m_destMax); lay->addLayout(l1); l1 = new QHBoxLayout; m_limitRange = new QCheckBox(i18n("Actual range only"), this); connect(m_limitRange, &QAbstractButton::toggled, this, &KeyframeImport::updateRange); connect(m_limitRange, &QAbstractButton::toggled, this, &KeyframeImport::updateDisplay); l1->addWidget(m_limitRange); l1->addStretch(10); lay->addLayout(l1); l1 = new QHBoxLayout; m_limitKeyframes = new QCheckBox(i18n("Limit keyframe number"), this); m_limitKeyframes->setChecked(true); m_limitNumber = new QSpinBox(this); m_limitNumber->setMinimum(1); m_limitNumber->setValue(20); l1->addWidget(m_limitKeyframes); l1->addWidget(m_limitNumber); l1->addStretch(10); lay->addLayout(l1); connect(m_limitKeyframes, &QCheckBox::toggled, m_limitNumber, &QSpinBox::setEnabled); connect(m_limitKeyframes, &QAbstractButton::toggled, this, &KeyframeImport::updateDisplay); connect(m_limitNumber, SIGNAL(valueChanged(int)), this, SLOT(updateDisplay())); connect(m_dataCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateDataDisplay())); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); lay->addWidget(buttonBox); m_isReady = true; updateDestinationRange(); updateDataDisplay(); } KeyframeImport::~KeyframeImport() = default; void KeyframeImport::resizeEvent(QResizeEvent *ev) { QWidget::resizeEvent(ev); updateDisplay(); } void KeyframeImport::updateDataDisplay() { QString comboData = m_dataCombo->currentData().toString(); auto type = m_dataCombo->currentData(Qt::UserRole + 1).value(); m_maximas = KeyframeModel::getRanges(comboData, m_model); m_sourceCombo->clear(); if (type == ParamType::KeyframeParam) { // 1 dimensional param. m_sourceCombo->addItem(m_dataCombo->currentText(), ImportRoles::SimpleValue); updateRange(); return; } double wDist = m_maximas.at(2).y() - m_maximas.at(2).x(); double hDist = m_maximas.at(3).y() - m_maximas.at(3).x(); m_sourceCombo->addItem(i18n("Geometry"), ImportRoles::FullGeometry); m_sourceCombo->addItem(i18n("Position"), ImportRoles::Position); m_sourceCombo->addItem(i18n("X"), ImportRoles::XOnly); m_sourceCombo->addItem(i18n("Y"), ImportRoles::YOnly); if (wDist > 0) { m_sourceCombo->addItem(i18n("Width"), ImportRoles::WidthOnly); } if (hDist > 0) { m_sourceCombo->addItem(i18n("Height"), ImportRoles::HeightOnly); } updateRange(); /*if (!m_inPoint->isValid()) { m_inPoint->blockSignals(true); m_outPoint->blockSignals(true); // m_inPoint->setRange(0, m_keyframeView->duration); m_inPoint->setPosition(0); m_outPoint->setPosition(m_model->data(m_targetCombo->currentData().toModelIndex(), AssetParameterModel::ParentDurationRole).toInt()); m_inPoint->blockSignals(false); m_outPoint->blockSignals(false); }*/ } void KeyframeImport::updateRange() { int pos = m_sourceCombo->currentData().toInt(); m_alignCombo->setEnabled(pos == ImportRoles::Position); QString rangeText; if (m_limitRange->isChecked()) { switch (pos) { case ImportRoles::SimpleValue: case ImportRoles::XOnly: rangeText = i18n("Source range %1 to %2", m_maximas.at(0).x(), m_maximas.at(0).y()); break; case ImportRoles::YOnly: rangeText = i18n("Source range %1 to %2", m_maximas.at(1).x(), m_maximas.at(1).y()); break; case ImportRoles::WidthOnly: rangeText = i18n("Source range %1 to %2", m_maximas.at(2).x(), m_maximas.at(2).y()); break; case ImportRoles::HeightOnly: rangeText = i18n("Source range %1 to %2", m_maximas.at(3).x(), m_maximas.at(3).y()); break; default: rangeText = i18n("Source range: (%1-%2), (%3-%4)", m_maximas.at(0).x(), m_maximas.at(0).y(), m_maximas.at(1).x(), m_maximas.at(1).y()); break; } } else { int profileWidth = pCore->getCurrentProfile()->width(); int profileHeight = pCore->getCurrentProfile()->height(); switch (pos) { case ImportRoles::SimpleValue: rangeText = i18n("Source range %1 to %2", qMin(0, m_maximas.at(0).x()), m_maximas.at(0).y()); break; case ImportRoles::XOnly: rangeText = i18n("Source range %1 to %2", qMin(0, m_maximas.at(0).x()), qMax(profileWidth, m_maximas.at(0).y())); break; case ImportRoles::YOnly: rangeText = i18n("Source range %1 to %2", qMin(0, m_maximas.at(1).x()), qMax(profileHeight, m_maximas.at(1).y())); break; case ImportRoles::WidthOnly: rangeText = i18n("Source range %1 to %2", qMin(0, m_maximas.at(2).x()), qMax(profileWidth, m_maximas.at(2).y())); break; case ImportRoles::HeightOnly: rangeText = i18n("Source range %1 to %2", qMin(0, m_maximas.at(3).x()), qMax(profileHeight, m_maximas.at(3).y())); break; default: rangeText = i18n("Source range: (%1-%2), (%3-%4)", qMin(0, m_maximas.at(0).x()), qMax(profileWidth, m_maximas.at(0).y()), qMin(0, m_maximas.at(1).x()), qMax(profileHeight, m_maximas.at(1).y())); break; } } m_sourceRangeLabel->setText(rangeText); updateDisplay(); } void KeyframeImport::updateDestinationRange() { if (m_simpleTargets.contains(m_targetCombo->currentText())) { // 1 dimension target m_destMin.setEnabled(true); m_destMax.setEnabled(true); m_limitRange->setEnabled(true); QString tag = m_targetCombo->currentData().toString(); double min = m_model->data(m_targetCombo->currentData().toModelIndex(), AssetParameterModel::MinRole).toDouble(); double max = m_model->data(m_targetCombo->currentData().toModelIndex(), AssetParameterModel::MaxRole).toDouble(); m_destMin.setRange(min, max); m_destMax.setRange(min, max); m_destMin.setValue(min); m_destMax.setValue(max); } else { int profileWidth = pCore->getCurrentProfile()->width(); m_destMin.setRange(-2 * profileWidth, 2 * profileWidth); m_destMax.setRange(-2 * profileWidth, 2 * profileWidth); m_destMin.setEnabled(false); m_destMax.setEnabled(false); m_limitRange->setEnabled(false); } } void KeyframeImport::updateDisplay() { if (!m_isReady) { // Not ready return; } QPixmap pix(m_previewLabel->width(), m_previewLabel->height()); pix.fill(Qt::transparent); QList maximas; int selectedtarget = m_sourceCombo->currentData().toInt(); int profileWidth = pCore->getCurrentProfile()->width(); int profileHeight = pCore->getCurrentProfile()->height(); if (!m_maximas.isEmpty()) { if (m_maximas.at(0).x() == m_maximas.at(0).y() || (selectedtarget == ImportRoles::YOnly || selectedtarget == ImportRoles::WidthOnly || selectedtarget == ImportRoles::HeightOnly)) { maximas << QPoint(); } else { if (m_limitRange->isChecked()) { maximas << m_maximas.at(0); } else { QPoint p1; if (selectedtarget == ImportRoles::SimpleValue) { p1 = QPoint(qMin(0, m_maximas.at(0).x()), m_maximas.at(0).y()); } else { p1 = QPoint(qMin(0, m_maximas.at(0).x()), qMax(profileWidth, m_maximas.at(0).y())); } maximas << p1; } } } if (m_maximas.count() > 1) { if (m_maximas.at(1).x() == m_maximas.at(1).y() || (selectedtarget == ImportRoles::XOnly || selectedtarget == ImportRoles::WidthOnly || selectedtarget == ImportRoles::HeightOnly)) { maximas << QPoint(); } else { if (m_limitRange->isChecked()) { maximas << m_maximas.at(1); } else { QPoint p2(qMin(0, m_maximas.at(1).x()), qMax(profileHeight, m_maximas.at(1).y())); maximas << p2; } } } if (m_maximas.count() > 2) { if (m_maximas.at(2).x() == m_maximas.at(2).y() || (selectedtarget == ImportRoles::XOnly || selectedtarget == ImportRoles::YOnly || selectedtarget == ImportRoles::HeightOnly)) { maximas << QPoint(); } else { if (m_limitRange->isChecked()) { maximas << m_maximas.at(2); } else { QPoint p3(qMin(0, m_maximas.at(2).x()), qMax(profileWidth, m_maximas.at(2).y())); maximas << p3; } } } if (m_maximas.count() > 3) { if (m_maximas.at(3).x() == m_maximas.at(3).y() || (selectedtarget == ImportRoles::XOnly || selectedtarget == ImportRoles::WidthOnly || selectedtarget == ImportRoles::YOnly)) { maximas << QPoint(); } else { if (m_limitRange->isChecked()) { maximas << m_maximas.at(3); } else { QPoint p4(qMin(0, m_maximas.at(3).x()), qMax(profileHeight, m_maximas.at(3).y())); maximas << p4; } } } drawKeyFrameChannels(pix, m_inPoint->getPosition(), m_outPoint->getPosition(), m_limitKeyframes->isChecked() ? m_limitNumber->value() : 0, palette().text().color()); m_previewLabel->setPixmap(pix); } QString KeyframeImport::selectedData() const { // return serialized keyframes if (m_simpleTargets.contains(m_targetCombo->currentText())) { // Exporting a 1 dimension animation int ix = m_sourceCombo->currentData().toInt(); QPoint maximas; if (m_limitRange->isChecked()) { maximas = m_maximas.at(ix); } else if (ix == ImportRoles::WidthOnly) { // Width maximas maximas = QPoint(qMin(m_maximas.at(ix).x(), 0), qMax(m_maximas.at(ix).y(), pCore->getCurrentProfile()->width())); } else { // Height maximas maximas = QPoint(qMin(m_maximas.at(ix).x(), 0), qMax(m_maximas.at(ix).y(), pCore->getCurrentProfile()->height())); } std::shared_ptr animData = KeyframeModel::getAnimation(m_model, m_dataCombo->currentData().toString()); std::shared_ptr anim(new Mlt::Animation(animData->get_animation("key"))); animData->anim_get_double("key", m_inPoint->getPosition(), m_outPoint->getPosition()); return anim->serialize_cut(); // m_keyframeView->getSingleAnimation(ix, m_inPoint->getPosition(), m_outPoint->getPosition(), m_offsetPoint->getPosition(), // m_limitKeyframes->isChecked() ? m_limitNumber->value() : 0, maximas, m_destMin.value(), m_destMax.value()); } //return QString(); std::shared_ptr animData = KeyframeModel::getAnimation(m_model, m_dataCombo->currentData().toString()); std::shared_ptr anim(new Mlt::Animation(animData->get_animation("key"))); animData->anim_get_rect("key", m_inPoint->getPosition(), m_outPoint->getPosition()); return anim->serialize_cut(); /*int pos = m_sourceCombo->currentData().toInt(); m_keyframeView->getOffsetAnimation(m_inPoint->getPosition(), m_outPoint->getPosition(), m_offsetPoint->getPosition(), m_limitKeyframes->isChecked() ?*/ // m_limitNumber->value() : 0, m_supportsAnim, pos == 11, rectOffset); } QString KeyframeImport::selectedTarget() const { return m_targetCombo->currentData().toString(); } void KeyframeImport::drawKeyFrameChannels(QPixmap &pix, int in, int out, int limitKeyframes, const QColor &textColor) { qDebug()<<"============= DRAWING KFR CHANNS: "<currentData().toString(); std::shared_ptr animData = KeyframeModel::getAnimation(m_model, m_dataCombo->currentData().toString()); QRect br(0, 0, pix.width(), pix.height()); double frameFactor = (double)(out - in) / br.width(); int offset = 1; if (limitKeyframes > 0) { offset = (out - in) / limitKeyframes / frameFactor; } double min = m_dataCombo->currentData(Qt::UserRole + 2).toDouble(); double max = m_dataCombo->currentData(Qt::UserRole + 3).toDouble(); double xDist; if (max > min) { xDist = max - min; } else { xDist = m_maximas.at(0).y() - m_maximas.at(0).x(); } double yDist = m_maximas.at(1).y() - m_maximas.at(1).x(); double wDist = m_maximas.at(2).y() - m_maximas.at(2).x(); double hDist = m_maximas.at(3).y() - m_maximas.at(3).x(); double xOffset = m_maximas.at(0).x(); double yOffset = m_maximas.at(1).x(); double wOffset = m_maximas.at(2).x(); double hOffset = m_maximas.at(3).x(); QColor cX(255, 0, 0, 100); QColor cY(0, 255, 0, 100); QColor cW(0, 0, 255, 100); QColor cH(255, 255, 0, 100); // Draw curves labels QPainter painter; painter.begin(&pix); QRectF txtRect = painter.boundingRect(br, QStringLiteral("t")); txtRect.setX(2); txtRect.setWidth(br.width() - 4); txtRect.moveTop(br.height() - txtRect.height()); QRectF drawnText; int maxHeight = br.height() - txtRect.height() - 2; painter.setPen(textColor); int rectSize = txtRect.height() / 2; if (xDist > 0) { painter.fillRect(txtRect.x(), txtRect.top() + rectSize / 2, rectSize, rectSize, cX); txtRect.setX(txtRect.x() + rectSize * 2); painter.drawText(txtRect, 0, i18nc("X as in x coordinate", "X") + QStringLiteral(" (%1-%2)").arg(m_maximas.at(0).x()).arg(m_maximas.at(0).y()), &drawnText); } if (yDist > 0) { if (drawnText.isValid()) { txtRect.setX(drawnText.right() + rectSize); } painter.fillRect(txtRect.x(), txtRect.top() + rectSize / 2, rectSize, rectSize, cY); txtRect.setX(txtRect.x() + rectSize * 2); painter.drawText(txtRect, 0, i18nc("Y as in y coordinate", "Y") + QStringLiteral(" (%1-%2)").arg(m_maximas.at(1).x()).arg(m_maximas.at(1).y()), &drawnText); } if (wDist > 0) { if (drawnText.isValid()) { txtRect.setX(drawnText.right() + rectSize); } painter.fillRect(txtRect.x(), txtRect.top() + rectSize / 2, rectSize, rectSize, cW); txtRect.setX(txtRect.x() + rectSize * 2); painter.drawText(txtRect, 0, i18n("Width") + QStringLiteral(" (%1-%2)").arg(m_maximas.at(2).x()).arg(m_maximas.at(2).y()), &drawnText); } if (hDist > 0) { if (drawnText.isValid()) { txtRect.setX(drawnText.right() + rectSize); } painter.fillRect(txtRect.x(), txtRect.top() + rectSize / 2, rectSize, rectSize, cH); txtRect.setX(txtRect.x() + rectSize * 2); painter.drawText(txtRect, 0, i18n("Height") + QStringLiteral(" (%1-%2)").arg(m_maximas.at(3).x()).arg(m_maximas.at(3).y()), &drawnText); } // Draw curves for (int i = 0; i < br.width(); i++) { mlt_rect rect = animData->anim_get_rect("key", (int)(i * frameFactor) + in); if (xDist > 0) { painter.setPen(cX); int val = (rect.x - xOffset) * maxHeight / xDist; qDebug() << "// DRAWINC CURVE : " << rect.x <<", POS: "<<((int)(i * frameFactor) + in)<< ", RESULT: " << val; painter.drawLine(i, maxHeight - val, i, maxHeight); } if (yDist > 0) { painter.setPen(cY); int val = (rect.y - yOffset) * maxHeight / yDist; painter.drawLine(i, maxHeight - val, i, maxHeight); } if (wDist > 0) { painter.setPen(cW); int val = (rect.w - wOffset) * maxHeight / wDist; qDebug() << "// OFFSET: " << wOffset << ", maxH: " << maxHeight << ", wDIst:" << wDist << " = " << val; painter.drawLine(i, maxHeight - val, i, maxHeight); } if (hDist > 0) { painter.setPen(cH); int val = (rect.h - hOffset) * maxHeight / hDist; painter.drawLine(i, maxHeight - val, i, maxHeight); } } if (offset > 1) { // Overlay limited keyframes curve cX.setAlpha(255); cY.setAlpha(255); cW.setAlpha(255); cH.setAlpha(255); mlt_rect rect1 = animData->anim_get_rect("key", in); int prevPos = 0; for (int i = offset; i < br.width(); i += offset) { mlt_rect rect2 = animData->anim_get_rect("key", (int)(i * frameFactor) + in); if (xDist > 0) { painter.setPen(cX); int val1 = (rect1.x - xOffset) * maxHeight / xDist; int val2 = (rect2.x - xOffset) * maxHeight / xDist; painter.drawLine(prevPos, maxHeight - val1, i, maxHeight - val2); } if (yDist > 0) { painter.setPen(cY); int val1 = (rect1.y - yOffset) * maxHeight / yDist; int val2 = (rect2.y - yOffset) * maxHeight / yDist; painter.drawLine(prevPos, maxHeight - val1, i, maxHeight - val2); } if (wDist > 0) { painter.setPen(cW); int val1 = (rect1.w - wOffset) * maxHeight / wDist; int val2 = (rect2.w - wOffset) * maxHeight / wDist; painter.drawLine(prevPos, maxHeight - val1, i, maxHeight - val2); } if (hDist > 0) { painter.setPen(cH); int val1 = (rect1.h - hOffset) * maxHeight / hDist; int val2 = (rect2.h - hOffset) * maxHeight / hDist; painter.drawLine(prevPos, maxHeight - val1, i, maxHeight - val2); } rect1 = rect2; prevPos = i; } } } void KeyframeImport::importSelectedData() { // Simple double value std::shared_ptr animData = KeyframeModel::getAnimation(m_model, selectedData()); std::shared_ptr anim(new Mlt::Animation(animData->get_animation("key"))); std::shared_ptr kfrModel = m_model->getKeyframeModel(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Geometry target QPoint rectOffset; int finalAlign = m_alignCombo->currentIndex(); QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); for (const auto &ix : m_indexes) { // update keyframes in other indexes KeyframeModel *km = kfrModel->getKeyModel(ix); qDebug()<<"== "<currentData().toModelIndex(); if (ix == m_targetCombo->currentData().toModelIndex()) { qDebug()<<"= = = \n\nPROCESSING KF IMPORT LOP: "<key_count()<<"\n\n==="; // Import our keyframes int frame = 0; KeyframeImport::ImportRoles convertMode = static_cast (m_sourceCombo->currentData().toInt()); for (int i = 0; i < anim->key_count(); i++) { frame = anim->key_get_frame(i); QVariant current = km->getInterpolatedValue(frame); if (convertMode == ImportRoles::SimpleValue) { double dval = animData->anim_get_double("key", frame); km->addKeyframe(GenTime(frame - m_inPoint->getPosition() + m_offsetPoint->getPosition(), pCore->getCurrentFps()), (KeyframeType)KdenliveSettings::defaultkeyframeinterp(), dval, true, undo, redo); continue; } QStringList kfrData = current.toString().split(QLatin1Char(' ')); // Safety check int size = kfrData.size(); switch (convertMode) { case ImportRoles::FullGeometry: case ImportRoles::HeightOnly: case ImportRoles::WidthOnly: if (size < 4) { continue; } break; case ImportRoles::Position: case ImportRoles::YOnly: if (size < 2) { continue; } break; default: if (size == 0) { continue; } break; } mlt_rect rect = animData->anim_get_rect("key", frame); if (convertMode == ImportRoles::Position) { switch (finalAlign) { case 1: // Align center rect.x += rect.w / 2; rect.y += rect.h / 2; break; case 2: //Align bottom right rect.x += rect.w; rect.y += rect.h; break; default: break; } } switch (convertMode) { case ImportRoles::FullGeometry: - kfrData[0] = locale.toString(rect.x); - kfrData[1] = locale.toString(rect.y); - kfrData[2] = locale.toString(rect.w); - kfrData[3] = locale.toString(rect.h); + kfrData[0] = locale.toString((int)rect.x); + kfrData[1] = locale.toString((int)rect.y); + kfrData[2] = locale.toString((int)rect.w); + kfrData[3] = locale.toString((int)rect.h); break; case ImportRoles::Position: - kfrData[0] = locale.toString(rect.x); - kfrData[1] = locale.toString(rect.y); + kfrData[0] = locale.toString((int)rect.x); + kfrData[1] = locale.toString((int)rect.y); break; case ImportRoles::SimpleValue: case ImportRoles::XOnly: - kfrData[0] = locale.toString(rect.x); + kfrData[0] = locale.toString((int)rect.x); break; case ImportRoles::YOnly: - kfrData[1] = locale.toString(rect.y); + kfrData[1] = locale.toString((int)rect.y); break; case ImportRoles::WidthOnly: - kfrData[2] = locale.toString(rect.w); + kfrData[2] = locale.toString((int)rect.w); break; case ImportRoles::HeightOnly: - kfrData[3] = locale.toString(rect.h); + kfrData[3] = locale.toString((int)rect.h); break; default: break; } current = kfrData.join(QLatin1Char(' ')); km->addKeyframe(GenTime(frame - m_inPoint->getPosition() + m_offsetPoint->getPosition(), pCore->getCurrentFps()), (KeyframeType)KdenliveSettings::defaultkeyframeinterp(), current, true, undo, redo); } } else { int frame = 0; for (int i = 0; i < anim->key_count(); i++) { frame = anim->key_get_frame(i); //frame += (m_inPoint->getPosition() - m_offsetPoint->getPosition()); QVariant current = km->getInterpolatedValue(frame); km->addKeyframe(GenTime(frame - m_inPoint->getPosition() + m_offsetPoint->getPosition(), pCore->getCurrentFps()), (KeyframeType)KdenliveSettings::defaultkeyframeinterp(), current, true, undo, redo); } } } pCore->pushUndo(undo, redo, i18n("Import keyframes from clipboard")); } int KeyframeImport::getImportType() const { if (m_simpleTargets.contains(m_targetCombo->currentText())) { return -1; } return m_sourceCombo->currentData().toInt(); } diff --git a/src/assets/view/widgets/listparamwidget.cpp b/src/assets/view/widgets/listparamwidget.cpp index 9a624ec86..fa8144f82 100644 --- a/src/assets/view/widgets/listparamwidget.cpp +++ b/src/assets/view/widgets/listparamwidget.cpp @@ -1,149 +1,149 @@ /*************************************************************************** * Copyright (C) 2016 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 "listparamwidget.h" #include "assets/model/assetparametermodel.hpp" #include "core.h" #include "mainwindow.h" ListParamWidget::ListParamWidget(std::shared_ptr model, QModelIndex index, QWidget *parent) : AbstractParamWidget(std::move(model), index, parent) { setupUi(this); // Get data from model QString name = m_model->data(m_index, AssetParameterModel::NameRole).toString(); QString comment = m_model->data(m_index, AssetParameterModel::CommentRole).toString(); // setup the comment setToolTip(comment); m_labelComment->setText(comment); m_widgetComment->setHidden(true); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); m_list->setIconSize(QSize(50, 30)); setMinimumHeight(m_list->sizeHint().height()); // setup the name m_labelName->setText(m_model->data(m_index, Qt::DisplayRole).toString()); slotRefresh(); - QLocale locale; // emit the signal of the base class when appropriate // The connection is ugly because the signal "currentIndexChanged" is overloaded in QComboBox connect(this->m_list, static_cast(&QComboBox::currentIndexChanged), [this](int) { emit valueChanged(m_index, m_list->itemData(m_list->currentIndex()).toString(), true); }); } void ListParamWidget::setCurrentIndex(int index) { m_list->setCurrentIndex(index); } void ListParamWidget::setCurrentText(const QString &text) { m_list->setCurrentText(text); } void ListParamWidget::addItem(const QString &text, const QVariant &value) { m_list->addItem(text, value); } void ListParamWidget::setItemIcon(int index, const QIcon &icon) { m_list->setItemIcon(index, icon); } void ListParamWidget::setIconSize(const QSize &size) { m_list->setIconSize(size); } void ListParamWidget::slotShowComment(bool show) { if (!m_labelComment->text().isEmpty()) { m_widgetComment->setVisible(show); } } QString ListParamWidget::getValue() { return m_list->currentData().toString(); } void ListParamWidget::slotRefresh() { const QSignalBlocker bk(m_list); m_list->clear(); QStringList names = m_model->data(m_index, AssetParameterModel::ListNamesRole).toStringList(); QStringList values = m_model->data(m_index, AssetParameterModel::ListValuesRole).toStringList(); QString value = m_model->data(m_index, AssetParameterModel::ValueRole).toString(); if (values.first() == QLatin1String("%lumaPaths")) { // Special case: Luma files // Create thumbnails if (pCore->getCurrentFrameSize().width() > 1000) { // HD project values = MainWindow::m_lumaFiles.value(QStringLiteral("16_9")); } else if (pCore->getCurrentFrameSize().height() > 1000) { values = MainWindow::m_lumaFiles.value(QStringLiteral("9_16")); } else if (pCore->getCurrentFrameSize().height() == pCore->getCurrentFrameSize().width()) { values = MainWindow::m_lumaFiles.value(QStringLiteral("square")); } else if (pCore->getCurrentFrameSize().height() == 480) { values = MainWindow::m_lumaFiles.value(QStringLiteral("NTSC")); } else { values = MainWindow::m_lumaFiles.value(QStringLiteral("PAL")); } m_list->addItem(i18n("None (Dissolve)")); for (int j = 0; j < values.count(); ++j) { const QString &entry = values.at(j); m_list->addItem(values.at(j).section(QLatin1Char('/'), -1), entry); if (!entry.isEmpty() && (entry.endsWith(QLatin1String(".png")) || entry.endsWith(QLatin1String(".pgm")))) { if (MainWindow::m_lumacache.contains(entry)) { m_list->setItemIcon(j + 1, QPixmap::fromImage(MainWindow::m_lumacache.value(entry))); } } } if (!value.isEmpty() && values.contains(value)) { m_list->setCurrentIndex(values.indexOf(value) + 1); } } else { if (names.count() != values.count()) { names = values; } QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); for (int i = 0; i < names.count(); i++) { QString val = values.at(i); bool ok; double num = val.toDouble(&ok); if (ok) { val = locale.toString(num); } m_list->addItem(names.at(i), val); } if (!value.isEmpty()) { int ix = m_list->findData(value); if (ix > -1) { m_list->setCurrentIndex(ix); } } } } diff --git a/src/doc/documentvalidator.cpp b/src/doc/documentvalidator.cpp index 5f45b0142..3e4b3831b 100644 --- a/src/doc/documentvalidator.cpp +++ b/src/doc/documentvalidator.cpp @@ -1,2383 +1,2385 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@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) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "documentvalidator.h" #include "bin/binplaylist.hpp" #include "core.h" #include "definitions.h" #include "effects/effectsrepository.hpp" #include "mainwindow.h" #include "transitions/transitionsrepository.hpp" #include "xml/xml.hpp" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif #include #include DocumentValidator::DocumentValidator(const QDomDocument &doc, QUrl documentUrl) : m_doc(doc) , m_url(std::move(documentUrl)) , m_modified(false) { } bool DocumentValidator::validate(const double currentVersion) { QDomElement mlt = m_doc.firstChildElement(QStringLiteral("mlt")); // At least the root element must be there if (mlt.isNull()) { return false; } QDomElement kdenliveDoc = mlt.firstChildElement(QStringLiteral("kdenlivedoc")); QString rootDir = mlt.attribute(QStringLiteral("root")); if (rootDir == QLatin1String("$CURRENTPATH")) { // The document was extracted from a Kdenlive archived project, fix root directory QString playlist = m_doc.toString(); playlist.replace(QLatin1String("$CURRENTPATH"), m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile()); m_doc.setContent(playlist); mlt = m_doc.firstChildElement(QStringLiteral("mlt")); kdenliveDoc = mlt.firstChildElement(QStringLiteral("kdenlivedoc")); } else if (rootDir.isEmpty()) { mlt.setAttribute(QStringLiteral("root"), m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile()); } // Previous MLT / Kdenlive versions used C locale by default QLocale documentLocale = QLocale::c(); if (mlt.hasAttribute(QStringLiteral("LC_NUMERIC"))) { // Check document numeric separator (added in Kdenlive 16.12.1 QDomElement main_playlist = mlt.firstChildElement(QStringLiteral("playlist")); QString sep = Xml::getXmlProperty(main_playlist, "kdenlive:docproperties.decimalPoint", QString()); QChar numericalSeparator; if (!sep.isEmpty()) { numericalSeparator = sep.at(0); } bool error = false; if (!numericalSeparator.isNull() && numericalSeparator != QLocale().decimalPoint()) { qCDebug(KDENLIVE_LOG) << " * ** LOCALE CHANGE REQUIRED: " << numericalSeparator << "!=" << QLocale().decimalPoint() << " / " << QLocale::system().decimalPoint(); // Change locale to match document QString requestedLocale = mlt.attribute(QStringLiteral("LC_NUMERIC")); documentLocale = QLocale(requestedLocale); #ifdef Q_OS_WIN // Most locales don't work on windows, so use C whenever possible if (numericalSeparator == QLatin1Char('.')) { #else if (numericalSeparator != documentLocale.decimalPoint() && numericalSeparator == QLatin1Char('.')) { #endif requestedLocale = QStringLiteral("C"); documentLocale = QLocale::c(); } #ifdef Q_OS_MAC setlocale(LC_NUMERIC_MASK, requestedLocale.toUtf8().constData()); #elif defined(Q_OS_WIN) std::locale::global(std::locale(requestedLocale.toUtf8().constData())); #else setlocale(LC_NUMERIC, requestedLocale.toUtf8().constData()); #endif if (numericalSeparator != documentLocale.decimalPoint()) { // Parse installed locales to find one matching const QList list = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale().script(), QLocale::AnyCountry); QLocale matching; for (const QLocale &loc : list) { if (loc.decimalPoint() == numericalSeparator) { matching = loc; qCDebug(KDENLIVE_LOG) << "Warning, document locale: " << mlt.attribute(QStringLiteral("LC_NUMERIC")) << " is not available, using: " << loc.name(); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, loc.name().toUtf8().constData()); #else setlocale(LC_NUMERIC_MASK, loc.name().toUtf8().constData()); #endif documentLocale = matching; break; } } error = numericalSeparator != documentLocale.decimalPoint(); } } else if (numericalSeparator.isNull()) { // Change locale to match document #ifndef Q_OS_MAC const QString newloc = QString::fromLatin1(setlocale(LC_NUMERIC, mlt.attribute(QStringLiteral("LC_NUMERIC")).toUtf8().constData())); #else const QString newloc = setlocale(LC_NUMERIC_MASK, mlt.attribute("LC_NUMERIC").toUtf8().constData()); #endif documentLocale = QLocale(mlt.attribute(QStringLiteral("LC_NUMERIC"))); error = newloc.isEmpty(); } else { // Document separator matching system separator documentLocale = QLocale(); } if (error) { // Requested locale not available, ask for install KMessageBox::sorry(QApplication::activeWindow(), i18n("The document was created in \"%1\" locale, which is not installed on your system. Please install that language pack. " "Until then, Kdenlive might not be able to correctly open the document.", mlt.attribute(QStringLiteral("LC_NUMERIC")))); } // Make sure Qt locale and C++ locale have the same numeric separator, might not be the case // With some locales since C++ and Qt use a different database for locales // localeconv()->decimal_point does not give reliable results on Windows #ifndef Q_OS_WIN char *separator = localeconv()->decimal_point; if (QString::fromUtf8(separator) != QString(documentLocale.decimalPoint())) { KMessageBox::sorry(QApplication::activeWindow(), i18n("There is a locale conflict on your system. The document uses locale %1 which uses a \"%2\" as numeric separator (in " "system libraries) but Qt expects \"%3\". You might not be able to correctly open the project.", mlt.attribute(QStringLiteral("LC_NUMERIC")), documentLocale.decimalPoint(), separator)); // qDebug()<<"------\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 at least have correct decimal point if (strncmp(separator, ".", 1) == 0) { documentLocale = QLocale::c(); } else if (strncmp(separator, ",", 1) == 0) { documentLocale = QLocale(QStringLiteral("fr_FR.UTF-8")); } } #endif } documentLocale.setNumberOptions(QLocale::OmitGroupSeparator); if (documentLocale.decimalPoint() != QLocale().decimalPoint()) { // If loading an older MLT file without LC_NUMERIC, set locale to C which was previously the default if (!mlt.hasAttribute(QStringLiteral("LC_NUMERIC"))) { #ifndef Q_OS_MAC setlocale(LC_NUMERIC, "C"); #else setlocale(LC_NUMERIC_MASK, "C"); #endif } QLocale::setDefault(documentLocale); if (documentLocale.decimalPoint() != QLocale().decimalPoint()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("There is a locale conflict. The document uses a \"%1\" as numeric separator, but your computer is configured to use " "\"%2\". Change your computer settings or you might not be able to correctly open the project.", documentLocale.decimalPoint(), QLocale().decimalPoint())); } // locale conversion might need to be redone // TODO reload repositories /*#ifndef Q_OS_MAC initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC, nullptr))); #else initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC_MASK, nullptr))); #endif */ } double version = -1; if (kdenliveDoc.isNull() || !kdenliveDoc.hasAttribute(QStringLiteral("version"))) { // Newer Kdenlive document version QDomElement main = mlt.firstChildElement(QStringLiteral("playlist")); version = Xml::getXmlProperty(main, QStringLiteral("kdenlive:docproperties.version")).toDouble(); } else { bool ok; version = documentLocale.toDouble(kdenliveDoc.attribute(QStringLiteral("version")), &ok); if (!ok) { // Could not parse version number, there is probably a conflict in decimal separator QLocale tempLocale = QLocale(mlt.attribute(QStringLiteral("LC_NUMERIC"))); version = tempLocale.toDouble(kdenliveDoc.attribute(QStringLiteral("version")), &ok); if (!ok) { version = kdenliveDoc.attribute(QStringLiteral("version")).toDouble(&ok); } if (!ok) { // Last try: replace comma with a dot QString versionString = kdenliveDoc.attribute(QStringLiteral("version")); if (versionString.contains(QLatin1Char(','))) { versionString.replace(QLatin1Char(','), QLatin1Char('.')); } version = versionString.toDouble(&ok); if (!ok) { qCDebug(KDENLIVE_LOG) << "// CANNOT PARSE VERSION NUMBER, ERROR!"; } } } } // Upgrade the document to the latest version if (!upgrade(version, currentVersion)) { return false; } if (version < 0.97) { checkOrphanedProducers(); } return true; /* // Check the syntax (this will be replaced by XSD validation with Qt 4.6) // and correct some errors { // Return (or create) the tractor QDomElement tractor = mlt.firstChildElement("tractor"); if (tractor.isNull()) { m_modified = true; tractor = m_doc.createElement("tractor"); tractor.setAttribute("global_feed", "1"); tractor.setAttribute("in", "0"); tractor.setAttribute("out", "-1"); tractor.setAttribute("id", "maintractor"); mlt.appendChild(tractor); } // Make sure at least one track exists, and they're equal in number to // to the maximum between MLT and Kdenlive playlists and tracks // // In older Kdenlive project files, one playlist is not a real track (the black track), we have: track count = playlist count- 1 // In newer Qt5 Kdenlive, the Bin playlist should not appear as a track. So we should have: track count = playlist count- 2 int trackOffset = 1; QDomNodeList playlists = m_doc.elementsByTagName("playlist"); // Remove "main bin" playlist that simply holds the bin's clips and is not a real playlist for (int i = 0; i < playlists.count(); ++i) { QString playlistId = playlists.at(i).toElement().attribute("id"); if (playlistId == BinController::binPlaylistId()) { // remove pseudo-playlist //playlists.at(i).parentNode().removeChild(playlists.at(i)); trackOffset = 2; break; } } int tracksMax = playlists.count() - trackOffset; // Remove the black track and bin track QDomNodeList tracks = tractor.elementsByTagName("track"); tracksMax = qMax(tracks.count() - 1, tracksMax); QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo"); tracksMax = qMax(tracksinfo.count(), tracksMax); tracksMax = qMax(1, tracksMax); // Force existence of one track if (playlists.count() - trackOffset < tracksMax || tracks.count() < tracksMax || tracksinfo.count() < tracksMax) { qCDebug(KDENLIVE_LOG) << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK"; m_modified = true; int difference; // use the MLT tracks as reference if (tracks.count() - 1 < tracksMax) { // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one. if (tracksinfo.count() != tracks.count() - 1) { // The Kdenlive tracks are not ok, clear and rebuild them QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo"); QDomNode tnode = tinfo.firstChild(); while (!tnode.isNull()) { tinfo.removeChild(tnode); tnode = tinfo.firstChild(); } for (int i = 1; i < tracks.count(); ++i) { QString hide = tracks.at(i).toElement().attribute("hide"); QDomElement newTrack = m_doc.createElement("trackinfo"); if (hide == "video") { // audio track; newTrack.setAttribute("type", "audio"); newTrack.setAttribute("blind", 1); newTrack.setAttribute("mute", 0); newTrack.setAttribute("lock", 0); } else { newTrack.setAttribute("blind", 0); newTrack.setAttribute("mute", 0); newTrack.setAttribute("lock", 0); } tinfo.appendChild(newTrack); } } } if (playlists.count() - 1 < tracksMax) { difference = tracksMax - (playlists.count() - 1); for (int i = 0; i < difference; ++i) { QDomElement playlist = m_doc.createElement("playlist"); mlt.appendChild(playlist); } } if (tracks.count() - 1 < tracksMax) { difference = tracksMax - (tracks.count() - 1); for (int i = 0; i < difference; ++i) { QDomElement track = m_doc.createElement("track"); tractor.appendChild(track); } } if (tracksinfo.count() < tracksMax) { QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo"); if (tracksinfoElm.isNull()) { tracksinfoElm = m_doc.createElement("tracksinfo"); kdenliveDoc.appendChild(tracksinfoElm); } difference = tracksMax - tracksinfo.count(); for (int i = 0; i < difference; ++i) { QDomElement trackinfo = m_doc.createElement("trackinfo"); trackinfo.setAttribute("mute", "0"); trackinfo.setAttribute("locked", "0"); tracksinfoElm.appendChild(trackinfo); } } } // TODO: check the tracks references // TODO: check internal mix transitions } updateEffects(); return true; */ } bool DocumentValidator::upgrade(double version, const double currentVersion) { qCDebug(KDENLIVE_LOG) << "Opening a document with version " << version << " / " << currentVersion; // No conversion needed if (qFuzzyCompare(version, currentVersion)) { return true; } // The document is too new if (version > currentVersion) { // qCDebug(KDENLIVE_LOG) << "Unable to open document with version " << version; KMessageBox::sorry( QApplication::activeWindow(), i18n("This project type is unsupported (version %1) and cannot be loaded.\nPlease consider upgrading your Kdenlive version.", version), i18n("Unable to open project")); return false; } // Unsupported document versions if (qFuzzyCompare(version, 0.5) || qFuzzyCompare(version, 0.7)) { // 0.7 is unsupported // qCDebug(KDENLIVE_LOG) << "Unable to open document with version " << version; KMessageBox::sorry(QApplication::activeWindow(), i18n("This project type is unsupported (version %1) and cannot be loaded.", version), i18n("Unable to open project")); return false; } // QDomNode infoXmlNode; QDomElement infoXml; QDomNodeList docs = m_doc.elementsByTagName(QStringLiteral("kdenlivedoc")); if (!docs.isEmpty()) { infoXmlNode = m_doc.elementsByTagName(QStringLiteral("kdenlivedoc")).at(0); infoXml = infoXmlNode.toElement(); infoXml.setAttribute(QStringLiteral("upgraded"), 1); } m_doc.documentElement().setAttribute(QStringLiteral("upgraded"), 1); if (version <= 0.6) { QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders QDomNode westley = m_doc.elementsByTagName(QStringLiteral("westley")).at(1); QDomNode tractor = m_doc.elementsByTagName(QStringLiteral("tractor")).at(0); QDomNode multitrack = m_doc.elementsByTagName(QStringLiteral("multitrack")).at(0); QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); QDomNode props = m_doc.elementsByTagName(QStringLiteral("properties")).at(0).toElement(); QString profile = props.toElement().attribute(QStringLiteral("videoprofile")); int startPos = props.toElement().attribute(QStringLiteral("timeline_position")).toInt(); if (profile == QLatin1String("dv_wide")) { profile = QStringLiteral("dv_pal_wide"); } // move playlists outside of tractor and add the tracks instead int max = playlists.count(); if (westley.isNull()) { westley = m_doc.createElement(QStringLiteral("westley")); m_doc.documentElement().appendChild(westley); } if (tractor.isNull()) { // qCDebug(KDENLIVE_LOG) << "// NO MLT PLAYLIST, building empty one"; QDomElement blank_tractor = m_doc.createElement(QStringLiteral("tractor")); westley.appendChild(blank_tractor); QDomElement blank_playlist = m_doc.createElement(QStringLiteral("playlist")); blank_playlist.setAttribute(QStringLiteral("id"), QStringLiteral("black_track")); westley.insertBefore(blank_playlist, QDomNode()); QDomElement blank_track = m_doc.createElement(QStringLiteral("track")); blank_track.setAttribute(QStringLiteral("producer"), QStringLiteral("black_track")); blank_tractor.appendChild(blank_track); QDomNodeList kdenlivetracks = m_doc.elementsByTagName(QStringLiteral("kdenlivetrack")); for (int i = 0; i < kdenlivetracks.count(); ++i) { blank_playlist = m_doc.createElement(QStringLiteral("playlist")); blank_playlist.setAttribute(QStringLiteral("id"), QStringLiteral("playlist") + QString::number(i)); westley.insertBefore(blank_playlist, QDomNode()); blank_track = m_doc.createElement(QStringLiteral("track")); blank_track.setAttribute(QStringLiteral("producer"), QStringLiteral("playlist") + QString::number(i)); blank_tractor.appendChild(blank_track); if (kdenlivetracks.at(i).toElement().attribute(QStringLiteral("cliptype")) == QLatin1String("Sound")) { blank_playlist.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); blank_track.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); } } } else for (int i = 0; i < max; ++i) { QDomNode n = playlists.at(i); westley.insertBefore(n, QDomNode()); QDomElement pl = n.toElement(); QDomElement track = m_doc.createElement(QStringLiteral("track")); QString trackType = pl.attribute(QStringLiteral("hide")); if (!trackType.isEmpty()) { track.setAttribute(QStringLiteral("hide"), trackType); } QString playlist_id = pl.attribute(QStringLiteral("id")); if (playlist_id.isEmpty()) { playlist_id = QStringLiteral("black_track"); pl.setAttribute(QStringLiteral("id"), playlist_id); } track.setAttribute(QStringLiteral("producer"), playlist_id); // tractor.appendChild(track); #define KEEP_TRACK_ORDER 1 #ifdef KEEP_TRACK_ORDER tractor.insertAfter(track, QDomNode()); #else // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0 // insertion sort - O( tracks*tracks ) // Note, this breaks _all_ transitions - but you can move them up and down afterwards. QDomElement tractor_elem = tractor.toElement(); if (!tractor_elem.isNull()) { QDomNodeList tracks = tractor_elem.elementsByTagName("track"); int size = tracks.size(); if (size == 0) { tractor.insertAfter(track, QDomNode()); } else { bool inserted = false; for (int i = 0; i < size; ++i) { QDomElement track_elem = tracks.at(i).toElement(); if (track_elem.isNull()) { tractor.insertAfter(track, QDomNode()); inserted = true; break; } else { // qCDebug(KDENLIVE_LOG) << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer"); if (playlist_id < track_elem.attribute("producer")) { tractor.insertBefore(track, track_elem); inserted = true; break; } } } // Reach here, no insertion, insert last if (!inserted) { tractor.insertAfter(track, QDomNode()); } } } else { qCWarning(KDENLIVE_LOG) << "tractor was not a QDomElement"; tractor.insertAfter(track, QDomNode()); } #endif } tractor.removeChild(multitrack); // audio track mixing transitions should not be added to track view, so add required attribute QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement tr = transitions.at(i).toElement(); if (tr.attribute(QStringLiteral("combine")) == QLatin1String("1") && tr.attribute(QStringLiteral("mlt_service")) == QLatin1String("mix")) { QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("internal_added")); QDomText value = m_doc.createTextNode(QStringLiteral("237")); property.appendChild(value); tr.appendChild(property); property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); value = m_doc.createTextNode(QStringLiteral("mix")); property.appendChild(value); tr.appendChild(property); } else { // convert transition QDomNamedNodeMap attrs = tr.attributes(); for (int j = 0; j < attrs.count(); ++j) { QString attrName = attrs.item(j).nodeName(); if (attrName != QLatin1String("in") && attrName != QLatin1String("out") && attrName != QLatin1String("id")) { QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), attrName); QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue()); property.appendChild(value); tr.appendChild(property); } } } } // move transitions after tracks for (int i = 0; i < max; ++i) { tractor.insertAfter(transitions.at(0), QDomNode()); } // Fix filters format QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); max = entries.count(); for (int i = 0; i < max; ++i) { QString last_id; int effectix = 0; QDomNode m = entries.at(i).firstChild(); while (!m.isNull()) { if (m.toElement().tagName() == QLatin1String("filter")) { QDomElement filt = m.toElement(); QDomNamedNodeMap attrs = filt.attributes(); QString current_id = filt.attribute(QStringLiteral("kdenlive_id")); if (current_id != last_id) { effectix++; last_id = current_id; } QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive_ix")); QDomText value = m_doc.createTextNode(QString::number(effectix)); e.appendChild(value); filt.appendChild(e); for (int j = 0; j < attrs.count(); ++j) { QDomAttr a = attrs.item(j).toAttr(); if (!a.isNull()) { // qCDebug(KDENLIVE_LOG) << " FILTER; adding :" << a.name() << ':' << a.value(); auto property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), a.name()); auto property_value = m_doc.createTextNode(a.value()); property.appendChild(property_value); filt.appendChild(property); } } } m = m.nextSibling(); } } /* QDomNodeList filters = m_doc.elementsByTagName("filter"); max = filters.count(); QString last_id; int effectix = 0; for (int i = 0; i < max; ++i) { QDomElement filt = filters.at(i).toElement(); QDomNamedNodeMap attrs = filt.attributes(); QString current_id = filt.attribute("kdenlive_id"); if (current_id != last_id) { effectix++; last_id = current_id; } QDomElement e = m_doc.createElement("property"); e.setAttribute("name", "kdenlive_ix"); QDomText value = m_doc.createTextNode(QString::number(1)); e.appendChild(value); filt.appendChild(e); for (int j = 0; j < attrs.count(); ++j) { QDomAttr a = attrs.item(j).toAttr(); if (!a.isNull()) { //qCDebug(KDENLIVE_LOG) << " FILTER; adding :" << a.name() << ':' << a.value(); QDomElement e = m_doc.createElement("property"); e.setAttribute("name", a.name()); QDomText value = m_doc.createTextNode(a.value()); e.appendChild(value); filt.appendChild(e); } } }*/ // fix slowmotion QDomNodeList producers = westley.toElement().elementsByTagName(QStringLiteral("producer")); max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.attribute(QStringLiteral("mlt_service")) == QLatin1String("framebuffer")) { QString slowmotionprod = prod.attribute(QStringLiteral("resource")); slowmotionprod.replace(QLatin1Char(':'), QLatin1Char('?')); // qCDebug(KDENLIVE_LOG) << "// FOUND WRONG SLOWMO, new: " << slowmotionprod; prod.setAttribute(QStringLiteral("resource"), slowmotionprod); } } // move producers to correct place, markers to a global list, fix clip descriptions QDomElement markers = m_doc.createElement(QStringLiteral("markers")); // This will get the xml producers: producers = m_doc.elementsByTagName(QStringLiteral("producer")); max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(0).toElement(); // add resource also as a property (to allow path correction in setNewResource()) // TODO: will it work with slowmotion? needs testing /*if (!prod.attribute("resource").isEmpty()) { QDomElement prop_resource = m_doc.createElement("property"); prop_resource.setAttribute("name", "resource"); QDomText resource = m_doc.createTextNode(prod.attribute("resource")); prop_resource.appendChild(resource); prod.appendChild(prop_resource); }*/ QDomNode m = prod.firstChild(); if (!m.isNull()) { if (m.toElement().tagName() == QLatin1String("markers")) { QDomNodeList prodchilds = m.childNodes(); int maxchild = prodchilds.count(); for (int k = 0; k < maxchild; ++k) { QDomElement mark = prodchilds.at(0).toElement(); mark.setAttribute(QStringLiteral("id"), prod.attribute(QStringLiteral("id"))); markers.insertAfter(mark, QDomNode()); } prod.removeChild(m); } else if (prod.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { // convert title clip if (m.toElement().tagName() == QLatin1String("textclip")) { QDomDocument tdoc; QDomElement titleclip = m.toElement(); QDomElement title = tdoc.createElement(QStringLiteral("kdenlivetitle")); tdoc.appendChild(title); QDomNodeList objects = titleclip.childNodes(); int maxchild = objects.count(); for (int k = 0; k < maxchild; ++k) { QDomElement ob = objects.at(k).toElement(); if (ob.attribute(QStringLiteral("type")) == QLatin1String("3")) { // text object - all of this goes into "xmldata"... QDomElement item = tdoc.createElement(QStringLiteral("item")); item.setAttribute(QStringLiteral("z-index"), ob.attribute(QStringLiteral("z"))); item.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsTextItem")); QDomElement position = tdoc.createElement(QStringLiteral("position")); position.setAttribute(QStringLiteral("x"), ob.attribute(QStringLiteral("x"))); position.setAttribute(QStringLiteral("y"), ob.attribute(QStringLiteral("y"))); QDomElement content = tdoc.createElement(QStringLiteral("content")); content.setAttribute(QStringLiteral("font"), ob.attribute(QStringLiteral("font_family"))); content.setAttribute(QStringLiteral("font-size"), ob.attribute(QStringLiteral("font_size"))); content.setAttribute(QStringLiteral("font-bold"), ob.attribute(QStringLiteral("bold"))); content.setAttribute(QStringLiteral("font-italic"), ob.attribute(QStringLiteral("italic"))); content.setAttribute(QStringLiteral("font-underline"), ob.attribute(QStringLiteral("underline"))); QString col = ob.attribute(QStringLiteral("color")); QColor c(col); content.setAttribute(QStringLiteral("font-color"), colorToString(c)); // todo: These fields are missing from the newly generated xmldata: // transform, startviewport, endviewport, background QDomText conttxt = tdoc.createTextNode(ob.attribute(QStringLiteral("text"))); content.appendChild(conttxt); item.appendChild(position); item.appendChild(content); title.appendChild(item); } else if (ob.attribute(QStringLiteral("type")) == QLatin1String("5")) { // rectangle object QDomElement item = tdoc.createElement(QStringLiteral("item")); item.setAttribute(QStringLiteral("z-index"), ob.attribute(QStringLiteral("z"))); item.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsRectItem")); QDomElement position = tdoc.createElement(QStringLiteral("position")); position.setAttribute(QStringLiteral("x"), ob.attribute(QStringLiteral("x"))); position.setAttribute(QStringLiteral("y"), ob.attribute(QStringLiteral("y"))); QDomElement content = tdoc.createElement(QStringLiteral("content")); QString col = ob.attribute(QStringLiteral("color")); QColor c(col); content.setAttribute(QStringLiteral("brushcolor"), colorToString(c)); QString rect = QStringLiteral("0,0,"); rect.append(ob.attribute(QStringLiteral("width"))); rect.append(QLatin1String(",")); rect.append(ob.attribute(QStringLiteral("height"))); content.setAttribute(QStringLiteral("rect"), rect); item.appendChild(position); item.appendChild(content); title.appendChild(item); } } prod.setAttribute(QStringLiteral("xmldata"), tdoc.toString()); // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder()); // prod.setAttribute("titlename", titleInfo.at(0)); // prod.setAttribute("resource", titleInfo.at(1)); ////qCDebug(KDENLIVE_LOG)<<"TITLE DATA:\n"< 0) { prod.setAttribute(QStringLiteral("out"), QString::number(duration)); } // The clip goes back in, but text clips should not go back in, at least not modified westley.insertBefore(prod, QDomNode()); } QDomNode westley0 = m_doc.elementsByTagName(QStringLiteral("westley")).at(0); if (!markers.firstChild().isNull()) { westley0.appendChild(markers); } /* * Convert as much of the kdenlivedoc as possible. Use the producer in * westley. First, remove the old stuff from westley, and add a new * empty one. Also, track the max id in order to use it for the adding * of groups/folders */ int max_kproducer_id = 0; westley0.removeChild(infoXmlNode); QDomElement infoXml_new = m_doc.createElement(QStringLiteral("kdenlivedoc")); infoXml_new.setAttribute(QStringLiteral("profile"), profile); infoXml.setAttribute(QStringLiteral("position"), startPos); // Add all the producers that has a resource in westley QDomElement westley_element = westley0.toElement(); if (westley_element.isNull()) { qCWarning(KDENLIVE_LOG) << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc"; } else { QDomNodeList wproducers = westley_element.elementsByTagName(QStringLiteral("producer")); int kmax = wproducers.count(); for (int i = 0; i < kmax; ++i) { QDomElement wproducer = wproducers.at(i).toElement(); if (wproducer.isNull()) { qCWarning(KDENLIVE_LOG) << "Found producer in westley0, that was not a QDomElement"; continue; } if (wproducer.attribute(QStringLiteral("id")) == QLatin1String("black")) { continue; } // We have to do slightly different things, depending on the type // qCDebug(KDENLIVE_LOG) << "Converting producer element with type" << wproducer.attribute("type"); if (wproducer.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { // qCDebug(KDENLIVE_LOG) << "Found TEXT element in producer" << endl; QDomElement kproducer = wproducer.cloneNode(true).toElement(); kproducer.setTagName(QStringLiteral("kdenlive_producer")); infoXml_new.appendChild(kproducer); /* * TODO: Perhaps needs some more changes here to * "frequency", aspect ratio as a float, frame_size, * channels, and later, resource and title name */ } else { QDomElement kproducer = m_doc.createElement(QStringLiteral("kdenlive_producer")); kproducer.setAttribute(QStringLiteral("id"), wproducer.attribute(QStringLiteral("id"))); if (!wproducer.attribute(QStringLiteral("description")).isEmpty()) { kproducer.setAttribute(QStringLiteral("description"), wproducer.attribute(QStringLiteral("description"))); } kproducer.setAttribute(QStringLiteral("resource"), wproducer.attribute(QStringLiteral("resource"))); kproducer.setAttribute(QStringLiteral("type"), wproducer.attribute(QStringLiteral("type"))); // Testing fix for 358 if (!wproducer.attribute(QStringLiteral("aspect_ratio")).isEmpty()) { kproducer.setAttribute(QStringLiteral("aspect_ratio"), wproducer.attribute(QStringLiteral("aspect_ratio"))); } if (!wproducer.attribute(QStringLiteral("source_fps")).isEmpty()) { kproducer.setAttribute(QStringLiteral("fps"), wproducer.attribute(QStringLiteral("source_fps"))); } if (!wproducer.attribute(QStringLiteral("length")).isEmpty()) { kproducer.setAttribute(QStringLiteral("duration"), wproducer.attribute(QStringLiteral("length"))); } infoXml_new.appendChild(kproducer); } if (wproducer.attribute(QStringLiteral("id")).toInt() > max_kproducer_id) { max_kproducer_id = wproducer.attribute(QStringLiteral("id")).toInt(); } } } #define LOOKUP_FOLDER 1 #ifdef LOOKUP_FOLDER /* * Look through all the folder elements of the old doc, for each folder, * for each producer, get the id, look it up in the new doc, set the * groupname and groupid. Note, this does not work at the moment - at * least one folder shows up missing, and clips with no folder does not * show up. */ // QDomElement infoXml_old = infoXmlNode.toElement(); if (!infoXml_old.isNull()) { QDomNodeList folders = infoXml_old.elementsByTagName(QStringLiteral("folder")); int fsize = folders.size(); int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers for (int i = 0; i < fsize; ++i) { QDomElement folder = folders.at(i).toElement(); if (!folder.isNull()) { QString groupName = folder.attribute(QStringLiteral("name")); // qCDebug(KDENLIVE_LOG) << "groupName: " << groupName << " with groupId: " << groupId; QDomNodeList fproducers = folder.elementsByTagName(QStringLiteral("producer")); int psize = fproducers.size(); for (int j = 0; j < psize; ++j) { QDomElement fproducer = fproducers.at(j).toElement(); if (!fproducer.isNull()) { QString id = fproducer.attribute(QStringLiteral("id")); // This is not very effective, but compared to loading the clips, its a breeze QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName(QStringLiteral("kdenlive_producer")); int kpsize = kdenlive_producers.size(); for (int k = 0; k < kpsize; ++k) { QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure if (id == kproducer.attribute(QStringLiteral("id"))) { // We do not check that it already is part of a folder kproducer.setAttribute(QStringLiteral("groupid"), groupId); kproducer.setAttribute(QStringLiteral("groupname"), groupName); break; } } } } ++groupId; } } } #endif QDomNodeList elements = westley.childNodes(); max = elements.count(); for (int i = 0; i < max; ++i) { QDomElement prod = elements.at(0).toElement(); westley0.insertAfter(prod, QDomNode()); } westley0.appendChild(infoXml_new); westley0.removeChild(westley); // adds information to QDomNodeList kproducers = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); QDomNodeList avfiles = infoXml_old.elementsByTagName(QStringLiteral("avfile")); // qCDebug(KDENLIVE_LOG) << "found" << avfiles.count() << "s and" << kproducers.count() << "s"; for (int i = 0; i < avfiles.count(); ++i) { QDomElement avfile = avfiles.at(i).toElement(); QDomElement kproducer; if (avfile.isNull()) { qCWarning(KDENLIVE_LOG) << "found an that is not a QDomElement"; } else { QString id = avfile.attribute(QStringLiteral("id")); // this is horrible, must be rewritten, it's just for test for (int j = 0; j < kproducers.count(); ++j) { ////qCDebug(KDENLIVE_LOG) << "checking with id" << kproducers.at(j).toElement().attribute("id"); if (kproducers.at(j).toElement().attribute(QStringLiteral("id")) == id) { kproducer = kproducers.at(j).toElement(); break; } } if (kproducer == QDomElement()) { qCWarning(KDENLIVE_LOG) << "no match for with id =" << id; } else { ////qCDebug(KDENLIVE_LOG) << "ready to set additional 's attributes (id =" << id << ')'; kproducer.setAttribute(QStringLiteral("channels"), avfile.attribute(QStringLiteral("channels"))); kproducer.setAttribute(QStringLiteral("duration"), avfile.attribute(QStringLiteral("duration"))); kproducer.setAttribute(QStringLiteral("frame_size"), avfile.attribute(QStringLiteral("width")) + QLatin1Char('x') + avfile.attribute(QStringLiteral("height"))); kproducer.setAttribute(QStringLiteral("frequency"), avfile.attribute(QStringLiteral("frequency"))); if (kproducer.attribute(QStringLiteral("description")).isEmpty() && !avfile.attribute(QStringLiteral("description")).isEmpty()) { kproducer.setAttribute(QStringLiteral("description"), avfile.attribute(QStringLiteral("description"))); } } } } infoXml = infoXml_new; } if (version <= 0.81) { // Add the tracks information QString tracksOrder = infoXml.attribute(QStringLiteral("tracks")); if (tracksOrder.isEmpty()) { QDomNodeList tracks = m_doc.elementsByTagName(QStringLiteral("track")); for (int i = 0; i < tracks.count(); ++i) { QDomElement track = tracks.at(i).toElement(); if (track.attribute(QStringLiteral("producer")) != QLatin1String("black_track")) { if (track.attribute(QStringLiteral("hide")) == QLatin1String("video")) { tracksOrder.append(QLatin1Char('a')); } else { tracksOrder.append(QLatin1Char('v')); } } } } QDomElement tracksinfo = m_doc.createElement(QStringLiteral("tracksinfo")); for (int i = 0; i < tracksOrder.size(); ++i) { QDomElement trackinfo = m_doc.createElement(QStringLiteral("trackinfo")); if (tracksOrder.data()[i] == QLatin1Char('a')) { trackinfo.setAttribute(QStringLiteral("type"), QStringLiteral("audio")); trackinfo.setAttribute(QStringLiteral("blind"), 1); } else { trackinfo.setAttribute(QStringLiteral("blind"), 0); } trackinfo.setAttribute(QStringLiteral("mute"), 0); trackinfo.setAttribute(QStringLiteral("locked"), 0); tracksinfo.appendChild(trackinfo); } infoXml.appendChild(tracksinfo); } if (version <= 0.82) { // Convert s in s (MLT extreme makeover) QDomNodeList westleyNodes = m_doc.elementsByTagName(QStringLiteral("westley")); for (int i = 0; i < westleyNodes.count(); ++i) { QDomElement westley = westleyNodes.at(i).toElement(); westley.setTagName(QStringLiteral("mlt")); } } if (version <= 0.83) { // Replace point size with pixel size in text titles if (m_doc.toString().contains(QStringLiteral("font-size"))) { KMessageBox::ButtonCode convert = KMessageBox::Continue; QDomNodeList kproducerNodes = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) { QDomElement kproducer = kproducerNodes.at(i).toElement(); if (kproducer.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { QDomDocument data; data.setContent(kproducer.attribute(QStringLiteral("xmldata"))); QDomNodeList items = data.firstChild().childNodes(); for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) { if (items.at(j).attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsTextItem")) { QDomNamedNodeMap textProperties = items.at(j).namedItem(QStringLiteral("content")).attributes(); if (textProperties.namedItem(QStringLiteral("font-pixel-size")).isNull() && !textProperties.namedItem(QStringLiteral("font-size")).isNull()) { // Ask the user if he wants to convert if (convert != KMessageBox::Yes && convert != KMessageBox::No) { convert = (KMessageBox::ButtonCode)KMessageBox::warningYesNo( QApplication::activeWindow(), i18n("Some of your text clips were saved with size in points, which means different sizes on different displays. Do " "you want to convert them to pixel size, making them portable? It is recommended you do this on the computer they " "were first created on, or you could have to adjust their size."), i18n("Update Text Clips")); } if (convert == KMessageBox::Yes) { QFont font; font.setPointSize(textProperties.namedItem(QStringLiteral("font-size")).nodeValue().toInt()); QDomElement content = items.at(j).namedItem(QStringLiteral("content")).toElement(); content.setAttribute(QStringLiteral("font-pixel-size"), QFontInfo(font).pixelSize()); content.removeAttribute(QStringLiteral("font-size")); kproducer.setAttribute(QStringLiteral("xmldata"), data.toString()); /* * You may be tempted to delete the preview file * to force its recreation: bad idea (see * http://www.kdenlive.org/mantis/view.php?id=749) */ } } } } } } } // Fill the element QDomElement docProperties = infoXml.firstChildElement(QStringLiteral("documentproperties")); if (docProperties.isNull()) { docProperties = m_doc.createElement(QStringLiteral("documentproperties")); docProperties.setAttribute(QStringLiteral("zonein"), infoXml.attribute(QStringLiteral("zonein"))); docProperties.setAttribute(QStringLiteral("zoneout"), infoXml.attribute(QStringLiteral("zoneout"))); docProperties.setAttribute(QStringLiteral("zoom"), infoXml.attribute(QStringLiteral("zoom"))); docProperties.setAttribute(QStringLiteral("position"), infoXml.attribute(QStringLiteral("position"))); infoXml.appendChild(docProperties); } } if (version <= 0.84) { // update the title clips to use the new MLT kdenlivetitle producer QDomNodeList kproducerNodes = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); for (int i = 0; i < kproducerNodes.count(); ++i) { QDomElement kproducer = kproducerNodes.at(i).toElement(); if (kproducer.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { QString data = kproducer.attribute(QStringLiteral("xmldata")); QString datafile = kproducer.attribute(QStringLiteral("resource")); if (!datafile.endsWith(QLatin1String(".kdenlivetitle"))) { datafile = QString(); kproducer.setAttribute(QStringLiteral("resource"), QString()); } QString id = kproducer.attribute(QStringLiteral("id")); QDomNodeList mltproducers = m_doc.elementsByTagName(QStringLiteral("producer")); bool foundData = false; bool foundResource = false; bool foundService = false; for (int j = 0; j < mltproducers.count(); ++j) { QDomElement wproducer = mltproducers.at(j).toElement(); if (wproducer.attribute(QStringLiteral("id")) == id) { QDomNodeList props = wproducer.childNodes(); for (int k = 0; k < props.count(); ++k) { if (props.at(k).toElement().attribute(QStringLiteral("name")) == QLatin1String("xmldata")) { props.at(k).firstChild().setNodeValue(data); foundData = true; } else if (props.at(k).toElement().attribute(QStringLiteral("name")) == QLatin1String("mlt_service")) { props.at(k).firstChild().setNodeValue(QStringLiteral("kdenlivetitle")); foundService = true; } else if (props.at(k).toElement().attribute(QStringLiteral("name")) == QLatin1String("resource")) { props.at(k).firstChild().setNodeValue(datafile); foundResource = true; } } if (!foundData) { QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("xmldata")); QDomText value = m_doc.createTextNode(data); e.appendChild(value); wproducer.appendChild(e); } if (!foundService) { QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); QDomText value = m_doc.createTextNode(QStringLiteral("kdenlivetitle")); e.appendChild(value); wproducer.appendChild(e); } if (!foundResource) { QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("resource")); QDomText value = m_doc.createTextNode(datafile); e.appendChild(value); wproducer.appendChild(e); } break; } } } } } if (version <= 0.85) { // update the LADSPA effects to use the new ladspa.id format instead of external xml file QDomNodeList effectNodes = m_doc.elementsByTagName(QStringLiteral("filter")); for (int i = 0; i < effectNodes.count(); ++i) { QDomElement effect = effectNodes.at(i).toElement(); if (Xml::getXmlProperty(effect, QStringLiteral("mlt_service")) == QLatin1String("ladspa")) { // Needs to be converted QStringList info = getInfoFromEffectName(Xml::getXmlProperty(effect, QStringLiteral("kdenlive_id"))); if (info.isEmpty()) { continue; } // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names Xml::setXmlProperty(effect, QStringLiteral("kdenlive_id"), info.at(0)); Xml::setXmlProperty(effect, QStringLiteral("tag"), info.at(0)); Xml::setXmlProperty(effect, QStringLiteral("mlt_service"), info.at(0)); Xml::removeXmlProperty(effect, QStringLiteral("src")); for (int j = 1; j < info.size(); ++j) { QString value = Xml::getXmlProperty(effect, info.at(j).section(QLatin1Char('='), 0, 0)); if (!value.isEmpty()) { // update parameter name Xml::renameXmlProperty(effect, info.at(j).section(QLatin1Char('='), 0, 0), info.at(j).section(QLatin1Char('='), 1, 1)); } } } } } if (version <= 0.86) { // Make sure we don't have avformat-novalidate producers, since it caused crashes QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (Xml::getXmlProperty(prod, QStringLiteral("mlt_service")) == QLatin1String("avformat-novalidate")) { Xml::setXmlProperty(prod, QStringLiteral("mlt_service"), QStringLiteral("avformat")); } } // There was a mistake in Geometry transitions where the last keyframe was created one frame after the end of transition, so fix it and move last // keyframe to real end of transition // Get profile info (width / height) int profileWidth; int profileHeight; QDomElement profile = m_doc.firstChildElement(QStringLiteral("profile")); if (profile.isNull()) { profile = infoXml.firstChildElement(QStringLiteral("profileinfo")); if (!profile.isNull()) { // old MLT format, we need to add profile QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomNode firstProd = m_doc.firstChildElement(QStringLiteral("producer")); QDomElement pr = profile.cloneNode().toElement(); pr.setTagName(QStringLiteral("profile")); mlt.insertBefore(pr, firstProd); } } if (profile.isNull()) { // could not find profile info, set PAL profileWidth = 720; profileHeight = 576; } else { profileWidth = profile.attribute(QStringLiteral("width")).toInt(); profileHeight = profile.attribute(QStringLiteral("height")).toInt(); } QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement trans = transitions.at(i).toElement(); int out = trans.attribute(QStringLiteral("out")).toInt() - trans.attribute(QStringLiteral("in")).toInt(); QString geom = Xml::getXmlProperty(trans, QStringLiteral("geometry")); Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight); Mlt::GeometryItem item; if (g->next_key(&item, out) == 0) { // We have a keyframe just after last frame, try to move it to last frame if (item.frame() == out + 1) { item.frame(out); g->insert(item); g->remove(out + 1); Xml::setXmlProperty(trans, QStringLiteral("geometry"), QString::fromLatin1(g->serialise())); } } delete g; } } if (version <= 0.87) { if (!m_doc.firstChildElement(QStringLiteral("mlt")).hasAttribute(QStringLiteral("LC_NUMERIC"))) { m_doc.firstChildElement(QStringLiteral("mlt")).setAttribute(QStringLiteral("LC_NUMERIC"), QStringLiteral("C")); } } if (version <= 0.88) { // convert to new MLT-only format QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomDocumentFragment frag = m_doc.createDocumentFragment(); // Create Bin Playlist QDomElement main_playlist = m_doc.createElement(QStringLiteral("playlist")); QDomElement prop = m_doc.createElement(QStringLiteral("property")); prop.setAttribute(QStringLiteral("name"), QStringLiteral("xml_retain")); QDomText val = m_doc.createTextNode(QStringLiteral("1")); prop.appendChild(val); main_playlist.appendChild(prop); // Move markers QDomNodeList markers = m_doc.elementsByTagName(QStringLiteral("marker")); for (int i = 0; i < markers.count(); ++i) { QDomElement marker = markers.at(i).toElement(); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:marker.") + marker.attribute(QStringLiteral("id")) + QLatin1Char(':') + marker.attribute(QStringLiteral("time"))); QDomText val_node = m_doc.createTextNode(marker.attribute(QStringLiteral("type")) + QLatin1Char(':') + marker.attribute(QStringLiteral("comment"))); property.appendChild(val_node); main_playlist.appendChild(property); } // Move guides QDomNodeList guides = m_doc.elementsByTagName(QStringLiteral("guide")); for (int i = 0; i < guides.count(); ++i) { QDomElement guide = guides.at(i).toElement(); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:guide.") + guide.attribute(QStringLiteral("time"))); QDomText val_node = m_doc.createTextNode(guide.attribute(QStringLiteral("comment"))); property.appendChild(val_node); main_playlist.appendChild(property); } // Move folders QDomNodeList folders = m_doc.elementsByTagName(QStringLiteral("folder")); for (int i = 0; i < folders.count(); ++i) { QDomElement folder = folders.at(i).toElement(); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:folder.-1.") + folder.attribute(QStringLiteral("id"))); QDomText val_node = m_doc.createTextNode(folder.attribute(QStringLiteral("name"))); property.appendChild(val_node); main_playlist.appendChild(property); } QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); main_playlist.setAttribute(QStringLiteral("id"), BinPlaylist::binPlaylistId); mlt.toElement().setAttribute(QStringLiteral("producer"), BinPlaylist::binPlaylistId); QStringList ids; QStringList slowmotionIds; QDomNode firstProd = m_doc.firstChildElement(QStringLiteral("producer")); QDomNodeList kdenlive_producers = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); // Rename all track producers to correct name: "id_playlistName" instead of "id_trackNumber" QMap trackRenaming; // Create a list of which producers / track on which the producer is QMap playlistForId; QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); for (int i = 0; i < entries.count(); i++) { QDomElement entry = entries.at(i).toElement(); QString entryId = entry.attribute(QStringLiteral("producer")); if (entryId == QLatin1String("black")) { continue; } bool audioOnlyProducer = false; if (trackRenaming.contains(entryId)) { // rename entry.setAttribute(QStringLiteral("producer"), trackRenaming.value(entryId)); continue; } if (entryId.endsWith(QLatin1String("_video"))) { // Video only producers are not track aware continue; } if (entryId.endsWith(QLatin1String("_audio"))) { // Audio only producer audioOnlyProducer = true; entryId = entryId.section(QLatin1Char('_'), 0, -2); } if (!entryId.contains(QLatin1Char('_'))) { // not a track producer playlistForId.insert(entryId, entry.parentNode().toElement().attribute(QStringLiteral("id"))); continue; } if (entryId.startsWith(QLatin1String("slowmotion:"))) { // Check broken slowmotion producers (they should not be track aware) QString newId = QStringLiteral("slowmotion:") + entryId.section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0) + QLatin1Char(':') + entryId.section(QLatin1Char(':'), 2); trackRenaming.insert(entryId, newId); entry.setAttribute(QStringLiteral("producer"), newId); continue; } QString track = entryId.section(QLatin1Char('_'), 1, 1); QString playlistId = entry.parentNode().toElement().attribute(QStringLiteral("id")); if (track == playlistId) { continue; } QString newId = entryId.section(QLatin1Char('_'), 0, 0) + QLatin1Char('_') + playlistId; if (audioOnlyProducer) { newId.append(QStringLiteral("_audio")); trackRenaming.insert(entryId + QStringLiteral("_audio"), newId); } else { trackRenaming.insert(entryId, newId); } entry.setAttribute(QStringLiteral("producer"), newId); } if (!trackRenaming.isEmpty()) { for (int i = 0; i < producers.count(); ++i) { QDomElement prod = producers.at(i).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (trackRenaming.contains(id)) { prod.setAttribute(QStringLiteral("id"), trackRenaming.value(id)); } } } // Create easily searchable index of original producers QMap m_source_producers; for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); m_source_producers.insert(id, prod); } for (int i = 0; i < producers.count(); ++i) { QDomElement prod = producers.at(i).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (id == QLatin1String("black")) { continue; } if (id.startsWith(QLatin1String("slowmotion"))) { // No need to process slowmotion producers QString slowmo = id.section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0); if (!slowmotionIds.contains(slowmo)) { slowmotionIds << slowmo; } continue; } QString prodId = id.section(QLatin1Char('_'), 0, 0); if (ids.contains(prodId)) { // Make sure we didn't create a duplicate if (ids.contains(id)) { // we have a duplicate, check if this needs to be a track producer QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); int a_ix = Xml::getXmlProperty(prod, QStringLiteral("audio_index")).toInt(); if (service == QLatin1String("xml") || service == QLatin1String("consumer") || (service.contains(QStringLiteral("avformat")) && a_ix != -1)) { // This should be a track producer, rename QString newId = id + QLatin1Char('_') + playlistForId.value(id); prod.setAttribute(QStringLiteral("id"), newId); for (int j = 0; j < entries.count(); j++) { QDomElement entry = entries.at(j).toElement(); QString entryId = entry.attribute(QStringLiteral("producer")); if (entryId == id) { entry.setAttribute(QStringLiteral("producer"), newId); } } } else { // This is a duplicate, remove mlt.removeChild(prod); i--; } } // Already processed, continue continue; } if (id == prodId) { // This is an original producer, move it to the main playlist QDomElement entry = m_doc.createElement(QStringLiteral("entry")); entry.setAttribute(QStringLiteral("producer"), id); main_playlist.appendChild(entry); QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); if (service == QLatin1String("kdenlivetitle")) { fixTitleProducerLocale(prod); } QDomElement source = m_source_producers.value(id); if (!source.isNull()) { updateProducerInfo(prod, source); entry.setAttribute(QStringLiteral("in"), QStringLiteral("0")); entry.setAttribute(QStringLiteral("out"), QString::number(source.attribute(QStringLiteral("duration")).toInt() - 1)); } frag.appendChild(prod); // Changing prod parent removes it from list, so rewind index i--; } else { QDomElement originalProd = prod.cloneNode().toElement(); originalProd.setAttribute(QStringLiteral("id"), prodId); if (id.endsWith(QLatin1String("_audio"))) { Xml::removeXmlProperty(originalProd, QStringLiteral("video_index")); } else if (id.endsWith(QLatin1String("_video"))) { Xml::removeXmlProperty(originalProd, QStringLiteral("audio_index")); } QDomElement source = m_source_producers.value(prodId); QDomElement entry = m_doc.createElement(QStringLiteral("entry")); if (!source.isNull()) { updateProducerInfo(originalProd, source); entry.setAttribute(QStringLiteral("in"), QStringLiteral("0")); entry.setAttribute(QStringLiteral("out"), QString::number(source.attribute(QStringLiteral("duration")).toInt() - 1)); } frag.appendChild(originalProd); entry.setAttribute(QStringLiteral("producer"), prodId); main_playlist.appendChild(entry); } ids.append(prodId); } // Make sure to include producers that were not in timeline for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (!ids.contains(id)) { // Clip was not in timeline, create it QDomElement originalProd = prod.cloneNode().toElement(); originalProd.setTagName(QStringLiteral("producer")); Xml::setXmlProperty(originalProd, QStringLiteral("resource"), originalProd.attribute(QStringLiteral("resource"))); updateProducerInfo(originalProd, prod); originalProd.removeAttribute(QStringLiteral("proxy")); originalProd.removeAttribute(QStringLiteral("type")); originalProd.removeAttribute(QStringLiteral("file_hash")); originalProd.removeAttribute(QStringLiteral("file_size")); originalProd.removeAttribute(QStringLiteral("frame_size")); originalProd.removeAttribute(QStringLiteral("zone_out")); originalProd.removeAttribute(QStringLiteral("zone_in")); originalProd.removeAttribute(QStringLiteral("name")); originalProd.removeAttribute(QStringLiteral("type")); originalProd.removeAttribute(QStringLiteral("duration")); originalProd.removeAttribute(QStringLiteral("cutzones")); int type = prod.attribute(QStringLiteral("type")).toInt(); QString mltService; switch (type) { case 4: mltService = QStringLiteral("colour"); break; case 5: case 7: mltService = QStringLiteral("qimage"); break; case 6: mltService = QStringLiteral("kdenlivetitle"); break; case 9: mltService = QStringLiteral("xml"); break; default: mltService = QStringLiteral("avformat"); break; } Xml::setXmlProperty(originalProd, QStringLiteral("mlt_service"), mltService); Xml::setXmlProperty(originalProd, QStringLiteral("mlt_type"), QStringLiteral("producer")); QDomElement entry = m_doc.createElement(QStringLiteral("entry")); entry.setAttribute(QStringLiteral("in"), QStringLiteral("0")); entry.setAttribute(QStringLiteral("out"), QString::number(prod.attribute(QStringLiteral("duration")).toInt() - 1)); entry.setAttribute(QStringLiteral("producer"), id); main_playlist.appendChild(entry); if (type == 6) { fixTitleProducerLocale(originalProd); } frag.appendChild(originalProd); ids << id; } } // Set clip folders for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); QString folder = prod.attribute(QStringLiteral("groupid")); QDomNodeList mlt_producers = frag.childNodes(); for (int k = 0; k < mlt_producers.count(); k++) { QDomElement mltprod = mlt_producers.at(k).toElement(); if (mltprod.tagName() != QLatin1String("producer")) { continue; } if (mltprod.attribute(QStringLiteral("id")) == id) { if (!folder.isEmpty()) { // We have found our producer, set folder info QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:folderid")); QDomText val_node = m_doc.createTextNode(folder); property.appendChild(val_node); mltprod.appendChild(property); } break; } } } // Make sure all slowmotion producers have a master clip for (int i = 0; i < slowmotionIds.count(); i++) { const QString &slo = slowmotionIds.at(i); if (!ids.contains(slo)) { // rebuild producer from Kdenlive's old xml format for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (id == slo) { // We found the kdenlive_producer, build MLT producer QDomElement original = m_doc.createElement(QStringLiteral("producer")); original.setAttribute(QStringLiteral("in"), 0); original.setAttribute(QStringLiteral("out"), prod.attribute(QStringLiteral("duration")).toInt() - 1); original.setAttribute(QStringLiteral("id"), id); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("resource")); QDomText val_node = m_doc.createTextNode(prod.attribute(QStringLiteral("resource"))); property.appendChild(val_node); original.appendChild(property); QDomElement prop2 = m_doc.createElement(QStringLiteral("property")); prop2.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); QDomText val2 = m_doc.createTextNode(QStringLiteral("avformat")); prop2.appendChild(val2); original.appendChild(prop2); QDomElement prop3 = m_doc.createElement(QStringLiteral("property")); prop3.setAttribute(QStringLiteral("name"), QStringLiteral("length")); QDomText val3 = m_doc.createTextNode(prod.attribute(QStringLiteral("duration"))); prop3.appendChild(val3); original.appendChild(prop3); QDomElement entry = m_doc.createElement(QStringLiteral("entry")); entry.setAttribute(QStringLiteral("in"), original.attribute(QStringLiteral("in"))); entry.setAttribute(QStringLiteral("out"), original.attribute(QStringLiteral("out"))); entry.setAttribute(QStringLiteral("producer"), id); main_playlist.appendChild(entry); frag.appendChild(original); ids << slo; break; } } } } frag.appendChild(main_playlist); mlt.insertBefore(frag, firstProd); } if (version < 0.91) { // Migrate track properties QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomNodeList old_tracks = m_doc.elementsByTagName(QStringLiteral("trackinfo")); QDomNodeList tracks = m_doc.elementsByTagName(QStringLiteral("track")); QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < old_tracks.count(); i++) { QString playlistName = tracks.at(i + 1).toElement().attribute(QStringLiteral("producer")); // find playlist for track QDomElement trackPlaylist; for (int j = 0; j < playlists.count(); j++) { if (playlists.at(j).toElement().attribute(QStringLiteral("id")) == playlistName) { trackPlaylist = playlists.at(j).toElement(); break; } } if (!trackPlaylist.isNull()) { QDomElement kdenliveTrack = old_tracks.at(i).toElement(); if (kdenliveTrack.attribute(QStringLiteral("type")) == QLatin1String("audio")) { Xml::setXmlProperty(trackPlaylist, QStringLiteral("kdenlive:audio_track"), QStringLiteral("1")); } if (kdenliveTrack.attribute(QStringLiteral("locked")) == QLatin1String("1")) { Xml::setXmlProperty(trackPlaylist, QStringLiteral("kdenlive:locked_track"), QStringLiteral("1")); } Xml::setXmlProperty(trackPlaylist, QStringLiteral("kdenlive:track_name"), kdenliveTrack.attribute(QStringLiteral("trackname"))); } } // Find bin playlist playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); QDomElement playlist; for (int i = 0; i < playlists.count(); i++) { if (playlists.at(i).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) { playlist = playlists.at(i).toElement(); break; } } // Migrate document notes QDomNodeList notesList = m_doc.elementsByTagName(QStringLiteral("documentnotes")); if (!notesList.isEmpty()) { QDomElement notes_elem = notesList.at(0).toElement(); QString notes = notes_elem.firstChild().nodeValue(); Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:documentnotes"), notes); } // Migrate clip groups QDomNodeList groupElement = m_doc.elementsByTagName(QStringLiteral("groups")); if (!groupElement.isEmpty()) { QDomElement groups = groupElement.at(0).toElement(); QDomDocument d2; d2.importNode(groups, true); Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:clipgroups"), d2.toString()); } // Migrate custom effects QDomNodeList effectsElement = m_doc.elementsByTagName(QStringLiteral("customeffects")); if (!effectsElement.isEmpty()) { QDomElement effects = effectsElement.at(0).toElement(); QDomDocument d2; d2.importNode(effects, true); Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:customeffects"), d2.toString()); } Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:docproperties.version"), QString::number(currentVersion)); if (!infoXml.isNull()) { Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:docproperties.projectfolder"), infoXml.attribute(QStringLiteral("projectfolder"))); } // Remove deprecated Kdenlive extra info from xml doc before sending it to MLT QDomElement docXml = mlt.firstChildElement(QStringLiteral("kdenlivedoc")); if (!docXml.isNull()) { mlt.removeChild(docXml); } } if (version < 0.92) { // Luma transition used for wipe is deprecated, we now use a composite, convert QDomNodeList transitionList = m_doc.elementsByTagName(QStringLiteral("transition")); QDomElement trans; for (int i = 0; i < transitionList.count(); i++) { trans = transitionList.at(i).toElement(); QString id = Xml::getXmlProperty(trans, QStringLiteral("kdenlive_id")); if (id == QLatin1String("luma")) { Xml::setXmlProperty(trans, QStringLiteral("kdenlive_id"), QStringLiteral("wipe")); Xml::setXmlProperty(trans, QStringLiteral("mlt_service"), QStringLiteral("composite")); bool reverse = Xml::getXmlProperty(trans, QStringLiteral("reverse")).toInt() != 0; Xml::setXmlProperty(trans, QStringLiteral("luma_invert"), Xml::getXmlProperty(trans, QStringLiteral("invert"))); Xml::setXmlProperty(trans, QStringLiteral("luma"), Xml::getXmlProperty(trans, QStringLiteral("resource"))); Xml::removeXmlProperty(trans, QStringLiteral("invert")); Xml::removeXmlProperty(trans, QStringLiteral("reverse")); Xml::removeXmlProperty(trans, QStringLiteral("resource")); if (reverse) { Xml::setXmlProperty(trans, QStringLiteral("geometry"), QStringLiteral("0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0")); } else { Xml::setXmlProperty(trans, QStringLiteral("geometry"), QStringLiteral("0%/0%:100%x100%:0;-1=0%/0%:100%x100%:100")); } Xml::setXmlProperty(trans, QStringLiteral("aligned"), QStringLiteral("0")); Xml::setXmlProperty(trans, QStringLiteral("fill"), QStringLiteral("1")); } } } if (version < 0.93) { // convert old keyframe filters to animated // these filters were "animated" by adding several instance of the filter, each one having a start and end tag. // We convert by parsing the start and end tags vor values and adding all to the new animated parameter QMap keyframeFilterToConvert; keyframeFilterToConvert.insert(QStringLiteral("volume"), QStringList() << QStringLiteral("gain") << QStringLiteral("end") << QStringLiteral("level")); keyframeFilterToConvert.insert(QStringLiteral("brightness"), QStringList() << QStringLiteral("start") << QStringLiteral("end") << QStringLiteral("level")); QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); for (int i = 0; i < entries.count(); i++) { QDomNode entry = entries.at(i); QDomNodeList effects = entry.toElement().elementsByTagName(QStringLiteral("filter")); QStringList parsedIds; for (int j = 0; j < effects.count(); j++) { QDomElement eff = effects.at(j).toElement(); QString id = Xml::getXmlProperty(eff, QStringLiteral("kdenlive_id")); if (keyframeFilterToConvert.contains(id) && !parsedIds.contains(id)) { parsedIds << id; QMap values; QStringList conversionParams = keyframeFilterToConvert.value(id); int offset = eff.attribute(QStringLiteral("in")).toInt(); int out = eff.attribute(QStringLiteral("out")).toInt(); convertKeyframeEffect(eff, conversionParams, values, offset); Xml::removeXmlProperty(eff, conversionParams.at(0)); Xml::removeXmlProperty(eff, conversionParams.at(1)); for (int k = j + 1; k < effects.count(); k++) { QDomElement subEffect = effects.at(k).toElement(); QString subId = Xml::getXmlProperty(subEffect, QStringLiteral("kdenlive_id")); if (subId == id) { convertKeyframeEffect(subEffect, conversionParams, values, offset); out = subEffect.attribute(QStringLiteral("out")).toInt(); entry.removeChild(subEffect); k--; } } QStringList parsedValues; QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); QMapIterator l(values); if (id == QLatin1String("volume")) { // convert old volume range (0-300) to new dB values (-60-60) while (l.hasNext()) { l.next(); double v = l.value(); if (v <= 0) { v = -60; } else { v = log10(v) * 20; } parsedValues << QString::number(l.key()) + QLatin1Char('=') + locale.toString(v); } } else { while (l.hasNext()) { l.next(); parsedValues << QString::number(l.key()) + QLatin1Char('=') + locale.toString(l.value()); } } Xml::setXmlProperty(eff, conversionParams.at(2), parsedValues.join(QLatin1Char(';'))); // Xml::setXmlProperty(eff, QStringLiteral("kdenlive:sync_in_out"), QStringLiteral("1")); eff.setAttribute(QStringLiteral("out"), out); } } } } if (version < 0.94) { // convert slowmotion effects/producers QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); QStringList slowmoIds; for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (id.startsWith(QLatin1String("slowmotion"))) { QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); if (service == QLatin1String("framebuffer")) { // convert to new timewarp producer prod.setAttribute(QStringLiteral("id"), id + QStringLiteral(":1")); slowmoIds << id; Xml::setXmlProperty(prod, QStringLiteral("mlt_service"), QStringLiteral("timewarp")); QString resource = Xml::getXmlProperty(prod, QStringLiteral("resource")); Xml::setXmlProperty(prod, QStringLiteral("warp_resource"), resource.section(QLatin1Char('?'), 0, 0)); Xml::setXmlProperty(prod, QStringLiteral("warp_speed"), resource.section(QLatin1Char('?'), 1).section(QLatin1Char(':'), 0, 0)); Xml::setXmlProperty(prod, QStringLiteral("resource"), resource.section(QLatin1Char('?'), 1) + QLatin1Char(':') + resource.section(QLatin1Char('?'), 0, 0)); Xml::setXmlProperty(prod, QStringLiteral("audio_index"), QStringLiteral("-1")); } } } if (!slowmoIds.isEmpty()) { producers = m_doc.elementsByTagName(QStringLiteral("entry")); max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); QString entryId = prod.attribute(QStringLiteral("producer")); if (slowmoIds.contains(entryId)) { prod.setAttribute(QStringLiteral("producer"), entryId + QStringLiteral(":1")); } } } // qCDebug(KDENLIVE_LOG)<<"------------------------\n"< markersList; QLocale locale; for (int i = 0; i < props.count(); ++i) { QDomNode n = props.at(i); QString prop = n.toElement().attribute(QStringLiteral("name")); if (prop.startsWith(QLatin1String("kdenlive:guide."))) { // Process guide double guidePos = locale.toDouble(prop.section(QLatin1Char('.'), 1)); QJsonObject currentGuide; currentGuide.insert(QStringLiteral("pos"), QJsonValue(GenTime(guidePos).frames(pCore->getCurrentFps()))); currentGuide.insert(QStringLiteral("comment"), QJsonValue(n.firstChild().nodeValue())); currentGuide.insert(QStringLiteral("type"), QJsonValue(0)); // Clear entry in old format n.toElement().setAttribute(QStringLiteral("name"), QStringLiteral("_")); guidesList.push_back(currentGuide); } else if (prop.startsWith(QLatin1String("kdenlive:marker."))) { // Process marker double markerPos = locale.toDouble(prop.section(QLatin1Char(':'), -1)); QString markerBinClip = prop.section(QLatin1Char('.'), 1).section(QLatin1Char(':'), 0, 0); QString markerData = n.firstChild().nodeValue(); int markerType = markerData.section(QLatin1Char(':'), 0, 0).toInt(); QString markerComment = markerData.section(QLatin1Char(':'), 1); QJsonObject currentMarker; currentMarker.insert(QStringLiteral("pos"), QJsonValue(GenTime(markerPos).frames(pCore->getCurrentFps()))); currentMarker.insert(QStringLiteral("comment"), QJsonValue(markerComment)); currentMarker.insert(QStringLiteral("type"), QJsonValue(markerType)); // Clear entry in old format n.toElement().setAttribute(QStringLiteral("name"), QStringLiteral("_")); if (markersList.contains(markerBinClip)) { // we already have a marker list for this clip QJsonArray markerList = markersList.value(markerBinClip); markerList.push_back(currentMarker); markersList.insert(markerBinClip, markerList); } else { QJsonArray markerList; markerList.push_back(currentMarker); markersList.insert(markerBinClip, markerList); } } } if (!guidesList.isEmpty()) { QJsonDocument json(guidesList); Xml::setXmlProperty(main_playlist, QStringLiteral("kdenlive:docproperties.guides"), json.toJson()); } // Update producers QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.isNull()) continue; // Move to new kdenlive:id format const QString id = prod.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); Xml::setXmlProperty(prod, QStringLiteral("kdenlive:id"), id); if (markersList.contains(id)) { QJsonDocument json(markersList.value(id)); Xml::setXmlProperty(prod, QStringLiteral("kdenlive:markers"), json.toJson()); } // Check image sequences with buggy begin frame number const QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); if (service == QLatin1String("pixbuf") || service == QLatin1String("qimage")) { QString resource = Xml::getXmlProperty(prod, QStringLiteral("resource")); if (resource.contains(QStringLiteral("?begin:"))) { resource.replace(QStringLiteral("?begin:"), QStringLiteral("?begin=")); Xml::setXmlProperty(prod, QStringLiteral("resource"), resource); } } } } if (version < 0.98) { // rename main bin playlist, create extra tracks for old type AV clips, port groups to JSon QJsonArray newGroups; QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); QDomNodeList masterProducers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomElement playlist; QDomNode mainplaylist; QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomNode tractor = mlt.firstChildElement(QStringLiteral("tractor")); // Build start trackIndex QMap trackIndex; QDomNodeList tracks = tractor.toElement().elementsByTagName(QStringLiteral("track")); for (int i = 0; i < tracks.count(); i++) { trackIndex.insert(QString::number(i), tracks.at(i).toElement().attribute(QStringLiteral("producer"))); } int trackOffset = 0; // AV clips are not supported anymore. Check if we have some and add extra audio tracks if necessary // Update the main bin name as well to be xml compliant for (int i = 0; i < playlists.count(); i++) { if (playlists.at(i).toElement().attribute(QStringLiteral("id")) == QLatin1String("main bin")) { playlists.at(i).toElement().setAttribute(QStringLiteral("id"), BinPlaylist::binPlaylistId); mainplaylist = playlists.at(i); QString oldGroups = Xml::getXmlProperty(mainplaylist.toElement(), QStringLiteral("kdenlive:clipgroups")); QDomDocument groupsDoc; groupsDoc.setContent(oldGroups); QDomNodeList groups = groupsDoc.elementsByTagName(QStringLiteral("group")); for (int g = 0; g < groups.count(); g++) { QDomNodeList elements = groups.at(g).childNodes(); QJsonArray array; for (int h = 0; h < elements.count(); h++) { QJsonObject item; item.insert(QLatin1String("type"), QJsonValue(QStringLiteral("Leaf"))); item.insert(QLatin1String("leaf"), QJsonValue(QLatin1String("clip"))); QString pos = elements.at(h).toElement().attribute(QStringLiteral("position")); QString track = trackIndex.value(elements.at(h).toElement().attribute(QStringLiteral("track"))); item.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(track).arg(pos))); array.push_back(item); } QJsonObject currentGroup; currentGroup.insert(QLatin1String("type"), QJsonValue(QStringLiteral("Normal"))); currentGroup.insert(QLatin1String("children"), array); newGroups.push_back(currentGroup); } } else { if (Xml::getXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:audio_track")) == QLatin1String("1")) { // Audio track, no need to process continue; } const QString playlistName = playlists.at(i).toElement().attribute(QStringLiteral("id")); QDomElement duplicate_playlist = m_doc.createElement(QStringLiteral("playlist")); duplicate_playlist.setAttribute(QStringLiteral("id"), QString("%1_duplicate").arg(playlistName)); QDomElement pltype = m_doc.createElement(QStringLiteral("property")); pltype.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:audio_track")); pltype.setNodeValue(QStringLiteral("1")); QDomText value1 = m_doc.createTextNode(QStringLiteral("1")); pltype.appendChild(value1); duplicate_playlist.appendChild(pltype); QDomElement plname = m_doc.createElement(QStringLiteral("property")); plname.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:track_name")); QDomText value = m_doc.createTextNode(i18n("extra audio")); plname.appendChild(value); duplicate_playlist.appendChild(plname); QDomNodeList producers = playlists.at(i).childNodes(); bool duplicationRequested = false; int pos = 0; for (int j = 0; j < producers.count(); j++) { if (producers.at(j).nodeName() == QLatin1String("blank")) { // blank, duplicate duplicate_playlist.appendChild(producers.at(j).cloneNode()); pos += producers.at(j).toElement().attribute(QStringLiteral("length")).toInt(); } else if (producers.at(j).nodeName() == QLatin1String("filter")) { // effect, duplicate duplicate_playlist.appendChild(producers.at(j).cloneNode()); } else if (producers.at(j).nodeName() != QLatin1String("entry")) { // property node, pass continue; } else if (producers.at(j).toElement().attribute(QStringLiteral("producer")).endsWith(playlistName)) { // This is an AV clip // Check master properties bool hasAudio = true; bool hasVideo = true; const QString currentId = producers.at(j).toElement().attribute(QStringLiteral("producer")); int in = producers.at(j).toElement().attribute(QStringLiteral("in")).toInt(); int out = producers.at(j).toElement().attribute(QStringLiteral("out")).toInt(); for (int k = 0; k < masterProducers.count(); k++) { if (masterProducers.at(k).toElement().attribute(QStringLiteral("id")) == currentId) { hasVideo = Xml::getXmlProperty(masterProducers.at(k).toElement(), QStringLiteral("video_index")) != QLatin1String("-1"); hasAudio = Xml::getXmlProperty(masterProducers.at(k).toElement(), QStringLiteral("audio_index")) != QLatin1String("-1"); break; } } if (!hasAudio) { // no duplication needed, replace with blank QDomElement duplicate = m_doc.createElement(QStringLiteral("blank")); duplicate.setAttribute(QStringLiteral("length"), QString::number(out - in + 1)); duplicate_playlist.appendChild(duplicate); pos += out - in + 1; continue; } QDomNode prod = producers.at(j).cloneNode(); Xml::setXmlProperty(prod.toElement(), QStringLiteral("set.test_video"), QStringLiteral("1")); duplicate_playlist.appendChild(prod); // Check if that is an audio clip on a video track if (!hasVideo) { // Audio clip on a video track, replace with blank and duplicate producers.at(j).toElement().setTagName("blank"); producers.at(j).toElement().setAttribute("length", QString::number(out - in + 1)); } else { // group newly created AVSplit group // We temporarily store track with their playlist name since track index will change // as we insert the duplicate tracks QJsonArray array; QJsonObject items; items.insert(QLatin1String("type"), QJsonValue(QStringLiteral("Leaf"))); items.insert(QLatin1String("leaf"), QJsonValue(QLatin1String("clip"))); items.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(playlistName).arg(pos))); array.push_back(items); QJsonObject itemb; itemb.insert(QLatin1String("type"), QJsonValue(QStringLiteral("Leaf"))); itemb.insert(QLatin1String("leaf"), QJsonValue(QLatin1String("clip"))); itemb.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(duplicate_playlist.attribute(QStringLiteral("id"))).arg(pos))); array.push_back(itemb); QJsonObject currentGroup; currentGroup.insert(QLatin1String("type"), QJsonValue(QStringLiteral("AVSplit"))); currentGroup.insert(QLatin1String("children"), array); newGroups.push_back(currentGroup); } duplicationRequested = true; pos += out - in + 1; } else { // no duplication needed, replace with blank QDomElement duplicate = m_doc.createElement(QStringLiteral("blank")); int in = producers.at(j).toElement().attribute(QStringLiteral("in")).toInt(); int out = producers.at(j).toElement().attribute(QStringLiteral("out")).toInt(); duplicate.setAttribute(QStringLiteral("length"), QString::number(out - in + 1)); duplicate_playlist.appendChild(duplicate); pos += out - in + 1; } } if (duplicationRequested) { // Plant the playlist at the end mlt.insertBefore(duplicate_playlist, tractor); QDomNode lastTrack = tractor.firstChildElement(QStringLiteral("track")); QDomElement duplicate = m_doc.createElement(QStringLiteral("track")); duplicate.setAttribute(QStringLiteral("producer"), QString("%1_duplicate").arg(playlistName)); duplicate.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); tractor.insertAfter(duplicate, lastTrack); trackOffset++; } } } if (trackOffset > 0) { // Some tracks were added, adjust compositions QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); int max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement t = transitions.at(i).toElement(); if (Xml::getXmlProperty(t, QStringLiteral("internal_added")).toInt() > 0) { // internal transitions will be rebuilt, no need to correct continue; } int a_track = Xml::getXmlProperty(t, QStringLiteral("a_track")).toInt(); int b_track = Xml::getXmlProperty(t, QStringLiteral("b_track")).toInt(); if (a_track > 0) { Xml::setXmlProperty(t, QStringLiteral("a_track"), QString::number(a_track + trackOffset)); } if (b_track > 0) { Xml::setXmlProperty(t, QStringLiteral("b_track"), QString::number(b_track + trackOffset)); } } } // Process groups data QJsonDocument json(newGroups); QString groupsData = QString(json.toJson()); tracks = tractor.toElement().elementsByTagName(QStringLiteral("track")); for (int i = 0; i < tracks.count(); i++) { // Replace track names with their current index in our view const QString trackId = QString("%1:").arg(tracks.at(i).toElement().attribute(QStringLiteral("producer"))); groupsData.replace(trackId, QString("%1:").arg(i - 1)); } Xml::setXmlProperty(mainplaylist.toElement(), QStringLiteral("kdenlive:docproperties.groups"), groupsData); } if (version < 0.99) { // rename main bin playlist, create extra tracks for old type AV clips, port groups to JSon QDomNodeList masterProducers = m_doc.elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < masterProducers.count(); i++) { QMap map = Xml::getXmlPropertyByWildcard(masterProducers.at(i).toElement(), QLatin1String("kdenlive:clipzone.")); if (map.isEmpty()) { continue; } QJsonArray list; QMapIterator j(map); while (j.hasNext()) { j.next(); Xml::removeXmlProperty(masterProducers.at(i).toElement(), j.key()); QJsonObject currentZone; currentZone.insert(QLatin1String("name"), QJsonValue(j.key().section(QLatin1Char('.'),1))); if (!j.value().contains(QLatin1Char(';'))) { // invalid zone continue; } currentZone.insert(QLatin1String("in"), QJsonValue(j.value().section(QLatin1Char(';'), 0, 0).toInt())); currentZone.insert(QLatin1String("out"), QJsonValue(j.value().section(QLatin1Char(';'), 1, 1).toInt())); list.push_back(currentZone); } QJsonDocument json(list); Xml::setXmlProperty(masterProducers.at(i).toElement(), QStringLiteral("kdenlive:clipzones"), QString(json.toJson())); } } m_modified = true; return true; } void DocumentValidator::convertKeyframeEffect(const QDomElement &effect, const QStringList ¶ms, QMap &values, int offset) { QLocale locale; int in = effect.attribute(QStringLiteral("in")).toInt() - offset; values.insert(in, locale.toDouble(Xml::getXmlProperty(effect, params.at(0)))); QString endValue = Xml::getXmlProperty(effect, params.at(1)); if (!endValue.isEmpty()) { int out = effect.attribute(QStringLiteral("out")).toInt() - offset; values.insert(out, locale.toDouble(endValue)); } } void DocumentValidator::updateProducerInfo(const QDomElement &prod, const QDomElement &source) { QString pxy = source.attribute(QStringLiteral("proxy")); if (pxy.length() > 1) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:proxy"), pxy); Xml::setXmlProperty(prod, QStringLiteral("kdenlive:originalurl"), source.attribute(QStringLiteral("resource"))); } if (source.hasAttribute(QStringLiteral("file_hash"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:file_hash"), source.attribute(QStringLiteral("file_hash"))); } if (source.hasAttribute(QStringLiteral("file_size"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:file_size"), source.attribute(QStringLiteral("file_size"))); } if (source.hasAttribute(QStringLiteral("name"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:clipname"), source.attribute(QStringLiteral("name"))); } if (source.hasAttribute(QStringLiteral("zone_out"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:zone_out"), source.attribute(QStringLiteral("zone_out"))); } if (source.hasAttribute(QStringLiteral("zone_in"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:zone_in"), source.attribute(QStringLiteral("zone_in"))); } if (source.hasAttribute(QStringLiteral("cutzones"))) { QString zoneData = source.attribute(QStringLiteral("cutzones")); const QStringList zoneList = zoneData.split(QLatin1Char(';')); int ct = 1; for (const QString &data : zoneList) { QString zoneName = data.section(QLatin1Char('-'), 2); if (zoneName.isEmpty()) { zoneName = i18n("Zone %1", ct++); } Xml::setXmlProperty(prod, QStringLiteral("kdenlive:clipzone.") + zoneName, data.section(QLatin1Char('-'), 0, 0) + QLatin1Char(';') + data.section(QLatin1Char('-'), 1, 1)); } } } QStringList DocumentValidator::getInfoFromEffectName(const QString &oldName) { QStringList info; // Returns a list to convert old Kdenlive ladspa effects if (oldName == QLatin1String("pitch_shift")) { info << QStringLiteral("ladspa.1433"); info << QStringLiteral("pitch=0"); } else if (oldName == QLatin1String("vinyl")) { info << QStringLiteral("ladspa.1905"); info << QStringLiteral("year=0"); info << QStringLiteral("rpm=1"); info << QStringLiteral("warping=2"); info << QStringLiteral("crackle=3"); info << QStringLiteral("wear=4"); } else if (oldName == QLatin1String("room_reverb")) { info << QStringLiteral("ladspa.1216"); info << QStringLiteral("room=0"); info << QStringLiteral("delay=1"); info << QStringLiteral("damp=2"); } else if (oldName == QLatin1String("reverb")) { info << QStringLiteral("ladspa.1423"); info << QStringLiteral("room=0"); info << QStringLiteral("damp=1"); } else if (oldName == QLatin1String("rate_scale")) { info << QStringLiteral("ladspa.1417"); info << QStringLiteral("rate=0"); } else if (oldName == QLatin1String("pitch_scale")) { info << QStringLiteral("ladspa.1193"); info << QStringLiteral("coef=0"); } else if (oldName == QLatin1String("phaser")) { info << QStringLiteral("ladspa.1217"); info << QStringLiteral("rate=0"); info << QStringLiteral("depth=1"); info << QStringLiteral("feedback=2"); info << QStringLiteral("spread=3"); } else if (oldName == QLatin1String("limiter")) { info << QStringLiteral("ladspa.1913"); info << QStringLiteral("gain=0"); info << QStringLiteral("limit=1"); info << QStringLiteral("release=2"); } else if (oldName == QLatin1String("equalizer_15")) { info << QStringLiteral("ladspa.1197"); info << QStringLiteral("1=0"); info << QStringLiteral("2=1"); info << QStringLiteral("3=2"); info << QStringLiteral("4=3"); info << QStringLiteral("5=4"); info << QStringLiteral("6=5"); info << QStringLiteral("7=6"); info << QStringLiteral("8=7"); info << QStringLiteral("9=8"); info << QStringLiteral("10=9"); info << QStringLiteral("11=10"); info << QStringLiteral("12=11"); info << QStringLiteral("13=12"); info << QStringLiteral("14=13"); info << QStringLiteral("15=14"); } else if (oldName == QLatin1String("equalizer")) { info << QStringLiteral("ladspa.1901"); info << QStringLiteral("logain=0"); info << QStringLiteral("midgain=1"); info << QStringLiteral("higain=2"); } else if (oldName == QLatin1String("declipper")) { info << QStringLiteral("ladspa.1195"); } return info; } QString DocumentValidator::colorToString(const QColor &c) { QString ret = QStringLiteral("%1,%2,%3,%4"); ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha()); return ret; } bool DocumentValidator::isProject() const { return m_doc.documentElement().tagName() == QLatin1String("mlt"); } bool DocumentValidator::isModified() const { return m_modified; } bool DocumentValidator::checkMovit() { QString playlist = m_doc.toString(); if (!playlist.contains(QStringLiteral("movit."))) { // Project does not use Movit GLSL effects, we can load it return true; } if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("The project file uses some GPU effects. GPU acceleration is not currently enabled.\nDo you want to convert the " "project to a non-GPU version?\nThis might result in data loss.")) != KMessageBox::Yes) { return false; } // Try to convert Movit filters to their non GPU equivalent QStringList convertedFilters; QStringList discardedFilters; bool hasWB = EffectsRepository::get()->exists(QStringLiteral("frei0r.colgate")); bool hasBlur = EffectsRepository::get()->exists(QStringLiteral("frei0r.IIRblur")); QString compositeTrans; if (TransitionsRepository::get()->exists(QStringLiteral("qtblend"))) { compositeTrans = QStringLiteral("qtblend"); } else if (TransitionsRepository::get()->exists(QStringLiteral("frei0r.cairoblend"))) { compositeTrans = QStringLiteral("frei0r.cairoblend"); } // Parse all effects in document QDomNodeList filters = m_doc.elementsByTagName(QStringLiteral("filter")); int max = filters.count(); for (int i = 0; i < max; ++i) { QDomElement filt = filters.at(i).toElement(); QString filterId = filt.attribute(QStringLiteral("id")); if (!filterId.startsWith(QLatin1String("movit."))) { continue; } if (filterId == QLatin1String("movit.white_balance") && hasWB) { // Convert to frei0r.colgate filt.setAttribute(QStringLiteral("id"), QStringLiteral("frei0r.colgate")); Xml::setXmlProperty(filt, QStringLiteral("kdenlive_id"), QStringLiteral("frei0r.colgate")); Xml::setXmlProperty(filt, QStringLiteral("tag"), QStringLiteral("frei0r.colgate")); Xml::setXmlProperty(filt, QStringLiteral("mlt_service"), QStringLiteral("frei0r.colgate")); Xml::renameXmlProperty(filt, QStringLiteral("neutral_color"), QStringLiteral("Neutral Color")); QString value = Xml::getXmlProperty(filt, QStringLiteral("color_temperature")); value = factorizeGeomValue(value, 15000.0); Xml::setXmlProperty(filt, QStringLiteral("color_temperature"), value); Xml::renameXmlProperty(filt, QStringLiteral("color_temperature"), QStringLiteral("Color Temperature")); convertedFilters << filterId; continue; } if (filterId == QLatin1String("movit.blur") && hasBlur) { // Convert to frei0r.IIRblur filt.setAttribute(QStringLiteral("id"), QStringLiteral("frei0r.IIRblur")); Xml::setXmlProperty(filt, QStringLiteral("kdenlive_id"), QStringLiteral("frei0r.IIRblur")); Xml::setXmlProperty(filt, QStringLiteral("tag"), QStringLiteral("frei0r.IIRblur")); Xml::setXmlProperty(filt, QStringLiteral("mlt_service"), QStringLiteral("frei0r.IIRblur")); Xml::renameXmlProperty(filt, QStringLiteral("radius"), QStringLiteral("Amount")); QString value = Xml::getXmlProperty(filt, QStringLiteral("Amount")); value = factorizeGeomValue(value, 14.0); Xml::setXmlProperty(filt, QStringLiteral("Amount"), value); convertedFilters << filterId; continue; } if (filterId == QLatin1String("movit.mirror")) { // Convert to MLT's mirror filt.setAttribute(QStringLiteral("id"), QStringLiteral("mirror")); Xml::setXmlProperty(filt, QStringLiteral("kdenlive_id"), QStringLiteral("mirror")); Xml::setXmlProperty(filt, QStringLiteral("tag"), QStringLiteral("mirror")); Xml::setXmlProperty(filt, QStringLiteral("mlt_service"), QStringLiteral("mirror")); Xml::setXmlProperty(filt, QStringLiteral("mirror"), QStringLiteral("flip")); convertedFilters << filterId; continue; } if (filterId.startsWith(QLatin1String("movit."))) { // TODO: implement conversion for more filters discardedFilters << filterId; } } // Parse all transitions in document QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement t = transitions.at(i).toElement(); QString transId = Xml::getXmlProperty(t, QStringLiteral("mlt_service")); if (!transId.startsWith(QLatin1String("movit."))) { continue; } if (transId == QLatin1String("movit.overlay") && !compositeTrans.isEmpty()) { // Convert to frei0r.cairoblend Xml::setXmlProperty(t, QStringLiteral("mlt_service"), compositeTrans); convertedFilters << transId; continue; } if (transId.startsWith(QLatin1String("movit."))) { // TODO: implement conversion for more filters discardedFilters << transId; } } convertedFilters.removeDuplicates(); discardedFilters.removeDuplicates(); if (discardedFilters.isEmpty()) { KMessageBox::informationList(QApplication::activeWindow(), i18n("The following filters/transitions were converted to non GPU versions:"), convertedFilters); } else { KMessageBox::informationList(QApplication::activeWindow(), i18n("The following filters/transitions were deleted from the project:"), discardedFilters); } m_modified = true; QString scene = m_doc.toString(); scene.replace(QLatin1String("movit."), QString()); m_doc.setContent(scene); return true; } QString DocumentValidator::factorizeGeomValue(const QString &value, double factor) { const QStringList vals = value.split(QLatin1Char(';')); QString result; QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); for (int i = 0; i < vals.count(); i++) { const QString &s = vals.at(i); QString key = s.section(QLatin1Char('='), 0, 0); QString val = s.section(QLatin1Char('='), 1, 1); double v = locale.toDouble(val) / factor; result.append(key + QLatin1Char('=') + locale.toString(v)); if (i + 1 < vals.count()) { result.append(QLatin1Char(';')); } } return result; } void DocumentValidator::checkOrphanedProducers() { QDomElement mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomElement main = mlt.firstChildElement(QStringLiteral("playlist")); QDomNodeList bin_producers = main.childNodes(); QStringList binProducers; for (int k = 0; k < bin_producers.count(); k++) { QDomElement mltprod = bin_producers.at(k).toElement(); if (mltprod.tagName() != QLatin1String("entry")) { continue; } binProducers << mltprod.attribute(QStringLiteral("producer")); } QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); QStringList allProducers; for (int i = 0; i < max; ++i) { QDomElement prod = producers.item(i).toElement(); if (prod.isNull()) { continue; } allProducers << prod.attribute(QStringLiteral("id")); } QDomDocumentFragment frag = m_doc.createDocumentFragment(); QDomDocumentFragment trackProds = m_doc.createDocumentFragment(); for (int i = 0; i < producers.count(); ++i) { QDomElement prod = producers.item(i).toElement(); if (prod.isNull()) { continue; } QString id = prod.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); if (id.startsWith(QLatin1String("slowmotion")) || id == QLatin1String("black")) { continue; } if (!binProducers.contains(id)) { QString binId = Xml::getXmlProperty(prod, QStringLiteral("kdenlive:binid")); Xml::setXmlProperty(prod, QStringLiteral("kdenlive:id"), binId); if (!binId.isEmpty() && binProducers.contains(binId)) { continue; } qCWarning(KDENLIVE_LOG) << " ///////// WARNING, FOUND UNKNOWN PRODUDER: " << id << " ----------------"; // This producer is unknown to Bin QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); QString distinctiveTag(QStringLiteral("resource")); if (service == QLatin1String("kdenlivetitle")) { distinctiveTag = QStringLiteral("xmldata"); } QString orphanValue = Xml::getXmlProperty(prod, distinctiveTag); for (int j = 0; j < producers.count(); j++) { // Search for a similar producer QDomElement binProd = producers.item(j).toElement(); binId = binProd.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); if (service != QLatin1String("timewarp") && (binId.startsWith(QLatin1String("slowmotion")) || !binProducers.contains(binId))) { continue; } QString binService = Xml::getXmlProperty(binProd, QStringLiteral("mlt_service")); qCDebug(KDENLIVE_LOG) << " / /LKNG FOR: " << service << " / " << orphanValue << ", checking: " << binProd.attribute(QStringLiteral("id")); if (service != binService) { continue; } QString binValue = Xml::getXmlProperty(binProd, distinctiveTag); if (binValue == orphanValue) { // Found probable source producer, replace frag.appendChild(prod); if (i > 0) { i--; } QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); for (int k = 0; k < entries.count(); k++) { QDomElement entry = entries.at(k).toElement(); if (entry.attribute(QStringLiteral("producer")) == id) { QString entryId = binId; if (service.contains(QStringLiteral("avformat")) || service == QLatin1String("xml") || service == QLatin1String("consumer")) { // We must use track producer, find track for this entry QString trackPlaylist = entry.parentNode().toElement().attribute(QStringLiteral("id")); entryId.append(QLatin1Char('_') + trackPlaylist); } if (!allProducers.contains(entryId)) { // The track producer does not exist, create a clone for it QDomElement cloned = binProd.cloneNode(true).toElement(); cloned.setAttribute(QStringLiteral("id"), entryId); trackProds.appendChild(cloned); allProducers << entryId; } entry.setAttribute(QStringLiteral("producer"), entryId); m_modified = true; } } continue; } } } } if (!trackProds.isNull()) { QDomNode firstProd = m_doc.firstChildElement(QStringLiteral("producer")); mlt.insertBefore(trackProds, firstProd); } } void DocumentValidator::fixTitleProducerLocale(QDomElement &producer) { QString data = Xml::getXmlProperty(producer, QStringLiteral("xmldata")); QDomDocument doc; doc.setContent(data); QDomNodeList nodes = doc.elementsByTagName(QStringLiteral("position")); bool fixed = false; for (int i = 0; i < nodes.count(); i++) { QDomElement pos = nodes.at(i).toElement(); QString x = pos.attribute(QStringLiteral("x")); QString y = pos.attribute(QStringLiteral("y")); if (x.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix x = x.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("x"), x); fixed = true; } if (y.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix y = y.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("y"), y); fixed = true; } } nodes = doc.elementsByTagName(QStringLiteral("content")); for (int i = 0; i < nodes.count(); i++) { QDomElement pos = nodes.at(i).toElement(); QString x = pos.attribute(QStringLiteral("font-outline")); QString y = pos.attribute(QStringLiteral("textwidth")); if (x.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix x = x.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("font-outline"), x); fixed = true; } if (y.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix y = y.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("textwidth"), y); fixed = true; } } if (fixed) { Xml::setXmlProperty(producer, QStringLiteral("xmldata"), doc.toString()); } } diff --git a/src/effects/effectstack/model/effectstackmodel.cpp b/src/effects/effectstack/model/effectstackmodel.cpp index 458bbfddc..e78de958e 100644 --- a/src/effects/effectstack/model/effectstackmodel.cpp +++ b/src/effects/effectstack/model/effectstackmodel.cpp @@ -1,1260 +1,1262 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "effectstackmodel.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "effectgroupmodel.hpp" #include "effectitemmodel.hpp" #include "effects/effectsrepository.hpp" #include "macros.hpp" #include "timeline2/model/timelinemodel.hpp" #include #include #include #include EffectStackModel::EffectStackModel(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack) : AbstractTreeModel() , m_effectStackEnabled(true) , m_ownerId(std::move(ownerId)) , m_undoStack(std::move(undo_stack)) , m_lock(QReadWriteLock::Recursive) , m_loadingExisting(false) { m_masterService = std::move(service); } std::shared_ptr EffectStackModel::construct(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack) { std::shared_ptr self(new EffectStackModel(std::move(service), ownerId, std::move(undo_stack))); self->rootItem = EffectGroupModel::construct(QStringLiteral("root"), self, true); return self; } void EffectStackModel::resetService(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_masterService = std::move(service); m_childServices.clear(); // replant all effects in new service for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->plant(m_masterService); } } void EffectStackModel::addService(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_childServices.emplace_back(std::move(service)); for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->plantClone(m_childServices.back()); } } void EffectStackModel::loadService(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_childServices.emplace_back(std::move(service)); for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->loadClone(m_childServices.back()); } } void EffectStackModel::removeService(const std::shared_ptr &service) { QWriteLocker locker(&m_lock); std::vector to_delete; for (int i = int(m_childServices.size()) - 1; i >= 0; --i) { auto ptr = m_childServices[uint(i)].lock(); if (service->get_int("_childid") == ptr->get_int("_childid")) { for (int j = 0; j < rootItem->childCount(); ++j) { std::static_pointer_cast(rootItem->child(j))->unplantClone(ptr); } to_delete.push_back(i); } } for (int i : to_delete) { m_childServices.erase(m_childServices.begin() + i); } } void EffectStackModel::removeCurrentEffect() { int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return; } std::shared_ptr effect = std::static_pointer_cast(rootItem->child(ix)); if (effect) { removeEffect(effect); } } void EffectStackModel::removeEffect(const std::shared_ptr &effect) { qDebug() << "* * ** REMOVING EFFECT FROM STACK!!!\n!!!!!!!!!"; QWriteLocker locker(&m_lock); Q_ASSERT(m_allItems.count(effect->getId()) > 0); int parentId = -1; if (auto ptr = effect->parentItem().lock()) parentId = ptr->getId(); int current = 0; if (auto srv = m_masterService.lock()) { current = srv->get_int("kdenlive:activeeffect"); if (current >= rootItem->childCount() - 1) { srv->set("kdenlive:activeeffect", --current); } } int currentRow = effect->row(); Fun undo = addItem_lambda(effect, parentId); if (currentRow != rowCount() - 1) { Fun move = moveItem_lambda(effect->getId(), currentRow, true); PUSH_LAMBDA(move, undo); } Fun redo = removeItem_lambda(effect->getId()); bool res = redo(); if (res) { int inFades = int(m_fadeIns.size()); int outFades = int(m_fadeOuts.size()); m_fadeIns.erase(effect->getId()); m_fadeOuts.erase(effect->getId()); inFades = int(m_fadeIns.size()) - inFades; outFades = int(m_fadeOuts.size()) - outFades; QString effectName = EffectsRepository::get()->getName(effect->getAssetId()); Fun update = [this, current, inFades, outFades]() { // Required to build the effect view if (current < 0 || rowCount() == 0) { // Stack is now empty emit dataChanged(QModelIndex(), QModelIndex(), {}); } else { QVector roles = {TimelineModel::EffectNamesRole}; if (inFades < 0) { roles << TimelineModel::FadeInRole; } if (outFades < 0) { roles << TimelineModel::FadeOutRole; } qDebug() << "// EMITTING UNDO DATA CHANGE: " << roles; emit dataChanged(QModelIndex(), QModelIndex(), roles); } // TODO: only update if effect is fade or keyframe /*if (inFades < 0) { pCore->updateItemModel(m_ownerId, QStringLiteral("fadein")); } else if (outFades < 0) { pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout")); }*/ pCore->updateItemKeyframes(m_ownerId); return true; }; Fun update2 = [this, inFades, outFades]() { // Required to build the effect view QVector roles = {TimelineModel::EffectNamesRole}; // TODO: only update if effect is fade or keyframe if (inFades < 0) { roles << TimelineModel::FadeInRole; } else if (outFades < 0) { roles << TimelineModel::FadeOutRole; } qDebug() << "// EMITTING REDO DATA CHANGE: " << roles; emit dataChanged(QModelIndex(), QModelIndex(), roles); pCore->updateItemKeyframes(m_ownerId); return true; }; update(); PUSH_LAMBDA(update, redo); PUSH_LAMBDA(update2, undo); PUSH_UNDO(undo, redo, i18n("Delete effect %1", effectName)); } else { qDebug() << "..........FAILED EFFECT DELETION"; } } bool EffectStackModel::copyXmlEffect(QDomElement effect) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = fromXml(effect, undo, redo); if (result) { PUSH_UNDO(undo, redo, i18n("Copy effect")); } return result; } QDomElement EffectStackModel::toXml(QDomDocument &document) { QDomElement container = document.createElement(QStringLiteral("effects")); int currentIn = pCore->getItemIn(m_ownerId); container.setAttribute(QStringLiteral("parentIn"), currentIn); for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); QDomElement sub = document.createElement(QStringLiteral("effect")); sub.setAttribute(QStringLiteral("id"), sourceEffect->getAssetId()); int filterIn = sourceEffect->filter().get_int("in"); int filterOut = sourceEffect->filter().get_int("out"); if (filterOut > filterIn) { sub.setAttribute(QStringLiteral("in"), filterIn); sub.setAttribute(QStringLiteral("out"), filterOut); } QStringList passProps {QStringLiteral("disable"), QStringLiteral("kdenlive:collapsed")}; for (const QString ¶m : passProps) { int paramVal = sourceEffect->filter().get_int(param.toUtf8().constData()); if (paramVal > 0) { Xml::setXmlProperty(sub, param, QString::number(paramVal)); } } QVector> params = sourceEffect->getAllParameters(); QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); for (const auto ¶m : params) { if (param.second.type() == QVariant::Double) { Xml::setXmlProperty(sub, param.first, locale.toString(param.second.toDouble())); } else { Xml::setXmlProperty(sub, param.first, param.second.toString()); } } container.appendChild(sub); } return container; } QDomElement EffectStackModel::rowToXml(int row, QDomDocument &document) { QDomElement container = document.createElement(QStringLiteral("effects")); if (row < 0 || row >= rootItem->childCount()) { return container; } int currentIn = pCore->getItemIn(m_ownerId); container.setAttribute(QStringLiteral("parentIn"), currentIn); std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(row)); QDomElement sub = document.createElement(QStringLiteral("effect")); sub.setAttribute(QStringLiteral("id"), sourceEffect->getAssetId()); int filterIn = sourceEffect->filter().get_int("in"); int filterOut = sourceEffect->filter().get_int("out"); if (filterOut > filterIn) { sub.setAttribute(QStringLiteral("in"), filterIn); sub.setAttribute(QStringLiteral("out"), filterOut); } QStringList passProps {QStringLiteral("disable"), QStringLiteral("kdenlive:collapsed")}; for (const QString ¶m : passProps) { int paramVal = sourceEffect->filter().get_int(param.toUtf8().constData()); if (paramVal > 0) { Xml::setXmlProperty(sub, param, QString::number(paramVal)); } } QVector> params = sourceEffect->getAllParameters(); QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); for (const auto ¶m : params) { if (param.second.type() == QVariant::Double) { Xml::setXmlProperty(sub, param.first, locale.toString(param.second.toDouble())); } else { Xml::setXmlProperty(sub, param.first, param.second.toString()); } } container.appendChild(sub); return container; } bool EffectStackModel::fromXml(const QDomElement &effectsXml, Fun &undo, Fun &redo) { QDomNodeList nodeList = effectsXml.elementsByTagName(QStringLiteral("effect")); int parentIn = effectsXml.attribute(QStringLiteral("parentIn")).toInt(); qDebug()<<"// GOT PREVIOUS PARENTIN: "<getItemIn(m_ownerId); PlaylistState::ClipState state = pCore->getItemState(m_ownerId); for (int i = 0; i < nodeList.count(); ++i) { QDomElement node = nodeList.item(i).toElement(); const QString effectId = node.attribute(QStringLiteral("id")); EffectType type = EffectsRepository::get()->getType(effectId); bool isAudioEffect = type == EffectType::Audio || type == EffectType::CustomAudio; if (isAudioEffect) { if (state != PlaylistState::AudioOnly) { continue; } } else if (state != PlaylistState::VideoOnly) { continue; } bool effectEnabled = true; if (Xml::hasXmlProperty(node, QLatin1String("disable"))) { effectEnabled = Xml::getXmlProperty(node, QLatin1String("disable")).toInt() != 1; } auto effect = EffectItemModel::construct(effectId, shared_from_this(), effectEnabled); const QString in = node.attribute(QStringLiteral("in")); const QString out = node.attribute(QStringLiteral("out")); if (!out.isEmpty()) { effect->filter().set("in", in.toUtf8().constData()); effect->filter().set("out", out.toUtf8().constData()); } QStringList keyframeParams = effect->getKeyframableParameters(); QVector> parameters; QDomNodeList params = node.elementsByTagName(QStringLiteral("property")); for (int j = 0; j < params.count(); j++) { QDomElement pnode = params.item(j).toElement(); const QString pName = pnode.attribute(QStringLiteral("name")); if (pName == QLatin1String("in") || pName == QLatin1String("out")) { continue; } if (keyframeParams.contains(pName)) { // This is a keyframable parameter, fix offset QString pValue = KeyframeModel::getAnimationStringWithOffset(effect, pnode.text(), currentIn - parentIn); parameters.append(QPair(pName, QVariant(pValue))); } else { parameters.append(QPair(pName, QVariant(pnode.text()))); } } effect->setParameters(parameters); Fun local_undo = removeItem_lambda(effect->getId()); // TODO the parent should probably not always be the root Fun local_redo = addItem_lambda(effect, rootItem->getId()); effect->prepareKeyframes(); connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { m_fadeIns.insert(effect->getId()); int duration = effect->filter().get_length() - 1; effect->filter().set("in", currentIn); effect->filter().set("out", currentIn + duration); } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { m_fadeOuts.insert(effect->getId()); int duration = effect->filter().get_length() - 1; int filterOut = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1; effect->filter().set("in", filterOut - duration); effect->filter().set("out", filterOut); } local_redo(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } if (true) { Fun update = [this]() { emit dataChanged(QModelIndex(), QModelIndex(), {}); return true; }; update(); PUSH_LAMBDA(update, redo); PUSH_LAMBDA(update, undo); } return true; } bool EffectStackModel::copyEffect(const std::shared_ptr &sourceItem, PlaylistState::ClipState state) { QWriteLocker locker(&m_lock); if (sourceItem->childCount() > 0) { // TODO: group return false; } bool audioEffect = sourceItem->isAudio(); if (audioEffect) { if (state == PlaylistState::VideoOnly) { // This effect cannot be used return false; } } else if (state == PlaylistState::AudioOnly) { return false; } std::shared_ptr sourceEffect = std::static_pointer_cast(sourceItem); const QString effectId = sourceEffect->getAssetId(); bool enabled = sourceEffect->isEnabled(); auto effect = EffectItemModel::construct(effectId, shared_from_this(), enabled); effect->setParameters(sourceEffect->getAllParameters()); if (!enabled) { effect->filter().set("disable", 1); } effect->filter().set("in", sourceEffect->filter().get_int("in")); effect->filter().set("out", sourceEffect->filter().get_int("out")); Fun local_undo = removeItem_lambda(effect->getId()); // TODO the parent should probably not always be the root Fun local_redo = addItem_lambda(effect, rootItem->getId()); effect->prepareKeyframes(); connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); QVector roles = {TimelineModel::EffectNamesRole}; if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { m_fadeIns.insert(effect->getId()); int duration = effect->filter().get_length() - 1; int in = pCore->getItemIn(m_ownerId); effect->filter().set("in", in); effect->filter().set("out", in + duration); roles << TimelineModel::FadeInRole; } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { m_fadeOuts.insert(effect->getId()); int duration = effect->filter().get_length() - 1; int out = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1; effect->filter().set("in", out - duration); effect->filter().set("out", out); roles << TimelineModel::FadeOutRole; } bool res = local_redo(); if (res) { Fun update = [this, roles]() { emit dataChanged(QModelIndex(), QModelIndex(), roles); return true; }; } return res; } bool EffectStackModel::appendEffect(const QString &effectId, bool makeCurrent) { QWriteLocker locker(&m_lock); auto effect = EffectItemModel::construct(effectId, shared_from_this()); PlaylistState::ClipState state = pCore->getItemState(m_ownerId); if (effect->isAudio()) { if (state == PlaylistState::VideoOnly) { // Cannot add effect to this clip return false; } } else if (state == PlaylistState::AudioOnly) { // Cannot add effect to this clip return false; } Fun undo = removeItem_lambda(effect->getId()); // TODO the parent should probably not always be the root Fun redo = addItem_lambda(effect, rootItem->getId()); effect->prepareKeyframes(); connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); int currentActive = getActiveEffect(); if (makeCurrent) { if (auto srvPtr = m_masterService.lock()) { srvPtr->set("kdenlive:activeeffect", rowCount()); } } bool res = redo(); if (res) { int inFades = 0; int outFades = 0; if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { int duration = effect->filter().get_length() - 1; int in = pCore->getItemIn(m_ownerId); effect->filter().set("in", in); effect->filter().set("out", in + duration); inFades++; } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { /*int duration = effect->filter().get_length() - 1; int out = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1; effect->filter().set("in", out - duration); effect->filter().set("out", out);*/ outFades++; } else if (m_ownerId.first == ObjectType::TimelineTrack) { effect->filter().set("out", pCore->getItemDuration(m_ownerId)); } Fun update = [this, inFades, outFades]() { // TODO: only update if effect is fade or keyframe QVector roles = {TimelineModel::EffectNamesRole}; if (inFades > 0) { roles << TimelineModel::FadeInRole; } else if (outFades > 0) { roles << TimelineModel::FadeOutRole; } pCore->updateItemKeyframes(m_ownerId); emit dataChanged(QModelIndex(), QModelIndex(), roles); return true; }; update(); PUSH_LAMBDA(update, redo); PUSH_LAMBDA(update, undo); PUSH_UNDO(undo, redo, i18n("Add effect %1", EffectsRepository::get()->getName(effectId))); } else if (makeCurrent) { if (auto srvPtr = m_masterService.lock()) { srvPtr->set("kdenlive:activeeffect", currentActive); } } return res; } bool EffectStackModel::adjustStackLength(bool adjustFromEnd, int oldIn, int oldDuration, int newIn, int duration, int offset, Fun &undo, Fun &redo, bool logUndo) { QWriteLocker locker(&m_lock); const int fadeInDuration = getFadePosition(true); const int fadeOutDuration = getFadePosition(false); int out = newIn + duration; for (const auto &leaf : rootItem->getLeaves()) { std::shared_ptr item = std::static_pointer_cast(leaf); if (item->effectItemType() == EffectItemType::Group) { // probably an empty group, ignore continue; } std::shared_ptr effect = std::static_pointer_cast(leaf); if (fadeInDuration > 0 && m_fadeIns.count(leaf->getId()) > 0) { int oldEffectIn = qMax(0, effect->filter().get_in()); int oldEffectOut = effect->filter().get_out(); qDebug() << "--previous effect: " << oldEffectIn << "-" << oldEffectOut; int effectDuration = qMin(effect->filter().get_length() - 1, duration); if (!adjustFromEnd && (oldIn != newIn || duration != oldDuration)) { // Clip start was resized, adjust effect in / out Fun operation = [effect, newIn, effectDuration, logUndo]() { effect->setParameter(QStringLiteral("in"), newIn, false); effect->setParameter(QStringLiteral("out"), newIn + effectDuration, logUndo); qDebug() << "--new effect: " << newIn << "-" << newIn + effectDuration; return true; }; bool res = operation(); if (!res) { return false; } Fun reverse = [effect, oldEffectIn, oldEffectOut, logUndo]() { effect->setParameter(QStringLiteral("in"), oldEffectIn, false); effect->setParameter(QStringLiteral("out"), oldEffectOut, logUndo); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } else if (effectDuration < oldEffectOut - oldEffectIn || (logUndo && effect->filter().get_int("_refout") > 0)) { // Clip length changed, shorter than effect length so resize int referenceEffectOut = effect->filter().get_int("_refout"); if (referenceEffectOut <= 0) { referenceEffectOut = oldEffectOut; effect->filter().set("_refout", referenceEffectOut); } Fun operation = [effect, oldEffectIn, effectDuration, logUndo]() { effect->setParameter(QStringLiteral("out"), oldEffectIn + effectDuration, logUndo); return true; }; bool res = operation(); if (!res) { return false; } if (logUndo) { Fun reverse = [effect, referenceEffectOut]() { effect->setParameter(QStringLiteral("out"), referenceEffectOut, true); effect->filter().set("_refout", (char *)nullptr); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } } } else if (fadeOutDuration > 0 && m_fadeOuts.count(leaf->getId()) > 0) { int effectDuration = qMin(fadeOutDuration, duration); int newFadeIn = out - effectDuration; int oldFadeIn = effect->filter().get_int("in"); int oldOut = effect->filter().get_int("out"); int referenceEffectIn = effect->filter().get_int("_refin"); if (referenceEffectIn <= 0) { referenceEffectIn = oldFadeIn; effect->filter().set("_refin", referenceEffectIn); } Fun operation = [effect, newFadeIn, out, logUndo]() { effect->setParameter(QStringLiteral("in"), newFadeIn, false); effect->setParameter(QStringLiteral("out"), out, logUndo); return true; }; bool res = operation(); if (!res) { return false; } if (logUndo) { Fun reverse = [effect, referenceEffectIn, oldOut]() { effect->setParameter(QStringLiteral("in"), referenceEffectIn, false); effect->setParameter(QStringLiteral("out"), oldOut, true); effect->filter().set("_refin", (char *)nullptr); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } } else { // Not a fade effect, check for keyframes std::shared_ptr keyframes = effect->getKeyframeModel(); if (keyframes != nullptr) { // Effect has keyframes, update these keyframes->resizeKeyframes(oldIn, oldIn + oldDuration, newIn, out - 1, offset, adjustFromEnd, undo, redo); QModelIndex index = getIndexFromItem(effect); Fun refresh = [effect, index]() { effect->dataChanged(index, index, QVector()); return true; }; refresh(); PUSH_LAMBDA(refresh, redo); PUSH_LAMBDA(refresh, undo); } else { if (m_ownerId.first == ObjectType::TimelineTrack) { int oldEffectOut = effect->filter().get_out(); Fun operation = [effect, out, logUndo]() { effect->setParameter(QStringLiteral("out"), out, logUndo); return true; }; bool res = operation(); if (!res) { return false; } if (logUndo) { Fun reverse = [effect, oldEffectOut]() { effect->setParameter(QStringLiteral("out"), oldEffectOut, true); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } } else { qDebug() << "// NULL Keyframes---------"; } } } } return true; } bool EffectStackModel::adjustFadeLength(int duration, bool fromStart, bool audioFade, bool videoFade, bool logUndo) { QWriteLocker locker(&m_lock); if (fromStart) { // Fade in if (m_fadeIns.empty()) { if (audioFade) { appendEffect(QStringLiteral("fadein")); } if (videoFade) { appendEffect(QStringLiteral("fade_from_black")); } } QList indexes; auto ptr = m_masterService.lock(); int in = 0; if (ptr) { in = ptr->get_int("in"); } int oldDuration = -1; for (int i = 0; i < rootItem->childCount(); ++i) { if (m_fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); if (oldDuration == -1) { oldDuration = effect->filter().get_length(); } effect->filter().set("in", in); duration = qMin(pCore->getItemDuration(m_ownerId), duration); effect->filter().set("out", in + duration); indexes << getIndexFromItem(effect); } } if (!indexes.isEmpty()) { emit dataChanged(indexes.first(), indexes.last(), QVector()); pCore->updateItemModel(m_ownerId, QStringLiteral("fadein")); if (videoFade) { int min = pCore->getItemPosition(m_ownerId); QSize range(min, min + qMax(duration, oldDuration)); pCore->refreshProjectRange(range); if (logUndo) { pCore->invalidateRange(range); } } } } else { // Fade out if (m_fadeOuts.empty()) { if (audioFade) { appendEffect(QStringLiteral("fadeout")); } if (videoFade) { appendEffect(QStringLiteral("fade_to_black")); } } int in = 0; auto ptr = m_masterService.lock(); if (ptr) { in = ptr->get_int("in"); } int itemDuration = pCore->getItemDuration(m_ownerId); int out = in + itemDuration; int oldDuration = -1; QList indexes; for (int i = 0; i < rootItem->childCount(); ++i) { if (m_fadeOuts.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); if (oldDuration == -1) { oldDuration = effect->filter().get_length(); } effect->filter().set("out", out - 1); duration = qMin(itemDuration, duration); effect->filter().set("in", out - duration); indexes << getIndexFromItem(effect); } } if (!indexes.isEmpty()) { emit dataChanged(indexes.first(), indexes.last(), QVector()); pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout")); if (videoFade) { int min = pCore->getItemPosition(m_ownerId); QSize range(min + itemDuration - qMax(duration, oldDuration), min + itemDuration); pCore->refreshProjectRange(range); if (logUndo) { pCore->invalidateRange(range); } } } } return true; } int EffectStackModel::getFadePosition(bool fromStart) { QWriteLocker locker(&m_lock); if (fromStart) { if (m_fadeIns.empty()) { return 0; } for (int i = 0; i < rootItem->childCount(); ++i) { if (*(m_fadeIns.begin()) == std::static_pointer_cast(rootItem->child(i))->getId()) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); return effect->filter().get_length() - 1; } } } else { if (m_fadeOuts.empty()) { return 0; } for (int i = 0; i < rootItem->childCount(); ++i) { if (*(m_fadeOuts.begin()) == std::static_pointer_cast(rootItem->child(i))->getId()) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); return effect->filter().get_length() - 1; } } } return 0; } bool EffectStackModel::removeFade(bool fromStart) { QWriteLocker locker(&m_lock); std::vector toRemove; for (int i = 0; i < rootItem->childCount(); ++i) { if ((fromStart && m_fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) || (!fromStart && m_fadeOuts.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0)) { toRemove.push_back(i); } } for (int i : toRemove) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); removeEffect(effect); } return true; } void EffectStackModel::moveEffect(int destRow, const std::shared_ptr &item) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allItems.count(item->getId()) > 0); int oldRow = item->row(); Fun undo = moveItem_lambda(item->getId(), oldRow); Fun redo = moveItem_lambda(item->getId(), destRow); bool res = redo(); if (res) { Fun update = [this]() { this->dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectNamesRole}); return true; }; update(); UPDATE_UNDO_REDO(update, update, undo, redo); auto effectId = std::static_pointer_cast(item)->getAssetId(); PUSH_UNDO(undo, redo, i18n("Move effect %1").arg(EffectsRepository::get()->getName(effectId))); } } void EffectStackModel::registerItem(const std::shared_ptr &item) { QWriteLocker locker(&m_lock); // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect"; QModelIndex ix; if (!item->isRoot()) { auto effectItem = std::static_pointer_cast(item); if (!m_loadingExisting) { // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect in " << m_childServices.size(); effectItem->plant(m_masterService); for (const auto &service : m_childServices) { // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting CLONE effect in " << (void *)service.lock().get(); effectItem->plantClone(service); } } effectItem->setEffectStackEnabled(m_effectStackEnabled); const QString &effectId = effectItem->getAssetId(); if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { m_fadeIns.insert(effectItem->getId()); } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { m_fadeOuts.insert(effectItem->getId()); } ix = getIndexFromItem(effectItem); if (!effectItem->isAudio() && !m_loadingExisting) { pCore->refreshProjectItem(m_ownerId); pCore->invalidateItem(m_ownerId); } } AbstractTreeModel::registerItem(item); } void EffectStackModel::deregisterItem(int id, TreeItem *item) { QWriteLocker locker(&m_lock); if (!item->isRoot()) { auto effectItem = static_cast(item); effectItem->unplant(m_masterService); for (const auto &service : m_childServices) { effectItem->unplantClone(service); } if (!effectItem->isAudio()) { pCore->refreshProjectItem(m_ownerId); pCore->invalidateItem(m_ownerId); } } AbstractTreeModel::deregisterItem(id, item); } void EffectStackModel::setEffectStackEnabled(bool enabled) { QWriteLocker locker(&m_lock); m_effectStackEnabled = enabled; // Recursively updates children states for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->setEffectStackEnabled(enabled); } emit dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectsEnabledRole}); emit enabledStateChanged(); } std::shared_ptr EffectStackModel::getEffectStackRow(int row, const std::shared_ptr &parentItem) { return std::static_pointer_cast(parentItem ? parentItem->child(row) : rootItem->child(row)); } bool EffectStackModel::importEffects(const std::shared_ptr &sourceStack, PlaylistState::ClipState state) { QWriteLocker locker(&m_lock); // TODO: manage fades, keyframes if clips don't have same size / in point bool found = false; bool effectEnabled = rootItem->childCount() > 0; int imported = 0; for (int i = 0; i < sourceStack->rowCount(); i++) { auto item = sourceStack->getEffectStackRow(i); // NO undo. this should only be used on project opening if (copyEffect(item, state)) { found = true; if (item->isEnabled()) { effectEnabled = true; } imported++; } } if (!effectEnabled && imported == 0) { effectEnabled = true; } m_effectStackEnabled = effectEnabled; if (!m_effectStackEnabled) { // Mark all effects as disabled by stack for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); sourceEffect->setEffectStackEnabled(false); sourceEffect->setEnabled(true); } } if (found) { modelChanged(); } return found; } void EffectStackModel::importEffects(const std::weak_ptr &service, PlaylistState::ClipState state, bool alreadyExist) { QWriteLocker locker(&m_lock); m_loadingExisting = alreadyExist; bool effectEnabled = true; if (auto ptr = service.lock()) { int max = ptr->filter_count(); int imported = 0; for (int i = 0; i < max; i++) { std::unique_ptr filter(ptr->filter(i)); if (filter->get_int("internal_added") > 0) { if (auto ms = m_masterService.lock()) { ms->attach(*filter.get()); } continue; } if (filter->get("kdenlive_id") == nullptr) { // don't consider internal MLT stuff continue; } const QString effectId = qstrdup(filter->get("kdenlive_id")); if (filter->get_int("disable") == 0) { effectEnabled = true; } // The MLT filter already exists, use it directly to create the effect std::shared_ptr effect; if (alreadyExist) { // effect is already plugged in the service effect = EffectItemModel::construct(std::move(filter), shared_from_this()); } else { // duplicate effect std::unique_ptr asset = EffectsRepository::get()->getEffect(effectId); asset->inherit(*(filter)); effect = EffectItemModel::construct(std::move(asset), shared_from_this()); } if (effect->isAudio()) { if (state == PlaylistState::VideoOnly) { // Don't import effect continue; } } else if (state == PlaylistState::AudioOnly) { // Don't import effect continue; } imported++; connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); Fun redo = addItem_lambda(effect, rootItem->getId()); effect->prepareKeyframes(); if (redo()) { if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { m_fadeIns.insert(effect->getId()); int clipIn = ptr->get_int("in"); if (effect->filter().get_int("in") != clipIn) { // Broken fade, fix int filterLength = effect->filter().get_length() - 1; effect->filter().set("in", clipIn); effect->filter().set("out", clipIn + filterLength); } } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { m_fadeOuts.insert(effect->getId()); int clipOut = ptr->get_int("out"); if (effect->filter().get_int("out") != clipOut) { // Broken fade, fix int filterLength = effect->filter().get_length() - 1; effect->filter().set("in", clipOut - filterLength); effect->filter().set("out", clipOut); } } } } if (imported == 0) { effectEnabled = true; } } m_effectStackEnabled = effectEnabled; if (!m_effectStackEnabled) { // Mark all effects as disabled by stack for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); sourceEffect->setEffectStackEnabled(false); sourceEffect->setEnabled(true); } } m_loadingExisting = false; modelChanged(); } void EffectStackModel::setActiveEffect(int ix) { QWriteLocker locker(&m_lock); if (auto ptr = m_masterService.lock()) { ptr->set("kdenlive:activeeffect", ix); } pCore->updateItemKeyframes(m_ownerId); } int EffectStackModel::getActiveEffect() const { QWriteLocker locker(&m_lock); if (auto ptr = m_masterService.lock()) { return ptr->get_int("kdenlive:activeeffect"); } return 0; } void EffectStackModel::slotCreateGroup(const std::shared_ptr &childEffect) { QWriteLocker locker(&m_lock); auto groupItem = EffectGroupModel::construct(QStringLiteral("group"), shared_from_this()); rootItem->appendChild(groupItem); groupItem->appendChild(childEffect); } ObjectId EffectStackModel::getOwnerId() const { return m_ownerId; } bool EffectStackModel::checkConsistency() { if (!AbstractTreeModel::checkConsistency()) { return false; } std::vector> allFilters; // We do a DFS on the tree to retrieve all the filters std::stack> stck; stck.push(std::static_pointer_cast(rootItem)); while (!stck.empty()) { auto current = stck.top(); stck.pop(); if (current->effectItemType() == EffectItemType::Effect) { if (current->childCount() > 0) { qDebug() << "ERROR: Found an effect with children"; return false; } allFilters.push_back(std::static_pointer_cast(current)); continue; } for (int i = current->childCount() - 1; i >= 0; --i) { stck.push(std::static_pointer_cast(current->child(i))); } } for (const auto &service : m_childServices) { auto ptr = service.lock(); if (!ptr) { qDebug() << "ERROR: unavailable service"; return false; } // MLT inserts some default normalizer filters that are not managed by Kdenlive, which explains why the filter count is not equal int kdenliveFilterCount = 0; for (int i = 0; i < ptr->filter_count(); i++) { std::shared_ptr filt(ptr->filter(i)); if (filt->get("kdenlive_id") != nullptr) { kdenliveFilterCount++; } // qDebug() << "FILTER: "<filter(i)->get("mlt_service"); } if (kdenliveFilterCount != (int)allFilters.size()) { qDebug() << "ERROR: Wrong filter count: " << kdenliveFilterCount << " = " << allFilters.size(); return false; } int ct = 0; for (uint i = 0; i < allFilters.size(); ++i) { while (ptr->filter(ct)->get("kdenlive_id") == nullptr && ct < ptr->filter_count()) { ct++; } auto mltFilter = ptr->filter(ct); auto currentFilter = allFilters[i]->filter(); if (QString(currentFilter.get("mlt_service")) != QLatin1String(mltFilter->get("mlt_service"))) { qDebug() << "ERROR: filter " << i << "differ: " << ct << ", " << currentFilter.get("mlt_service") << " = " << mltFilter->get("mlt_service"); return false; } QVector> params = allFilters[i]->getAllParameters(); for (const auto &val : params) { // Check parameters values if (val.second != QVariant(mltFilter->get(val.first.toUtf8().constData()))) { qDebug() << "ERROR: filter " << i << "PARAMETER MISMATCH: " << val.first << " = " << val.second << " != " << mltFilter->get(val.first.toUtf8().constData()); return false; } } ct++; } } return true; } void EffectStackModel::adjust(const QString &effectId, const QString &effectName, double value) { QWriteLocker locker(&m_lock); for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); if (effectId == sourceEffect->getAssetId()) { sourceEffect->setParameter(effectName, QString::number(value)); return; } } } std::shared_ptr EffectStackModel::getAssetModelById(const QString &effectId) { QWriteLocker locker(&m_lock); for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); if (effectId == sourceEffect->getAssetId()) { return std::static_pointer_cast(sourceEffect); } } return nullptr; } bool EffectStackModel::hasFilter(const QString &effectId) const { READ_LOCK(); return rootItem->accumulate_const(false, [effectId](bool b, std::shared_ptr it) { if (b) return true; auto item = std::static_pointer_cast(it); if (item->effectItemType() == EffectItemType::Group) { return false; } auto sourceEffect = std::static_pointer_cast(it); return effectId == sourceEffect->getAssetId(); }); } double EffectStackModel::getFilterParam(const QString &effectId, const QString ¶mName) { READ_LOCK(); for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); if (effectId == sourceEffect->getAssetId()) { return sourceEffect->filter().get_double(paramName.toUtf8().constData()); } } return 0.0; } KeyframeModel *EffectStackModel::getEffectKeyframeModel() { if (rootItem->childCount() == 0) return nullptr; int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0 || ix >= rootItem->childCount()) { return nullptr; } std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix)); std::shared_ptr listModel = sourceEffect->getKeyframeModel(); if (listModel) { return listModel->getKeyModel(); } return nullptr; } void EffectStackModel::replugEffect(const std::shared_ptr &asset) { QWriteLocker locker(&m_lock); auto effectItem = std::static_pointer_cast(asset); int oldRow = effectItem->row(); int count = rowCount(); for (int ix = oldRow; ix < count; ix++) { auto item = std::static_pointer_cast(rootItem->child(ix)); item->unplant(m_masterService); for (const auto &service : m_childServices) { item->unplantClone(service); } } std::unique_ptr effect = EffectsRepository::get()->getEffect(effectItem->getAssetId()); effect->inherit(effectItem->filter()); effectItem->resetAsset(std::move(effect)); for (int ix = oldRow; ix < count; ix++) { auto item = std::static_pointer_cast(rootItem->child(ix)); item->plant(m_masterService); for (const auto &service : m_childServices) { item->plantClone(service); } } } void EffectStackModel::cleanFadeEffects(bool outEffects, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); const auto &toDelete = outEffects ? m_fadeOuts : m_fadeIns; for (int id : toDelete) { auto effect = std::static_pointer_cast(getItemById(id)); Fun operation = removeItem_lambda(id); if (operation()) { Fun reverse = addItem_lambda(effect, rootItem->getId()); UPDATE_UNDO_REDO(operation, reverse, undo, redo); } } if (!toDelete.empty()) { Fun updateRedo = [this, toDelete, outEffects]() { for (int id : toDelete) { if (outEffects) { m_fadeOuts.erase(id); } else { m_fadeIns.erase(id); } } QVector roles = {TimelineModel::EffectNamesRole}; roles << (outEffects ? TimelineModel::FadeOutRole : TimelineModel::FadeInRole); emit dataChanged(QModelIndex(), QModelIndex(), roles); pCore->updateItemKeyframes(m_ownerId); return true; }; updateRedo(); PUSH_LAMBDA(updateRedo, redo); } } const QString EffectStackModel::effectNames() const { QStringList effects; for (int i = 0; i < rootItem->childCount(); ++i) { effects.append(EffectsRepository::get()->getName(std::static_pointer_cast(rootItem->child(i))->getAssetId())); } return effects.join(QLatin1Char('/')); } bool EffectStackModel::isStackEnabled() const { return m_effectStackEnabled; } bool EffectStackModel::addEffectKeyFrame(int frame, double normalisedVal) { if (rootItem->childCount() == 0) return false; int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return false; } std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix)); std::shared_ptr listModel = sourceEffect->getKeyframeModel(); if (m_ownerId.first == ObjectType::TimelineTrack) { sourceEffect->filter().set("out", pCore->getItemDuration(m_ownerId)); } return listModel->addKeyframe(frame, normalisedVal); } bool EffectStackModel::removeKeyFrame(int frame) { if (rootItem->childCount() == 0) return false; int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return false; } std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix)); std::shared_ptr listModel = sourceEffect->getKeyframeModel(); return listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps())); } bool EffectStackModel::updateKeyFrame(int oldFrame, int newFrame, QVariant normalisedVal) { if (rootItem->childCount() == 0) return false; int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return false; } std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix)); std::shared_ptr listModel = sourceEffect->getKeyframeModel(); if (m_ownerId.first == ObjectType::TimelineTrack) { sourceEffect->filter().set("out", pCore->getItemDuration(m_ownerId)); } return listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), std::move(normalisedVal)); } diff --git a/src/effects/effectstack/view/collapsibleeffectview.cpp b/src/effects/effectstack/view/collapsibleeffectview.cpp index dd09a9a7c..172bf4de9 100644 --- a/src/effects/effectstack/view/collapsibleeffectview.cpp +++ b/src/effects/effectstack/view/collapsibleeffectview.cpp @@ -1,835 +1,836 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "collapsibleeffectview.hpp" #include "assets/view/assetparameterview.hpp" #include "assets/view/widgets/colorwheel.h" #include "core.h" #include "dialogs/clipcreationdialog.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectitemmodel.hpp" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include CollapsibleEffectView::CollapsibleEffectView(const std::shared_ptr &effectModel, QSize frameSize, const QImage &icon, QWidget *parent) : AbstractCollapsibleWidget(parent) , m_view(nullptr) , m_model(effectModel) , m_regionEffect(false) , m_blockWheel(false) { QString effectId = effectModel->getAssetId(); QString effectName = EffectsRepository::get()->getName(effectId); if (effectId == QLatin1String("region")) { m_regionEffect = true; decoframe->setObjectName(QStringLiteral("decoframegroup")); } setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); // decoframe->setProperty("active", true); // m_info.fromString(effect.attribute(QStringLiteral("kdenlive_info"))); // setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); buttonUp->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-up"))); buttonUp->setToolTip(i18n("Move effect up")); buttonDown->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-down"))); buttonDown->setToolTip(i18n("Move effect down")); buttonDel->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-deleffect"))); buttonDel->setToolTip(i18n("Delete effect")); // buttonUp->setEnabled(canMoveUp); // buttonDown->setEnabled(!lastEffect); if (effectId == QLatin1String("speed")) { // Speed effect is a "pseudo" effect, cannot be moved buttonUp->setVisible(false); buttonDown->setVisible(false); m_isMovable = false; setAcceptDrops(false); } else { setAcceptDrops(true); } // checkAll->setToolTip(i18n("Enable/Disable all effects")); // buttonShowComments->setIcon(QIcon::fromTheme("help-about")); // buttonShowComments->setToolTip(i18n("Show additional information for the parameters")); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); m_collapse = new KDualAction(i18n("Collapse Effect"), i18n("Expand Effect"), this); m_collapse->setActiveIcon(QIcon::fromTheme(QStringLiteral("arrow-right"))); collapseButton->setDefaultAction(m_collapse); m_collapse->setActive(m_model->isCollapsed()); connect(m_collapse, &KDualAction::activeChanged, this, &CollapsibleEffectView::slotSwitch); if (effectModel->rowCount() == 0) { // Effect has no paramerter m_collapse->setInactiveIcon(QIcon::fromTheme(QStringLiteral("tools-wizard"))); collapseButton->setEnabled(false); } else { m_collapse->setInactiveIcon(QIcon::fromTheme(QStringLiteral("arrow-down"))); } auto *l = static_cast(frame->layout()); m_colorIcon = new QLabel(this); l->insertWidget(0, m_colorIcon); m_colorIcon->setFixedSize(collapseButton->sizeHint()); title = new QLabel(this); l->insertWidget(2, title); m_keyframesButton = new QToolButton(this); m_keyframesButton->setIcon(QIcon::fromTheme(QStringLiteral("adjustcurves"))); m_keyframesButton->setAutoRaise(true); m_keyframesButton->setCheckable(true); m_keyframesButton->setToolTip(i18n("Enable Keyframes")); l->insertWidget(3, m_keyframesButton); // Enable button m_enabledButton = new KDualAction(i18n("Disable Effect"), i18n("Enable Effect"), this); m_enabledButton->setActiveIcon(QIcon::fromTheme(QStringLiteral("hint"))); m_enabledButton->setInactiveIcon(QIcon::fromTheme(QStringLiteral("visibility"))); enabledButton->setDefaultAction(m_enabledButton); connect(m_model.get(), &AssetParameterModel::enabledChange, this, &CollapsibleEffectView::enableView); m_groupAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Create Group"), this); connect(m_groupAction, &QAction::triggered, this, &CollapsibleEffectView::slotCreateGroup); if (m_regionEffect) { effectName.append(':' + QUrl(Xml::getXmlParameter(m_effect, QStringLiteral("resource"))).fileName()); } // Color thumb m_colorIcon->setScaledContents(true); m_colorIcon->setPixmap(QPixmap::fromImage(icon)); title->setText(effectName); frame->setMinimumHeight(collapseButton->sizeHint().height()); m_view = new AssetParameterView(this); const std::shared_ptr effectParamModel = std::static_pointer_cast(effectModel); m_view->setModel(effectParamModel, frameSize); connect(m_view, &AssetParameterView::seekToPos, this, &AbstractCollapsibleWidget::seekToPos); connect(m_view, &AssetParameterView::updateHeight, this, &CollapsibleEffectView::updateHeight); connect(this, &CollapsibleEffectView::refresh, m_view, &AssetParameterView::slotRefresh); m_keyframesButton->setVisible(m_view->keyframesAllowed()); auto *lay = new QVBoxLayout(widgetFrame); lay->setContentsMargins(0, 0, 0, 0); lay->setSpacing(0); lay->addWidget(m_view); connect(m_keyframesButton, &QToolButton::toggled, [this](bool toggle) { m_view->toggleKeyframes(toggle); }); if (!effectParamModel->hasMoreThanOneKeyframe()) { // No keyframe or only one, allow hiding bool hideByDefault = effectParamModel->data(effectParamModel->index(0, 0), AssetParameterModel::HideKeyframesFirstRole).toBool(); if (hideByDefault) { m_view->toggleKeyframes(false); } else { m_keyframesButton->setChecked(true); } } else { m_keyframesButton->setChecked(true); } // Presets presetButton->setIcon(QIcon::fromTheme(QStringLiteral("adjustlevels"))); presetButton->setMenu(m_view->presetMenu()); presetButton->setToolTip(i18n("Presets")); // Main menu m_menu = new QMenu(this); if (effectModel->rowCount() == 0) { collapseButton->setEnabled(false); m_view->setVisible(false); } m_menu->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save Effect"), this, SLOT(slotSaveEffect())); if (!m_regionEffect) { /*if (m_info.groupIndex == -1) { m_menu->addAction(m_groupAction); }*/ m_menu->addAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Create Region"), this, SLOT(slotCreateRegion())); } // setupWidget(info, metaInfo); menuButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); menuButton->setMenu(m_menu); if (!effectModel->isEnabled()) { title->setEnabled(false); m_colorIcon->setEnabled(false); if (KdenliveSettings::disable_effect_parameters()) { widgetFrame->setEnabled(false); } m_enabledButton->setActive(true); } else { m_enabledButton->setActive(false); } connect(m_enabledButton, &KDualAction::activeChangedByUser, this, &CollapsibleEffectView::slotDisable); connect(buttonUp, &QAbstractButton::clicked, this, &CollapsibleEffectView::slotEffectUp); connect(buttonDown, &QAbstractButton::clicked, this, &CollapsibleEffectView::slotEffectDown); connect(buttonDel, &QAbstractButton::clicked, this, &CollapsibleEffectView::slotDeleteEffect); for (QSpinBox *sp : findChildren()) { sp->installEventFilter(this); sp->setFocusPolicy(Qt::StrongFocus); } for (QComboBox *cb : findChildren()) { cb->installEventFilter(this); cb->setFocusPolicy(Qt::StrongFocus); } for (QProgressBar *cb : findChildren()) { cb->installEventFilter(this); cb->setFocusPolicy(Qt::StrongFocus); } for (WheelContainer *cb : findChildren()) { cb->installEventFilter(this); cb->setFocusPolicy(Qt::StrongFocus); } for (QDoubleSpinBox *cb : findChildren()) { cb->installEventFilter(this); cb->setFocusPolicy(Qt::StrongFocus); } QMetaObject::invokeMethod(this, "slotSwitch", Qt::QueuedConnection, Q_ARG(bool, m_model->isCollapsed())); } CollapsibleEffectView::~CollapsibleEffectView() { qDebug() << "deleting collapsibleeffectview"; } void CollapsibleEffectView::setWidgetHeight(qreal value) { widgetFrame->setFixedHeight(m_view->contentHeight() * value); } void CollapsibleEffectView::slotCreateGroup() { emit createGroup(m_model); } void CollapsibleEffectView::slotCreateRegion() { QString allExtensions = ClipCreationDialog::getExtensions().join(QLatin1Char(' ')); const QString dialogFilter = allExtensions + QLatin1Char(' ') + QLatin1Char('|') + i18n("All Supported Files") + QStringLiteral("\n* ") + QLatin1Char('|') + i18n("All Files"); QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); if (clipFolder.isEmpty()) { clipFolder = QDir::homePath(); } QPointer d = new QFileDialog(QApplication::activeWindow(), QString(), clipFolder, dialogFilter); d->setFileMode(QFileDialog::ExistingFile); if (d->exec() == QDialog::Accepted && !d->selectedUrls().isEmpty()) { KRecentDirs::add(QStringLiteral(":KdenliveClipFolder"), d->selectedUrls().first().adjusted(QUrl::RemoveFilename).toLocalFile()); emit createRegion(effectIndex(), d->selectedUrls().first()); } delete d; } void CollapsibleEffectView::slotUnGroup() { emit unGroup(this); } bool CollapsibleEffectView::eventFilter(QObject *o, QEvent *e) { if (e->type() == QEvent::Enter) { frame->setProperty("mouseover", true); frame->setStyleSheet(frame->styleSheet()); return QWidget::eventFilter(o, e); } if (e->type() == QEvent::Wheel) { auto *we = static_cast(e); if (!m_blockWheel || we->modifiers() != Qt::NoModifier) { e->accept(); return false; } if (qobject_cast(o)) { if (!qobject_cast(o)->hasFocus()) { e->ignore(); return true; } e->accept(); return false; } if (qobject_cast(o)) { if (qobject_cast(o)->focusPolicy() == Qt::WheelFocus) { e->accept(); return false; } e->ignore(); return true; } if (qobject_cast(o)) { if (!qobject_cast(o)->hasFocus()) { e->ignore(); return true; } e->accept(); return false; } if (qobject_cast(o)) { if (!qobject_cast(o)->hasFocus()) { e->ignore(); return true; } e->accept(); return false; } } return QWidget::eventFilter(o, e); } QDomElement CollapsibleEffectView::effect() const { return m_effect; } QDomElement CollapsibleEffectView::effectForSave() const { QDomElement effect = m_effect.cloneNode().toElement(); effect.removeAttribute(QStringLiteral("kdenlive_ix")); /* if (m_paramWidget) { int in = m_paramWidget->range().x(); EffectsController::offsetKeyframes(in, effect); } */ return effect; } bool CollapsibleEffectView::isActive() const { return decoframe->property("active").toBool(); } bool CollapsibleEffectView::isEnabled() const { return m_enabledButton->isActive(); } void CollapsibleEffectView::slotActivateEffect(QModelIndex ix) { // m_colorIcon->setEnabled(active); bool active = ix.row() == m_model->row(); decoframe->setProperty("active", active); decoframe->setStyleSheet(decoframe->styleSheet()); if (active) { pCore->getMonitor(m_model->monitorId)->slotShowEffectScene(needsMonitorEffectScene()); } m_view->initKeyframeView(active); } void CollapsibleEffectView::mousePressEvent(QMouseEvent *e) { m_dragStart = e->globalPos(); if (!decoframe->property("active").toBool()) { // Activate effect if not already active emit activateEffect(m_model); } QWidget::mousePressEvent(e); } void CollapsibleEffectView::mouseMoveEvent(QMouseEvent *e) { if ((e->globalPos() - m_dragStart).manhattanLength() < QApplication::startDragDistance()) { QPixmap pix = frame->grab(); emit startDrag(pix, m_model); } QWidget::mouseMoveEvent(e); } void CollapsibleEffectView::mouseDoubleClickEvent(QMouseEvent *event) { if (frame->underMouse() && collapseButton->isEnabled()) { event->accept(); m_collapse->setActive(!m_collapse->isActive()); } else { event->ignore(); } } void CollapsibleEffectView::mouseReleaseEvent(QMouseEvent *event) { m_dragStart = QPoint(); if (!decoframe->property("active").toBool()) { // emit activateEffect(effectIndex()); } QWidget::mouseReleaseEvent(event); } void CollapsibleEffectView::slotDisable(bool disable) { QString effectId = m_model->getAssetId(); QString effectName = EffectsRepository::get()->getName(effectId); std::static_pointer_cast(m_model)->markEnabled(effectName, !disable); } void CollapsibleEffectView::slotDeleteEffect() { emit deleteEffect(m_model); } void CollapsibleEffectView::slotEffectUp() { emit moveEffect(qMax(0, m_model->row() - 1), m_model); } void CollapsibleEffectView::slotEffectDown() { emit moveEffect(m_model->row() + 2, m_model); } void CollapsibleEffectView::slotSaveEffect() { QString name = QInputDialog::getText(this, i18n("Save Effect"), i18n("Name for saved effect: ")); if (name.trimmed().isEmpty()) { return; } QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } if (dir.exists(name + QStringLiteral(".xml"))) if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", name + QStringLiteral(".xml"))) == KMessageBox::No) { return; } QDomDocument doc; // Get base effect xml QString effectId = m_model->getAssetId(); QDomElement effect = EffectsRepository::get()->getXml(effectId); // Adjust param values QVector> currentValues = m_model->getAllParameters(); QMap values; QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); for (const auto ¶m : currentValues) { if (param.second.type() == QVariant::Double) { values.insert(param.first, locale.toString(param.second.toDouble())); } else { values.insert(param.first, param.second.toString()); } } QDomNodeList params = effect.elementsByTagName("parameter"); for (int i = 0; i < params.count(); ++i) { const QString paramName = params.item(i).toElement().attribute("name"); const QString paramType = params.item(i).toElement().attribute("type"); if (paramType == QLatin1String("fixed") || !values.contains(paramName)) { continue; } params.item(i).toElement().setAttribute(QStringLiteral("value"), values.value(paramName)); } doc.appendChild(doc.importNode(effect, true)); effect = doc.firstChild().toElement(); effect.removeAttribute(QStringLiteral("kdenlive_ix")); effect.setAttribute(QStringLiteral("id"), name); QString masterType = effect.attribute(QLatin1String("type")); effect.setAttribute(QStringLiteral("type"), (masterType == QLatin1String("audio") || masterType == QLatin1String("customAudio")) ? QStringLiteral("customAudio") : QStringLiteral("customVideo")); /* if (m_paramWidget) { int in = m_paramWidget->range().x(); EffectsController::offsetKeyframes(in, effect); } */ QDomElement effectname = effect.firstChildElement(QStringLiteral("name")); effect.removeChild(effectname); effectname = doc.createElement(QStringLiteral("name")); QDomText nametext = doc.createTextNode(name); effectname.appendChild(nametext); effect.insertBefore(effectname, QDomNode()); QDomElement effectprops = effect.firstChildElement(QStringLiteral("properties")); effectprops.setAttribute(QStringLiteral("id"), name); effectprops.setAttribute(QStringLiteral("type"), QStringLiteral("custom")); QFile file(dir.absoluteFilePath(name + QStringLiteral(".xml"))); if (file.open(QFile::WriteOnly | QFile::Truncate)) { QTextStream out(&file); out << doc.toString(); } file.close(); emit reloadEffect(dir.absoluteFilePath(name + QStringLiteral(".xml"))); } void CollapsibleEffectView::slotResetEffect() { m_view->resetValues(); } void CollapsibleEffectView::updateHeight() { if (m_view->height() == widgetFrame->height()) { return; } widgetFrame->setFixedHeight(m_collapse->isActive() ? 0 : m_view->height()); setFixedHeight(widgetFrame->height() + frame->minimumHeight() + 2 * (contentsMargins().top() + decoframe->lineWidth())); emit switchHeight(m_model, height()); } void CollapsibleEffectView::slotSwitch(bool collapse) { widgetFrame->setFixedHeight(collapse ? 0 : m_view->height()); setFixedHeight(widgetFrame->height() + frame->minimumHeight() + 2 * (contentsMargins().top() + decoframe->lineWidth())); m_model->setCollapsed(collapse); emit switchHeight(m_model, height()); } void CollapsibleEffectView::setGroupIndex(int ix) { Q_UNUSED(ix) /*if (m_info.groupIndex == -1 && ix != -1) { m_menu->removeAction(m_groupAction); } else if (m_info.groupIndex != -1 && ix == -1) { m_menu->addAction(m_groupAction); } m_info.groupIndex = ix; m_effect.setAttribute(QStringLiteral("kdenlive_info"), m_info.toString());*/ } void CollapsibleEffectView::setGroupName(const QString &groupName){ Q_UNUSED(groupName) /*m_info.groupName = groupName; m_effect.setAttribute(QStringLiteral("kdenlive_info"), m_info.toString());*/ } QString CollapsibleEffectView::infoString() const { return QString(); // m_info.toString(); } void CollapsibleEffectView::removeFromGroup() { /*if (m_info.groupIndex != -1) { m_menu->addAction(m_groupAction); } m_info.groupIndex = -1; m_info.groupName.clear(); m_effect.setAttribute(QStringLiteral("kdenlive_info"), m_info.toString()); emit parameterChanged(m_original_effect, m_effect, effectIndex());*/ } int CollapsibleEffectView::groupIndex() const { return -1; // m_info.groupIndex; } int CollapsibleEffectView::effectIndex() const { if (m_effect.isNull()) { return -1; } return m_effect.attribute(QStringLiteral("kdenlive_ix")).toInt(); } void CollapsibleEffectView::updateWidget(const ItemInfo &info, const QDomElement &effect) { // cleanup /* delete m_paramWidget; m_paramWidget = nullptr; */ m_effect = effect; setupWidget(info); } void CollapsibleEffectView::updateFrameInfo() { /* if (m_paramWidget) { m_paramWidget->refreshFrameInfo(); } */ } void CollapsibleEffectView::setActiveKeyframe(int kf) { Q_UNUSED(kf) /* if (m_paramWidget) { m_paramWidget->setActiveKeyframe(kf); } */ } void CollapsibleEffectView::setupWidget(const ItemInfo &info) { Q_UNUSED(info) /* if (m_effect.isNull()) { // //qCDebug(KDENLIVE_LOG) << "// EMPTY EFFECT STACK"; return; } delete m_paramWidget; m_paramWidget = nullptr; if (m_effect.attribute(QStringLiteral("tag")) == QLatin1String("region")) { m_regionEffect = true; QDomNodeList effects = m_effect.elementsByTagName(QStringLiteral("effect")); QDomNodeList origin_effects = m_original_effect.elementsByTagName(QStringLiteral("effect")); m_paramWidget = new ParameterContainer(m_effect, info, metaInfo, widgetFrame); QWidget *container = new QWidget(widgetFrame); QVBoxLayout *vbox = static_cast(widgetFrame->layout()); vbox->addWidget(container); // m_paramWidget = new ParameterContainer(m_effect.toElement(), info, metaInfo, container); for (int i = 0; i < effects.count(); ++i) { bool canMoveUp = true; if (i == 0 || effects.at(i - 1).toElement().attribute(QStringLiteral("id")) == QLatin1String("speed")) { canMoveUp = false; } CollapsibleEffectView *coll = new CollapsibleEffectView(effects.at(i).toElement(), origin_effects.at(i).toElement(), info, metaInfo, canMoveUp, i == effects.count() - 1, container); m_subParamWidgets.append(coll); connect(coll, &CollapsibleEffectView::parameterChanged, this, &CollapsibleEffectView::slotUpdateRegionEffectParams); // container = new QWidget(widgetFrame); vbox->addWidget(coll); // p = new ParameterContainer(effects.at(i).toElement(), info, isEffect, container); } } else { m_paramWidget = new ParameterContainer(m_effect, info, metaInfo, widgetFrame); connect(m_paramWidget, &ParameterContainer::disableCurrentFilter, this, &CollapsibleEffectView::slotDisable); connect(m_paramWidget, &ParameterContainer::importKeyframes, this, &CollapsibleEffectView::importKeyframes); if (m_effect.firstChildElement(QStringLiteral("parameter")).isNull()) { // Effect has no parameter, don't allow expand collapseButton->setEnabled(false); collapseButton->setVisible(false); widgetFrame->setVisible(false); } } if (collapseButton->isEnabled() && m_info.isCollapsed) { widgetFrame->setVisible(false); collapseButton->setArrowType(Qt::RightArrow); } connect(m_paramWidget, &ParameterContainer::parameterChanged, this, &CollapsibleEffectView::parameterChanged); connect(m_paramWidget, &ParameterContainer::startFilterJob, this, &CollapsibleEffectView::startFilterJob); connect(this, &CollapsibleEffectView::syncEffectsPos, m_paramWidget, &ParameterContainer::syncEffectsPos); connect(m_paramWidget, &ParameterContainer::checkMonitorPosition, this, &CollapsibleEffectView::checkMonitorPosition); connect(m_paramWidget, &ParameterContainer::seekTimeline, this, &CollapsibleEffectView::seekTimeline); connect(m_paramWidget, &ParameterContainer::importClipKeyframes, this, &CollapsibleEffectView::prepareImportClipKeyframes); */ } bool CollapsibleEffectView::isGroup() const { return false; } void CollapsibleEffectView::updateTimecodeFormat() { /* m_paramWidget->updateTimecodeFormat(); if (!m_subParamWidgets.isEmpty()) { // we have a group for (int i = 0; i < m_subParamWidgets.count(); ++i) { m_subParamWidgets.at(i)->updateTimecodeFormat(); } } */ } void CollapsibleEffectView::slotUpdateRegionEffectParams(const QDomElement & /*old*/, const QDomElement & /*e*/, int /*ix*/) { // qCDebug(KDENLIVE_LOG)<<"// EMIT CHANGE SUBEFFECT.....:"; emit parameterChanged(m_original_effect, m_effect, effectIndex()); } void CollapsibleEffectView::slotSyncEffectsPos(int pos) { emit syncEffectsPos(pos); } void CollapsibleEffectView::dragEnterEvent(QDragEnterEvent *event) { Q_UNUSED(event) /* if (event->mimeData()->hasFormat(QStringLiteral("kdenlive/effectslist"))) { frame->setProperty("target", true); frame->setStyleSheet(frame->styleSheet()); event->acceptProposedAction(); } else if (m_paramWidget->doesAcceptDrops() && event->mimeData()->hasFormat(QStringLiteral("kdenlive/geometry")) && event->source()->objectName() != QStringLiteral("ParameterContainer")) { event->setDropAction(Qt::CopyAction); event->setAccepted(true); } else { QWidget::dragEnterEvent(event); } */ } void CollapsibleEffectView::dragLeaveEvent(QDragLeaveEvent * /*event*/) { frame->setProperty("target", false); frame->setStyleSheet(frame->styleSheet()); } void CollapsibleEffectView::importKeyframes(const QString &kf) { QMap keyframes; if (kf.contains(QLatin1Char('\n'))) { const QStringList params = kf.split(QLatin1Char('\n'), QString::SkipEmptyParts); for (const QString ¶m : params) { keyframes.insert(param.section(QLatin1Char('='), 0, 0), param.section(QLatin1Char('='), 1)); } } else { keyframes.insert(kf.section(QLatin1Char('='), 0, 0), kf.section(QLatin1Char('='), 1)); } emit importClipKeyframes(AVWidget, m_itemInfo, m_effect.cloneNode().toElement(), keyframes); } void CollapsibleEffectView::dropEvent(QDropEvent *event) { if (event->mimeData()->hasFormat(QStringLiteral("kdenlive/geometry"))) { if (event->source()->objectName() == QStringLiteral("ParameterContainer")) { return; } // emit activateEffect(effectIndex()); QString itemData = event->mimeData()->data(QStringLiteral("kdenlive/geometry")); importKeyframes(itemData); return; } frame->setProperty("target", false); frame->setStyleSheet(frame->styleSheet()); const QString effects = QString::fromUtf8(event->mimeData()->data(QStringLiteral("kdenlive/effectslist"))); // event->acceptProposedAction(); QDomDocument doc; doc.setContent(effects, true); QDomElement e = doc.documentElement(); int ix = e.attribute(QStringLiteral("kdenlive_ix")).toInt(); int currentEffectIx = effectIndex(); if (ix == currentEffectIx || e.attribute(QStringLiteral("id")) == QLatin1String("speed")) { // effect dropped on itself, or unmovable speed dropped, reject event->ignore(); return; } if (ix == 0 || e.tagName() == QLatin1String("effectgroup")) { if (e.tagName() == QLatin1String("effectgroup")) { // moving a group QDomNodeList subeffects = e.elementsByTagName(QStringLiteral("effect")); if (subeffects.isEmpty()) { event->ignore(); return; } event->setDropAction(Qt::MoveAction); event->accept(); /* EffectInfo info; info.fromString(subeffects.at(0).toElement().attribute(QStringLiteral("kdenlive_info"))); if (info.groupIndex >= 0) { // Moving group QList effectsIds; // Collect moved effects ids for (int i = 0; i < subeffects.count(); ++i) { QDomElement effect = subeffects.at(i).toElement(); effectsIds << effect.attribute(QStringLiteral("kdenlive_ix")).toInt(); } // emit moveEffect(effectsIds, currentEffectIx, info.groupIndex, info.groupName); } else { // group effect dropped from effect list if (m_info.groupIndex > -1) { // TODO: Should we merge groups?? } emit addEffect(e); }*/ emit addEffect(e); return; } // effect dropped from effects list, add it e.setAttribute(QStringLiteral("kdenlive_ix"), ix); /*if (m_info.groupIndex > -1) { // Dropped on a group e.setAttribute(QStringLiteral("kdenlive_info"), m_info.toString()); }*/ event->setDropAction(Qt::CopyAction); event->accept(); emit addEffect(e); return; } // emit moveEffect(QList() << ix, currentEffectIx, m_info.groupIndex, m_info.groupName); event->setDropAction(Qt::MoveAction); event->accept(); } void CollapsibleEffectView::adjustButtons(int ix, int max) { buttonUp->setEnabled(ix > 0); buttonDown->setEnabled(ix < max - 1); } MonitorSceneType CollapsibleEffectView::needsMonitorEffectScene() const { if (!m_model->isEnabled() || !m_view) { return MonitorSceneDefault; } return m_view->needsMonitorEffectScene(); } void CollapsibleEffectView::setKeyframes(const QString &tag, const QString &keyframes) { Q_UNUSED(tag) Q_UNUSED(keyframes) /* m_paramWidget->setKeyframes(tag, keyframes); */ } bool CollapsibleEffectView::isMovable() const { return m_isMovable; } void CollapsibleEffectView::prepareImportClipKeyframes() { emit importClipKeyframes(AVWidget, m_itemInfo, m_effect.cloneNode().toElement(), QMap()); } void CollapsibleEffectView::enableView(bool enabled) { m_enabledButton->setActive(enabled); title->setEnabled(!enabled); m_colorIcon->setEnabled(!enabled); if (enabled) { if (KdenliveSettings::disable_effect_parameters()) { widgetFrame->setEnabled(false); } } else { widgetFrame->setEnabled(true); } } void CollapsibleEffectView::blockWheenEvent(bool block) { m_blockWheel = block; } diff --git a/src/jobs/cutclipjob.cpp b/src/jobs/cutclipjob.cpp index b90a83774..71d4df2fa 100644 --- a/src/jobs/cutclipjob.cpp +++ b/src/jobs/cutclipjob.cpp @@ -1,218 +1,217 @@ /*************************************************************************** * * * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@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) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "cutclipjob.h" #include "bin/bin.h" #include "bin/clipcreator.hpp" #include "jobmanager.h" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "macros.hpp" #include "ui_cutjobdialog_ui.h" #include #include #include #include #include #include CutClipJob::CutClipJob(const QString &binId, const QString sourcePath, GenTime inTime, GenTime outTime, const QString destPath, QStringList encodingParams) : AbstractClipJob(CUTJOB, binId) , m_sourceUrl(sourcePath) , m_destUrl(destPath) , m_done(false) , m_jobProcess(nullptr) , m_in(inTime) , m_out(outTime) , m_encodingParams(encodingParams) , m_jobDuration((int)(outTime - inTime).seconds()) { } const QString CutClipJob::getDescription() const { return i18n("Extract Clip Zone"); } // static int CutClipJob::prepareJob(const std::shared_ptr &ptr, const std::vector &binIds, int parentId, QString undoString, GenTime inTime, GenTime outTime) { if (binIds.empty()) { return -1; } const QString mainId = *binIds.begin(); auto binClip = pCore->projectItemModel()->getClipByBinID(mainId); ClipType::ProducerType type = binClip->clipType(); if (type != ClipType::AV && type != ClipType::Audio && type != ClipType::Video) { //m_errorMessage.prepend(i18n("Cannot extract zone for this clip type.")); return -1; } if (KdenliveSettings::ffmpegpath().isEmpty()) { // FFmpeg not detected, cannot process the Job //m_errorMessage.prepend(i18n("Failed to create cut. FFmpeg not found, please set path in Kdenlive's settings Environment")); return -1; } const QString source = binClip->url(); QString transcoderExt = source.section(QLatin1Char('.'), -1); QFileInfo finfo(source); QString fileName = finfo.fileName().section(QLatin1Char('.'), 0, -2); QDir dir = finfo.absoluteDir(); QString inString = QString::number((int)inTime.seconds()); QString outString = QString::number((int)outTime.seconds()); QString path = dir.absoluteFilePath(fileName + QString("-%1-%2.").arg(inString).arg(outString) + transcoderExt); QPointer d = new QDialog(QApplication::activeWindow()); Ui::CutJobDialog_UI ui; ui.setupUi(d); ui.extra_params->setVisible(false); ui.add_clip->setChecked(KdenliveSettings::add_new_clip()); ui.file_url->setMode(KFile::File); ui.extra_params->setMaximumHeight(QFontMetrics(QApplication::font()).lineSpacing() * 5); ui.file_url->setUrl(QUrl::fromLocalFile(path)); QFontMetrics fm = ui.file_url->lineEdit()->fontMetrics(); ui.file_url->setMinimumWidth(fm.boundingRect(ui.file_url->text().left(50)).width() * 1.4); ui.button_more->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); ui.extra_params->setPlainText(QStringLiteral("-acodec copy -vcodec copy")); QString mess = i18n("Extracting %1 out of %2", Timecode::getStringTimecode((outTime -inTime).frames(pCore->getCurrentFps()), pCore->getCurrentFps(), true), binClip->getStringDuration()); ui.info_label->setText(mess); if (d->exec() != QDialog::Accepted) { delete d; return -1; } path = ui.file_url->url().toLocalFile(); QStringList encodingParams = ui.extra_params->toPlainText().split(QLatin1Char(' '), QString::SkipEmptyParts); KdenliveSettings::setAdd_new_clip(ui.add_clip->isChecked()); delete d; if (QFile::exists(path)) { KIO::RenameDialog renameDialog(qApp->activeWindow(), i18n("File already exists"), QUrl::fromLocalFile(path), QUrl::fromLocalFile(path), KIO::RenameDialog_Option::RenameDialog_Overwrite ); if (renameDialog.exec() != QDialog::Rejected) { QUrl final = renameDialog.newDestUrl(); if (final.isValid()) { path = final.toLocalFile(); } } else { return -1; } } return ptr->startJob_noprepare(binIds, parentId, std::move(undoString), source, inTime, outTime, dir.absoluteFilePath(path), encodingParams); //return ptr->startJob_noprepare(binIds, parentId, std::move(undoString), mainId, source, inTime, outTime, dir.absoluteFilePath(path), encodingParams); //return ptr->startJob(binIds, parentId, std::move(undoString), std::make_shared(mainId, source, inTime, outTime, dir.absoluteFilePath(path))); } bool CutClipJob::startJob() { - QLocale locale; bool result; if (m_destUrl == m_sourceUrl) { m_errorMessage.append(i18n("You cannot overwrite original clip.")); m_done = true; return false; } QStringList params = {QStringLiteral("-y"),QStringLiteral("-stats"),QStringLiteral("-v"),QStringLiteral("error"),QStringLiteral("-noaccurate_seek"),QStringLiteral("-ss"),QString::number(m_in.seconds()),QStringLiteral("-i"),m_sourceUrl, QStringLiteral("-t"), QString::number((m_out-m_in).seconds()),QStringLiteral("-avoid_negative_ts"),QStringLiteral("make_zero")}; params << m_encodingParams << m_destUrl; m_jobProcess = std::make_unique(new QProcess); connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &CutClipJob::processLogInfo); connect(this, &CutClipJob::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection); m_jobProcess->start(KdenliveSettings::ffmpegpath(), params, QIODevice::ReadOnly); m_jobProcess->waitForFinished(-1); result = m_jobProcess->exitStatus() == QProcess::NormalExit; // remove temporary playlist if it exists if (result) { if (QFileInfo(m_destUrl).size() == 0) { QFile::remove(m_destUrl); // File was not created m_done = false; m_errorMessage.append(i18n("Failed to create file.")); } else { m_done = true; } } else { // Proxy process crashed QFile::remove(m_destUrl); m_done = false; m_errorMessage.append(QString::fromUtf8(m_jobProcess->readAll())); } return result; } void CutClipJob::processLogInfo() { const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError()); m_logDetails.append(buffer); int progress = 0; // Parse FFmpeg output if (m_jobDuration == 0) { if (buffer.contains(QLatin1String("Duration:"))) { QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified(); if (!data.isEmpty()) { QStringList numbers = data.split(QLatin1Char(':')); if (numbers.size() < 3) { return; } m_jobDuration = (int)(numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble()); } } } else if (buffer.contains(QLatin1String("time="))) { QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0); if (!time.isEmpty()) { QStringList numbers = time.split(QLatin1Char(':')); if (numbers.size() < 3) { progress = (int)time.toDouble(); if (progress == 0) { return; } } else { progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble(); } } emit jobProgress((int)(100.0 * progress / m_jobDuration)); } } bool CutClipJob::commitResult(Fun &undo, Fun &redo) { Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; if (!KdenliveSettings::add_new_clip()) { return true; } QStringList ids = pCore->projectItemModel()->getClipByUrl(QFileInfo(m_destUrl)); if (!ids.isEmpty()) { // Clip was already inserted in bin, will be reloaded automatically, don't add twice return true; } QString folderId = QStringLiteral("-1"); auto id = ClipCreator::createClipFromFile(m_destUrl, folderId, pCore->projectItemModel(), undo, redo); return id != QStringLiteral("-1"); } diff --git a/src/jobs/filterjob.cpp b/src/jobs/filterjob.cpp index 29ea4dadc..3974efd3d 100644 --- a/src/jobs/filterjob.cpp +++ b/src/jobs/filterjob.cpp @@ -1,265 +1,266 @@ /*************************************************************************** * * * Copyright (C) 2015 by Jean-Baptiste Mardelle (jb@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) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "filterjob.h" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "meltjob.h" #include "project/clipstabilize.h" #include "project/dialogs/clipspeed.h" #include "ui_scenecutdialog_ui.h" #include #include #include #include // static QList FilterJob::filterClips(const QList &clips, const QStringList ¶ms) { QString condition; if (params.count() > 3) { // there is a condition for this job, for example operate only on vcodec=mpeg1video condition = params.at(3); } QList result; for (int i = 0; i < clips.count(); i++) { ProjectClip *clip = clips.at(i); ClipType type = clip->clipType(); if (type != AV && type != Audio && type != Video) { // Clip will not be processed by this job continue; } if (!condition.isEmpty() && !clip->matches(condition)) { // Clip does not match requested condition, do not process continue; } result << clip; } return result; } QHash FilterJob::prepareJob(const QList &clips, const QStringList ¶meters) { QHash jobs; QStringList sources; sources.reserve(clips.count()); for (int i = 0; i < clips.count(); i++) { sources << clips.at(i)->url(); } const QString filterName = parameters.constFirst(); if (filterName == QLatin1String("timewarp")) { QMap producerParams = QMap(); QMap filterParams = QMap(); QMap consumerParams = QMap(); QMap extraParams = QMap(); producerParams.insert(QStringLiteral("in"), QStringLiteral("0")); producerParams.insert(QStringLiteral("out"), QStringLiteral("-1")); extraParams.insert(QStringLiteral("projecttreefilter"), QStringLiteral("1")); // Reverse clip using project profile since playlists can only be included with same fps // extraParams.insert(QStringLiteral("producer_profile"), QStringLiteral("1")); bool multipleSelection = clips.count() > 1; QPointer d = new ClipSpeed(clips.count() == 1 ? QUrl::fromLocalFile(sources.constFirst() + QStringLiteral(".mlt")) : QUrl::fromLocalFile(sources.constFirst()).adjusted(QUrl::RemoveFilename), multipleSelection, QApplication::activeWindow()); if (d->exec() == QDialog::Accepted) { QLocale locale; + locale.setNumberOptions(QLocale::OmitGroupSeparator); QString speedString = QStringLiteral("timewarp:%1:").arg(locale.toString(d->speed() / 100)); QDir destFolder; if (multipleSelection) { destFolder = QDir(d->selectedUrl().toLocalFile()); } for (int i = 0; i < clips.count(); i++) { QString prodstring = speedString + sources.at(i); producerParams.insert(QStringLiteral("producer"), prodstring); QString destination; if (multipleSelection) { destination = destFolder.absoluteFilePath(QUrl::fromLocalFile(sources.at(i)).fileName() + QStringLiteral(".mlt")); } else { destination = d->selectedUrl().toLocalFile(); } if (QFile::exists(destination)) { if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("File %1 already exists.\nDo you want to overwrite it?", destination)) != KMessageBox::Yes) { continue; } } consumerParams.insert(QStringLiteral("consumer"), QStringLiteral("xml:") + destination); ProjectClip *clip = clips.at(i); auto *job = new MeltJob(clip->clipType(), clip->AbstractProjectItem::clipId(), producerParams, filterParams, consumerParams, extraParams); job->description = i18n("Reverse clip"); job->setAddClipToProject(1); jobs.insert(clip, job); } } delete d; return jobs; } if (filterName == QLatin1String("motion_est")) { // Show config dialog QPointer d = new QDialog(QApplication::activeWindow()); Ui::SceneCutDialog_UI ui; ui.setupUi(d); // Set up categories for (size_t i = 0; i < MarkerListModel::markerTypes.size(); ++i) { ui.marker_type->insertItem((int)i, i18n("Category %1", i)); ui.marker_type->setItemData((int)i, MarkerListModel::markerTypes[i], Qt::DecorationRole); } ui.marker_type->setCurrentIndex(KdenliveSettings::default_marker_type()); if (d->exec() != QDialog::Accepted) { delete d; return jobs; } // Autosplit filter QMap producerParams = QMap(); QMap filterParams = QMap(); QMap consumerParams = QMap(); // Producer params // None // Filter params, use a smaller region of the image to speed up operation // In fact, it's faster to rescale whole image than using part of it (bounding=\"25%x25%:15%x15\") filterParams.insert(QStringLiteral("filter"), filterName); filterParams.insert(QStringLiteral("shot_change_list"), QStringLiteral("0")); filterParams.insert(QStringLiteral("denoise"), QStringLiteral("0")); // Consumer consumerParams.insert(QStringLiteral("consumer"), QStringLiteral("null")); consumerParams.insert(QStringLiteral("all"), QStringLiteral("1")); consumerParams.insert(QStringLiteral("terminate_on_pause"), QStringLiteral("1")); consumerParams.insert(QStringLiteral("real_time"), QStringLiteral("-1")); // We just want to find scene change, set all methods to the fastest consumerParams.insert(QStringLiteral("rescale"), QStringLiteral("nearest")); consumerParams.insert(QStringLiteral("deinterlace_method"), QStringLiteral("onefield")); consumerParams.insert(QStringLiteral("top_field_first"), QStringLiteral("-1")); // Extra QMap extraParams; extraParams.insert(QStringLiteral("key"), QStringLiteral("shot_change_list")); extraParams.insert(QStringLiteral("projecttreefilter"), QStringLiteral("1")); QString keyword(QStringLiteral("%count")); extraParams.insert(QStringLiteral("resultmessage"), i18n("Found %1 scenes.", keyword)); extraParams.insert(QStringLiteral("resize_profile"), QStringLiteral("160")); if (ui.store_data->isChecked()) { // We want to save result as clip metadata extraParams.insert(QStringLiteral("storedata"), QStringLiteral("1")); } if (ui.zone_only->isChecked()) { // We want to analyze only clip zone extraParams.insert(QStringLiteral("zoneonly"), QStringLiteral("1")); } if (ui.add_markers->isChecked()) { // We want to create markers extraParams.insert(QStringLiteral("addmarkers"), QString::number(ui.marker_type->currentIndex())); extraParams.insert(QStringLiteral("label"), i18n("Scene ")); } if (ui.cut_scenes->isChecked()) { // We want to cut scenes extraParams.insert(QStringLiteral("cutscenes"), QStringLiteral("1")); } delete d; for (int i = 0; i < clips.count(); i++) { // Set clip specific infos // in and out int in = 0; int out = -1; ProjectClip *clip = clips.at(i); if (extraParams.contains(QStringLiteral("zoneonly"))) { // Analyse clip zone only, remove in / out and replace with zone QPoint zone = clip->zone(); in = zone.x(); out = zone.y(); } producerParams.insert(QStringLiteral("in"), QString::number(in)); producerParams.insert(QStringLiteral("out"), QString::number(out)); producerParams.insert(QStringLiteral("producer"), sources.at(i)); // Destination // Since this job is only doing analysis, we have a null consumer and no destination auto *job = new MeltJob(clip->clipType(), clip->AbstractProjectItem::clipId(), producerParams, filterParams, consumerParams, extraParams); job->description = i18n("Auto split"); jobs.insert(clip, job); } return jobs; } if (filterName == QLatin1String("vidstab") || filterName == QLatin1String("videostab2") || filterName == QLatin1String("videostab")) { // vidstab int out = 100000; if (clips.count() == 1) { out = clips.constFirst()->duration().frames(KdenliveSettings::project_fps()); } QPointer d = new ClipStabilize(sources, filterName); if (d->exec() == QDialog::Accepted) { QMap producerParams = d->producerParams(); QMap filterParams = d->filterParams(); QMap consumerParams = d->consumerParams(); QMap extraParams; extraParams.insert(QStringLiteral("producer_profile"), QStringLiteral("1")); QString destination = d->destination(); QUrl trffile; for (int i = 0; i < clips.count(); i++) { // Set clip specific infos // in and out int clip_in = 0; int clip_out = -1; ProjectClip *clip = clips.at(i); if (extraParams.contains(QStringLiteral("zoneonly"))) { // Analyse clip zone only, remove in / out and replace with zone QPoint zone = clip->zone(); clip_in = zone.x(); clip_out = zone.y(); } producerParams.insert(QStringLiteral("in"), QString::number(clip_in)); producerParams.insert(QStringLiteral("out"), QString::number(clip_out)); producerParams.insert(QStringLiteral("producer"), sources.at(i)); // Consumer QString consumerName = d->consumerParams().value(QStringLiteral("consumer")); if (clips.count() == 1) { // We only have one clip, destination points to the final url consumerParams.insert(QStringLiteral("consumer"), consumerName + QLatin1Char(':') + destination); trffile = QUrl::fromLocalFile(destination + QStringLiteral(".trf")); } else { // Filter several clips, destination points to a folder QString mltfile = destination + QFileInfo(clip->url()).fileName() + QStringLiteral(".mlt"); consumerParams.insert(QStringLiteral("consumer"), consumerName + QLatin1Char(':') + mltfile); trffile = QUrl::fromLocalFile(mltfile + QStringLiteral(".trf")); } consumerParams.insert(QStringLiteral("real_time"), QStringLiteral("-1")); // Append a 'filename' parameter for saving vidstab data filterParams.insert(QStringLiteral("filename"), trffile.toLocalFile()); auto *job = new MeltJob(clip->clipType(), clip->AbstractProjectItem::clipId(), producerParams, filterParams, consumerParams, extraParams); job->setAddClipToProject(d->autoAddClip() ? clip->parent()->AbstractProjectItem::clipId().toInt() : -100); job->description = d->desc(); jobs.insert(clip, job); } } delete d; return jobs; } return jobs; } diff --git a/src/mltcontroller/clippropertiescontroller.cpp b/src/mltcontroller/clippropertiescontroller.cpp index 13181672e..f028b929e 100644 --- a/src/mltcontroller/clippropertiescontroller.cpp +++ b/src/mltcontroller/clippropertiescontroller.cpp @@ -1,1362 +1,1366 @@ /* Copyright (C) 2015 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "clippropertiescontroller.h" #include "bin/model/markerlistmodel.hpp" #include "clipcontroller.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" #include "timecodedisplay.h" #include