diff --git a/src/assets/keyframes/view/keyframeview.cpp b/src/assets/keyframes/view/keyframeview.cpp index 649ea3209..cf399135c 100644 --- a/src/assets/keyframes/view/keyframeview.cpp +++ b/src/assets/keyframes/view/keyframeview.cpp @@ -1,281 +1,283 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive (www.kdenlive.org). * * * * Kdenlive 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. * * * * Kdenlive 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 Kdenlive. If not, see . * ***************************************************************************/ #include "keyframeview.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "core.h" #include "kdenlivesettings.h" #include #include #include #include KeyframeView::KeyframeView(std::shared_ptr model, QWidget *parent) : QWidget(parent) , m_model(model) , m_duration(1) , m_position(0) , m_currentKeyframe(-1) , m_currentKeyframeOriginal(-1) , m_hoverKeyframe(-1) , m_scale(1) , m_currentType(KeyframeType::Linear) { setMouseTracking(true); setMinimumSize(QSize(150, 20)); setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum)); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); QPalette p = palette(); KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window, KSharedConfig::openConfig(KdenliveSettings::colortheme())); m_colSelected = palette().highlight().color(); m_colKeyframe = scheme.foreground(KColorScheme::NormalText).color(); m_size = QFontInfo(font()).pixelSize() * 1.8; m_lineHeight = m_size / 2; setMinimumHeight(m_size); setMaximumHeight(m_size); - connect(m_model.get(), &KeyframeModelList::modelChanged, [&](){ - emit atKeyframe(m_model->hasKeyframe(m_position)); - update(); - }); + connect(m_model.get(), &KeyframeModelList::modelChanged, this, &KeyframeView::slotModelChanged); } +void KeyframeView::slotModelChanged() +{ + emit atKeyframe(m_model->hasKeyframe(m_position)); + update(); +} void KeyframeView::slotSetPosition(int pos) { if (pos != m_position) { m_position = pos; emit atKeyframe(m_model->hasKeyframe(pos)); emit seekToPos(pos); update(); } } void KeyframeView::slotAddKeyframe(int pos) { if (pos < 0) { pos = m_position; } m_model->addKeyframe(GenTime(pos, pCore->getCurrentFps()), m_currentType); } void KeyframeView::slotAddRemove() { if (m_model->hasKeyframe(m_position)) { slotRemoveKeyframe(m_position); } else { slotAddKeyframe(m_position); } } void KeyframeView::slotRemoveKeyframe(int pos) { if (pos < 0) { pos = m_position; } m_model->removeKeyframe(GenTime(pos, pCore->getCurrentFps())); } void KeyframeView::setDuration(int dur) { m_duration = dur; } void KeyframeView::slotGoToNext() { if (m_position == m_duration) { return; } bool ok; auto next = m_model->getNextKeyframe(GenTime(m_position, pCore->getCurrentFps()), &ok); if (ok) { slotSetPosition(next.first.frames(pCore->getCurrentFps())); } else { // no keyframe after current position slotSetPosition(m_duration); } } void KeyframeView::slotGoToPrev() { if (m_position == 0) { return; } bool ok; auto prev = m_model->getPrevKeyframe(GenTime(m_position, pCore->getCurrentFps()), &ok); if (ok) { slotSetPosition(prev.first.frames(pCore->getCurrentFps())); } else { // no keyframe after current position slotSetPosition(m_duration); } } void KeyframeView::mousePressEvent(QMouseEvent *event) { int pos = event->x() / m_scale; if (event->y() < m_lineHeight && event->button() == Qt::LeftButton) { bool ok; GenTime position(pos, pCore->getCurrentFps()); auto keyframe = m_model->getClosestKeyframe(position, &ok); if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos) < 5) { m_currentKeyframeOriginal = keyframe.first.frames(pCore->getCurrentFps()); if (m_model->moveKeyframe(keyframe.first, position, false)) { m_currentKeyframe = pos; slotSetPosition(pos); return; } } } // no keyframe next to mouse m_currentKeyframe = m_currentKeyframeOriginal = -1; slotSetPosition(pos); update(); } void KeyframeView::mouseMoveEvent(QMouseEvent *event) { int pos = qBound(0, (int)(event->x() / m_scale), m_duration); GenTime position(pos, pCore->getCurrentFps()); if ((event->buttons() & Qt::LeftButton) != 0u) { if (m_currentKeyframe >= 0) { if (!m_model->hasKeyframe(pos)) { // snap to position cursor if (KdenliveSettings::snaptopoints() && qAbs(pos - m_position) < 5 && !m_model->hasKeyframe(m_position)) { pos = m_position; } GenTime currentPos(m_currentKeyframe, pCore->getCurrentFps()); if (m_model->moveKeyframe(currentPos, position, false)) { m_currentKeyframe = pos; } } } slotSetPosition(pos); return; } if (event->y() < m_lineHeight) { bool ok; auto keyframe = m_model->getClosestKeyframe(position, &ok); if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos) < 5) { m_hoverKeyframe = keyframe.first.frames(pCore->getCurrentFps()); setCursor(Qt::PointingHandCursor); update(); return; } } if (m_hoverKeyframe != -1) { m_hoverKeyframe = -1; setCursor(Qt::ArrowCursor); update(); } } void KeyframeView::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event) if (m_currentKeyframe >= 0) { GenTime initPos(m_currentKeyframeOriginal, pCore->getCurrentFps()); GenTime targetPos(m_currentKeyframe, pCore->getCurrentFps()); bool ok1 = m_model->moveKeyframe(targetPos, initPos, false); bool ok2 = m_model->moveKeyframe(initPos, targetPos, true); qDebug() << "RELEASING keyframe move"<getCurrentFps())<getCurrentFps()); } } void KeyframeView::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && event->y() < m_lineHeight) { int pos = qBound(0, (int)(event->x() / m_scale), m_duration); GenTime position(pos, pCore->getCurrentFps()); bool ok; auto keyframe = m_model->getClosestKeyframe(position, &ok); if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos) < 5) { m_model->removeKeyframe(keyframe.first); if (keyframe.first.frames(pCore->getCurrentFps()) == m_currentKeyframe) { m_currentKeyframe = m_currentKeyframeOriginal = -1; } if (keyframe.first.frames(pCore->getCurrentFps()) == m_position) { emit atKeyframe(false); } return; } // add new keyframe m_model->addKeyframe(position, m_currentType); } else { QWidget::mouseDoubleClickEvent(event); } } void KeyframeView::wheelEvent(QWheelEvent *event) { int change = event->delta() < 0 ? -1 : 1; int pos = qBound(0, m_position + change, m_duration); slotSetPosition(pos); } void KeyframeView::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QStylePainter p(this); m_scale = width() / (double)(m_duration); // p.translate(0, m_lineHeight); int headOffset = m_lineHeight / 1.5; /* * keyframes */ for (const auto &keyframe : *m_model.get()) { int pos = keyframe.first.frames(pCore->getCurrentFps()); if (pos == m_currentKeyframe || pos == m_hoverKeyframe) { p.setBrush(m_colSelected); } else { p.setBrush(m_colKeyframe); } int scaledPos = pos * m_scale; p.drawLine(scaledPos, headOffset, scaledPos, m_lineHeight + (headOffset / 2)); p.drawEllipse(scaledPos - headOffset / 2, 0, headOffset, headOffset); } p.setPen(palette().dark().color()); /* * Time-"line" */ p.setPen(m_colKeyframe); p.drawLine(0, m_lineHeight + (headOffset / 2), width(), m_lineHeight + (headOffset / 2)); /* * current position */ QPolygon pa(3); int cursorwidth = (m_size - (m_lineHeight + headOffset / 2)) / 2 + 1; QPolygonF position = QPolygonF() << QPointF(-cursorwidth, m_size) << QPointF(cursorwidth, m_size) << QPointF(0, m_lineHeight + (headOffset / 2) + 1); position.translate(m_position * m_scale, 0); p.setBrush(m_colKeyframe); p.drawPolygon(position); } diff --git a/src/assets/keyframes/view/keyframeview.hpp b/src/assets/keyframes/view/keyframeview.hpp index 9180149ca..1e87b8f3d 100644 --- a/src/assets/keyframes/view/keyframeview.hpp +++ b/src/assets/keyframes/view/keyframeview.hpp @@ -1,85 +1,86 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive (www.kdenlive.org). * * * * Kdenlive 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. * * * * Kdenlive 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 Kdenlive. If not, see . * ***************************************************************************/ #ifndef KEYFRAMEVIEW_H #define KEYFRAMEVIEW_H #include "assets/keyframes/model/keyframemodel.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include #include class KeyframeModelList; class KeyframeView : public QWidget { Q_OBJECT public: explicit KeyframeView(std::shared_ptr model, QWidget *parent = nullptr); void setDuration(int dur); public slots: /* @brief moves the current position*/ void slotSetPosition(int pos); /* @brief remove the keyframe at given position If pos is negative, we remove keyframe at current position */ void slotRemoveKeyframe(int pos); /* @brief Add a keyframe with given parameter value at given pos. If pos is negative, then keyframe is added at current position */ void slotAddKeyframe(int pos = -1); /* @brief If there is a keyframe at current position, it is removed. Otherwise, we add a new one with given value. */ void slotAddRemove(); void slotGoToNext(); void slotGoToPrev(); + void slotModelChanged(); protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; private: std::shared_ptr m_model; int m_duration; int m_position; int m_currentKeyframe; int m_currentKeyframeOriginal; int m_hoverKeyframe; int m_lineHeight; double m_scale; int m_size; KeyframeType m_currentType; QColor m_colSelected; QColor m_colKeyframe; QColor m_colKeyframeBg; signals: void seekToPos(int pos); void atKeyframe(bool); }; #endif diff --git a/src/assets/model/assetparametermodel.cpp b/src/assets/model/assetparametermodel.cpp index 23223fa80..e2dc4bb7f 100644 --- a/src/assets/model/assetparametermodel.cpp +++ b/src/assets/model/assetparametermodel.cpp @@ -1,357 +1,385 @@ /*************************************************************************** * 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 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(); 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; } + +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; +} + diff --git a/src/assets/model/assetparametermodel.hpp b/src/assets/model/assetparametermodel.hpp index 2dea29954..9e0e9ed24 100644 --- a/src/assets/model/assetparametermodel.hpp +++ b/src/assets/model/assetparametermodel.hpp @@ -1,150 +1,167 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef ASSETPARAMETERMODEL_H #define ASSETPARAMETERMODEL_H #include "definitions.h" #include "klocalizedstring.h" #include #include #include #include #include #include +class KeyframeModelList; /* @brief This class is the model for a list of parameters. The behaviour of a transition or an effect is typically controlled by several parameters. This class exposes this parameters as a list that can be rendered using the relevant widgets. Note that internally parameters are not sorted in any ways, because some effects like sox need a precise order */ enum class ParamType { Double, List, Bool, Switch, RestrictedAnim, // animated 1 dimensional param with linear support only Animated, AnimatedRect, Geometry, Addedgeometry, KeyframeParam, Color, Position, Curve, Bezier_spline, Roto_spline, Wipe, Url, Keywords, Fontfamily, Filterjob, Readonly }; Q_DECLARE_METATYPE(ParamType) class AssetParameterModel : public QAbstractListModel, public enable_shared_from_this_virtual { Q_OBJECT public: explicit AssetParameterModel(Mlt::Properties *asset, const QDomElement &assetXml, const QString &assetId, ObjectId ownerId, Kdenlive::MonitorId monitor = Kdenlive::ProjectMonitor, QObject *parent = nullptr); virtual ~AssetParameterModel(); enum { NameRole = Qt::UserRole + 1, TypeRole, CommentRole, MinRole, MaxRole, DefaultRole, SuffixRole, DecimalsRole, ValueRole, ListValuesRole, ListNamesRole, FactorRole, OpacityRole, InRole, OutRole, ParentInRole, ParentDurationRole }; /* @brief Returns the id of the asset represented by this object */ QString getAssetId() const; /* @brief Set the parameter with given name to the given value */ Q_INVOKABLE void setParameter(const QString &name, const QString &value); Q_INVOKABLE void setParameter(const QString &name, double &value); /* @brief Return all the parameters as pairs (parameter name, parameter value) */ QVector> getAllParameters() const; /* @brief Sets the value of a list of parameters @param params contains the pairs (parameter name, parameter value) */ void setParameters(const QVector> ¶ms); /* Which monitor is attached to this asset (clip/project) */ Kdenlive::MonitorId monitorId; QVariant data(const QModelIndex &index, int role) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; /* @brief Returns the id of the actual object associated with this asset */ ObjectId getOwnerId() const; + /* @brief Returns the keyframe model associated with this asset + Return empty ptr if there is no keyframable parameter in the asset or if prepareKeyframes was not called + */ + std::shared_ptr getKeyframeModel(); + + /* @brief Must be called before using the keyframes of this model */ + void prepareKeyframes(); + protected: /* @brief Helper function to retrieve the type of a parameter given the string corresponding to it*/ static ParamType paramTypeFromStr(const QString &type); static QString getDefaultKeyframes(int start, const QString &defaultValue, bool linearOnly); /* @brief Helper function to get an attribute from a dom element, given its name. The function additionally parses following keywords: - %width and %height that are replaced with profile's height and width. If keywords are found, mathematical operations are supported for double type params. For example "%width -1" is a valid value. */ static QVariant parseAttribute(const QString &attribute, const QDomElement &element, QVariant defaultValue = QVariant()); + + /* @brief Helper function to register one more parameter that is keyframable. + @param index is the index corresponding to this parameter + */ + void addKeyframeParam(const QModelIndex index); + struct ParamRow { ParamType type; QDomElement xml; QVariant value; QString name; }; QDomElement m_xml; QString m_assetId; ObjectId m_ownerId; std::unordered_map m_params; // Store all parameters by name std::unordered_map m_fixedParams; // We store values of fixed parameters aside QVector m_rows; // We store the params name in order of parsing. The order is important (cf some effects like sox) std::unique_ptr m_asset; + + std::shared_ptr m_keyframes; }; #endif diff --git a/src/assets/view/assetparameterview.cpp b/src/assets/view/assetparameterview.cpp index efb316a3d..0080d15e5 100644 --- a/src/assets/view/assetparameterview.cpp +++ b/src/assets/view/assetparameterview.cpp @@ -1,158 +1,159 @@ /*************************************************************************** * 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 "core.h" #include "widgets/animationwidget.h" #include #include #include #include #include AssetParameterView::AssetParameterView(QWidget *parent) : QWidget(parent) { m_lay = new QVBoxLayout(this); m_lay->setContentsMargins(2, 2, 2, 2); m_lay->setSpacing(2); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); } void AssetParameterView::setModel(const std::shared_ptr &model, QPair range, bool addSpacer) { qDebug() << "set model " << model.get(); unsetModel(); QMutexLocker lock(&m_lock); m_model = model; + m_model->prepareKeyframes(); connect(m_model.get(), &AssetParameterModel::dataChanged, this, &AssetParameterView::refresh); AnimationWidget *animWidget = nullptr; for (int i = 0; i < model->rowCount(); ++i) { QModelIndex index = model->index(i, 0); auto type = model->data(index, AssetParameterModel::TypeRole).value(); if (animWidget && (type == ParamType::Geometry || type == ParamType::Animated || type == ParamType::RestrictedAnim)) { // Animation widget can have some extra params that should'nt build a new widget // TODO refac // animWidget->addParameter(index); } else { auto w = AbstractParamWidget::construct(model, index, range, this); if (type == ParamType::Geometry || type == ParamType::Animated || type == ParamType::RestrictedAnim || type == ParamType::AnimatedRect) { animWidget = static_cast(w); } connect(w, &AbstractParamWidget::valueChanged, this, &AssetParameterView::commitChanges); connect(w, &AbstractParamWidget::seekToPos, this, &AssetParameterView::seekToPos); m_lay->addWidget(w); m_widgets.push_back(w); } } if (addSpacer) { m_lay->addStretch(); } } void AssetParameterView::resetValues() { QMutexLocker lock(&m_lock); 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(); QString defaultValue = m_model->data(index, AssetParameterModel::DefaultRole).toString(); m_model->setParameter(name, defaultValue); refresh(index, index, QVector()); } } void AssetParameterView::setRange(QPair range) { qDebug() << "SETTING RANGE"<slotSetRange(range); } } 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 AssetCommand *command = new AssetCommand(m_model, index, value); 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); } // 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 Q_ASSERT(bottomRight.row() < (int)m_widgets.size()); for (auto i = (size_t)topLeft.row(); i <= (size_t)bottomRight.row(); ++i) { m_widgets[i]->slotRefresh(); } } int AssetParameterView::contentHeight() const { return m_lay->sizeHint().height(); } diff --git a/src/assets/view/widgets/keyframewidget.cpp b/src/assets/view/widgets/keyframewidget.cpp index e417ce021..4d4155572 100644 --- a/src/assets/view/widgets/keyframewidget.cpp +++ b/src/assets/view/widgets/keyframewidget.cpp @@ -1,155 +1,155 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive (www.kdenlive.org). * * * * Kdenlive 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. * * * * Kdenlive 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 Kdenlive. If not, see . * ***************************************************************************/ #include "keyframewidget.hpp" #include "assets/keyframes/view/keyframeview.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "assets/model/assetparametermodel.hpp" #include "core.h" #include "monitor/monitor.h" #include "timecode.h" #include "timecodedisplay.h" #include "utils/KoIconUtils.h" #include #include #include KeyframeWidget::KeyframeWidget(std::shared_ptr model, QModelIndex index, QWidget *parent) : AbstractParamWidget(model, index, parent) { - m_keyframes = std::shared_ptr(new KeyframeModelList(model, index, pCore->undoStack())); + m_keyframes = model->getKeyframeModel(); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); auto *l = new QGridLayout(this); bool ok = false; int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok); Q_ASSERT(ok); m_keyframeview = new KeyframeView(m_keyframes, this); m_keyframeview->setDuration(duration); m_buttonAddDelete = new QToolButton(this); m_buttonAddDelete->setAutoRaise(true); m_buttonAddDelete->setIcon(KoIconUtils::themedIcon(QStringLiteral("list-add"))); m_buttonAddDelete->setToolTip(i18n("Add keyframe")); m_buttonPrevious = new QToolButton(this); m_buttonPrevious->setAutoRaise(true); m_buttonPrevious->setIcon(KoIconUtils::themedIcon(QStringLiteral("media-skip-backward"))); m_buttonPrevious->setToolTip(i18n("Go to previous keyframe")); m_buttonNext = new QToolButton(this); m_buttonNext->setAutoRaise(true); m_buttonNext->setIcon(KoIconUtils::themedIcon(QStringLiteral("media-skip-forward"))); m_buttonNext->setToolTip(i18n("Go to next keyframe")); m_time = new TimecodeDisplay(pCore->getMonitor(m_model->monitorId)->timecode(), this); m_time->setRange(0, duration); l->addWidget(m_keyframeview, 0, 0, 1, -1); l->addWidget(m_buttonPrevious, 1, 0); l->addWidget(m_buttonAddDelete, 1, 1); l->addWidget(m_buttonNext, 1, 2); l->addWidget(m_time, 1, 3, Qt::AlignRight); connect(m_time, &TimecodeDisplay::timeCodeEditingFinished, [&](){slotSetPosition(-1, true);}); connect(m_keyframeview, &KeyframeView::seekToPos, [&](int p){slotSetPosition(p, true);}); connect(m_keyframeview, &KeyframeView::atKeyframe, this, &KeyframeWidget::slotAtKeyframe); connect(m_buttonAddDelete, &QAbstractButton::pressed, m_keyframeview, &KeyframeView::slotAddRemove); connect(m_buttonPrevious, &QAbstractButton::pressed, m_keyframeview, &KeyframeView::slotGoToPrev); connect(m_buttonNext, &QAbstractButton::pressed, m_keyframeview, &KeyframeView::slotGoToNext); } KeyframeWidget::~KeyframeWidget() { delete m_keyframeview; delete m_buttonAddDelete; delete m_buttonPrevious; delete m_buttonNext; delete m_time; } void KeyframeWidget::slotSetPosition(int pos, bool update) { if (pos < 0) { pos = m_time->getValue(); m_keyframeview->slotSetPosition(pos); } else { m_time->setValue(pos); m_keyframeview->slotSetPosition(pos); } if (update) { emit seekToPos(pos); } } int KeyframeWidget::getPosition() const { return m_time->getValue(); } void KeyframeWidget::addKeyframe(int pos) { blockSignals(true); m_keyframeview->slotAddKeyframe(pos); blockSignals(false); setEnabled(true); } void KeyframeWidget::updateTimecodeFormat() { m_time->slotUpdateTimeCodeFormat(); } void KeyframeWidget::slotAtKeyframe(bool atKeyframe) { if (atKeyframe) { m_buttonAddDelete->setIcon(KoIconUtils::themedIcon(QStringLiteral("list-remove"))); m_buttonAddDelete->setToolTip(i18n("Delete keyframe")); } else { m_buttonAddDelete->setIcon(KoIconUtils::themedIcon(QStringLiteral("list-add"))); m_buttonAddDelete->setToolTip(i18n("Add keyframe")); } } void KeyframeWidget::slotSetRange(QPair /*range*/) { bool ok = false; int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok); Q_ASSERT(ok); m_keyframeview->setDuration(duration); m_time->setRange(0, duration); } void KeyframeWidget::slotRefresh() { // update duration slotSetRange(QPair(-1, -1)); // refresh keyframes m_keyframes->refresh(); } diff --git a/src/effects/effectstack/view/collapsibleeffectview.cpp b/src/effects/effectstack/view/collapsibleeffectview.cpp index 02f37907a..5f0d5987f 100644 --- a/src/effects/effectstack/view/collapsibleeffectview.cpp +++ b/src/effects/effectstack/view/collapsibleeffectview.cpp @@ -1,807 +1,808 @@ /*************************************************************************** * 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 "core.h" #include "dialogs/clipcreationdialog.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectitemmodel.hpp" #include "effectslist/effectslist.h" #include "kdenlivesettings.h" #include "mltcontroller/effectscontroller.h" #include "utils/KoIconUtils.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include CollapsibleEffectView::CollapsibleEffectView(std::shared_ptr effectModel, QPair range, QImage icon, QWidget *parent) : AbstractCollapsibleWidget(parent) /* , m_effect(effect) , m_itemInfo(info) , m_original_effect(original_effect) , m_isMovable(true)*/ , m_model(effectModel) , m_view(nullptr) , m_regionEffect(false) { QString effectId = effectModel->getAssetId(); QString effectName = EffectsRepository::get()->getName(effectId); if (effectId == QLatin1String("region")) { m_regionEffect = true; decoframe->setObjectName(QStringLiteral("decoframegroup")); } filterWheelEvent = true; // decoframe->setProperty("active", true); // m_info.fromString(effect.attribute(QStringLiteral("kdenlive_info"))); // setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); buttonUp->setIcon(KoIconUtils::themedIcon(QStringLiteral("kdenlive-up"))); QSize iconSize = buttonUp->iconSize(); buttonUp->setMaximumSize(iconSize); buttonDown->setMaximumSize(iconSize); menuButton->setMaximumSize(iconSize); enabledButton->setMaximumSize(iconSize); buttonDel->setMaximumSize(iconSize); buttonUp->setToolTip(i18n("Move effect up")); buttonDown->setIcon(KoIconUtils::themedIcon(QStringLiteral("kdenlive-down"))); buttonDown->setToolTip(i18n("Move effect down")); buttonDel->setIcon(KoIconUtils::themedIcon(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(KoIconUtils::themedIcon("help-about")); // buttonShowComments->setToolTip(i18n("Show additional information for the parameters")); m_collapse = new KDualAction(i18n("Collapse Effect"), i18n("Expand Effect"), this); m_collapse->setActiveIcon(KoIconUtils::themedIcon(QStringLiteral("arrow-right"))); m_collapse->setInactiveIcon(KoIconUtils::themedIcon(QStringLiteral("arrow-down"))); collapseButton->setDefaultAction(m_collapse); connect(m_collapse, &KDualAction::activeChanged, this, &CollapsibleEffectView::slotSwitch); QHBoxLayout *l = static_cast(frame->layout()); m_colorIcon = new QLabel(this); l->insertWidget(0, m_colorIcon); m_colorIcon->setFixedSize(icon.size()); title = new QLabel(this); l->insertWidget(2, title); m_enabledButton = new KDualAction(i18n("Disable Effect"), i18n("Enable Effect"), this); m_enabledButton->setActiveIcon(KoIconUtils::themedIcon(QStringLiteral("hint"))); m_enabledButton->setInactiveIcon(KoIconUtils::themedIcon(QStringLiteral("visibility"))); enabledButton->setDefaultAction(m_enabledButton); m_groupAction = new QAction(KoIconUtils::themedIcon(QStringLiteral("folder-new")), i18n("Create Group"), this); connect(m_groupAction, &QAction::triggered, this, &CollapsibleEffectView::slotCreateGroup); if (m_regionEffect) { effectName.append(':' + QUrl(EffectsList::parameter(m_effect, QStringLiteral("resource"))).fileName()); } // Color thumb m_colorIcon->setPixmap(QPixmap::fromImage(icon)); title->setText(effectName); m_view = new AssetParameterView(this); m_view->setModel(std::static_pointer_cast(effectModel), range); connect(m_view, &AssetParameterView::seekToPos, this, &AbstractCollapsibleWidget::seekToPos); QVBoxLayout *lay = new QVBoxLayout(widgetFrame); lay->setContentsMargins(0, 0, 0, 0); lay->setSpacing(0); lay->addWidget(m_view); m_menu = new QMenu(this); if (effectModel->rowCount() > 0) { m_menu->addAction(KoIconUtils::themedIcon(QStringLiteral("view-refresh")), i18n("Reset Effect"), this, SLOT(slotResetEffect())); } else { collapseButton->setEnabled(false); m_view->setVisible(false); } m_menu->addAction(KoIconUtils::themedIcon(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(KoIconUtils::themedIcon(QStringLiteral("folder-new")), i18n("Create Region"), this, SLOT(slotCreateRegion())); } // setupWidget(info, metaInfo); menuButton->setIcon(KoIconUtils::themedIcon(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, SIGNAL(activeChangedByUser(bool)), this, SLOT(slotDisable(bool))); connect(buttonUp, &QAbstractButton::clicked, this, &CollapsibleEffectView::slotEffectUp); connect(buttonDown, &QAbstractButton::clicked, this, &CollapsibleEffectView::slotEffectDown); connect(buttonDel, &QAbstractButton::clicked, this, &CollapsibleEffectView::slotDeleteEffect); Q_FOREACH (QSpinBox *sp, findChildren()) { sp->installEventFilter(this); sp->setFocusPolicy(Qt::StrongFocus); } Q_FOREACH (KComboBox *cb, findChildren()) { cb->installEventFilter(this); cb->setFocusPolicy(Qt::StrongFocus); } Q_FOREACH (QProgressBar *cb, findChildren()) { cb->installEventFilter(this); cb->setFocusPolicy(Qt::StrongFocus); } } CollapsibleEffectView::~CollapsibleEffectView() { - // delete m_paramWidget; + qDebug() << "deleting collapsibleeffectview"; + delete m_view; delete m_menu; } 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) { QWheelEvent *we = static_cast(e); if (!filterWheelEvent || we->modifiers() != Qt::NoModifier) { 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)->focusPolicy() == Qt::WheelFocus) { e->accept(); return false; } e->ignore(); return true; } if (qobject_cast(o)) { if (qobject_cast(o)->focusPolicy() == Qt::WheelFocus) { e->accept(); return false; } e->ignore(); return true; } } 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) { decoframe->setProperty("active", ix.row() == m_model->row()); decoframe->setStyleSheet(decoframe->styleSheet()); } void CollapsibleEffectView::setActive(bool activate) { /* decoframe->setProperty("active", activate); decoframe->setStyleSheet(decoframe->styleSheet()); if (m_paramWidget) { m_paramWidget->connectMonitor(activate); } if (activate) { m_colorIcon->setPixmap(m_iconPix); } else { // desaturate icon QPixmap alpha = m_iconPix; QPainter p(&alpha); p.setCompositionMode(QPainter::CompositionMode_DestinationIn); p.fillRect(alpha.rect(), QColor(80, 80, 80, 80)); p.end(); m_colorIcon->setPixmap(alpha); } */ } void CollapsibleEffectView::mousePressEvent(QMouseEvent *e) { m_dragStart = e->globalPos(); 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() + 1, 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; QDomElement effect = m_effect.cloneNode().toElement(); doc.appendChild(doc.importNode(effect, true)); effect = doc.firstChild().toElement(); effect.removeAttribute(QStringLiteral("kdenlive_ix")); effect.setAttribute(QStringLiteral("id"), name); effect.setAttribute(QStringLiteral("type"), QStringLiteral("custom")); /* 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 reloadEffects(); } void CollapsibleEffectView::slotResetEffect() { m_view->resetValues(); } void CollapsibleEffectView::slotSwitch(bool expand) { slotShow(expand); emit switchHeight(m_model, expand ? frame->height() + 4 : frame->height() + m_view->contentHeight() + 4); setFixedHeight(expand ? frame->height() + 4 : frame->height() + m_view->contentHeight() + 4); widgetFrame->setVisible(!expand); /*if (!expand) { widgetFrame->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); widgetFrame->setFixedHeight(m_view->contentHeight()); } else { widgetFrame->setFixedHeight(QWIDGETSIZE_MAX); }*/ /*const QRect final_geometry = expand ? QRect(0, 0, width(), title->height()) : QRect(rect().topLeft(), size()); QPropertyAnimation *anim = new QPropertyAnimation(this, "geometry", this); anim->setDuration(200); anim->setEasingCurve(QEasingCurve::InOutQuad); anim->setEndValue(final_geometry); //connect(anim, SIGNAL(valueChanged(const QVariant &)), SLOT(animationChanged(const QVariant &))); connect(anim, SIGNAL(finished()), SLOT(animationFinished())); anim->start(QPropertyAnimation::DeleteWhenStopped);*/ } void CollapsibleEffectView::animationChanged(const QVariant &geom) { parentWidget()->setFixedHeight(geom.toRect().height()); } void CollapsibleEffectView::animationFinished() { if (m_collapse->isActive()) { widgetFrame->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored); } else { widgetFrame->setFixedHeight(m_view->contentHeight()); } } void CollapsibleEffectView::slotShow(bool show) { if (show) { // collapseButton->setArrowType(Qt::DownArrow); m_info.isCollapsed = false; } else { // collapseButton->setArrowType(Qt::RightArrow); m_info.isCollapsed = true; } updateCollapsedState(); } void CollapsibleEffectView::groupStateChanged(bool collapsed) { m_info.groupIsCollapsed = collapsed; updateCollapsedState(); } void CollapsibleEffectView::updateCollapsedState() { QString info = m_info.toString(); if (info != m_effect.attribute(QStringLiteral("kdenlive_info"))) { m_effect.setAttribute(QStringLiteral("kdenlive_info"), info); emit parameterChanged(m_original_effect, m_effect, effectIndex()); } } void CollapsibleEffectView::setGroupIndex(int 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) { m_info.groupName = groupName; m_effect.setAttribute(QStringLiteral("kdenlive_info"), m_info.toString()); } QString CollapsibleEffectView::infoString() const { return 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 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) { /* if (m_paramWidget) { m_paramWidget->setActiveKeyframe(kf); } */ } void CollapsibleEffectView::setupWidget(const ItemInfo &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) { /* 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; } EffectInfo info; info.fromString(subeffects.at(0).toElement().attribute(QStringLiteral("kdenlive_info"))); event->setDropAction(Qt::MoveAction); event->accept(); 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); } 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_paramWidget != nullptr) && !m_enabledButton->isActive()) { return m_paramWidget->needsMonitorEffectScene(); } */ return MonitorSceneDefault; } void CollapsibleEffectView::setRange(QPair range) { if (m_view) { m_view->setRange(range); } } void CollapsibleEffectView::setKeyframes(const QString &tag, const QString &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()); } diff --git a/tests/keyframetest.cpp b/tests/keyframetest.cpp index 657c886d7..f1691ba4a 100644 --- a/tests/keyframetest.cpp +++ b/tests/keyframetest.cpp @@ -1,218 +1,219 @@ #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)); + effect->prepareKeyframes(); 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(); } SECTION("Move keyframes + undo") { auto state0 = [&]() { REQUIRE(model->rowCount() == 1); REQUIRE(check_anim_identity(model)); }; state0(); REQUIRE(model->addKeyframe(GenTime(1.1), KeyframeType::Linear, 42)); auto state1 = [&](double pos) { REQUIRE(model->rowCount() == 2); REQUIRE(check_anim_identity(model)); REQUIRE(model->hasKeyframe(GenTime(pos))); bool ok; auto k = model->getKeyframe(GenTime(pos), &ok); REQUIRE(ok); auto k0 = model->getKeyframe(GenTime(0), &ok); REQUIRE(ok); auto k1 = model->getClosestKeyframe(GenTime(pos + 10), &ok); REQUIRE(ok); REQUIRE(k == k1); auto k2 = model->getNextKeyframe(GenTime(pos - 0.3), &ok); REQUIRE(ok); REQUIRE(k == k2); auto k3 = model->getPrevKeyframe(GenTime(pos - 0.3), &ok); REQUIRE(ok); REQUIRE(k3 == k0); auto k4 = model->getPrevKeyframe(GenTime(pos + 0.3), &ok); REQUIRE(ok); REQUIRE(k == k4); model->getNextKeyframe(GenTime(pos + 0.3), &ok); REQUIRE_FALSE(ok); }; state1(1.1); REQUIRE(model->moveKeyframe(GenTime(1.1), GenTime(2.6), true)); state1(2.6); undoStack->undo(); state1(1.1); undoStack->redo(); state1(2.6); REQUIRE(model->moveKeyframe(GenTime(2.6), GenTime(6.1), true)); state1(6.1); undoStack->undo(); state1(2.6); undoStack->undo(); state1(1.1); undoStack->redo(); state1(2.6); undoStack->redo(); state1(6.1); REQUIRE(model->addKeyframe(GenTime(12.6), KeyframeType::Discrete, 33)); REQUIRE_FALSE(model->moveKeyframe(GenTime(6.1), GenTime(12.6), true)); undoStack->undo(); state1(6.1); } }