File indexing completed on 2024-05-19 04:54:25

0001 /*
0002     SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "effectstackview.hpp"
0007 #include "assets/assetlist/view/asseticonprovider.hpp"
0008 #include "assets/assetpanel.hpp"
0009 #include "assets/view/assetparameterview.hpp"
0010 #include "builtstack.hpp"
0011 #include "collapsibleeffectview.hpp"
0012 #include "core.h"
0013 #include "effects/effectsrepository.hpp"
0014 #include "effects/effectstack/model/effectitemmodel.hpp"
0015 #include "effects/effectstack/model/effectstackmodel.hpp"
0016 #include "kdenlivesettings.h"
0017 #include "monitor/monitor.h"
0018 
0019 #include <QDir>
0020 #include <QDrag>
0021 #include <QFormLayout>
0022 #include <QDragEnterEvent>
0023 #include <QFontDatabase>
0024 #include <QInputDialog>
0025 #include <QMimeData>
0026 #include <QMutexLocker>
0027 #include <QPainter>
0028 #include <QScrollBar>
0029 #include <QTreeView>
0030 #include <QVBoxLayout>
0031 
0032 #include "utils/KMessageBox_KdenliveCompat.h"
0033 #include <KMessageBox>
0034 #include <utility>
0035 
0036 int dragRow = -1;
0037 
0038 WidgetDelegate::WidgetDelegate(QObject *parent)
0039     : QStyledItemDelegate(parent)
0040 {
0041 }
0042 
0043 QSize WidgetDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0044 {
0045     QSize s = QStyledItemDelegate::sizeHint(option, index);
0046     if (m_height.contains(index)) {
0047         s.setHeight(m_height.value(index));
0048     }
0049     return s;
0050 }
0051 
0052 void WidgetDelegate::setHeight(const QModelIndex &index, int height)
0053 {
0054     m_height[index] = height;
0055     Q_EMIT sizeHintChanged(index);
0056 }
0057 
0058 int WidgetDelegate::height(const QModelIndex &index) const
0059 {
0060     return m_height.value(index);
0061 }
0062 
0063 void WidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0064 {
0065     QStyleOptionViewItem opt(option);
0066     initStyleOption(&opt, index);
0067     QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
0068     if (index.row() == dragRow && !opt.rect.isNull()) {
0069         QPen pen(QPalette().highlight().color());
0070         pen.setWidth(4);
0071         painter->setPen(pen);
0072         painter->drawLine(opt.rect.topLeft(), opt.rect.topRight());
0073     }
0074 
0075     style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
0076 }
0077 
0078 EffectStackView::EffectStackView(AssetPanel *parent)
0079     : QWidget(parent)
0080     , m_model(nullptr)
0081     , m_thumbnailer(new AssetIconProvider(true, this))
0082 {
0083     m_lay = new QVBoxLayout(this);
0084     m_lay->setContentsMargins(0, 0, 0, 0);
0085     m_lay->setSpacing(0);
0086     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0087     setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
0088     setAcceptDrops(true);
0089     /*m_builtStack = new BuiltStack(parent);
0090     m_builtStack->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
0091     m_lay->addWidget(m_builtStack);
0092     m_builtStack->setVisible(KdenliveSettings::showbuiltstack());*/
0093     m_effectsTree = new QTreeView(this);
0094     m_effectsTree->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum);
0095     m_effectsTree->setHeaderHidden(true);
0096     m_effectsTree->setRootIsDecorated(false);
0097     m_effectsTree->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0098     QString style = QStringLiteral("QTreeView {border: none;}");
0099     // m_effectsTree->viewport()->setAutoFillBackground(false);
0100     m_effectsTree->setStyleSheet(style);
0101     m_effectsTree->setVisible(!KdenliveSettings::showbuiltstack());
0102     m_effectsTree->setItemDelegateForColumn(0, new WidgetDelegate(this));
0103     m_lay->addWidget(m_effectsTree);
0104     m_lay->addStretch(10);
0105 
0106     m_scrollTimer.setSingleShot(true);
0107     m_scrollTimer.setInterval(250);
0108     connect(&m_scrollTimer, &QTimer::timeout, this, &EffectStackView::checkScrollBar);
0109 
0110     m_timerHeight.setSingleShot(true);
0111     m_timerHeight.setInterval(50);
0112 }
0113 
0114 EffectStackView::~EffectStackView()
0115 {
0116     delete m_thumbnailer;
0117 }
0118 
0119 void EffectStackView::dragEnterEvent(QDragEnterEvent *event)
0120 {
0121     if (event->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) {
0122         if (event->source() == this) {
0123             event->setDropAction(Qt::MoveAction);
0124         } else {
0125             event->setDropAction(Qt::CopyAction);
0126         }
0127         event->setAccepted(true);
0128     } else {
0129         event->setAccepted(false);
0130     }
0131 }
0132 
0133 void EffectStackView::dragLeaveEvent(QDragLeaveEvent *event)
0134 {
0135     event->accept();
0136     dragRow = -1;
0137     repaint();
0138 }
0139 
0140 void EffectStackView::dragMoveEvent(QDragMoveEvent *event)
0141 {
0142     dragRow = m_model->rowCount();
0143     for (int i = 0; i < m_model->rowCount(); i++) {
0144         auto item = m_model->getEffectStackRow(i);
0145         if (item->childCount() > 0) {
0146             // TODO: group
0147             continue;
0148         }
0149         std::shared_ptr<EffectItemModel> eff = std::static_pointer_cast<EffectItemModel>(item);
0150         QModelIndex ix = m_model->getIndexFromItem(eff);
0151         QWidget *w = m_effectsTree->indexWidget(ix);
0152 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0153         if (w && w->geometry().contains(event->pos())) {
0154 #else
0155         if (w && w->geometry().contains(event->position().toPoint())) {
0156 #endif
0157             if (event->source() == this) {
0158                 QString sourceData = event->mimeData()->data(QStringLiteral("kdenlive/effectsource"));
0159                 int oldRow = sourceData.section(QLatin1Char(','), 2, 2).toInt();
0160                 if (i == oldRow + 1) {
0161                     dragRow = -1;
0162                     break;
0163                 }
0164             }
0165             dragRow = i;
0166             break;
0167         }
0168     }
0169     if (dragRow == m_model->rowCount() && event->source() == this) {
0170         QString sourceData = event->mimeData()->data(QStringLiteral("kdenlive/effectsource"));
0171         int oldRow = sourceData.section(QLatin1Char(','), 2, 2).toInt();
0172         if (dragRow == oldRow + 1) {
0173             dragRow = -1;
0174         }
0175     }
0176     repaint();
0177 }
0178 
0179 void EffectStackView::dropEvent(QDropEvent *event)
0180 {
0181     qDebug() << ":::: DROP BEGIN EVENT....";
0182     if (dragRow < 0) {
0183         return;
0184     }
0185     QString effectId = event->mimeData()->data(QStringLiteral("kdenlive/effect"));
0186     if (event->source() == this) {
0187         QString sourceData = event->mimeData()->data(QStringLiteral("kdenlive/effectsource"));
0188         int oldRow = sourceData.section(QLatin1Char(','), 2, 2).toInt();
0189         qDebug() << "// MOVING EFFECT FROM : " << oldRow << " TO " << dragRow;
0190         if (dragRow == oldRow || (dragRow == m_model->rowCount() && oldRow == dragRow - 1)) {
0191             return;
0192         }
0193         QMetaObject::invokeMethod(m_model.get(), "moveEffectByRow", Qt::QueuedConnection, Q_ARG(int, dragRow), Q_ARG(int, oldRow));
0194     } else {
0195         bool added = false;
0196         if (dragRow < m_model->rowCount()) {
0197             if (m_model->appendEffect(effectId) && m_model->rowCount() > 0) {
0198                 added = true;
0199                 m_model->moveEffect(dragRow, m_model->getEffectStackRow(m_model->rowCount() - 1));
0200             }
0201         } else {
0202             if (m_model->appendEffect(effectId) && m_model->rowCount() > 0) {
0203                 added = true;
0204                 m_model->setActiveEffect(m_model->rowCount() - 1);
0205             }
0206         }
0207         if (added) {
0208             m_scrollTimer.start();
0209         }
0210     }
0211     dragRow = -1;
0212     event->acceptProposedAction();
0213     qDebug() << ":::: DROP END EVENT....";
0214 }
0215 
0216 void EffectStackView::paintEvent(QPaintEvent *event)
0217 {
0218     if (dragRow == m_model->rowCount()) {
0219         QWidget::paintEvent(event);
0220         QPainter p(this);
0221         QPen pen(palette().highlight().color());
0222         pen.setWidth(4);
0223         p.setPen(pen);
0224         p.drawLine(0, m_effectsTree->height(), width(), m_effectsTree->height());
0225     }
0226 }
0227 
0228 void EffectStackView::setModel(std::shared_ptr<EffectStackModel> model, const QSize frameSize)
0229 {
0230     qDebug() << "MUTEX LOCK!!!!!!!!!!!! setmodel";
0231     m_mutex.lock();
0232     QItemSelectionModel *m = m_effectsTree->selectionModel();
0233     unsetModel(false);
0234     m_effectsTree->setModel(nullptr);
0235     m_model.reset();
0236     if (m) {
0237         delete m;
0238     }
0239     m_effectsTree->setFixedHeight(0);
0240     m_model = std::move(model);
0241     m_sourceFrameSize = frameSize;
0242     m_effectsTree->setModel(m_model.get());
0243     m_effectsTree->setColumnHidden(1, true);
0244     m_effectsTree->setAcceptDrops(true);
0245     m_effectsTree->setDragDropMode(QAbstractItemView::DragDrop);
0246     m_effectsTree->setDragEnabled(true);
0247     m_effectsTree->setUniformRowHeights(false);
0248     m_mutex.unlock();
0249     qDebug() << "MUTEX UNLOCK!!!!!!!!!!!! setmodel";
0250     loadEffects();
0251     m_scrollTimer.start();
0252     connect(m_model.get(), &EffectStackModel::dataChanged, this, &EffectStackView::refresh);
0253     connect(m_model.get(), &EffectStackModel::enabledStateChanged, this, &EffectStackView::changeEnabledState);
0254     connect(m_model.get(), &EffectStackModel::currentChanged, this, &EffectStackView::activateEffect, Qt::DirectConnection);
0255     connect(this, &EffectStackView::removeCurrentEffect, m_model.get(), &EffectStackModel::removeCurrentEffect);
0256     // m_builtStack->setModel(model, stackOwner());
0257 }
0258 
0259 void EffectStackView::activateEffect(const QModelIndex &ix, bool active)
0260 {
0261     m_effectsTree->setCurrentIndex(ix);
0262     auto *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(ix));
0263     if (w) {
0264         w->slotActivateEffect(active);
0265     }
0266 }
0267 
0268 void EffectStackView::changeEnabledState()
0269 {
0270     int max = m_model->rowCount();
0271     int currentActive = m_model->getActiveEffect();
0272     if (currentActive < max && currentActive > -1) {
0273         auto item = m_model->getEffectStackRow(currentActive);
0274         QModelIndex ix = m_model->getIndexFromItem(item);
0275         auto *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(ix));
0276         w->updateScene();
0277     }
0278     Q_EMIT updateEnabledState();
0279 }
0280 
0281 void EffectStackView::loadEffects()
0282 {
0283     // QMutexLocker lock(&m_mutex);
0284     int max = m_model->rowCount();
0285     qDebug() << "MUTEX LOCK!!!!!!!!!!!! loadEffects COUNT: " << max;
0286     if (max == 0) {
0287         // blank stack
0288         ObjectId item = m_model->getOwnerId();
0289         pCore->getMonitor(item.type == KdenliveObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor)
0290             ->slotShowEffectScene(MonitorSceneDefault);
0291         updateTreeHeight();
0292         return;
0293     }
0294     int active = qBound(0, m_model->getActiveEffect(), max - 1);
0295     bool hasLift = false;
0296     QModelIndex activeIndex;
0297     connect(&m_timerHeight, &QTimer::timeout, this, &EffectStackView::updateTreeHeight, Qt::UniqueConnection);
0298     for (int i = 0; i < max; i++) {
0299         std::shared_ptr<AbstractEffectItem> item = m_model->getEffectStackRow(i);
0300         if (item->childCount() > 0) {
0301             // group, create sub stack
0302             continue;
0303         }
0304         std::shared_ptr<EffectItemModel> effectModel = std::static_pointer_cast<EffectItemModel>(item);
0305         CollapsibleEffectView *view = nullptr;
0306         // We need to rebuild the effect view
0307         if (effectModel->getAssetId() == QLatin1String("lift_gamma_gain")) {
0308             hasLift = true;
0309         }
0310         const QString assetName = EffectsRepository::get()->getName(effectModel->getAssetId());
0311         view = new CollapsibleEffectView(assetName, effectModel, m_sourceFrameSize, this);
0312         connect(view, &CollapsibleEffectView::deleteEffect, m_model.get(), &EffectStackModel::removeEffect);
0313         connect(view, &CollapsibleEffectView::moveEffect, m_model.get(), &EffectStackModel::moveEffect);
0314         connect(view, &CollapsibleEffectView::reloadEffect, this, &EffectStackView::reloadEffect);
0315         connect(view, &CollapsibleEffectView::switchHeight, this, &EffectStackView::slotAdjustDelegate, Qt::DirectConnection);
0316         connect(view, &CollapsibleEffectView::startDrag, this, &EffectStackView::slotStartDrag);
0317         connect(view, &CollapsibleEffectView::activateEffect, this, [=](int row) { m_model->setActiveEffect(row); });
0318         connect(view, &CollapsibleEffectView::createGroup, m_model.get(), &EffectStackModel::slotCreateGroup);
0319         connect(view, &CollapsibleEffectView::showEffectZone, pCore.get(), &Core::showEffectZone);
0320         connect(this, &EffectStackView::blockWheelEvent, view, &CollapsibleEffectView::blockWheelEvent);
0321         connect(view, &CollapsibleEffectView::seekToPos, this, [this](int pos) {
0322             // at this point, the effects returns a pos relative to the clip. We need to convert it to a global time
0323             int clipIn = pCore->getItemPosition(m_model->getOwnerId());
0324             Q_EMIT seekToPos(pos + clipIn);
0325         });
0326         connect(this, &EffectStackView::switchCollapsedView, view, &CollapsibleEffectView::switchCollapsed);
0327 
0328         connect(pCore.get(), &Core::updateEffectZone, view, [=](const QPoint p, bool withUndo) {
0329             // Update current effect zone
0330             if (view->isActive()) {
0331                 view->updateInOut({p.x(), p.y()}, withUndo);
0332             }
0333         });
0334         QModelIndex ix = m_model->getIndexFromItem(effectModel);
0335         if (active == i) {
0336             effectModel->setActive(true);
0337             activeIndex = ix;
0338         }
0339         m_effectsTree->setIndexWidget(ix, view);
0340 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0341         auto *del = static_cast<WidgetDelegate *>(m_effectsTree->itemDelegate(ix));
0342 #else
0343         auto *del = static_cast<WidgetDelegate *>(m_effectsTree->itemDelegateForIndex(ix));
0344 #endif
0345         del->setHeight(ix, view->height());
0346         view->buttonUp->setEnabled(i > 0);
0347         view->buttonDown->setEnabled(i < max - 1);
0348     }
0349     if (!hasLift) {
0350         updateTreeHeight();
0351     }
0352     if (activeIndex.isValid()) {
0353         m_effectsTree->setCurrentIndex(activeIndex);
0354         auto *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(activeIndex));
0355         if (w) {
0356             w->slotActivateEffect(true);
0357         }
0358     }
0359     if (hasLift) {
0360         // Some effects have a complex timed layout, so we need to wait a bit before getting the correct position for the effect
0361         QTimer::singleShot(100, this, &EffectStackView::slotFocusEffect);
0362     } else {
0363         slotFocusEffect();
0364     }
0365     qDebug() << "MUTEX UNLOCK!!!!!!!!!!!! loadEffects";
0366 }
0367 
0368 void EffectStackView::updateTreeHeight()
0369 {
0370     // For some reason, the treeview height does not update correctly, so enforce it
0371     QMutexLocker lk(&m_mutex);
0372     if (!m_model) {
0373         return;
0374     }
0375     int totalHeight = 0;
0376     for (int j = 0; j < m_model->rowCount(); j++) {
0377         std::shared_ptr<AbstractEffectItem> item2 = m_model->getEffectStackRow(j);
0378         std::shared_ptr<EffectItemModel> eff = std::static_pointer_cast<EffectItemModel>(item2);
0379         QModelIndex idx = m_model->getIndexFromItem(eff);
0380         auto w = m_effectsTree->indexWidget(idx);
0381         if (w) {
0382             totalHeight += w->height();
0383         }
0384     }
0385     if (totalHeight != m_effectsTree->height()) {
0386         m_effectsTree->setFixedHeight(totalHeight);
0387         m_scrollTimer.start();
0388     }
0389 }
0390 
0391 void EffectStackView::slotStartDrag(const QPixmap pix, const QString assetId, ObjectId sourceObject, int row)
0392 {
0393     auto *drag = new QDrag(this);
0394     drag->setPixmap(pix);
0395     auto *mime = new QMimeData;
0396     mime->setData(QStringLiteral("kdenlive/effect"), assetId.toUtf8());
0397     // TODO this will break if source effect is not on the stack of a timeline clip
0398     QByteArray effectSource;
0399     effectSource += QString::number(int(sourceObject.type)).toUtf8();
0400     effectSource += ',';
0401     effectSource += QString::number(int(sourceObject.itemId)).toUtf8();
0402     effectSource += ',';
0403     effectSource += QString::number(row).toUtf8();
0404     effectSource += ',';
0405     if (sourceObject.type == KdenliveObjectType::BinClip) {
0406         effectSource += QByteArray();
0407     } else {
0408         // Keep a reference to the timeline model
0409         effectSource += pCore->currentTimelineId().toString().toUtf8();
0410     }
0411     mime->setData(QStringLiteral("kdenlive/effectsource"), effectSource);
0412 
0413     // Assign ownership of the QMimeData object to the QDrag object.
0414     drag->setMimeData(mime);
0415     // Start the drag and drop operation
0416     drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction);
0417 }
0418 
0419 void EffectStackView::slotAdjustDelegate(const std::shared_ptr<EffectItemModel> &effectModel, int newHeight)
0420 {
0421     if (!m_model) {
0422         return;
0423     }
0424     QModelIndex ix = m_model->getIndexFromItem(effectModel);
0425     if (ix.isValid()) {
0426 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0427         auto *del = static_cast<WidgetDelegate *>(m_effectsTree->itemDelegate(ix));
0428 #else
0429         auto *del = static_cast<WidgetDelegate *>(m_effectsTree->itemDelegateForIndex(ix));
0430 #endif
0431         if (del) {
0432             del->setHeight(ix, newHeight);
0433             m_timerHeight.start();
0434         }
0435     }
0436 }
0437 
0438 void EffectStackView::resizeEvent(QResizeEvent *event)
0439 {
0440     QWidget::resizeEvent(event);
0441     m_scrollTimer.start();
0442 }
0443 
0444 void EffectStackView::refresh(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
0445 {
0446     Q_UNUSED(roles)
0447     if (!topLeft.isValid() || !bottomRight.isValid()) {
0448         loadEffects();
0449         return;
0450     }
0451     for (int i = topLeft.row(); i <= bottomRight.row(); ++i) {
0452         for (int j = topLeft.column(); j <= bottomRight.column(); ++j) {
0453             CollapsibleEffectView *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(m_model->index(i, j, topLeft.parent())));
0454             if (w) {
0455                 Q_EMIT w->refresh();
0456             }
0457         }
0458     }
0459 }
0460 
0461 void EffectStackView::unsetModel(bool reset)
0462 {
0463     // Release ownership of smart pointer
0464     Kdenlive::MonitorId id = Kdenlive::NoMonitor;
0465     if (m_model) {
0466         ObjectId item = m_model->getOwnerId();
0467         pCore->showEffectZone(item, {0, 0}, false);
0468         id = item.type == KdenliveObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor;
0469         disconnect(m_model.get(), &EffectStackModel::dataChanged, this, &EffectStackView::refresh);
0470         disconnect(m_model.get(), &EffectStackModel::enabledStateChanged, this, &EffectStackView::changeEnabledState);
0471         disconnect(this, &EffectStackView::removeCurrentEffect, m_model.get(), &EffectStackModel::removeCurrentEffect);
0472         disconnect(m_model.get(), &EffectStackModel::currentChanged, this, &EffectStackView::activateEffect);
0473         disconnect(&m_timerHeight, &QTimer::timeout, this, &EffectStackView::updateTreeHeight);
0474         Q_EMIT pCore->disconnectEffectStack();
0475         if (reset) {
0476             QMutexLocker lock(&m_mutex);
0477             m_model.reset();
0478             m_effectsTree->setModel(nullptr);
0479         }
0480         pCore->getMonitor(id)->slotShowEffectScene(MonitorSceneDefault);
0481     }
0482 }
0483 
0484 ObjectId EffectStackView::stackOwner() const
0485 {
0486     if (m_model) {
0487         return m_model->getOwnerId();
0488     }
0489     return ObjectId();
0490 }
0491 
0492 bool EffectStackView::addEffect(const QString &effectId)
0493 {
0494     if (m_model) {
0495         return m_model->appendEffect(effectId, true);
0496     }
0497     return false;
0498 }
0499 
0500 bool EffectStackView::isEmpty() const
0501 {
0502     return m_model == nullptr ? true : m_model->rowCount() == 0;
0503 }
0504 
0505 void EffectStackView::enableStack(bool enable)
0506 {
0507     if (m_model) {
0508         m_model->setEffectStackEnabled(enable);
0509     }
0510 }
0511 
0512 bool EffectStackView::isStackEnabled() const
0513 {
0514     if (m_model) {
0515         return m_model->isStackEnabled();
0516     }
0517     return false;
0518 }
0519 
0520 void EffectStackView::switchCollapsed()
0521 {
0522     if (m_model) {
0523         int max = m_model->rowCount();
0524         int active = qBound(0, m_model->getActiveEffect(), max - 1);
0525         Q_EMIT switchCollapsedView(active);
0526     }
0527 }
0528 
0529 void EffectStackView::slotFocusEffect()
0530 {
0531     Q_EMIT scrollView(m_effectsTree->visualRect(m_effectsTree->currentIndex()));
0532 }
0533 
0534 void EffectStackView::slotSaveStack()
0535 {
0536     if (m_model->rowCount() == 1) {
0537         int currentActive = m_model->getActiveEffect();
0538         if (currentActive > -1) {
0539             auto item = m_model->getEffectStackRow(currentActive);
0540             QModelIndex ix = m_model->getIndexFromItem(item);
0541             auto *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(ix));
0542             w->slotSaveEffect();
0543             return;
0544         }
0545     }
0546     if (m_model->rowCount() <= 1) {
0547         KMessageBox::error(this, i18n("No effect selected."));
0548         return;
0549     }
0550     QDialog dialog(this);
0551     QFormLayout form(&dialog);
0552 
0553     dialog.setWindowTitle(i18nc("@title:window", "Save current Effect Stack"));
0554 
0555     auto *stackName = new QLineEdit(&dialog);
0556     auto *stackDescription = new QTextEdit(&dialog);
0557     
0558     form.addRow(i18n("Name for saved stack:"), stackName);
0559     form.addRow(i18n("Description:"), stackDescription);
0560 
0561 
0562     QDialogButtonBox buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
0563     form.addRow(&buttonBox);
0564 
0565     QObject::connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
0566     QObject::connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
0567     
0568     if (dialog.exec() == QDialog::Accepted) {
0569         QString name = stackName->text();
0570         QString description = stackDescription->toPlainText();
0571         if (name.trimmed().isEmpty()) {
0572             KMessageBox::error(this, i18n("No name provided, effect stack not saved."));
0573             return;
0574         }
0575 
0576         QString effectfilename = name + QStringLiteral(".xml");
0577 
0578         if (description.trimmed().isEmpty()) {
0579             if (KMessageBox::questionTwoActions(this, i18n("No description provided. \nSave effect with no description?"), {}, KStandardGuiItem::save(),
0580                                                 KStandardGuiItem::dontSave()) == KMessageBox::SecondaryAction) {
0581                 return;
0582             }
0583         }
0584 
0585         QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/"));
0586         if (!dir.exists()) {
0587             dir.mkpath(QStringLiteral("."));
0588         }
0589 
0590         if (dir.exists(effectfilename)) {
0591             if (KMessageBox::questionTwoActions(this, i18n("File %1 already exists.\nDo you want to overwrite it?", effectfilename), {},
0592                                                 KStandardGuiItem::overwrite(), KStandardGuiItem::cancel()) == KMessageBox::SecondaryAction) {
0593                 return;
0594             }
0595         }
0596 
0597         QDomDocument doc;
0598         
0599         QDomElement effect = doc.createElement(QStringLiteral("effectgroup"));
0600         effect.setAttribute(QStringLiteral("id"), name);
0601         
0602         QDomElement describtionNode = doc.createElement(QString("description"));
0603         QDomText descriptionText = doc.createTextNode(description);
0604         describtionNode.appendChild(descriptionText);
0605         
0606         effect.appendChild(describtionNode);
0607         effect.setAttribute(QStringLiteral("description"), description);
0608 
0609         auto item = m_model->getEffectStackRow(0);
0610         if (item->isAudio()) {
0611             effect.setAttribute(QStringLiteral("type"), QStringLiteral("customAudio"));
0612         }
0613         effect.setAttribute(QStringLiteral("parentIn"), pCore->getItemIn(m_model->getOwnerId()));
0614         doc.appendChild(effect);
0615         for (int i = 0; i <= m_model->rowCount(); ++i) {
0616             CollapsibleEffectView *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(m_model->index(i, 0, QModelIndex())));
0617             if (w) {
0618                 effect.appendChild(doc.importNode(w->toXml().documentElement(), true));
0619             }
0620         }
0621         QFile file(dir.absoluteFilePath(effectfilename));
0622         if (file.open(QFile::WriteOnly | QFile::Truncate)) {
0623             QTextStream out(&file);
0624     #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0625             out.setCodec("UTF-8");
0626     #endif
0627             out << doc.toString();
0628         } else {
0629             KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", file.fileName()));
0630         }
0631         file.close();
0632         Q_EMIT reloadEffect(dir.absoluteFilePath(effectfilename));
0633     }
0634 }
0635 
0636 /*
0637 void EffectStackView::switchBuiltStack(bool show)
0638 {
0639     m_builtStack->setVisible(show);
0640     m_effectsTree->setVisible(!show);
0641     KdenliveSettings::setShowbuiltstack(show);
0642 }
0643 */
0644 
0645 void EffectStackView::slotGoToKeyframe(bool next)
0646 {
0647     int max = m_model->rowCount();
0648     int currentActive = m_model->getActiveEffect();
0649     if (currentActive < max && currentActive > -1) {
0650         auto item = m_model->getEffectStackRow(currentActive);
0651         QModelIndex ix = m_model->getIndexFromItem(item);
0652         auto *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(ix));
0653         if (next) {
0654             w->slotNextKeyframe();
0655         } else {
0656             w->slotPreviousKeyframe();
0657         }
0658     }
0659 }
0660 
0661 void EffectStackView::addRemoveKeyframe()
0662 {
0663     int max = m_model->rowCount();
0664     int currentActive = m_model->getActiveEffect();
0665     if (currentActive < max && currentActive > -1) {
0666         auto item = m_model->getEffectStackRow(currentActive);
0667         QModelIndex ix = m_model->getIndexFromItem(item);
0668         auto *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(ix));
0669         w->addRemoveKeyframe();
0670     }
0671 }
0672 
0673 void EffectStackView::sendStandardCommand(int command)
0674 {
0675     int max = m_model->rowCount();
0676     int currentActive = m_model->getActiveEffect();
0677     if (currentActive < max && currentActive > -1) {
0678         auto item = m_model->getEffectStackRow(currentActive);
0679         QModelIndex ix = m_model->getIndexFromItem(item);
0680         auto *w = static_cast<CollapsibleEffectView *>(m_effectsTree->indexWidget(ix));
0681         w->sendStandardCommand(command);
0682     }
0683 }