Commit b097aa30 authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Revamp the keyframes copy/paste. We now have copy/paste icons in the effect...

Revamp the keyframes copy/paste. We now have copy/paste icons in the effect stack toolbar that work as expected. Standard shortcuts also now work (Ctrl+C/V)
Fixes #1566
parent b7fcf807
......@@ -539,3 +539,12 @@ void AssetPanel::updateAssetPosition(int itemId)
}
}
}
void AssetPanel::sendStandardCommand(int command)
{
if (m_effectStackWidget->isVisible()) {
m_effectStackWidget->sendStandardCommand(command);
} else if (m_transitionWidget->isVisible()) {
m_transitionWidget->sendStandardCommand(command);
}
}
......@@ -54,6 +54,8 @@ public:
ObjectId effectStackOwner();
/** @brief Add an effect to the current stack owner */
bool addEffect(const QString &effectId);
/** @brief Used to pass a standard action like copy or paste to the effect stack widget */
void sendStandardCommand(int command);
public slots:
/** @brief Clear panel if displaying itemId */
......
......@@ -622,3 +622,14 @@ QModelIndex KeyframeModelList::getIndexAtRow(int row)
}
return QModelIndex();
}
std::vector<QPersistentModelIndex> KeyframeModelList::getIndexes()
{
std::vector<QPersistentModelIndex> keys;
keys.reserve(m_parameters.size());
for (auto kv : m_parameters) {
keys.push_back(kv.first);
}
return keys;
}
......@@ -159,6 +159,8 @@ public:
/** @brief Check that all keyframable parameters have the same keyframes on loading
* (that's how our model works) */
void checkConsistency();
/** @brief Returns the indexes of all parameters */
std::vector<QPersistentModelIndex> getIndexes();
protected:
/** @brief Helper function to apply a given operation on all parameters */
......
......@@ -111,6 +111,11 @@ void KeyframeView::initKeyframePos()
emit atKeyframe(m_model->hasKeyframe(m_position), m_model->singleKeyframe());
}
const QVector<int> KeyframeView::selectedKeyframesIndexes()
{
return m_model->selectedKeyframes();
}
void KeyframeView::slotDuplicateKeyframe()
{
int offset = pCore->getItemIn(m_model->getOwnerId());
......
......@@ -24,6 +24,8 @@ public:
const QString getAssetId();
/** @brief Copy a keyframe parameter to selected keyframes. */
void copyCurrentValue(const QModelIndex &ix, const QString &paramName);
/** @brief Returns the index (position in model) of the currently selected keyframes. */
const QVector<int> selectedKeyframesIndexes();
public slots:
/** @brief moves the current position*/
......
......@@ -904,7 +904,7 @@ QVector<QPair<QString, QVariant>> AssetParameterModel::getAllParameters() const
return res;
}
QJsonDocument AssetParameterModel::toJson(bool includeFixed) const
QJsonDocument AssetParameterModel::toJson(QVector<int> selection, bool includeFixed) const
{
QJsonArray list;
if (includeFixed) {
......@@ -965,15 +965,33 @@ QJsonDocument AssetParameterModel::toJson(bool includeFixed) const
currentParam.insert(QLatin1String("name"), QJsonValue(param.first));
currentParam.insert(QLatin1String("DisplayName"), QJsonValue(param.second.name));
currentParam.insert(QLatin1String("value"),
if (
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
param.second.value.type() == QVariant::Double
param.second.value.type() == QVariant::Double
#else
param.second.value.typeId() == QMetaType::Double
param.second.value.typeId() == QMetaType::Double
#endif
? QJsonValue(param.second.value.toDouble())
: QJsonValue(param.second.value.toString()));
) {
currentParam.insert(QLatin1String("value"), QJsonValue(param.second.value.toDouble()));
} else {
QString resultValue = param.second.value.toString();
if (selection.isEmpty()) {
currentParam.insert(QLatin1String("value"), QJsonValue(resultValue));
} else {
// Filter out unwanted keyframes
QStringList values = resultValue.split(QLatin1Char(';'));
QStringList remainingValues;
int ix = 0;
for (auto &val : values) {
if (selection.contains(ix)) {
remainingValues << val;
}
ix++;
}
currentParam.insert(QLatin1String("value"), QJsonValue(remainingValues.join(QLatin1Char(';'))));
}
}
int type = data(ix, AssetParameterModel::TypeRole).toInt();
double min = data(ix, AssetParameterModel::MinRole).toDouble();
double max = data(ix, AssetParameterModel::MaxRole).toDouble();
......@@ -999,6 +1017,9 @@ QJsonDocument AssetParameterModel::toJson(bool includeFixed) const
int size = x.split(";").length();
QString value;
for (int i = 0; i < size; i++) {
if (!selection.isEmpty() && !selection.contains(i)) {
continue;
}
QSize frameSize = pCore->getCurrentFrameSize();
QString pos = x.split(";").at(i).split("=").at(0);
double xval = x.split(";").at(i).split("=").at(1).toDouble();
......@@ -1358,7 +1379,7 @@ bool AssetParameterModel::hasMoreThanOneKeyframe() const
return false;
}
int AssetParameterModel::time_to_frames(const QString &time)
int AssetParameterModel::time_to_frames(const QString &time) const
{
return m_asset->time_to_frames(time.toUtf8().constData());
}
......
......@@ -149,8 +149,11 @@ public:
QVector<QPair<QString, QVariant>> getAllParameters() const;
/** @brief Get a parameter value from its name */
const QVariant getParamFromName(const QString &paramName);
/** @brief Returns a json definition of the effect with all param values */
QJsonDocument toJson(bool includeFixed = true) const;
/** @brief Returns a json definition of the effect with all param values
* @param selection only export keyframes matching the selected index, or all keyframes if empty
* @param includeFixed if true, also export the fixed (non user editable) parameters for this effect
*/
QJsonDocument toJson(QVector<int> selection = {}, bool includeFixed = true) const;
/** @brief Returns the interpolated value at the given position with all param values as json*/
QJsonDocument valueAsJson(int pos, bool includeFixed = true) const;
......@@ -179,7 +182,7 @@ public:
void resetAsset(std::unique_ptr<Mlt::Properties> asset);
/** @brief Returns true if the effect has more than one keyframe */
bool hasMoreThanOneKeyframe() const;
int time_to_frames(const QString &time);
int time_to_frames(const QString &time) const;
void passProperties(Mlt::Properties &target);
/** @brief Returns a list of the parameter names that are keyframable */
QStringList getKeyframableParameters() const;
......
......@@ -98,6 +98,7 @@ void AssetParameterView::setModel(const std::shared_ptr<AssetParameterModel> &mo
connect(this, &AssetParameterView::nextKeyframe, m_mainKeyframeWidget, &KeyframeWidget::goToNext);
connect(this, &AssetParameterView::previousKeyframe, m_mainKeyframeWidget, &KeyframeWidget::goToPrevious);
connect(this, &AssetParameterView::addRemoveKeyframe, m_mainKeyframeWidget, &KeyframeWidget::addRemove);
connect(this, &AssetParameterView::sendStandardCommand, m_mainKeyframeWidget, &KeyframeWidget::sendStandardCommand);
} else {
m_lay->addWidget(w);
minHeight += w->minimumHeight();
......
......@@ -99,4 +99,6 @@ signals:
void nextKeyframe();
void previousKeyframe();
void addRemoveKeyframe();
/** @brief Used to pass a standard action like copy or paste to the effect stack widget */
void sendStandardCommand(int command);
};
......@@ -28,11 +28,15 @@
#include <KDualAction>
#include <KLocalizedString>
#include <KSelectAction>
#include <KStandardAction>
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QDialogButtonBox>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QMenu>
#include <QPointer>
#include <QStyle>
......@@ -80,14 +84,22 @@ KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QMode
// Move keyframe to cursor
m_centerAction = new QAction(QIcon::fromTheme(QStringLiteral("align-horizontal-center")), i18n("Move selected keyframe to cursor"), this);
// Duplicate selected keyframe at cursor pos
m_copyAction = new QAction(QIcon::fromTheme(QStringLiteral("keyframe-duplicate")), i18n("Duplicate selected keyframe"), this);
// Apply current value to selected keyframes
m_copyAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy keyframes"), this);
connect(m_copyAction, &QAction::triggered, this, &KeyframeWidget::slotCopySelectedKeyframes);
m_copyAction->setToolTip(i18n("Copy keyframes"));
m_copyAction->setWhatsThis(
xi18nc("@info:whatsthis", "Copy keyframes. Copy the selected keyframes, or current parameters values if no keyframe is selected."));
m_pasteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Paste keyframe"), this);
connect(m_pasteAction, &QAction::triggered, this, &KeyframeWidget::slotPasteKeyframeFromClipBoard);
m_pasteAction->setToolTip(i18n("Paste keyframes"));
m_pasteAction->setWhatsThis(xi18nc("@info:whatsthis", "Paste keyframes. Paste clipboard data as keyframes at current position."));
auto *applyAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Apply current position value to selected keyframes"), this);
// Keyframe type widget
m_selectType = new KSelectAction(QIcon::fromTheme(QStringLiteral("keyframes")), i18n("Keyframe interpolation"), this);
m_selectType = new KSelectAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Keyframe interpolation"), this);
QAction *linear = new QAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Linear"), this);
linear->setData(int(mlt_keyframe_linear));
linear->setCheckable(true);
......@@ -102,7 +114,10 @@ KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QMode
m_selectType->addAction(curve);
m_selectType->setCurrentAction(linear);
connect(m_selectType, static_cast<void (KSelectAction::*)(QAction *)>(&KSelectAction::triggered), this, &KeyframeWidget::slotEditKeyframeType);
m_selectType->setToolBarMode(KSelectAction::ComboBoxMode);
m_selectType->setToolBarMode(KSelectAction::MenuMode);
m_selectType->setToolTip(i18n("Keyframe interpolation"));
m_selectType->setWhatsThis(xi18nc("@info:whatsthis", "Keyframe interpolation. This defines which interpolation will be used for the current keyframe."));
m_toolbar = new QToolBar(this);
m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
int size = style()->pixelMetric(QStyle::PM_SmallIconSize);
......@@ -120,7 +135,7 @@ KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QMode
m_toolbar->addAction(nextKFAction);
m_toolbar->addAction(m_centerAction);
m_toolbar->addAction(m_copyAction);
m_toolbar->addAction(applyAction);
m_toolbar->addAction(m_pasteAction);
m_toolbar->addAction(m_selectType);
QAction *seekKeyframe = new QAction(i18n("Seek to Keyframe on Select"), this);
......@@ -128,16 +143,13 @@ KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QMode
seekKeyframe->setChecked(KdenliveSettings::keyframeseek());
connect(seekKeyframe, &QAction::triggered, [&](bool selected) { KdenliveSettings::setKeyframeseek(selected); });
// copy/paste keyframes from clipboard
QAction *copy = new QAction(i18n("Copy Keyframes to Clipboard"), this);
QAction *copy = new QAction(i18n("Copy All Keyframes to Clipboard"), this);
connect(copy, &QAction::triggered, this, &KeyframeWidget::slotCopyKeyframes);
QAction *copyValue = new QAction(i18n("Copy Value at Cursor Position to Clipboard"), this);
connect(copyValue, &QAction::triggered, this, &KeyframeWidget::slotCopyValueAtCursorPos);
QAction *paste = new QAction(i18n("Import Keyframes from Clipboard…"), this);
connect(paste, &QAction::triggered, this, &KeyframeWidget::slotImportKeyframes);
if (m_model->data(index, AssetParameterModel::TypeRole).value<ParamType>() == ParamType::ColorWheel) {
// TODO color wheel doesn't support keyframe import/export yet
copy->setVisible(false);
copyValue->setVisible(false);
paste->setVisible(false);
}
// Remove keyframes
......@@ -185,8 +197,8 @@ KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QMode
menuAction->setPopupMode(QToolButton::InstantPopup);
menuAction->addAction(seekKeyframe);
menuAction->addAction(copy);
menuAction->addAction(copyValue);
menuAction->addAction(paste);
menuAction->addAction(applyAction);
menuAction->addSeparator();
menuAction->addAction(kfType);
menuAction->addAction(removeNext);
......@@ -220,7 +232,6 @@ KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QMode
connect(m_keyframeview, &KeyframeView::activateEffect, this, &KeyframeWidget::activateEffect);
connect(m_centerAction, &QAction::triggered, m_keyframeview, &KeyframeView::slotCenterKeyframe);
connect(m_copyAction, &QAction::triggered, m_keyframeview, &KeyframeView::slotDuplicateKeyframe);
connect(applyAction, &QAction::triggered, this, [this]() {
QMultiMap<QPersistentModelIndex, QString> paramList;
QList<QPersistentModelIndex> rectParams;
......@@ -374,6 +385,7 @@ void KeyframeWidget::slotEditKeyframeType(QAction *action)
{
int type = action->data().toInt();
m_keyframeview->slotEditType(type, m_index);
m_selectType->setIcon(action->icon());
emit activateEffect();
}
......@@ -385,6 +397,7 @@ void KeyframeWidget::slotRefreshParams()
while (auto ac = m_selectType->action(i)) {
if (ac->data().toInt() == int(keyType)) {
m_selectType->setCurrentItem(i);
m_selectType->setIcon(ac->icon());
break;
}
i++;
......@@ -444,7 +457,6 @@ void KeyframeWidget::slotAtKeyframe(bool atKeyframe, bool singleKeyframe)
{
m_addDeleteAction->setActive(!atKeyframe);
m_centerAction->setEnabled(!atKeyframe);
m_copyAction->setEnabled(!atKeyframe);
emit updateEffectKeyframe(atKeyframe || singleKeyframe);
m_selectType->setEnabled(atKeyframe || singleKeyframe);
for (const auto &w : m_parameters) {
......@@ -550,7 +562,7 @@ void KeyframeWidget::addParameter(const QPersistentModelIndex &index)
});
connect(colorWheelWidget, &LumaLiftGainParam::updateHeight, this, [&](int h) {
setFixedHeight(m_baseHeight + m_addedHeight + h);
emit updateHeight();
ColorWheel emit updateHeight();
});
paramWidget = colorWheelWidget;
} else if (type == ParamType::Roto_spline) {
......@@ -703,12 +715,97 @@ void KeyframeWidget::showKeyframes(bool enable)
void KeyframeWidget::slotCopyKeyframes()
{
QJsonDocument effectDoc = m_model->toJson(false);
QJsonDocument effectDoc = m_model->toJson({}, false);
if (effectDoc.isEmpty()) {
return;
}
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(QString(effectDoc.toJson()));
pCore->displayMessage(i18n("Keyframes copied"), InformationMessage);
}
void KeyframeWidget::slotPasteKeyframeFromClipBoard()
{
QClipboard *clipboard = QApplication::clipboard();
QString values = clipboard->text();
auto json = QJsonDocument::fromJson(values.toLocal8Bit());
Fun undo = []() { return true; };
Fun redo = []() { return true; };
if (!json.isArray()) {
pCore->displayMessage(i18n("No valid keyframe data in clipboard"), InformationMessage);
return;
}
auto list = json.array();
QMap<QString, QMap<int, QString>> storedValues;
for (const auto &entry : qAsConst(list)) {
if (!entry.isObject()) {
qDebug() << "Warning : Skipping invalid marker data";
continue;
}
auto entryObj = entry.toObject();
if (!entryObj.contains(QLatin1String("name"))) {
qDebug() << "Warning : Skipping invalid marker data (does not contain name)";
continue;
}
QString value = entryObj[QLatin1String("value")].toString();
ParamType kfrType = entryObj[QLatin1String("type")].toVariant().value<ParamType>();
if (m_model->isAnimated(kfrType)) {
QStringList stringVals = value.split(QLatin1Char(';'), Qt::SkipEmptyParts);
QMap<int, QString> values;
for (auto &val : stringVals) {
int position = m_model->time_to_frames(val.section(QLatin1Char('='), 0, 0));
values.insert(position, val.section(QLatin1Char('='), 1));
}
storedValues.insert(entryObj[QLatin1String("name")].toString(), values);
} else {
QMap<int, QString> values;
values.insert(0, value);
storedValues.insert(entryObj[QLatin1String("name")].toString(), values);
}
}
int destPos = getPosition();
std::vector<QPersistentModelIndex> indexes = m_keyframes->getIndexes();
for (const auto &ix : indexes) {
auto paramName = m_model->data(ix, AssetParameterModel::NameRole).toString();
if (storedValues.contains(paramName)) {
KeyframeModel *km = m_keyframes->getKeyModel(ix);
QMap<int, QString> values = storedValues.value(paramName);
int offset = values.keys().first();
QMapIterator<int, QString> i(values);
while (i.hasNext()) {
i.next();
km->addKeyframe(GenTime(destPos + i.key() - offset, pCore->getCurrentFps()), KeyframeType::Linear, i.value(), true, undo, redo);
}
} else {
qDebug() << "::: NOT FOUND PARAM: " << paramName << " in list: " << storedValues.keys();
}
}
pCore->pushUndo(undo, redo, i18n("Paste keyframe"));
}
void KeyframeWidget::slotCopySelectedKeyframes()
{
const QVector<int> results = m_keyframeview->selectedKeyframesIndexes();
if (results.isEmpty()) {
// No keyframes selected, use current position values
QJsonDocument effectDoc = m_model->valueAsJson(getPosition(), false);
if (effectDoc.isEmpty()) {
pCore->displayMessage(i18n("Cannot copy current parameter values"), InformationMessage);
return;
}
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(QString(effectDoc.toJson()));
pCore->displayMessage(i18n("Current values copied"), InformationMessage);
} else {
QJsonDocument effectDoc = m_model->toJson(results, false);
if (effectDoc.isEmpty()) {
return;
}
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(QString(effectDoc.toJson()));
pCore->displayMessage(i18n("Current values copied"), InformationMessage);
}
}
void KeyframeWidget::slotCopyValueAtCursorPos()
......@@ -719,6 +816,7 @@ void KeyframeWidget::slotCopyValueAtCursorPos()
}
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(QString(effectDoc.toJson()));
pCore->displayMessage(i18n("Current values copied"), InformationMessage);
}
void KeyframeWidget::slotImportKeyframes()
......@@ -749,3 +847,18 @@ void KeyframeWidget::slotSeekToKeyframe(int ix)
int pos = m_keyframes->getPosAtIndex(ix).frames(pCore->getCurrentFps());
slotSetPosition(pos, true);
}
void KeyframeWidget::sendStandardCommand(int command)
{
switch (command) {
case KStandardAction::Copy:
m_copyAction->trigger();
break;
case KStandardAction::Paste:
m_pasteAction->trigger();
break;
default:
qDebug() << ":::: UNKNOWN COMMAND: " << command;
break;
}
}
......@@ -52,6 +52,8 @@ public slots:
/** @brief initialize qml overlay
*/
void slotInitMonitor(bool active) override;
/** @brief Activate a standard action passed from the mainwindow, like copy or paste */
void sendStandardCommand(int command);
public slots:
void slotSetPosition(int pos = -1, bool update = true);
......@@ -62,6 +64,9 @@ private slots:
void slotAtKeyframe(bool atKeyframe, bool singleKeyframe);
void slotEditKeyframeType(QAction *action);
void slotUpdateKeyframesFromMonitor(const QPersistentModelIndex &index, const QVariant &res);
/** @brief Paste a keyframe from clipboard */
void slotPasteKeyframeFromClipBoard();
void slotCopySelectedKeyframes();
void slotCopyKeyframes();
void slotCopyValueAtCursorPos();
void slotImportKeyframes();
......@@ -79,6 +84,7 @@ private:
KDualAction *m_addDeleteAction;
QAction *m_centerAction;
QAction *m_copyAction;
QAction *m_pasteAction;
KSelectAction *m_selectType;
TimecodeDisplay *m_time;
MonitorSceneType m_neededScene;
......
......@@ -991,3 +991,8 @@ void CollapsibleEffectView::slotHideKeyframes(bool hide)
{
m_model->setKeyframesHidden(hide);
}
void CollapsibleEffectView::sendStandardCommand(int command)
{
m_view->sendStandardCommand(command);
}
......@@ -72,6 +72,8 @@ public:
void slotNextKeyframe();
void slotPreviousKeyframe();
void addRemoveKeyframe();
/** @brief Used to pass a standard action like copy or paste to the effect stack widget */
void sendStandardCommand(int command);
public slots:
void slotSyncEffectsPos(int pos);
......
......@@ -635,3 +635,15 @@ void EffectStackView::addRemoveKeyframe()
w->addRemoveKeyframe();
}
}
void EffectStackView::sendStandardCommand(int command)
{
int max = m_model->rowCount();
int currentActive = m_model->getActiveEffect();
if (currentActive < max && currentActive > -1) {
auto item = m_model->getEffectStackRow(currentActive);
QModelIndex ix = m_model->getIndexFromItem(item);
auto *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(ix));
w->sendStandardCommand(command);
}
}
......@@ -63,6 +63,8 @@ public:
*/
void slotGoToKeyframe(bool next);
void addRemoveKeyframe();
/** @brief Used to pass a standard action like copy or paste to the effect stack widget */
void sendStandardCommand(int command);
public slots:
/** @brief Save current effect stack
......
......@@ -3306,11 +3306,27 @@ void MainWindow::showKeyBinding(const QString &text)
void MainWindow::slotCopy()
{
QWidget *widget = QApplication::focusWidget();
while ((widget != nullptr) && widget != this) {
if (widget == m_effectStackDock) {
m_assetPanel->sendStandardCommand(KStandardAction::Copy);
return;
}
widget = widget->parentWidget();
}
getCurrentTimeline()->controller()->copyItem();
}
void MainWindow::slotPaste()
{
QWidget *widget = QApplication::focusWidget();
while ((widget != nullptr) && widget != this) {
if (widget == m_effectStackDock) {
m_assetPanel->sendStandardCommand(KStandardAction::Paste);
return;
}
widget = widget->parentWidget();
}
getCurrentTimeline()->controller()->pasteItem();
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment