File indexing completed on 2024-05-12 04:52:54

0001 /*
0002     SPDX-FileCopyrightText: 2011 Till Theato <root@ttill.de>
0003     SPDX-FileCopyrightText: 2017 Nicolas Carion
0004     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "keyframewidget.hpp"
0008 #include "assets/keyframes/model/corners/cornershelper.hpp"
0009 #include "assets/keyframes/model/keyframemodel.hpp"
0010 #include "assets/keyframes/model/keyframemodellist.hpp"
0011 #include "assets/keyframes/model/rect/recthelper.hpp"
0012 #include "assets/keyframes/model/rotoscoping/rotohelper.hpp"
0013 #include "assets/keyframes/view/keyframeview.hpp"
0014 #include "assets/model/assetparametermodel.hpp"
0015 #include "assets/view/widgets/keyframeimport.h"
0016 #include "core.h"
0017 #include "effects/effectsrepository.hpp"
0018 #include "kdenlivesettings.h"
0019 #include "lumaliftgainparam.hpp"
0020 #include "monitor/monitor.h"
0021 #include "utils/timecode.h"
0022 #include "widgets/choosecolorwidget.h"
0023 #include "widgets/doublewidget.h"
0024 #include "widgets/geometrywidget.h"
0025 #include "widgets/timecodedisplay.h"
0026 
0027 #include <KActionCategory>
0028 #include <KActionMenu>
0029 #include <KDualAction>
0030 #include <KLocalizedString>
0031 #include <KSelectAction>
0032 #include <KStandardAction>
0033 #include <QApplication>
0034 #include <QCheckBox>
0035 #include <QClipboard>
0036 #include <QDialogButtonBox>
0037 #include <QJsonArray>
0038 #include <QJsonDocument>
0039 #include <QJsonObject>
0040 #include <QJsonValue>
0041 #include <QMenu>
0042 #include <QPointer>
0043 #include <QStyle>
0044 #include <QToolButton>
0045 #include <QVBoxLayout>
0046 #include <kwidgetsaddons_version.h>
0047 #include <utility>
0048 
0049 KeyframeWidget::KeyframeWidget(std::shared_ptr<AssetParameterModel> model, QModelIndex index, QSize frameSize, QWidget *parent)
0050     : AbstractParamWidget(std::move(model), index, parent)
0051     , m_monitorHelper(nullptr)
0052     , m_neededScene(MonitorSceneType::MonitorSceneDefault)
0053     , m_sourceFrameSize(frameSize.isValid() && !frameSize.isNull() ? frameSize : pCore->getCurrentFrameSize())
0054     , m_baseHeight(0)
0055     , m_addedHeight(0)
0056 {
0057     setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
0058     m_lay = new QVBoxLayout(this);
0059     m_lay->setSpacing(0);
0060 
0061     bool ok = false;
0062     int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok);
0063     Q_ASSERT(ok);
0064     m_model->prepareKeyframes();
0065     m_keyframes = m_model->getKeyframeModel();
0066     m_keyframeview = new KeyframeView(m_keyframes, duration, this);
0067 
0068     m_addDeleteAction = new KDualAction(this);
0069     m_addDeleteAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("keyframe-add")));
0070     m_addDeleteAction->setActiveText(i18n("Add keyframe"));
0071     m_addDeleteAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("keyframe-remove")));
0072     m_addDeleteAction->setInactiveText(i18n("Delete keyframe"));
0073 
0074     connect(m_addDeleteAction, &KDualAction::triggered, m_keyframeview, &KeyframeView::slotAddRemove);
0075     connect(this, &KeyframeWidget::addRemove, m_keyframeview, &KeyframeView::slotAddRemove);
0076 
0077     auto *previousKFAction = new QAction(QIcon::fromTheme(QStringLiteral("keyframe-previous")), i18n("Go to previous keyframe"), this);
0078     connect(previousKFAction, &QAction::triggered, m_keyframeview, &KeyframeView::slotGoToPrev);
0079     connect(this, &KeyframeWidget::goToPrevious, m_keyframeview, &KeyframeView::slotGoToPrev);
0080 
0081     auto *nextKFAction = new QAction(QIcon::fromTheme(QStringLiteral("keyframe-next")), i18n("Go to next keyframe"), this);
0082     connect(nextKFAction, &QAction::triggered, m_keyframeview, &KeyframeView::slotGoToNext);
0083     connect(this, &KeyframeWidget::goToNext, m_keyframeview, &KeyframeView::slotGoToNext);
0084 
0085     // Move keyframe to cursor
0086     m_centerAction = new QAction(QIcon::fromTheme(QStringLiteral("align-horizontal-center")), i18n("Move selected keyframe to cursor"), this);
0087 
0088     // Apply current value to selected keyframes
0089     m_copyAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy keyframes"), this);
0090     connect(m_copyAction, &QAction::triggered, this, &KeyframeWidget::slotCopySelectedKeyframes);
0091     m_copyAction->setToolTip(i18n("Copy keyframes"));
0092     m_copyAction->setWhatsThis(
0093         xi18nc("@info:whatsthis", "Copy keyframes. Copy the selected keyframes, or current parameters values if no keyframe is selected."));
0094 
0095     m_pasteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Paste keyframe"), this);
0096     connect(m_pasteAction, &QAction::triggered, this, &KeyframeWidget::slotPasteKeyframeFromClipBoard);
0097     m_pasteAction->setToolTip(i18n("Paste keyframes"));
0098     m_pasteAction->setWhatsThis(xi18nc("@info:whatsthis", "Paste keyframes. Paste clipboard data as keyframes at current position."));
0099 
0100     auto *applyAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Apply current position value to selected keyframes"), this);
0101 
0102     // Keyframe type widget
0103     m_selectType = new KSelectAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Keyframe interpolation"), this);
0104     QMap<KeyframeType, QAction *> kfTypeHandles;
0105     for (auto it = KeyframeTypeName.cbegin(); it != KeyframeTypeName.cend(); it++) { // Order is fixed due to the nature of <map>
0106         QAction *tmp = new QAction(QIcon::fromTheme(KeyframeModel::getIconByKeyframeType(it.key())), it.value(), this);
0107         tmp->setData(int(it.key()));
0108         tmp->setCheckable(true);
0109         kfTypeHandles.insert(it.key(), tmp);
0110         m_selectType->addAction(kfTypeHandles[it.key()]);
0111     }
0112     m_selectType->setCurrentAction(kfTypeHandles[KeyframeType::Linear]);
0113 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 240, 0)
0114     connect(m_selectType, &KSelectAction::actionTriggered, this, &KeyframeWidget::slotEditKeyframeType);
0115 #else
0116     connect(m_selectType, static_cast<void (KSelectAction::*)(QAction *)>(&KSelectAction::triggered), this, &KeyframeWidget::slotEditKeyframeType);
0117 #endif
0118     m_selectType->setToolBarMode(KSelectAction::MenuMode);
0119     m_selectType->setToolTip(i18n("Keyframe interpolation"));
0120     m_selectType->setWhatsThis(xi18nc("@info:whatsthis", "Keyframe interpolation. This defines which interpolation will be used for the current keyframe."));
0121 
0122     m_toolbar = new QToolBar(this);
0123     m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
0124     int size = style()->pixelMetric(QStyle::PM_SmallIconSize);
0125     m_toolbar->setIconSize(QSize(size, size));
0126 
0127     Monitor *monitor = pCore->getMonitor(m_model->monitorId);
0128     connect(monitor, &Monitor::seekPosition, this, &KeyframeWidget::monitorSeek, Qt::UniqueConnection);
0129     connect(pCore.get(), &Core::disconnectEffectStack, this, &KeyframeWidget::disconnectEffectStack);
0130 
0131     m_time = new TimecodeDisplay(this);
0132     m_time->setRange(0, duration - 1);
0133 
0134     m_toolbar->addAction(previousKFAction);
0135     m_toolbar->addAction(m_addDeleteAction);
0136     m_toolbar->addAction(nextKFAction);
0137     m_toolbar->addAction(m_centerAction);
0138     m_toolbar->addAction(m_copyAction);
0139     m_toolbar->addAction(m_pasteAction);
0140     m_toolbar->addAction(m_selectType);
0141 
0142     QAction *seekKeyframe = new QAction(i18n("Seek to Keyframe on Select"), this);
0143     seekKeyframe->setCheckable(true);
0144     seekKeyframe->setChecked(KdenliveSettings::keyframeseek());
0145     connect(seekKeyframe, &QAction::triggered, [&](bool selected) { KdenliveSettings::setKeyframeseek(selected); });
0146     // copy/paste keyframes from clipboard
0147     QAction *copy = new QAction(i18n("Copy All Keyframes to Clipboard"), this);
0148     connect(copy, &QAction::triggered, this, &KeyframeWidget::slotCopyKeyframes);
0149     QAction *paste = new QAction(i18n("Import Keyframes from Clipboard…"), this);
0150     connect(paste, &QAction::triggered, this, &KeyframeWidget::slotImportKeyframes);
0151     if (m_model->data(index, AssetParameterModel::TypeRole).value<ParamType>() == ParamType::ColorWheel) {
0152         // TODO color wheel doesn't support keyframe import/export yet
0153         copy->setVisible(false);
0154         paste->setVisible(false);
0155     }
0156     // Remove keyframes
0157     QAction *removeNext = new QAction(i18n("Remove all Keyframes After Cursor"), this);
0158     connect(removeNext, &QAction::triggered, this, &KeyframeWidget::slotRemoveNextKeyframes);
0159 
0160     // Default kf interpolation
0161     KSelectAction *kfType = new KSelectAction(i18n("Default Keyframe Type"), this);
0162     QAction *discrete2 =
0163         new QAction(QIcon::fromTheme(KeyframeModel::getIconByKeyframeType(KeyframeType::Discrete)), KeyframeTypeName.value(KeyframeType::Discrete), this);
0164     discrete2->setData(int(KeyframeType::Discrete));
0165     discrete2->setCheckable(true);
0166     kfType->addAction(discrete2);
0167     QAction *linear2 =
0168         new QAction(QIcon::fromTheme(KeyframeModel::getIconByKeyframeType(KeyframeType::Linear)), KeyframeTypeName.value(KeyframeType::Linear), this);
0169     linear2->setData(int(KeyframeType::Linear));
0170     linear2->setCheckable(true);
0171     kfType->addAction(linear2);
0172     QAction *curve2 =
0173         new QAction(QIcon::fromTheme(KeyframeModel::getIconByKeyframeType(KeyframeType::Curve)), KeyframeTypeName.value(KeyframeType::Curve), this);
0174     curve2->setData(int(KeyframeType::Curve));
0175     curve2->setCheckable(true);
0176     kfType->addAction(curve2);
0177     switch (KdenliveSettings::defaultkeyframeinterp()) {
0178     case int(KeyframeType::Discrete):
0179         kfType->setCurrentAction(discrete2);
0180         break;
0181     case int(KeyframeType::Curve):
0182         kfType->setCurrentAction(curve2);
0183         break;
0184     default:
0185         kfType->setCurrentAction(linear2);
0186         break;
0187     }
0188 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 240, 0)
0189     connect(kfType, &KSelectAction::actionTriggered, this, [&](QAction *ac) { KdenliveSettings::setDefaultkeyframeinterp(ac->data().toInt()); });
0190 #else
0191     connect(kfType, static_cast<void (KSelectAction::*)(QAction *)>(&KSelectAction::triggered), this,
0192             [&](QAction *ac) { KdenliveSettings::setDefaultkeyframeinterp(ac->data().toInt()); });
0193 #endif
0194 
0195     // rotoscoping only supports linear keyframes
0196     if (m_model->getAssetId() == QLatin1String("rotoscoping")) {
0197         m_selectType->setVisible(false);
0198         m_selectType->setCurrentAction(kfTypeHandles[KeyframeType::Linear]);
0199         kfType->setVisible(false);
0200         kfType->setCurrentAction(linear2);
0201     }
0202 
0203     // Menu toolbutton
0204     auto *menuAction = new KActionMenu(QIcon::fromTheme(QStringLiteral("application-menu")), i18n("Options"), this);
0205     menuAction->setWhatsThis(
0206         xi18nc("@info:whatsthis", "Opens a list of further actions for managing keyframes (for example: copy to and pasting keyframes from clipboard)."));
0207     menuAction->setPopupMode(QToolButton::InstantPopup);
0208     menuAction->addAction(seekKeyframe);
0209     menuAction->addAction(copy);
0210     menuAction->addAction(paste);
0211     menuAction->addAction(applyAction);
0212     menuAction->addSeparator();
0213     menuAction->addAction(kfType);
0214     menuAction->addAction(removeNext);
0215     m_toolbar->addAction(menuAction);
0216 
0217     m_lay->addWidget(m_keyframeview);
0218     auto *hlay = new QHBoxLayout;
0219     hlay->addWidget(m_toolbar);
0220     hlay->addWidget(m_time);
0221     hlay->addStretch();
0222     m_lay->addLayout(hlay);
0223 
0224     connect(m_time, &TimecodeDisplay::timeCodeEditingFinished, this, [&]() { slotSetPosition(-1, true); });
0225     connect(m_keyframeview, &KeyframeView::seekToPos, this, [&](int pos) {
0226         int in = m_model->data(m_index, AssetParameterModel::InRole).toInt();
0227         bool canHaveZone = m_model->getOwnerId().type == KdenliveObjectType::Master || m_model->getOwnerId().type == KdenliveObjectType::TimelineTrack;
0228         if (pos < 0) {
0229             m_time->setValue(0);
0230             m_keyframeview->slotSetPosition(0, true);
0231         } else {
0232             m_time->setValue(qMax(0, pos - in));
0233             m_keyframeview->slotSetPosition(pos, true);
0234         }
0235         m_addDeleteAction->setEnabled(pos > 0);
0236         slotRefreshParams();
0237 
0238         Q_EMIT seekToPos(pos + (canHaveZone ? in : 0));
0239     });
0240     connect(m_keyframeview, &KeyframeView::atKeyframe, this, &KeyframeWidget::slotAtKeyframe);
0241     connect(m_keyframeview, &KeyframeView::modified, this, &KeyframeWidget::slotRefreshParams);
0242     connect(m_keyframeview, &KeyframeView::activateEffect, this, &KeyframeWidget::activateEffect);
0243 
0244     connect(m_centerAction, &QAction::triggered, m_keyframeview, &KeyframeView::slotCenterKeyframe);
0245     connect(applyAction, &QAction::triggered, this, [this]() {
0246         QMultiMap<QPersistentModelIndex, QString> paramList;
0247         QList<QPersistentModelIndex> rectParams;
0248         for (const auto &w : m_parameters) {
0249             auto type = m_model->data(w.first, AssetParameterModel::TypeRole).value<ParamType>();
0250             if (type == ParamType::AnimatedRect) {
0251                 if (m_model->data(w.first, AssetParameterModel::OpacityRole).toBool()) {
0252                     paramList.insert(w.first, i18n("Opacity"));
0253                 }
0254                 paramList.insert(w.first, i18n("Height"));
0255                 paramList.insert(w.first, i18n("Width"));
0256                 paramList.insert(w.first, i18n("Y position"));
0257                 paramList.insert(w.first, i18n("X position"));
0258                 rectParams << w.first;
0259             } else {
0260                 paramList.insert(w.first, m_model->data(w.first, Qt::DisplayRole).toString());
0261             }
0262         }
0263         if (paramList.count() == 0) {
0264             qDebug() << "=== No parameter to copy, aborting";
0265             return;
0266         }
0267         if (paramList.count() == 1) {
0268             m_keyframeview->copyCurrentValue(m_keyframes->getIndexAtRow(0), QString());
0269             return;
0270         }
0271         // More than one param
0272         QDialog d(this);
0273         QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
0274         auto *l = new QVBoxLayout;
0275         d.setLayout(l);
0276         l->addWidget(new QLabel(i18n("Select parameters to copy"), &d));
0277 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0278         QMapIterator<QPersistentModelIndex, QString> i(paramList);
0279 #else
0280         QMultiMapIterator<QPersistentModelIndex, QString> i(paramList);
0281 #endif
0282         while (i.hasNext()) {
0283             i.next();
0284             auto *cb = new QCheckBox(i.value(), this);
0285             cb->setProperty("index", i.key());
0286             l->addWidget(cb);
0287         }
0288         l->addWidget(buttonBox);
0289         d.connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::reject);
0290         d.connect(buttonBox, &QDialogButtonBox::accepted, &d, &QDialog::accept);
0291         if (d.exec() != QDialog::Accepted) {
0292             return;
0293         }
0294         paramList.clear();
0295         QList<QCheckBox *> cbs = d.findChildren<QCheckBox *>();
0296         QMap<QPersistentModelIndex, QStringList> params;
0297         for (auto c : qAsConst(cbs)) {
0298             // qDebug()<<"=== FOUND CBS: "<<KLocalizedString::removeAcceleratorMarker(c->text());
0299             if (c->isChecked()) {
0300                 QPersistentModelIndex ix = c->property("index").toModelIndex();
0301                 if (rectParams.contains(ix)) {
0302                     // Check param name
0303                     QString cbName = KLocalizedString::removeAcceleratorMarker(c->text());
0304                     if (cbName == i18n("Opacity")) {
0305                         if (params.contains(ix)) {
0306                             params[ix] << QStringLiteral("spinO");
0307                         } else {
0308                             params.insert(ix, {QStringLiteral("spinO")});
0309                         }
0310                     } else if (cbName == i18n("Height")) {
0311                         if (params.contains(ix)) {
0312                             params[ix] << QStringLiteral("spinH");
0313                         } else {
0314                             params.insert(ix, {QStringLiteral("spinH")});
0315                         }
0316                     } else if (cbName == i18n("Width")) {
0317                         if (params.contains(ix)) {
0318                             params[ix] << QStringLiteral("spinW");
0319                         } else {
0320                             params.insert(ix, {QStringLiteral("spinW")});
0321                         }
0322                     } else if (cbName == i18n("X position")) {
0323                         if (params.contains(ix)) {
0324                             params[ix] << QStringLiteral("spinX");
0325                         } else {
0326                             params.insert(ix, {QStringLiteral("spinX")});
0327                         }
0328                     } else if (cbName == i18n("Y position")) {
0329                         if (params.contains(ix)) {
0330                             params[ix] << QStringLiteral("spinY");
0331                         } else {
0332                             params.insert(ix, {QStringLiteral("spinY")});
0333                         }
0334                     }
0335                     if (!params.contains(ix)) {
0336                         params.insert(ix, {});
0337                     }
0338                 } else {
0339                     params.insert(ix, {});
0340                 }
0341             }
0342         }
0343         QMapIterator<QPersistentModelIndex, QStringList> p(params);
0344         while (p.hasNext()) {
0345             p.next();
0346             m_keyframeview->copyCurrentValue(p.key(), p.value().join(QLatin1Char(' ')));
0347         }
0348         return;
0349     });
0350     // m_baseHeight = m_keyframeview->height() + m_selectType->defaultWidget()->sizeHint().height();
0351     QMargins mrg = m_lay->contentsMargins();
0352     m_baseHeight = m_keyframeview->height() + m_toolbar->sizeHint().height();
0353     m_addedHeight = mrg.top() + mrg.bottom();
0354     setFixedHeight(m_baseHeight + m_addedHeight);
0355     addParameter(index);
0356 }
0357 
0358 KeyframeWidget::~KeyframeWidget()
0359 {
0360     delete m_keyframeview;
0361     delete m_time;
0362 }
0363 
0364 void KeyframeWidget::disconnectEffectStack()
0365 {
0366     Monitor *monitor = pCore->getMonitor(m_model->monitorId);
0367     disconnect(monitor, &Monitor::seekPosition, this, &KeyframeWidget::monitorSeek);
0368 }
0369 
0370 void KeyframeWidget::monitorSeek(int pos)
0371 {
0372     int in = 0;
0373     int out = 0;
0374     bool canHaveZone = m_model->getOwnerId().type == KdenliveObjectType::Master || m_model->getOwnerId().type == KdenliveObjectType::TimelineTrack;
0375     if (canHaveZone) {
0376         bool ok = false;
0377         in = m_model->data(m_index, AssetParameterModel::InRole).toInt(&ok);
0378         out = m_model->data(m_index, AssetParameterModel::OutRole).toInt(&ok);
0379         Q_ASSERT(ok);
0380     }
0381     if (in == 0 && out == 0) {
0382         in = pCore->getItemPosition(m_model->getOwnerId());
0383         out = in + pCore->getItemDuration(m_model->getOwnerId());
0384     }
0385     bool isInRange = pos >= in && pos < out;
0386     connectMonitor(isInRange && m_model->isActive());
0387     m_addDeleteAction->setEnabled(isInRange && pos > in);
0388     int framePos = qBound(in, pos, out) - in;
0389     if (isInRange && framePos != m_time->getValue()) {
0390         slotSetPosition(framePos, false);
0391     }
0392 }
0393 
0394 void KeyframeWidget::slotEditKeyframeType(QAction *action)
0395 {
0396     int type = action->data().toInt();
0397     m_keyframeview->slotEditType(type, m_index);
0398     m_selectType->setIcon(action->icon());
0399     Q_EMIT activateEffect();
0400 }
0401 
0402 void KeyframeWidget::slotRefreshParams()
0403 {
0404     int pos = getPosition();
0405     KeyframeType keyType = m_keyframes->keyframeType(GenTime(pos, pCore->getCurrentFps()));
0406     int i = 0;
0407     while (auto ac = m_selectType->action(i)) {
0408         if (ac->data().toInt() == int(keyType)) {
0409             m_selectType->setCurrentItem(i);
0410             m_selectType->setIcon(ac->icon());
0411             break;
0412         }
0413         i++;
0414     }
0415     for (const auto &w : m_parameters) {
0416         auto type = m_model->data(w.first, AssetParameterModel::TypeRole).value<ParamType>();
0417         if (type == ParamType::KeyframeParam) {
0418             (static_cast<DoubleWidget *>(w.second))->setValue(m_keyframes->getInterpolatedValue(pos, w.first).toDouble());
0419         } else if (type == ParamType::AnimatedRect) {
0420             const QString val = m_keyframes->getInterpolatedValue(pos, w.first).toString();
0421             const QStringList vals = val.split(QLatin1Char(' '));
0422             QRect rect;
0423             double opacity = -1;
0424             if (vals.count() >= 4) {
0425                 rect = QRect(vals.at(0).toInt(), vals.at(1).toInt(), vals.at(2).toInt(), vals.at(3).toInt());
0426                 if (vals.count() > 4) {
0427                     opacity = vals.at(4).toDouble();
0428                 }
0429             }
0430             (static_cast<GeometryWidget *>(w.second))->setValue(rect, opacity);
0431         } else if (type == ParamType::ColorWheel) {
0432             (static_cast<LumaLiftGainParam *>(w.second)->slotRefresh(pos));
0433         } else if (type == ParamType::Color) {
0434             const QString value = m_keyframes->getInterpolatedValue(pos, w.first).toString();
0435             (static_cast<ChooseColorWidget *>(w.second)->slotColorModified(QColorUtils::stringToColor(value)));
0436         }
0437     }
0438     if (m_monitorHelper && m_model->isActive()) {
0439         m_monitorHelper->refreshParams(pos);
0440         return;
0441     }
0442 }
0443 void KeyframeWidget::slotSetPosition(int pos, bool update)
0444 {
0445     bool canHaveZone = m_model->getOwnerId().type == KdenliveObjectType::Master || m_model->getOwnerId().type == KdenliveObjectType::TimelineTrack;
0446     int offset = 0;
0447     if (pos < 0) {
0448         if (canHaveZone) {
0449             offset = m_model->data(m_index, AssetParameterModel::InRole).toInt();
0450         }
0451         pos = m_time->getValue();
0452     } else {
0453         m_time->setValue(pos);
0454     }
0455     m_keyframeview->slotSetPosition(pos, true);
0456     m_addDeleteAction->setEnabled(pos > 0);
0457     slotRefreshParams();
0458 
0459     if (update) {
0460         Q_EMIT seekToPos(pos + offset);
0461     }
0462 }
0463 
0464 int KeyframeWidget::getPosition() const
0465 {
0466     return m_time->getValue() + pCore->getItemIn(m_model->getOwnerId());
0467 }
0468 
0469 void KeyframeWidget::slotAtKeyframe(bool atKeyframe, bool singleKeyframe)
0470 {
0471     m_addDeleteAction->setActive(!atKeyframe);
0472     m_centerAction->setEnabled(!atKeyframe);
0473     Q_EMIT updateEffectKeyframe(atKeyframe || singleKeyframe);
0474     m_selectType->setEnabled(atKeyframe || singleKeyframe);
0475     for (const auto &w : m_parameters) {
0476         w.second->setEnabled(atKeyframe || singleKeyframe);
0477     }
0478 }
0479 
0480 void KeyframeWidget::slotRefresh()
0481 {
0482     // update duration
0483     bool ok = false;
0484     int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok);
0485     Q_ASSERT(ok);
0486     int in = m_model->data(m_index, AssetParameterModel::InRole).toInt(&ok);
0487     Q_ASSERT(ok);
0488     int out = in + duration;
0489 
0490     m_keyframeview->setDuration(duration);
0491     m_time->setRange(0, duration - 1);
0492     if (m_model->monitorId == Kdenlive::ProjectMonitor) {
0493         monitorSeek(pCore->getMonitorPosition());
0494     } else {
0495         int pos = m_time->getValue();
0496         bool isInRange = pos >= in && pos < out;
0497         connectMonitor(isInRange && m_model->isActive());
0498         m_addDeleteAction->setEnabled(isInRange && pos > in);
0499         int framePos = qBound(in, pos, out) - in;
0500         if (isInRange && framePos != m_time->getValue()) {
0501             slotSetPosition(framePos, false);
0502         }
0503     }
0504     slotRefreshParams();
0505 }
0506 
0507 void KeyframeWidget::resetKeyframes()
0508 {
0509     // update duration
0510     bool ok = false;
0511     int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok);
0512     Q_ASSERT(ok);
0513     m_model->data(m_index, AssetParameterModel::InRole).toInt(&ok);
0514     Q_ASSERT(ok);
0515     // reset keyframes
0516     m_keyframes->refresh();
0517     // m_model->dataChanged(QModelIndex(), QModelIndex());
0518     m_keyframeview->setDuration(duration);
0519     m_time->setRange(0, duration - 1);
0520     slotRefreshParams();
0521 }
0522 
0523 void KeyframeWidget::addParameter(const QPersistentModelIndex &index)
0524 {
0525     // Retrieve parameters from the model
0526     QString name = m_model->data(index, Qt::DisplayRole).toString();
0527     QString comment = m_model->data(index, AssetParameterModel::CommentRole).toString();
0528     QString suffix = m_model->data(index, AssetParameterModel::SuffixRole).toString();
0529 
0530     auto type = m_model->data(index, AssetParameterModel::TypeRole).value<ParamType>();
0531     // Construct object
0532     QWidget *labelWidget = nullptr;
0533     QWidget *paramWidget = nullptr;
0534     if (type == ParamType::AnimatedRect) {
0535         m_neededScene = MonitorSceneType::MonitorSceneGeometry;
0536         int inPos = m_model->data(index, AssetParameterModel::ParentInRole).toInt();
0537         QPair<int, int> range(inPos, inPos + m_model->data(index, AssetParameterModel::ParentDurationRole).toInt());
0538         const QString value = m_keyframes->getInterpolatedValue(getPosition(), index).toString();
0539         m_monitorHelper = new KeyframeMonitorHelper(pCore->getMonitor(m_model->monitorId), m_model, index, this);
0540         QRect rect;
0541         double opacity = 0;
0542         QStringList vals = value.split(QLatin1Char(' '));
0543         if (vals.count() > 3) {
0544             rect = QRect(vals.at(0).toInt(), vals.at(1).toInt(), vals.at(2).toInt(), vals.at(3).toInt());
0545             if (vals.count() > 4) {
0546                 opacity = vals.at(4).toDouble();
0547             }
0548         }
0549         // qtblend uses an opacity value in the (0-1) range, while older geometry effects use (0-100)
0550         GeometryWidget *geomWidget = new GeometryWidget(pCore->getMonitor(m_model->monitorId), range, rect, opacity, m_sourceFrameSize, false,
0551                                                         m_model->data(m_index, AssetParameterModel::OpacityRole).toBool(), this);
0552         connect(geomWidget, &GeometryWidget::valueChanged, this, [this, index](const QString &v) {
0553             Q_EMIT activateEffect();
0554             m_keyframes->updateKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), QVariant(v), index);
0555         });
0556         connect(geomWidget, &GeometryWidget::updateMonitorGeometry, this, [this](const QRect r) {
0557             if (m_model->isActive()) {
0558                 pCore->getMonitor(m_model->monitorId)->setUpEffectGeometry(r);
0559             }
0560         });
0561         paramWidget = geomWidget;
0562     } else if (type == ParamType::ColorWheel) {
0563         auto colorWheelWidget = new LumaLiftGainParam(m_model, index, this);
0564         connect(colorWheelWidget, &LumaLiftGainParam::valuesChanged, this,
0565                 [this, index](const QList<QModelIndex> &indexes, const QStringList &sourceList, const QStringList &list, bool createUndo) {
0566                     Q_EMIT activateEffect();
0567                     if (createUndo) {
0568                         m_keyframes->updateMultiKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), sourceList, list, indexes);
0569                     } else {
0570                         // Execute without creating an undo/redo entry
0571                         auto *parentCommand = new QUndoCommand();
0572                         m_keyframes->updateMultiKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), sourceList, list, indexes, parentCommand);
0573                         parentCommand->redo();
0574                         delete parentCommand;
0575                     }
0576                 });
0577         connect(colorWheelWidget, &LumaLiftGainParam::updateHeight, this, [&](int h) {
0578             setFixedHeight(m_baseHeight + m_addedHeight + h);
0579             Q_EMIT updateHeight();
0580         });
0581         paramWidget = colorWheelWidget;
0582     } else if (type == ParamType::Roto_spline) {
0583         m_monitorHelper = new RotoHelper(pCore->getMonitor(m_model->monitorId), m_model, index, this);
0584         m_neededScene = MonitorSceneType::MonitorSceneRoto;
0585     } else if (type == ParamType::Color) {
0586         QString value = m_keyframes->getInterpolatedValue(getPosition(), index).toString();
0587         bool alphaEnabled = m_model->data(index, AssetParameterModel::AlphaRole).toBool();
0588         labelWidget = new QLabel(name, this);
0589         auto colorWidget = new ChooseColorWidget(this, QColorUtils::stringToColor(value), alphaEnabled);
0590         colorWidget->setToolTip(comment);
0591         connect(colorWidget, &ChooseColorWidget::modified, this, [this, index, alphaEnabled](const QColor &color) {
0592             Q_EMIT activateEffect();
0593             m_keyframes->updateKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), QVariant(QColorUtils::colorToString(color, alphaEnabled)), index);
0594         });
0595         paramWidget = colorWidget;
0596 
0597     } else {
0598         if (m_model->getAssetId() == QLatin1String("frei0r.c0rners")) {
0599             if (m_neededScene == MonitorSceneDefault && !m_monitorHelper) {
0600                 m_neededScene = MonitorSceneType::MonitorSceneCorners;
0601                 m_monitorHelper = new CornersHelper(pCore->getMonitor(m_model->monitorId), m_model, index, this);
0602                 connect(this, &KeyframeWidget::addIndex, m_monitorHelper, &CornersHelper::addIndex);
0603             } else {
0604                 if (type == ParamType::KeyframeParam) {
0605                     int paramName = m_model->data(index, AssetParameterModel::NameRole).toInt();
0606                     if (paramName < 8) {
0607                         Q_EMIT addIndex(index);
0608                     }
0609                 }
0610             }
0611         }
0612         if (m_model->getAssetId().contains(QLatin1String("frei0r.alphaspot"))) {
0613             if (m_neededScene == MonitorSceneDefault && !m_monitorHelper) {
0614                 m_neededScene = MonitorSceneType::MonitorSceneGeometry;
0615                 m_monitorHelper = new RectHelper(pCore->getMonitor(m_model->monitorId), m_model, index, this);
0616                 connect(this, &KeyframeWidget::addIndex, m_monitorHelper, &RectHelper::addIndex);
0617             } else {
0618                 if (type == ParamType::KeyframeParam) {
0619                     QString paramName = m_model->data(index, AssetParameterModel::NameRole).toString();
0620                     if (paramName.contains(QLatin1String("Position X")) || paramName.contains(QLatin1String("Position Y")) ||
0621                         paramName.contains(QLatin1String("Size X")) || paramName.contains(QLatin1String("Size Y"))) {
0622                         Q_EMIT addIndex(index);
0623                     }
0624                 }
0625             }
0626         }
0627         double value = m_keyframes->getInterpolatedValue(getPosition(), index).toDouble();
0628         double min = m_model->data(index, AssetParameterModel::MinRole).toDouble();
0629         double max = m_model->data(index, AssetParameterModel::MaxRole).toDouble();
0630         double defaultValue = m_model->data(index, AssetParameterModel::DefaultRole).toDouble();
0631         int decimals = m_model->data(index, AssetParameterModel::DecimalsRole).toInt();
0632         double factor = m_model->data(index, AssetParameterModel::FactorRole).toDouble();
0633         factor = qFuzzyIsNull(factor) ? 1 : factor;
0634         auto doubleWidget = new DoubleWidget(name, value, min, max, factor, defaultValue, comment, -1, suffix, decimals,
0635                                              m_model->data(index, AssetParameterModel::OddRole).toBool(), this);
0636         connect(doubleWidget, &DoubleWidget::valueChanged, this, [this, index](double v) {
0637             Q_EMIT activateEffect();
0638             m_keyframes->updateKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), QVariant(v), index);
0639         });
0640         doubleWidget->setDragObjectName(QString::number(index.row()));
0641         paramWidget = doubleWidget;
0642     }
0643     if (paramWidget) {
0644         m_parameters[index] = paramWidget;
0645         if (labelWidget) {
0646             auto *hbox = new QHBoxLayout(this);
0647             hbox->setContentsMargins(0, 0, 0, 0);
0648             hbox->setSpacing(0);
0649             hbox->addWidget(labelWidget, 1);
0650             hbox->addWidget(paramWidget, 1);
0651             m_lay->addLayout(hbox);
0652         } else {
0653             m_lay->addWidget(paramWidget);
0654         }
0655         m_addedHeight += paramWidget->minimumHeight();
0656         setFixedHeight(m_baseHeight + m_addedHeight);
0657     }
0658 }
0659 
0660 void KeyframeWidget::slotInitMonitor(bool active)
0661 {
0662     connectMonitor(active);
0663     Monitor *monitor = pCore->getMonitor(m_model->monitorId);
0664     if (m_keyframeview) {
0665         m_keyframeview->initKeyframePos();
0666         connect(monitor, &Monitor::updateScene, m_keyframeview, &KeyframeView::slotModelChanged, Qt::UniqueConnection);
0667     }
0668 }
0669 
0670 void KeyframeWidget::connectMonitor(bool active)
0671 {
0672     if (m_monitorHelper) {
0673         if (m_model->isActive()) {
0674             connect(m_monitorHelper, &KeyframeMonitorHelper::updateKeyframeData, this, &KeyframeWidget::slotUpdateKeyframesFromMonitor, Qt::UniqueConnection);
0675             if (m_monitorHelper->connectMonitor(active)) {
0676                 slotRefreshParams();
0677             }
0678         } else {
0679             m_monitorHelper->connectMonitor(false);
0680             disconnect(m_monitorHelper, &KeyframeMonitorHelper::updateKeyframeData, this, &KeyframeWidget::slotUpdateKeyframesFromMonitor);
0681         }
0682     }
0683     Monitor *monitor = pCore->getMonitor(m_model->monitorId);
0684     if (active) {
0685         connect(monitor, &Monitor::seekToNextKeyframe, m_keyframeview, &KeyframeView::slotGoToNext, Qt::UniqueConnection);
0686         connect(monitor, &Monitor::seekToPreviousKeyframe, m_keyframeview, &KeyframeView::slotGoToPrev, Qt::UniqueConnection);
0687         connect(monitor, &Monitor::addRemoveKeyframe, m_keyframeview, &KeyframeView::slotAddRemove, Qt::UniqueConnection);
0688         connect(this, &KeyframeWidget::updateEffectKeyframe, monitor, &Monitor::setEffectKeyframe, Qt::DirectConnection);
0689         connect(monitor, &Monitor::seekToKeyframe, this, &KeyframeWidget::slotSeekToKeyframe, Qt::UniqueConnection);
0690     } else {
0691         disconnect(monitor, &Monitor::seekToNextKeyframe, m_keyframeview, &KeyframeView::slotGoToNext);
0692         disconnect(monitor, &Monitor::seekToPreviousKeyframe, m_keyframeview, &KeyframeView::slotGoToPrev);
0693         disconnect(monitor, &Monitor::addRemoveKeyframe, m_keyframeview, &KeyframeView::slotAddRemove);
0694         disconnect(this, &KeyframeWidget::updateEffectKeyframe, monitor, &Monitor::setEffectKeyframe);
0695         disconnect(monitor, &Monitor::seekToKeyframe, this, &KeyframeWidget::slotSeekToKeyframe);
0696     }
0697     for (const auto &w : m_parameters) {
0698         auto type = m_model->data(w.first, AssetParameterModel::TypeRole).value<ParamType>();
0699         if (type == ParamType::AnimatedRect) {
0700             (static_cast<GeometryWidget *>(w.second))->connectMonitor(active);
0701             break;
0702         }
0703     }
0704 }
0705 
0706 void KeyframeWidget::slotUpdateKeyframesFromMonitor(const QPersistentModelIndex &index, const QVariant &res)
0707 {
0708     Q_EMIT activateEffect();
0709     if (m_keyframes->isEmpty()) {
0710         GenTime pos(pCore->getItemIn(m_model->getOwnerId()) + m_time->getValue(), pCore->getCurrentFps());
0711         if (m_time->getValue() > 0) {
0712             // First add keyframe at start of the clip
0713             GenTime pos0(pCore->getItemIn(m_model->getOwnerId()), pCore->getCurrentFps());
0714             m_keyframes->addKeyframe(pos0, KeyframeType::Linear);
0715             m_keyframes->updateKeyframe(pos0, res, index);
0716             // For rotoscoping, don't add a second keyframe at cursor pos
0717             auto type = m_model->data(index, AssetParameterModel::TypeRole).value<ParamType>();
0718             if (type == ParamType::Roto_spline) {
0719                 return;
0720             }
0721         }
0722         // Next add keyframe at playhead position
0723         m_keyframes->addKeyframe(pos, KeyframeType::Linear);
0724         m_keyframes->updateKeyframe(pos, res, index);
0725     } else if (m_keyframes->hasKeyframe(getPosition()) || m_keyframes->singleKeyframe()) {
0726         GenTime pos(getPosition(), pCore->getCurrentFps());
0727         // Auto add keyframe only if there already is more than 1 keyframe
0728         if (!m_keyframes->singleKeyframe() && KdenliveSettings::autoKeyframe() && m_neededScene == MonitorSceneType::MonitorSceneRoto) {
0729             m_keyframes->addKeyframe(pos, KeyframeType::Linear);
0730         }
0731         m_keyframes->updateKeyframe(pos, res, index);
0732     } else {
0733         qDebug() << "==== NO KFR AT: " << getPosition();
0734     }
0735 }
0736 
0737 MonitorSceneType KeyframeWidget::requiredScene() const
0738 {
0739     qDebug() << "// // // RESULTING REQUIRED SCENE: " << m_neededScene;
0740     return m_neededScene;
0741 }
0742 
0743 bool KeyframeWidget::keyframesVisible() const
0744 {
0745     return m_keyframeview->isVisible();
0746 }
0747 
0748 void KeyframeWidget::showKeyframes(bool enable)
0749 {
0750     if (enable && m_toolbar->isVisible()) {
0751         return;
0752     }
0753     m_toolbar->setVisible(enable);
0754     m_keyframeview->setVisible(enable);
0755     m_time->setVisible(enable);
0756     setFixedHeight(m_addedHeight + (enable ? m_baseHeight : 0));
0757 }
0758 
0759 void KeyframeWidget::slotCopyKeyframes()
0760 {
0761     QJsonDocument effectDoc = m_model->toJson({}, false);
0762     if (effectDoc.isEmpty()) {
0763         return;
0764     }
0765     QClipboard *clipboard = QApplication::clipboard();
0766     clipboard->setText(QString(effectDoc.toJson()));
0767     pCore->displayMessage(i18n("Keyframes copied"), InformationMessage);
0768 }
0769 
0770 void KeyframeWidget::slotPasteKeyframeFromClipBoard()
0771 {
0772     QClipboard *clipboard = QApplication::clipboard();
0773     QString values = clipboard->text();
0774     auto json = QJsonDocument::fromJson(values.toUtf8());
0775     Fun undo = []() { return true; };
0776     Fun redo = []() { return true; };
0777     if (!json.isArray()) {
0778         pCore->displayMessage(i18n("No valid keyframe data in clipboard"), InformationMessage);
0779         return;
0780     }
0781     auto list = json.array();
0782     QMap<QString, QMap<int, QVariant>> storedValues;
0783     for (const auto &entry : qAsConst(list)) {
0784         if (!entry.isObject()) {
0785             qDebug() << "Warning : Skipping invalid marker data";
0786             continue;
0787         }
0788         auto entryObj = entry.toObject();
0789         if (!entryObj.contains(QLatin1String("name"))) {
0790             qDebug() << "Warning : Skipping invalid marker data (does not contain name)";
0791             continue;
0792         }
0793 
0794         ParamType kfrType = entryObj[QLatin1String("type")].toVariant().value<ParamType>();
0795         if (m_model->isAnimated(kfrType)) {
0796             QMap<int, QVariant> values;
0797             if (kfrType == ParamType::Roto_spline) {
0798                 auto value = entryObj.value(QLatin1String("value"));
0799                 if (value.isObject()) {
0800                     QJsonObject obj = value.toObject();
0801                     QStringList keys = obj.keys();
0802                     for (auto &k : keys) {
0803                         values.insert(k.toInt(), obj.value(k));
0804                     }
0805                 } else if (value.isArray()) {
0806                     auto list = value.toArray();
0807                     for (const auto &entry : qAsConst(list)) {
0808                         if (!entry.isObject()) {
0809                             qDebug() << "Warning : Skipping invalid category data";
0810                             continue;
0811                         }
0812                         QJsonObject obj = entry.toObject();
0813                         QStringList keys = obj.keys();
0814                         for (auto &k : keys) {
0815                             values.insert(k.toInt(), obj.value(k));
0816                         }
0817                     }
0818                 } else {
0819                     pCore->displayMessage(i18n("No valid keyframe data in clipboard"), InformationMessage);
0820                     qDebug() << "::: Invalid ROTO VALUE, ABORTING PASTE\n" << value;
0821                     return;
0822                 }
0823             } else {
0824                 const QString value = entryObj.value(QLatin1String("value")).toString();
0825                 if (value.isEmpty()) {
0826                     pCore->displayMessage(i18n("No valid keyframe data in clipboard"), InformationMessage);
0827                     qDebug() << "::: Invalid KFR VALUE, ABORTING PASTE\n" << value;
0828                     return;
0829                 }
0830                 const QStringList stringVals = value.split(QLatin1Char(';'), Qt::SkipEmptyParts);
0831                 for (auto &val : stringVals) {
0832                     int position = m_model->time_to_frames(val.section(QLatin1Char('='), 0, 0));
0833                     values.insert(position, val.section(QLatin1Char('='), 1));
0834                 }
0835             }
0836             storedValues.insert(entryObj[QLatin1String("name")].toString(), values);
0837         } else {
0838             const QString value = entryObj.value(QLatin1String("value")).toString();
0839             QMap<int, QVariant> values;
0840             values.insert(0, value);
0841             storedValues.insert(entryObj[QLatin1String("name")].toString(), values);
0842         }
0843     }
0844     int destPos = getPosition();
0845 
0846     std::vector<QPersistentModelIndex> indexes = m_keyframes->getIndexes();
0847     for (const auto &ix : indexes) {
0848         auto paramName = m_model->data(ix, AssetParameterModel::NameRole).toString();
0849         if (storedValues.contains(paramName)) {
0850             KeyframeModel *km = m_keyframes->getKeyModel(ix);
0851             QMap<int, QVariant> values = storedValues.value(paramName);
0852             int offset = values.keys().first();
0853             QMapIterator<int, QVariant> i(values);
0854             while (i.hasNext()) {
0855                 i.next();
0856                 km->addKeyframe(GenTime(destPos + i.key() - offset, pCore->getCurrentFps()), KeyframeType::Linear, i.value(), true, undo, redo);
0857             }
0858         } else {
0859             qDebug() << "::: NOT FOUND PARAM: " << paramName << " in list: " << storedValues.keys();
0860         }
0861     }
0862     pCore->pushUndo(undo, redo, i18n("Paste keyframe"));
0863 }
0864 
0865 void KeyframeWidget::slotCopySelectedKeyframes()
0866 {
0867     const QVector<int> results = m_keyframeview->selectedKeyframesIndexes();
0868     QJsonDocument effectDoc = m_model->toJson(results, false);
0869     if (effectDoc.isEmpty()) {
0870         pCore->displayMessage(i18n("Cannot copy current parameter values"), InformationMessage);
0871         return;
0872     }
0873     QClipboard *clipboard = QApplication::clipboard();
0874     clipboard->setText(QString(effectDoc.toJson()));
0875     pCore->displayMessage(i18n("Current values copied"), InformationMessage);
0876 }
0877 
0878 void KeyframeWidget::slotCopyValueAtCursorPos()
0879 {
0880     QJsonDocument effectDoc = m_model->valueAsJson(getPosition(), false);
0881     if (effectDoc.isEmpty()) {
0882         return;
0883     }
0884     QClipboard *clipboard = QApplication::clipboard();
0885     clipboard->setText(QString(effectDoc.toJson()));
0886     pCore->displayMessage(i18n("Current values copied"), InformationMessage);
0887 }
0888 
0889 void KeyframeWidget::slotImportKeyframes()
0890 {
0891     QClipboard *clipboard = QApplication::clipboard();
0892     QString values = clipboard->text();
0893     QList<QPersistentModelIndex> indexes;
0894     for (const auto &w : m_parameters) {
0895         indexes << w.first;
0896     }
0897     if (m_neededScene == MonitorSceneRoto) {
0898         indexes << m_monitorHelper->getIndexes();
0899     }
0900     QPointer<KeyframeImport> import = new KeyframeImport(values, m_model, indexes, m_model->data(m_index, AssetParameterModel::ParentInRole).toInt(),
0901                                                          m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(), this);
0902     import->show();
0903     connect(import, &KeyframeImport::updateQmlView, this, &KeyframeWidget::slotRefreshParams);
0904 }
0905 
0906 void KeyframeWidget::slotRemoveNextKeyframes()
0907 {
0908     int pos = m_time->getValue() + m_model->data(m_index, AssetParameterModel::ParentInRole).toInt();
0909     m_keyframes->removeNextKeyframes(GenTime(pos, pCore->getCurrentFps()));
0910 }
0911 
0912 void KeyframeWidget::slotSeekToKeyframe(int ix)
0913 {
0914     int pos = m_keyframes->getPosAtIndex(ix).frames(pCore->getCurrentFps());
0915     slotSetPosition(pos, true);
0916 }
0917 
0918 void KeyframeWidget::sendStandardCommand(int command)
0919 {
0920     switch (command) {
0921     case KStandardAction::Copy:
0922         m_copyAction->trigger();
0923         break;
0924     case KStandardAction::Paste:
0925         m_pasteAction->trigger();
0926         break;
0927     default:
0928         qDebug() << ":::: UNKNOWN COMMAND: " << command;
0929         break;
0930     }
0931 }