diff --git a/src/assets/abstractassetsrepository.ipp b/src/assets/abstractassetsrepository.ipp index 69df2321a..0075c6cef 100644 --- a/src/assets/abstractassetsrepository.ipp +++ b/src/assets/abstractassetsrepository.ipp @@ -1,233 +1,234 @@ /*************************************************************************** * 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 "xml/xml.hpp" #include #include #include #include #include #ifdef Q_OS_MAC #include #endif template AbstractAssetsRepository::AbstractAssetsRepository() { } template void AbstractAssetsRepository::init() { // Warning: Mlt::Factory::init() resets the locale to the default system value, make sure we keep correct locale #ifndef Q_OS_MAC setlocale(LC_NUMERIC, nullptr); #else setlocale(LC_NUMERIC_MASK, nullptr); #endif // Parse effects blacklist parseBlackList(assetBlackListPath()); // Retrieve the list of MLT's available assets. QScopedPointer assets(retrieveListFromMlt()); int max = assets->count(); for (int i = 0; i < max; ++i) { Info info; QString name = assets->get_name(i); info.id = name; if (!m_blacklist.contains(name) && parseInfoFromMlt(name, info)) { m_assets[name] = info; } else { if (m_blacklist.contains(name)) { qDebug() << name << "is blacklisted"; } else { qDebug() << "WARNING : Fails to parse " << name; } } } // We now parse custom effect xml // Set the directories to look into for effects. QStringList asset_dirs = assetDirs(); /* Parsing of custom xml works as follows: we parse all custom files. Each of them contains a tag, which is the corresponding mlt asset, and an id that is the name of the asset. Note that several custom files can correspond to the same tag, and in that case they must have different ids. We do the parsing in a map from ids to parse info, and then we add them to the asset list, while discarding the bare version of each tag (the one with no file associated) */ std::unordered_map customAssets; for (const auto &dir : asset_dirs) { QDir current_dir(dir); QStringList filter; filter << QStringLiteral("*.xml"); QStringList fileList = current_dir.entryList(filter, QDir::Files); for (const auto &file : fileList) { QString path = current_dir.absoluteFilePath(file); parseCustomAssetFile(path, customAssets); } } // We add the custom assets for (const auto &custom : customAssets) { if (m_assets.count(custom.second.mltId) > 0) { m_assets.erase(custom.second.mltId); } if (m_assets.count(custom.first) == 0) { m_assets[custom.first] = custom.second; } else { qDebug() << "Error: conflicting asset name " << custom.first; } } } template void AbstractAssetsRepository::parseBlackList(const QString &path) { + qDebug() << "BLACKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK"< bool AbstractAssetsRepository::parseInfoFromMlt(const QString &assetId, Info &res) { QScopedPointer metadata(getMetadata(assetId)); if (metadata && metadata->is_valid()) { if (metadata->get("title") && metadata->get("identifier") && strlen(metadata->get("title")) > 0) { res.name = metadata->get("title"); res.name[0] = res.name[0].toUpper(); res.description = metadata->get("description"); res.author = metadata->get("creator"); res.version_str = metadata->get("version"); res.version = metadata->get_double("version"); res.id = res.mltId = assetId; parseType(metadata, res); return true; } } return false; } template bool AbstractAssetsRepository::exists(const QString &assetId) const { return m_assets.count(assetId) > 0; } template QVector> AbstractAssetsRepository::getNames() const { QVector> res; res.reserve((int)m_assets.size()); for (const auto &asset : m_assets) { res.push_back({asset.first, asset.second.name}); } std::sort(res.begin(), res.end(), [](const QPair &a, const QPair &b) { return a.second < b.second; }); return res; } template AssetType AbstractAssetsRepository::getType(const QString &assetId) const { Q_ASSERT(m_assets.count(assetId) > 0); return m_assets.at(assetId).type; } template QString AbstractAssetsRepository::getName(const QString &assetId) const { Q_ASSERT(m_assets.count(assetId) > 0); return m_assets.at(assetId).name; } template QString AbstractAssetsRepository::getDescription(const QString &assetId) const { Q_ASSERT(m_assets.count(assetId) > 0); return m_assets.at(assetId).description; } template bool AbstractAssetsRepository::isFavorite(const QString & /*assetId*/) const { // TODO return true; } template bool AbstractAssetsRepository::parseInfoFromXml(const QDomElement ¤tAsset, Info &res) const { QLocale locale; // We first deal with locale if (currentAsset.hasAttribute(QStringLiteral("LC_NUMERIC"))) { // set a locale for that effect locale = QLocale(currentAsset.attribute(QStringLiteral("LC_NUMERIC"))); } locale.setNumberOptions(QLocale::OmitGroupSeparator); QString tag = currentAsset.attribute(QStringLiteral("tag"), QString()); QString id = currentAsset.attribute(QStringLiteral("id"), QString()); if (id.isEmpty()) { id = tag; } if (!exists(tag)) { qDebug() << "++++++ Unknown asset : " << tag; return false; } // Check if there is a maximal version set if (currentAsset.hasAttribute(QStringLiteral("version"))) { // a specific version of the filter is required if (locale.toDouble(currentAsset.attribute(QStringLiteral("version"))) > m_assets.at(tag).version) { return false; } } res = m_assets.at(tag); res.id = id; res.mltId = tag; // Update description if the xml provide one QString description = Xml::getSubTagContent(currentAsset, QStringLiteral("description")); if (!description.isEmpty()) { res.description = description; } // Update name if the xml provide one QString name = Xml::getSubTagContent(currentAsset, QStringLiteral("name")); if (!name.isEmpty()) { res.name = name; } return true; } template QDomElement AbstractAssetsRepository::getXml(const QString &assetId) const { if (m_assets.count(assetId) == 0) { qDebug() << "Error : Requesting info on unknown transition " << assetId; return QDomElement(); } return m_assets.at(assetId).xml; } diff --git a/src/assets/keyframes/model/keyframemodel.cpp b/src/assets/keyframes/model/keyframemodel.cpp index 8aa922e28..ae109f9f8 100644 --- a/src/assets/keyframes/model/keyframemodel.cpp +++ b/src/assets/keyframes/model/keyframemodel.cpp @@ -1,594 +1,598 @@ /*************************************************************************** * 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 "doc/docundostack.hpp" #include "core.h" #include "assets/model/assetparametermodel.hpp" #include "macros.hpp" #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:"<getCurrentFps())< 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; double oldValue = m_keyframeList[pos].second; local_undo = updateKeyframe_lambda(pos, oldType, oldValue, notify); local_redo = updateKeyframe_lambda(pos, type, value, notify); } else { qDebug() << "True addittion"; 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(GenTime pos, KeyframeType type, double 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, 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) { qDebug() << "Going to remove keyframe at "<getCurrentFps()); qDebug() << "before"< 0); KeyframeType oldType = m_keyframeList[pos].first; double oldValue = m_keyframeList[pos].second; Fun local_undo = addKeyframe_lambda(pos, oldType, oldValue, true); Fun local_redo = deleteKeyframe_lambda(pos, true); qDebug() << "before2"< 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, Fun &undo, Fun &redo) { qDebug() << "starting to move keyframe"<getCurrentFps())<getCurrentFps()); QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(oldPos) > 0); KeyframeType oldType = m_keyframeList[oldPos].first; double oldValue = m_keyframeList[oldPos].second; if (oldPos == pos ) return true; Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; qDebug() << getAnimProperty(); bool res = removeKeyframe(oldPos, local_undo, local_redo); qDebug() << "Move keyframe finished deletion:"< 0); if (oldPos == pos ) return true; Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = moveKeyframe(oldPos, pos, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move keyframe")); } return res; } bool KeyframeModel::updateKeyframe(GenTime pos, double value, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(pos) > 0); KeyframeType oldType = m_keyframeList[pos].first; double oldValue = m_keyframeList[pos].second; if (qAbs(oldValue - value) < 1e-6) return true; auto operation = updateKeyframe_lambda(pos, oldType, oldValue, true); auto reverse = updateKeyframe_lambda(pos, oldType, value, true); bool res = operation(); if (res) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return res; } bool KeyframeModel::updateKeyframe(GenTime pos, double value) { QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(pos) > 0); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = updateKeyframe(pos, value, undo, redo); if (res) { PUSH_UNDO(undo, redo, i18n("Update keyframe")); } return res; } Fun KeyframeModel::updateKeyframe_lambda(GenTime pos, KeyframeType type, double value, bool notify) { QWriteLocker locker(&m_lock); return [this, pos, type, value, notify]() { qDebug() << "udpate lambda"<getCurrentFps())< 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), QVector() << TypeRole << ValueRole); return true; }; } Fun KeyframeModel::addKeyframe_lambda(GenTime pos, KeyframeType type, double value, bool notify) { QWriteLocker locker(&m_lock); return [this, notify, pos, type, value]() { qDebug() << "add lambda"<getCurrentFps())<(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"<getCurrentFps())< 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"< KeyframeModel::roleNames() const { QHash roles; roles[PosRole] = "position"; roles[FrameRole] = "frame"; roles[TypeRole] = "type"; roles[ValueRole] = "value"; 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 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()); } 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; }; for (const auto& m : m_keyframeList) { all_pos.push_back(m.first); } 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); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } 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; } QString KeyframeModel::getAnimProperty() const { QString prop; bool first = true; for (const auto keyframe : m_keyframeList) { if (first) { first = false; } else { prop += QStringLiteral(";"); } prop += QString::number(keyframe.first.frames(pCore->getCurrentFps())); switch (keyframe.second.first) { case KeyframeType::Linear: prop += QStringLiteral("="); break; case KeyframeType::Discrete: prop += QStringLiteral("|="); break; case KeyframeType::Curve: prop += QStringLiteral("~="); break; } prop += QString::number(keyframe.second.second); } return prop; } 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; } 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; } void KeyframeModel::parseAnimProperty(const QString &prop) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; Mlt::Properties mlt_prop; mlt_prop.set("key", prop.toUtf8().constData()); + // This is a fake query to force the animation to be parsed + (void)mlt_prop.anim_get_int("key", 0, 0); Mlt::Animation *anim = mlt_prop.get_anim("key"); + qDebug() << "Found"<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); double value = mlt_prop.anim_get_double("key", frame); addKeyframe(GenTime(frame, pCore->getCurrentFps()), convertFromMltType(type), value, false, undo, redo); } delete anim; /* std::vector > separators({QStringLiteral("="), QStringLiteral("|="), QStringLiteral("~=")}); QStringList list = prop.split(';', QString::SkipEmptyParts); for (const auto& k : list) { bool found = false; KeyframeType type; QStringList values; for (const auto &sep : separators) { if (k.contains(sep.first)) { found = true; type = sep.second; values = k.split(sep.first); break; } } if (!found || values.size() != 2) { qDebug() << "ERROR while parsing value of keyframe"<getCurrentFps()); return getInterpolatedValue(pos); } double KeyframeModel::getInterpolatedValue(const GenTime &pos) const { int p = pos.frames(pCore->getCurrentFps()); if (m_keyframeList.count(pos) > 0) { return m_keyframeList.at(pos).second; } 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; prop.anim_set("keyframe", prev->second.second, prev->first.frames(pCore->getCurrentFps()), 0, convertToMltType(prev->second.first) ); prop.anim_set("keyframe", next->second.second, next->first.frames(pCore->getCurrentFps()), 0, convertToMltType(next->second.first) ); return prop.anim_get_double("keyframe", p); } void KeyframeModel::sendModification() { if (auto ptr = m_model.lock()) { Q_ASSERT(m_index.isValid()); QString name = ptr->data(m_index, AssetParameterModel::NameRole).toString(); auto type = ptr->data(m_index, AssetParameterModel::TypeRole).value(); QString data; if (type == ParamType::KeyframeParam) { data = getAnimProperty(); ptr->setParameter(name, data); } else { Q_ASSERT(false); //Not implemented, TODO } m_lastData = data; ptr->dataChanged(m_index, m_index); } } void KeyframeModel::refresh() { qDebug() << "REFRESHING KEYFRAME"; if (auto ptr = m_model.lock()) { Q_ASSERT(m_index.isValid()); auto type = ptr->data(m_index, AssetParameterModel::TypeRole).value(); QString data = ptr->data(m_index, AssetParameterModel::ValueRole).toString(); qDebug() << "FOUND DATA KEYFRAME" << data; if (data == m_lastData) { // nothing to do return; } // first, try to convert to double bool ok = false; double value = data.toDouble(&ok); if (ok) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; addKeyframe(GenTime(), KeyframeType::Linear, value, false, undo, redo); qDebug() << "KEYFRAME ADDED"<. * ***************************************************************************/ #include "assetparametermodel.hpp" #include "core.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "profiles/profilemodel.hpp" #include #include #include AssetParameterModel::AssetParameterModel(Mlt::Properties *asset, const QDomElement &assetXml, const QString &assetId, ObjectId ownerId, Kdenlive::MonitorId monitor, QObject *parent) : QAbstractListModel(parent) , monitorId(monitor) , m_xml(assetXml) , m_assetId(assetId) , m_ownerId(ownerId) , m_asset(asset) { Q_ASSERT(asset->is_valid()); QDomNodeList nodeList = m_xml.elementsByTagName(QStringLiteral("parameter")); bool needsLocaleConversion = false; QChar separator, oldSeparator; // Check locale if (m_xml.hasAttribute(QStringLiteral("LC_NUMERIC"))) { QLocale locale = QLocale(m_xml.attribute(QStringLiteral("LC_NUMERIC"))); if (locale.decimalPoint() != QLocale().decimalPoint()) { needsLocaleConversion = true; separator = QLocale().decimalPoint(); oldSeparator = locale.decimalPoint(); } } qDebug() << "XML parsing of " << assetId << ". found : " << nodeList.count(); + qDebug() << assetXml.text(); 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")); if (value.isNull()) { value = parseAttribute(QStringLiteral("default"), currentParameter).toString(); } bool isFixed = (type == QLatin1String("fixed")); if (isFixed) { m_fixedParams[name] = value; } qDebug() << "PARAMETER"<is_valid()); QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); bool conversionSuccess; double doubleValue = locale.toDouble(value, &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(), value.toUtf8().constData()); if (m_fixedParams.count(name) == 0) { m_params[name].value = value; } else { m_fixedParams[name] = value; } } pCore->refreshProjectItem(m_ownerId); pCore->invalidateItem(m_ownerId); } void AssetParameterModel::setParameter(const QString &name, double &value) { 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; } pCore->refreshProjectItem(m_ownerId); 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 MinRole: return parseAttribute(QStringLiteral("min"), element); case MaxRole: return parseAttribute(QStringLiteral("max"), element); case FactorRole: return parseAttribute(QStringLiteral("factor"), element, 1); case DecimalsRole: return parseAttribute(QStringLiteral("decimals"), element); case DefaultRole: return parseAttribute(QStringLiteral("default"), element); case SuffixRole: return element.attribute(QStringLiteral("suffix")); case OpacityRole: return element.attribute(QStringLiteral("opacity")) != QLatin1String("false"); case ValueRole: { QString value = m_asset->get(paramName.toUtf8().constData()); return value.isEmpty() ? (element.attribute(QStringLiteral("value")).isNull() ? parseAttribute(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(',')); } } 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::RestrictedAnim; } else if (type == QLatin1String("animated")) { return ParamType::Animated; } 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")) { return ParamType::KeyframeParam; } else if (type == QLatin1String("color")) { return ParamType::Color; } 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; } 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 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(); // 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)); if (type == ParamType::Double) { // Use a Mlt::Properties to parse mathematical operators Mlt::Properties p; p.set("eval", content.toLatin1().constData()); return p.get_double("eval"); } } else if (type == ParamType::Double) { QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); return locale.toDouble(content); } if (attribute == QLatin1String("default")) { if (type == ParamType::RestrictedAnim) { content = getDefaultKeyframes(0, content, true); } } return content; } 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; } void AssetParameterModel::setParameters(const QVector> ¶ms) { for (const auto ¶m : params) { setParameter(param.first, param.second.toString()); } } ObjectId AssetParameterModel::getOwnerId() const { return m_ownerId; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b9480c8fd..9f5bfdbe8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,30 +1,31 @@ ############################ # Tests ############################ SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -fexceptions") SET(Tests_SRCS tests/TestMain.cpp tests/compositiontest.cpp tests/effectstest.cpp tests/groupstest.cpp + tests/keyframetest.cpp tests/markertest.cpp tests/modeltest.cpp tests/regressions.cpp tests/snaptest.cpp tests/test_utils.cpp tests/treetest.cpp PARENT_SCOPE ) include_directories( ${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/src ${MLT_INCLUDE_DIR} ${MLTPP_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/lib/external ${CMAKE_CURRENT_SOURCE_DIR}/lib ../src ) diff --git a/tests/TestMain.cpp b/tests/TestMain.cpp index 01fc45d49..5b0128fa6 100644 --- a/tests/TestMain.cpp +++ b/tests/TestMain.cpp @@ -1,24 +1,25 @@ #define CATCH_CONFIG_RUNNER #include "catch.hpp" #include "core.h" #include #include #include /* This file is intended to remain empty. Write your tests in a file with a name corresponding to what you're testing */ int main(int argc, char *argv[]) { QApplication app(argc, argv); + app.setApplicationName(QStringLiteral("kdenlive")); std::unique_ptr repo(Mlt::Factory::init(nullptr)); Core::build(); int result = Catch::Session().run(argc, argv); // global clean-up... // delete repo; Mlt::Factory::close(); return (result < 0xff ? result : 0xff); } diff --git a/tests/keyframetest.cpp b/tests/keyframetest.cpp new file mode 100644 index 000000000..675c3e9e9 --- /dev/null +++ b/tests/keyframetest.cpp @@ -0,0 +1,164 @@ +#include "test_utils.hpp" + +using namespace fakeit; + +bool test_model_equality(std::shared_ptr m1, std::shared_ptr m2) +{ + // we cheat a bit by simply comparing the underlying map + qDebug() << "Equality test"<m_keyframeList.size()<m_keyframeList.size(); + return m1->m_keyframeList == m2->m_keyframeList; +} + +bool check_anim_identity(std::shared_ptr m) +{ + auto m2 = std::shared_ptr(new KeyframeModel(m->m_model, m->m_index, m->m_undoStack)); + m2->parseAnimProperty(m->getAnimProperty()); + return test_model_equality(m, m2); +} + +TEST_CASE("Keyframe model", "[KeyframeModel]") +{ + std::shared_ptr undoStack = std::make_shared(nullptr); + std::shared_ptr guideModel = std::make_shared(undoStack); + // Here we do some trickery to enable testing. + // We mock the project class so that the undoStack function returns our undoStack + + Mock pmMock; + When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); + + ProjectManager &mocked = pmMock.get(); + pCore->m_projectManager = &mocked; + + Mlt::Profile pr; + std::shared_ptr producer = std::make_shared(pr, "color", "red"); + auto effectstack = EffectStackModel::construct(producer, {ObjectType::TimelineClip, 0}, undoStack); + + effectstack->appendEffect(QStringLiteral("audiobalance")); + REQUIRE(effectstack->checkConsistency()); + REQUIRE(effectstack->rowCount() == 1); + auto effect = std::dynamic_pointer_cast(effectstack->getEffectStackRow(0)); + qDebug() << effect->getAssetId() << effect->getAllParameters(); + + REQUIRE(effect->rowCount() == 1); + QModelIndex index = effect->index(0, 0); + + auto model = std::shared_ptr(new KeyframeModel(effect, index, undoStack)); + + SECTION("Add/remove + undo") + { + auto state0 = [&]() { + REQUIRE(model->rowCount() == 1); + REQUIRE(check_anim_identity(model)); + }; + state0(); + + REQUIRE(model->addKeyframe(GenTime(1.1), KeyframeType::Linear, 42)); + auto state1 = [&]() { + REQUIRE(model->rowCount() == 2); + REQUIRE(check_anim_identity(model)); + REQUIRE(model->hasKeyframe(GenTime(1.1))); + bool ok; + auto k = model->getKeyframe(GenTime(1.1), &ok); + REQUIRE(ok); + auto k0 = model->getKeyframe(GenTime(0), &ok); + REQUIRE(ok); + auto k1 = model->getClosestKeyframe(GenTime(0.655555), &ok); + REQUIRE(ok); + REQUIRE(k == k1); + auto k2 = model->getNextKeyframe(GenTime(0.5), &ok); + REQUIRE(ok); + REQUIRE(k == k2); + auto k3 = model->getPrevKeyframe(GenTime(0.5), &ok); + REQUIRE(ok); + REQUIRE(k3 == k0); + auto k4 = model->getPrevKeyframe(GenTime(10), &ok); + REQUIRE(ok); + REQUIRE(k == k4); + model->getNextKeyframe(GenTime(10), &ok); + REQUIRE_FALSE(ok); + }; + state1(); + + undoStack->undo(); state0(); + undoStack->redo(); state1(); + + REQUIRE(model->addKeyframe(GenTime(12.6), KeyframeType::Discrete, 33)); + auto state2 = [&]() { + REQUIRE(model->rowCount() == 3); + REQUIRE(check_anim_identity(model)); + REQUIRE(model->hasKeyframe(GenTime(1.1))); + REQUIRE(model->hasKeyframe(GenTime(12.6))); + bool ok; + auto k = model->getKeyframe(GenTime(1.1), &ok); + REQUIRE(ok); + auto k0 = model->getKeyframe(GenTime(0), &ok); + REQUIRE(ok); + auto kk = model->getKeyframe(GenTime(12.6), &ok); + REQUIRE(ok); + auto k1 = model->getClosestKeyframe(GenTime(0.655555), &ok); + REQUIRE(ok); + REQUIRE(k == k1); + auto k2 = model->getNextKeyframe(GenTime(0.5), &ok); + REQUIRE(ok); + REQUIRE(k == k2); + auto k3 = model->getPrevKeyframe(GenTime(0.5), &ok); + REQUIRE(ok); + REQUIRE(k3 == k0); + auto k4 = model->getPrevKeyframe(GenTime(10), &ok); + REQUIRE(ok); + REQUIRE(k == k4); + auto k5 = model->getNextKeyframe(GenTime(10), &ok); + REQUIRE(ok); + REQUIRE(k5 == kk); + }; + state2(); + + undoStack->undo(); state1(); + undoStack->undo(); state0(); + undoStack->redo(); state1(); + undoStack->redo(); state2(); + + + REQUIRE(model->removeKeyframe(GenTime(1.1))); + auto state3 = [&]() { + REQUIRE(model->rowCount() == 2); + REQUIRE(check_anim_identity(model)); + REQUIRE(model->hasKeyframe(GenTime(12.6))); + bool ok; + auto k = model->getKeyframe(GenTime(1.1), &ok); + REQUIRE_FALSE(ok); + auto k0 = model->getKeyframe(GenTime(0), &ok); + REQUIRE(ok); + auto kk = model->getKeyframe(GenTime(12.6), &ok); + REQUIRE(ok); + auto k1 = model->getClosestKeyframe(GenTime(0.655555), &ok); + REQUIRE(ok); + REQUIRE(k == k0); + auto k2 = model->getNextKeyframe(GenTime(0.5), &ok); + REQUIRE(ok); + REQUIRE(kk == k2); + auto k3 = model->getPrevKeyframe(GenTime(0.5), &ok); + REQUIRE(ok); + REQUIRE(k3 == k0); + auto k4 = model->getPrevKeyframe(GenTime(10), &ok); + REQUIRE(ok); + REQUIRE(k0 == k4); + auto k5 = model->getNextKeyframe(GenTime(10), &ok); + REQUIRE(ok); + REQUIRE(k5 == kk); + }; + state3(); + + undoStack->undo(); state2(); + undoStack->undo(); state1(); + undoStack->undo(); state0(); + undoStack->redo(); state1(); + undoStack->redo(); state2(); + undoStack->redo(); state3(); + + REQUIRE(model->removeAllKeyframes()); + state0(); + undoStack->undo(); state3(); + undoStack->redo(); state0(); + } +} diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index d73c72c67..e628be05a 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -1,78 +1,83 @@ #pragma once #include "catch.hpp" #include "doc/docundostack.hpp" #include "bin/model/markerlistmodel.hpp" #include #include #include #include #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #pragma GCC diagnostic push #include "fakeit.hpp" #include #include #include #include #define private public #define protected public #include "timeline2/model/timelinemodel.hpp" #include "project/projectmanager.h" #include "core.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "timeline2/model/timelinefunctions.hpp" #include "bin/projectitemmodel.h" #include "bin/projectfolder.h" #include "bin/projectclip.h" #include "bin/clipcreator.hpp" +#include "assets/keyframes/model/keyframemodel.hpp" +#include "assets/model/assetparametermodel.hpp" +#include "effects/effectstack/model/effectstackmodel.hpp" +#include "effects/effectsrepository.hpp" +#include "effects/effectstack/model/effectitemmodel.hpp" using namespace fakeit; #define RESET() \ timMock.Reset(); \ Fake(Method(timMock, adjustAssetRange)); \ Spy(Method(timMock, _resetView)); \ Spy(Method(timMock, _beginInsertRows)); \ Spy(Method(timMock, _beginRemoveRows)); \ Spy(Method(timMock, _endInsertRows)); \ Spy(Method(timMock, _endRemoveRows)); \ Spy(OverloadedMethod(timMock, notifyChange, void(const QModelIndex&, const QModelIndex&, bool, bool, bool))); \ Spy(OverloadedMethod(timMock, notifyChange, void(const QModelIndex&, const QModelIndex&, QVector))); #define NO_OTHERS() \ VerifyNoOtherInvocations(Method(timMock, _beginRemoveRows)); \ VerifyNoOtherInvocations(Method(timMock, _beginInsertRows)); \ VerifyNoOtherInvocations(Method(timMock, _endRemoveRows)); \ VerifyNoOtherInvocations(Method(timMock, _endInsertRows)); \ VerifyNoOtherInvocations(OverloadedMethod(timMock, notifyChange, void(const QModelIndex&, const QModelIndex&, bool, bool, bool))); \ VerifyNoOtherInvocations(OverloadedMethod(timMock, notifyChange, void(const QModelIndex&, const QModelIndex&, QVector))); \ RESET(); #define CHECK_MOVE(times) \ Verify(Method(timMock, _beginRemoveRows) + \ Method(timMock, _endRemoveRows) + \ Method(timMock, _beginInsertRows) + \ Method(timMock, _endInsertRows) \ ).Exactly(times); \ NO_OTHERS(); #define CHECK_INSERT(times) \ Verify(Method(timMock, _beginInsertRows) + \ Method(timMock, _endInsertRows) \ ).Exactly(times); \ NO_OTHERS(); #define CHECK_REMOVE(times) \ Verify(Method(timMock, _beginRemoveRows) + \ Method(timMock, _endRemoveRows) \ ).Exactly(times); \ NO_OTHERS(); #define CHECK_RESIZE(times) \ Verify(OverloadedMethod(timMock, notifyChange, void(const QModelIndex&, const QModelIndex&, bool, bool, bool))).Exactly(times); \ NO_OTHERS(); QString createProducer(Mlt::Profile &prof, std::string color, std::shared_ptr binModel, int length = 20);