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

0001 /*
0002     SPDX-FileCopyrightText: 2017 Nicolas Carion
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 #include "effectstackmodel.hpp"
0006 #include "assets/keyframes/model/keyframemodellist.hpp"
0007 #include "core.h"
0008 #include "doc/docundostack.hpp"
0009 #include "effectgroupmodel.hpp"
0010 #include "effectitemmodel.hpp"
0011 #include "effects/effectsrepository.hpp"
0012 #include "macros.hpp"
0013 #include "mainwindow.h"
0014 #include "timeline2/model/timelinemodel.hpp"
0015 #include <profiles/profilemodel.hpp>
0016 #include <stack>
0017 #include <utility>
0018 #include <vector>
0019 
0020 EffectStackModel::EffectStackModel(std::weak_ptr<Mlt::Service> service, ObjectId ownerId, std::weak_ptr<DocUndoStack> undo_stack)
0021     : AbstractTreeModel()
0022     , m_masterService(std::move(service))
0023     , m_effectStackEnabled(true)
0024     , m_ownerId(std::move(ownerId))
0025     , m_undoStack(std::move(undo_stack))
0026     , m_lock(QReadWriteLock::Recursive)
0027     , m_loadingExisting(false)
0028 {
0029 }
0030 
0031 std::shared_ptr<EffectStackModel> EffectStackModel::construct(std::weak_ptr<Mlt::Service> service, ObjectId ownerId, std::weak_ptr<DocUndoStack> undo_stack)
0032 {
0033     std::shared_ptr<EffectStackModel> self(new EffectStackModel(std::move(service), ownerId, std::move(undo_stack)));
0034     self->rootItem = EffectGroupModel::construct(QStringLiteral("root"), self, true);
0035     return self;
0036 }
0037 
0038 void EffectStackModel::resetService(std::weak_ptr<Mlt::Service> service)
0039 {
0040     QWriteLocker locker(&m_lock);
0041     m_masterService = std::move(service);
0042     m_childServices.clear();
0043     // replant all effects in new service
0044     for (int i = 0; i < rootItem->childCount(); ++i) {
0045         std::static_pointer_cast<EffectItemModel>(rootItem->child(i))->plant(m_masterService);
0046     }
0047 }
0048 
0049 void EffectStackModel::addService(std::weak_ptr<Mlt::Service> service)
0050 {
0051     QWriteLocker locker(&m_lock);
0052     m_childServices.emplace_back(std::move(service));
0053     for (int i = 0; i < rootItem->childCount(); ++i) {
0054         std::static_pointer_cast<EffectItemModel>(rootItem->child(i))->plantClone(m_childServices.back());
0055     }
0056 }
0057 
0058 void EffectStackModel::loadService(std::weak_ptr<Mlt::Service> service)
0059 {
0060     QWriteLocker locker(&m_lock);
0061     m_childServices.emplace_back(std::move(service));
0062     for (int i = 0; i < rootItem->childCount(); ++i) {
0063         std::static_pointer_cast<EffectItemModel>(rootItem->child(i))->loadClone(m_childServices.back());
0064     }
0065 }
0066 
0067 void EffectStackModel::removeService(const std::shared_ptr<Mlt::Service> &service)
0068 {
0069     QWriteLocker locker(&m_lock);
0070     std::vector<int> to_delete;
0071     for (int i = int(m_childServices.size()) - 1; i >= 0; --i) {
0072         auto ptr = m_childServices[uint(i)].lock();
0073         if (ptr && service->get_int("_childid") == ptr->get_int("_childid")) {
0074             for (int j = 0; j < rootItem->childCount(); ++j) {
0075                 std::static_pointer_cast<EffectItemModel>(rootItem->child(j))->unplantClone(ptr);
0076             }
0077             to_delete.push_back(i);
0078         }
0079     }
0080     for (int i : to_delete) {
0081         m_childServices.erase(m_childServices.begin() + i);
0082     }
0083 }
0084 
0085 void EffectStackModel::removeCurrentEffect()
0086 {
0087     int ix = getActiveEffect();
0088     if (ix < 0 || ix >= rootItem->childCount()) {
0089         return;
0090     }
0091     std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
0092     if (effect) {
0093         removeEffect(effect);
0094     }
0095 }
0096 
0097 void EffectStackModel::removeAllEffects(Fun &undo, Fun &redo)
0098 {
0099     QWriteLocker locker(&m_lock);
0100     int current = getActiveEffect();
0101     while (rootItem->childCount() > 0) {
0102         std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(0));
0103         int parentId = -1;
0104         if (auto ptr = effect->parentItem().lock()) parentId = ptr->getId();
0105         Fun local_undo = addItem_lambda(effect, parentId);
0106         Fun local_redo = removeItem_lambda(effect->getId());
0107         local_redo();
0108         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
0109     }
0110     std::unordered_set<int> fadeIns = m_fadeIns;
0111     std::unordered_set<int> fadeOuts = m_fadeOuts;
0112     Fun undo_current = [this, current, fadeIns, fadeOuts]() {
0113         if (auto srv = m_masterService.lock()) {
0114             srv->set("kdenlive:activeeffect", current);
0115         }
0116         m_fadeIns = fadeIns;
0117         m_fadeOuts = fadeOuts;
0118         QVector<int> roles = {TimelineModel::EffectNamesRole};
0119         if (!m_fadeIns.empty()) {
0120             roles << TimelineModel::FadeInRole;
0121         }
0122         if (!m_fadeOuts.empty()) {
0123             roles << TimelineModel::FadeOutRole;
0124         }
0125         Q_EMIT dataChanged(QModelIndex(), QModelIndex(), roles);
0126         pCore->updateItemKeyframes(m_ownerId);
0127         return true;
0128     };
0129     Fun redo_current = [this]() {
0130         if (auto srv = m_masterService.lock()) {
0131             srv->set("kdenlive:activeeffect", -1);
0132         }
0133         QVector<int> roles = {TimelineModel::EffectNamesRole};
0134         if (!m_fadeIns.empty()) {
0135             roles << TimelineModel::FadeInRole;
0136         }
0137         if (!m_fadeOuts.empty()) {
0138             roles << TimelineModel::FadeOutRole;
0139         }
0140         m_fadeIns.clear();
0141         m_fadeOuts.clear();
0142         Q_EMIT dataChanged(QModelIndex(), QModelIndex(), roles);
0143         pCore->updateItemKeyframes(m_ownerId);
0144         return true;
0145     };
0146     redo_current();
0147     PUSH_LAMBDA(redo_current, redo);
0148     PUSH_LAMBDA(undo_current, undo);
0149 }
0150 
0151 void EffectStackModel::removeEffect(const std::shared_ptr<EffectItemModel> &effect)
0152 {
0153     qDebug() << "* * ** REMOVING EFFECT FROM STACK!!!\n!!!!!!!!!";
0154     QWriteLocker locker(&m_lock);
0155     Q_ASSERT(m_allItems.count(effect->getId()) > 0);
0156     int parentId = -1;
0157     if (auto ptr = effect->parentItem().lock()) parentId = ptr->getId();
0158     int current = getActiveEffect();
0159     if (current >= rootItem->childCount() - 1) {
0160         setActiveEffect(current - 1);
0161     }
0162     int currentRow = effect->row();
0163     Fun undo = addItem_lambda(effect, parentId);
0164     if (currentRow != rowCount() - 1) {
0165         Fun move = moveItem_lambda(effect->getId(), currentRow, true);
0166         PUSH_LAMBDA(move, undo);
0167     }
0168     Fun redo = removeItem_lambda(effect->getId());
0169     bool res = redo();
0170     if (res) {
0171         int inFades = int(m_fadeIns.size());
0172         int outFades = int(m_fadeOuts.size());
0173         m_fadeIns.erase(effect->getId());
0174         m_fadeOuts.erase(effect->getId());
0175         inFades = int(m_fadeIns.size()) - inFades;
0176         outFades = int(m_fadeOuts.size()) - outFades;
0177         QString effectName = EffectsRepository::get()->getName(effect->getAssetId());
0178         Fun update = [this, inFades, outFades]() {
0179             // Required to build the effect view
0180             if (rowCount() == 0) {
0181                 // Stack is now empty
0182                 Q_EMIT dataChanged(QModelIndex(), QModelIndex(), {});
0183             } else {
0184                 QVector<int> roles = {TimelineModel::EffectNamesRole};
0185                 if (inFades < 0) {
0186                     roles << TimelineModel::FadeInRole;
0187                 }
0188                 if (outFades < 0) {
0189                     roles << TimelineModel::FadeOutRole;
0190                 }
0191                 Q_EMIT dataChanged(QModelIndex(), QModelIndex(), roles);
0192             }
0193             // TODO: only update if effect is fade or keyframe
0194             /*if (inFades < 0) {
0195                 pCore->updateItemModel(m_ownerId, QStringLiteral("fadein"));
0196             } else if (outFades < 0) {
0197                 pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout"));
0198             }*/
0199             updateEffectZones();
0200             pCore->updateItemKeyframes(m_ownerId);
0201             return true;
0202         };
0203         Fun update2 = [this, inFades, outFades, current]() {
0204             // Required to build the effect view
0205             QVector<int> roles = {TimelineModel::EffectNamesRole};
0206             // TODO: only update if effect is fade or keyframe
0207             if (inFades < 0) {
0208                 roles << TimelineModel::FadeInRole;
0209             } else if (outFades < 0) {
0210                 roles << TimelineModel::FadeOutRole;
0211             }
0212             Q_EMIT dataChanged(QModelIndex(), QModelIndex(), roles);
0213             updateEffectZones();
0214             pCore->updateItemKeyframes(m_ownerId);
0215             setActiveEffect(current);
0216             return true;
0217         };
0218         update();
0219         PUSH_LAMBDA(update, redo);
0220         PUSH_LAMBDA(update2, undo);
0221         PUSH_UNDO(undo, redo, i18n("Delete effect %1", effectName));
0222     } else {
0223         qDebug() << "..........FAILED EFFECT DELETION";
0224     }
0225 }
0226 
0227 bool EffectStackModel::copyXmlEffect(const QDomElement &effect)
0228 {
0229     std::function<bool(void)> undo = []() { return true; };
0230     std::function<bool(void)> redo = []() { return true; };
0231     bool result = fromXml(effect, undo, redo);
0232     if (result) {
0233         PUSH_UNDO(undo, redo, i18n("Copy effect"));
0234     }
0235     return result;
0236 }
0237 
0238 bool EffectStackModel::copyXmlEffectWithUndo(const QDomElement &effect, Fun &undo, Fun &redo)
0239 {
0240     bool result = fromXml(effect, undo, redo);
0241     return result;
0242 }
0243 
0244 QDomElement EffectStackModel::toXml(QDomDocument &document)
0245 {
0246     QDomElement container = document.createElement(QStringLiteral("effects"));
0247     int currentIn = pCore->getItemIn(m_ownerId);
0248     container.setAttribute(QStringLiteral("parentIn"), currentIn);
0249     for (int i = 0; i < rootItem->childCount(); ++i) {
0250         std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
0251         QDomElement sub = document.createElement(QStringLiteral("effect"));
0252         sub.setAttribute(QStringLiteral("id"), sourceEffect->getAssetId());
0253         int filterIn = sourceEffect->filter().get_int("in");
0254         int filterOut = sourceEffect->filter().get_int("out");
0255         if (filterOut > filterIn) {
0256             sub.setAttribute(QStringLiteral("in"), filterIn);
0257             sub.setAttribute(QStringLiteral("out"), filterOut);
0258         }
0259         QStringList passProps{QStringLiteral("disable"), QStringLiteral("kdenlive:collapsed")};
0260         for (const QString &param : passProps) {
0261             int paramVal = sourceEffect->filter().get_int(param.toUtf8().constData());
0262             if (paramVal > 0) {
0263                 Xml::setXmlProperty(sub, param, QString::number(paramVal));
0264             }
0265         }
0266         QVector<QPair<QString, QVariant>> params = sourceEffect->getAllParameters();
0267         for (const auto &param : qAsConst(params)) {
0268             Xml::setXmlProperty(sub, param.first, param.second.toString());
0269         }
0270         container.appendChild(sub);
0271     }
0272     return container;
0273 }
0274 
0275 QDomElement EffectStackModel::rowToXml(const QUuid &uuid, int row, QDomDocument &document)
0276 {
0277     QDomElement container = document.createElement(QStringLiteral("effects"));
0278     if (row < 0 || row >= rootItem->childCount()) {
0279         return container;
0280     }
0281     int currentIn = pCore->getItemIn(uuid, m_ownerId);
0282     container.setAttribute(QStringLiteral("parentIn"), currentIn);
0283     std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(row));
0284     QDomElement sub = document.createElement(QStringLiteral("effect"));
0285     sub.setAttribute(QStringLiteral("id"), sourceEffect->getAssetId());
0286     int filterIn = sourceEffect->filter().get_int("in");
0287     int filterOut = sourceEffect->filter().get_int("out");
0288     if (filterOut > filterIn) {
0289         sub.setAttribute(QStringLiteral("in"), filterIn);
0290         sub.setAttribute(QStringLiteral("out"), filterOut);
0291     }
0292     QStringList passProps{QStringLiteral("disable"), QStringLiteral("kdenlive:collapsed")};
0293     for (const QString &param : passProps) {
0294         int paramVal = sourceEffect->filter().get_int(param.toUtf8().constData());
0295         if (paramVal > 0) {
0296             Xml::setXmlProperty(sub, param, QString::number(paramVal));
0297         }
0298     }
0299     QVector<QPair<QString, QVariant>> params = sourceEffect->getAllParameters();
0300     for (const auto &param : qAsConst(params)) {
0301         Xml::setXmlProperty(sub, param.first, param.second.toString());
0302     }
0303     container.appendChild(sub);
0304     return container;
0305 }
0306 
0307 bool EffectStackModel::fromXml(const QDomElement &effectsXml, Fun &undo, Fun &redo)
0308 {
0309     QDomNodeList nodeList = effectsXml.elementsByTagName(QStringLiteral("effect"));
0310     int parentIn = effectsXml.attribute(QStringLiteral("parentIn")).toInt();
0311     qDebug() << "// GOT PREVIOUS PARENTIN: " << parentIn << "\n\n=======\n=======\n\n";
0312     int currentIn = pCore->getItemIn(m_ownerId);
0313     PlaylistState::ClipState state = pCore->getItemState(m_ownerId);
0314     bool effectAdded = false;
0315     for (int i = 0; i < nodeList.count(); ++i) {
0316         QDomElement node = nodeList.item(i).toElement();
0317         const QString effectId = node.attribute(QStringLiteral("id"));
0318         bool isAudioEffect = EffectsRepository::get()->isAudioEffect(effectId);
0319         if (state == PlaylistState::VideoOnly) {
0320             if (isAudioEffect) {
0321                 continue;
0322             }
0323         } else if (state == PlaylistState::AudioOnly) {
0324             if (!isAudioEffect) {
0325                 continue;
0326             }
0327         }
0328         if (m_ownerId.type == KdenliveObjectType::TimelineClip && EffectsRepository::get()->isUnique(effectId) && hasEffect(effectId)) {
0329             pCore->displayMessage(i18n("Effect %1 cannot be added twice.", EffectsRepository::get()->getName(effectId)), ErrorMessage);
0330             return false;
0331         }
0332         bool effectEnabled = true;
0333         if (Xml::hasXmlProperty(node, QLatin1String("disable"))) {
0334             effectEnabled = Xml::getXmlProperty(node, QLatin1String("disable")).toInt() != 1;
0335         }
0336         auto effect = EffectItemModel::construct(effectId, shared_from_this(), effectEnabled);
0337         const QString in = node.attribute(QStringLiteral("in"));
0338         const QString out = node.attribute(QStringLiteral("out"));
0339         if (!out.isEmpty()) {
0340             effect->filter().set("in", in.toUtf8().constData());
0341             effect->filter().set("out", out.toUtf8().constData());
0342         }
0343         QMap<QString, std::pair<ParamType, bool>> keyframeParams = effect->getKeyframableParameters();
0344         QVector<QPair<QString, QVariant>> parameters;
0345         QDomNodeList params = node.elementsByTagName(QStringLiteral("property"));
0346         for (int j = 0; j < params.count(); j++) {
0347             QDomElement pnode = params.item(j).toElement();
0348             const QString pName = pnode.attribute(QStringLiteral("name"));
0349             if (pName == QLatin1String("in") || pName == QLatin1String("out")) {
0350                 continue;
0351             }
0352             if (keyframeParams.contains(pName)) {
0353                 // This is a keyframable parameter, fix offset
0354                 int currentDuration = pCore->getItemDuration(m_ownerId);
0355                 if (currentDuration > 1) {
0356                     currentDuration--;
0357                     currentDuration += currentIn;
0358                 }
0359                 QString pValue = KeyframeModel::getAnimationStringWithOffset(effect, pnode.text(), currentIn - parentIn, currentDuration,
0360                                                                              keyframeParams.value(pName).first, keyframeParams.value(pName).second);
0361                 parameters.append(QPair<QString, QVariant>(pName, QVariant(pValue)));
0362             } else {
0363                 parameters.append(QPair<QString, QVariant>(pName, QVariant(pnode.text())));
0364             }
0365         }
0366         effect->setParameters(parameters);
0367         Fun local_undo = removeItem_lambda(effect->getId());
0368         // TODO the parent should probably not always be the root
0369         Fun local_redo = addItem_lambda(effect, rootItem->getId());
0370         effect->prepareKeyframes();
0371         connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
0372         connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
0373         connect(effect.get(), &AssetParameterModel::showEffectZone, this, &EffectStackModel::updateEffectZones);
0374         if (effectId.startsWith(QLatin1String("fadein")) || effectId.startsWith(QLatin1String("fade_from_"))) {
0375             m_fadeIns.insert(effect->getId());
0376             int duration = effect->filter().get_length() - 1;
0377             effect->filter().set("in", currentIn);
0378             effect->filter().set("out", currentIn + duration);
0379             if (effectId.startsWith(QLatin1String("fade_"))) {
0380                 if (effect->filter().get("alpha") == QLatin1String("1")) {
0381                     // Adjust level value to match filter end
0382                     effect->filter().set("level", "0=0;-1=1");
0383                 } else if (effect->filter().get("level") == QLatin1String("1")) {
0384                     effect->filter().set("alpha", "0=0;-1=1");
0385                 }
0386             }
0387         } else if (effectId.startsWith(QLatin1String("fadeout")) || effectId.startsWith(QLatin1String("fade_to_"))) {
0388             m_fadeOuts.insert(effect->getId());
0389             int duration = effect->filter().get_length() - 1;
0390             int filterOut = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1;
0391             effect->filter().set("in", filterOut - duration);
0392             effect->filter().set("out", filterOut);
0393             if (effectId.startsWith(QLatin1String("fade_"))) {
0394                 if (effect->filter().get("alpha") == QLatin1String("1")) {
0395                     // Adjust level value to match filter end
0396                     effect->filter().set("level", "0=1;-1=0");
0397                 } else if (effect->filter().get("level") == QLatin1String("1")) {
0398                     effect->filter().set("alpha", "0=1;-1=0");
0399                 }
0400             }
0401         }
0402         local_redo();
0403         effectAdded = true;
0404         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
0405     }
0406     if (effectAdded) {
0407         Fun update = [this]() {
0408             Q_EMIT dataChanged(QModelIndex(), QModelIndex(), {});
0409             return true;
0410         };
0411         update();
0412         PUSH_LAMBDA(update, redo);
0413         PUSH_LAMBDA(update, undo);
0414     }
0415     return effectAdded;
0416 }
0417 
0418 bool EffectStackModel::copyEffect(const std::shared_ptr<AbstractEffectItem> &sourceItem, PlaylistState::ClipState state, bool logUndo)
0419 {
0420     QWriteLocker locker(&m_lock);
0421     if (sourceItem->childCount() > 0) {
0422         // TODO: group
0423         return false;
0424     }
0425     bool audioEffect = sourceItem->isAudio();
0426     if (state == PlaylistState::VideoOnly) {
0427         if (audioEffect) {
0428             // This effect cannot be used
0429             return false;
0430         }
0431     } else if (state == PlaylistState::AudioOnly) {
0432         if (!audioEffect) {
0433             return false;
0434         }
0435     }
0436     std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(sourceItem);
0437     const QString effectId = sourceEffect->getAssetId();
0438     if (m_ownerId.type == KdenliveObjectType::TimelineClip && EffectsRepository::get()->isUnique(effectId) && hasEffect(effectId)) {
0439         pCore->displayMessage(i18n("Effect %1 cannot be added twice.", EffectsRepository::get()->getName(effectId)), ErrorMessage);
0440         return false;
0441     }
0442     bool enabled = sourceEffect->isEnabled();
0443     auto effect = EffectItemModel::construct(effectId, shared_from_this(), enabled);
0444     effect->setParameters(sourceEffect->getAllParameters());
0445     if (!enabled) {
0446         effect->filter().set("disable", 1);
0447     }
0448     if (m_ownerId.type == KdenliveObjectType::TimelineTrack || m_ownerId.type == KdenliveObjectType::Master) {
0449         effect->filter().set("in", 0);
0450         effect->filter().set("out", pCore->getItemDuration(m_ownerId) - 1);
0451     } else {
0452         effect->filter().set("in", sourceEffect->filter().get_int("in"));
0453         effect->filter().set("out", sourceEffect->filter().get_int("out"));
0454     }
0455     Fun local_undo = removeItem_lambda(effect->getId());
0456     // TODO the parent should probably not always be the root
0457     Fun local_redo = addItem_lambda(effect, rootItem->getId());
0458     effect->prepareKeyframes();
0459     connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
0460     connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
0461     connect(effect.get(), &AssetParameterModel::showEffectZone, this, &EffectStackModel::updateEffectZones);
0462     QVector<int> roles = {TimelineModel::EffectNamesRole};
0463     if (effectId.startsWith(QLatin1String("fadein")) || effectId.startsWith(QLatin1String("fade_from_"))) {
0464         m_fadeIns.insert(effect->getId());
0465         int duration = effect->filter().get_length() - 1;
0466         int in = pCore->getItemIn(m_ownerId);
0467         effect->filter().set("in", in);
0468         effect->filter().set("out", in + duration);
0469         roles << TimelineModel::FadeInRole;
0470     } else if (effectId.startsWith(QLatin1String("fadeout")) || effectId.startsWith(QLatin1String("fade_to_"))) {
0471         m_fadeOuts.insert(effect->getId());
0472         int duration = effect->filter().get_length() - 1;
0473         int out = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1;
0474         effect->filter().set("in", out - duration);
0475         effect->filter().set("out", out);
0476         roles << TimelineModel::FadeOutRole;
0477     }
0478     bool res = local_redo();
0479     if (res) {
0480         Fun update = [this, roles]() {
0481             Q_EMIT dataChanged(QModelIndex(), QModelIndex(), roles);
0482             return true;
0483         };
0484         update();
0485         if (logUndo) {
0486             PUSH_LAMBDA(update, local_redo);
0487             PUSH_LAMBDA(update, local_undo);
0488             pCore->pushUndo(local_undo, local_redo, i18n("Paste effect"));
0489         }
0490     }
0491     return res;
0492 }
0493 
0494 bool EffectStackModel::appendEffectWithUndo(const QString &effectId, Fun &undo, Fun &redo)
0495 {
0496     return doAppendEffect(effectId, false, {}, undo, redo);
0497 }
0498 
0499 bool EffectStackModel::appendEffect(const QString &effectId, bool makeCurrent, stringMap params)
0500 {
0501     Fun undo = []() { return true; };
0502     Fun redo = []() { return true; };
0503     bool result = doAppendEffect(effectId, makeCurrent, params, undo, redo);
0504     if (result) {
0505         PUSH_UNDO(undo, redo, i18n("Add effect %1", EffectsRepository::get()->getName(effectId)));
0506     }
0507     return result;
0508 }
0509 
0510 bool EffectStackModel::doAppendEffect(const QString &effectId, bool makeCurrent, stringMap params, Fun &undo, Fun &redo)
0511 {
0512     QWriteLocker locker(&m_lock);
0513     if (m_ownerId.type == KdenliveObjectType::TimelineClip && EffectsRepository::get()->isUnique(effectId) && hasEffect(effectId)) {
0514         pCore->displayMessage(i18n("Effect %1 cannot be added twice.", EffectsRepository::get()->getName(effectId)), ErrorMessage);
0515         return false;
0516     }
0517     std::unordered_set<int> previousFadeIn = m_fadeIns;
0518     std::unordered_set<int> previousFadeOut = m_fadeOuts;
0519     if (EffectsRepository::get()->isGroup(effectId)) {
0520         QDomElement doc = EffectsRepository::get()->getXml(effectId);
0521         return copyXmlEffect(doc);
0522     }
0523     auto effect = EffectItemModel::construct(effectId, shared_from_this());
0524     PlaylistState::ClipState state = pCore->getItemState(m_ownerId);
0525     if (state == PlaylistState::VideoOnly) {
0526         if (effect->isAudio()) {
0527             // Cannot add effect to this clip
0528             pCore->displayMessage(i18n("Cannot add effect to clip"), ErrorMessage);
0529             return false;
0530         }
0531     } else if (state == PlaylistState::AudioOnly) {
0532         if (!effect->isAudio()) {
0533             // Cannot add effect to this clip
0534             pCore->displayMessage(i18n("Cannot add effect to clip"), ErrorMessage);
0535             return false;
0536         }
0537     }
0538     QMapIterator<QString, QString> i(params);
0539     while (i.hasNext()) {
0540         i.next();
0541         effect->filter().set(i.key().toUtf8().constData(), i.value().toUtf8().constData());
0542     }
0543     Fun local_undo = removeItem_lambda(effect->getId());
0544     // TODO the parent should probably not always be the root
0545     Fun local_redo = addItem_lambda(effect, rootItem->getId());
0546     effect->prepareKeyframes();
0547     connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
0548     connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
0549     connect(effect.get(), &AssetParameterModel::showEffectZone, this, &EffectStackModel::updateEffectZones);
0550     int currentActive = getActiveEffect();
0551     if (makeCurrent) {
0552         setActiveEffect(rowCount());
0553     }
0554     bool res = local_redo();
0555     if (res) {
0556         int inFades = 0;
0557         int outFades = 0;
0558         if (effectId.startsWith(QLatin1String("fadein")) || effectId.startsWith(QLatin1String("fade_from_"))) {
0559             int duration = effect->filter().get_length() - 1;
0560             int in = pCore->getItemIn(m_ownerId);
0561             effect->filter().set("in", in);
0562             effect->filter().set("out", in + duration);
0563             inFades++;
0564         } else if (effectId.startsWith(QLatin1String("fadeout")) || effectId.startsWith(QLatin1String("fade_to_"))) {
0565             /*int duration = effect->filter().get_length() - 1;
0566             int out = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1;
0567             effect->filter().set("in", out - duration);
0568             effect->filter().set("out", out);*/
0569             outFades++;
0570         } else if (m_ownerId.type == KdenliveObjectType::TimelineTrack) {
0571             effect->filter().set("out", pCore->getItemDuration(m_ownerId));
0572         }
0573         Fun update = [this, inFades, outFades]() {
0574             // TODO: only update if effect is fade or keyframe
0575             QVector<int> roles = {TimelineModel::EffectNamesRole};
0576             if (inFades > 0) {
0577                 roles << TimelineModel::FadeInRole;
0578             } else if (outFades > 0) {
0579                 roles << TimelineModel::FadeOutRole;
0580             }
0581             pCore->updateItemKeyframes(m_ownerId);
0582             Q_EMIT dataChanged(QModelIndex(), QModelIndex(), roles);
0583             return true;
0584         };
0585         Fun update_undo = [this, inFades, outFades, previousFadeIn, previousFadeOut]() {
0586             // TODO: only update if effect is fade or keyframe
0587             QVector<int> roles = {TimelineModel::EffectNamesRole};
0588             if (inFades > 0) {
0589                 m_fadeIns = previousFadeIn;
0590                 roles << TimelineModel::FadeInRole;
0591             } else if (outFades > 0) {
0592                 m_fadeOuts = previousFadeOut;
0593                 roles << TimelineModel::FadeOutRole;
0594             }
0595             pCore->updateItemKeyframes(m_ownerId);
0596             Q_EMIT dataChanged(QModelIndex(), QModelIndex(), roles);
0597             return true;
0598         };
0599         update();
0600         PUSH_LAMBDA(update, local_redo);
0601         PUSH_LAMBDA(update_undo, local_undo);
0602         PUSH_LAMBDA(local_redo, redo);
0603         PUSH_LAMBDA(local_undo, undo);
0604     } else if (makeCurrent) {
0605         setActiveEffect(currentActive);
0606     }
0607     return res;
0608 }
0609 
0610 bool EffectStackModel::adjustStackLength(bool adjustFromEnd, int oldIn, int oldDuration, int newIn, int duration, int offset, Fun &undo, Fun &redo,
0611                                          bool logUndo)
0612 {
0613     QWriteLocker locker(&m_lock);
0614     const int fadeInDuration = getFadePosition(true);
0615     const int fadeOutDuration = getFadePosition(false);
0616     int out = newIn + duration;
0617     for (const auto &leaf : rootItem->getLeaves()) {
0618         std::shared_ptr<AbstractEffectItem> item = std::static_pointer_cast<AbstractEffectItem>(leaf);
0619         if (item->effectItemType() == EffectItemType::Group) {
0620             // probably an empty group, ignore
0621             continue;
0622         }
0623         std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(leaf);
0624         if (fadeInDuration > 0 && m_fadeIns.count(leaf->getId()) > 0) {
0625             // Adjust fade in
0626             int oldEffectIn = qMax(0, effect->filter().get_in());
0627             int oldEffectOut = effect->filter().get_out();
0628             qDebug() << "--previous effect: " << oldEffectIn << "-" << oldEffectOut;
0629             int effectDuration = qMin(effect->filter().get_length() - 1, duration);
0630             if (!adjustFromEnd && (oldIn != newIn || duration != oldDuration)) {
0631                 // Clip start was resized, adjust effect in / out
0632                 Fun operation = [effect, newIn, effectDuration, logUndo]() {
0633                     effect->setParameter(QStringLiteral("in"), newIn, false);
0634                     effect->setParameter(QStringLiteral("out"), newIn + effectDuration, logUndo);
0635                     qDebug() << "--new effect: " << newIn << "-" << newIn + effectDuration;
0636                     return true;
0637                 };
0638                 bool res = operation();
0639                 if (!res) {
0640                     return false;
0641                 }
0642                 Fun reverse = [effect, oldEffectIn, oldEffectOut, logUndo]() {
0643                     effect->setParameter(QStringLiteral("in"), oldEffectIn, false);
0644                     effect->setParameter(QStringLiteral("out"), oldEffectOut, logUndo);
0645                     return true;
0646                 };
0647                 PUSH_LAMBDA(operation, redo);
0648                 PUSH_LAMBDA(reverse, undo);
0649             } else if (effectDuration < oldEffectOut - oldEffectIn || (logUndo && effect->filter().get_int("_refout") > 0)) {
0650                 // Clip length changed, shorter than effect length so resize
0651                 int referenceEffectOut = effect->filter().get_int("_refout");
0652                 if (referenceEffectOut <= 0) {
0653                     referenceEffectOut = oldEffectOut;
0654                     effect->filter().set("_refout", referenceEffectOut);
0655                 }
0656                 Fun operation = [effect, oldEffectIn, effectDuration, logUndo]() {
0657                     effect->setParameter(QStringLiteral("out"), oldEffectIn + effectDuration, logUndo);
0658                     return true;
0659                 };
0660                 bool res = operation();
0661                 if (!res) {
0662                     return false;
0663                 }
0664                 if (logUndo) {
0665                     Fun reverse = [effect, referenceEffectOut]() {
0666                         effect->setParameter(QStringLiteral("out"), referenceEffectOut, true);
0667                         effect->filter().set("_refout", nullptr);
0668                         return true;
0669                     };
0670                     PUSH_LAMBDA(operation, redo);
0671                     PUSH_LAMBDA(reverse, undo);
0672                 }
0673             }
0674         } else if (fadeOutDuration > 0 && m_fadeOuts.count(leaf->getId()) > 0) {
0675             // Adjust fade out
0676             int effectDuration = qMin(fadeOutDuration, duration);
0677             int newFadeIn = out - effectDuration;
0678             int oldFadeIn = effect->filter().get_int("in");
0679             int oldOut = effect->filter().get_int("out");
0680             int referenceEffectIn = effect->filter().get_int("_refin");
0681             if (referenceEffectIn <= 0) {
0682                 referenceEffectIn = oldFadeIn;
0683                 effect->filter().set("_refin", referenceEffectIn);
0684             }
0685             Fun operation = [effect, newFadeIn, out, logUndo]() {
0686                 effect->setParameter(QStringLiteral("in"), newFadeIn, false);
0687                 effect->setParameter(QStringLiteral("out"), out, logUndo);
0688                 return true;
0689             };
0690             bool res = operation();
0691             if (!res) {
0692                 return false;
0693             }
0694             if (logUndo) {
0695                 Fun reverse = [effect, referenceEffectIn, oldOut]() {
0696                     effect->setParameter(QStringLiteral("in"), referenceEffectIn, false);
0697                     effect->setParameter(QStringLiteral("out"), oldOut, true);
0698                     effect->filter().set("_refin", nullptr);
0699                     return true;
0700                 };
0701                 PUSH_LAMBDA(operation, redo);
0702                 PUSH_LAMBDA(reverse, undo);
0703             }
0704         } else {
0705             // Not a fade effect, check for keyframes
0706             bool hasZone = effect->filter().get_int("kdenlive:force_in_out") == 1;
0707             std::shared_ptr<KeyframeModelList> keyframes = effect->getKeyframeModel();
0708             if (keyframes != nullptr) {
0709                 // Effect has keyframes, update these
0710                 keyframes->resizeKeyframes(oldIn, oldIn + oldDuration, newIn, out - 1, offset, adjustFromEnd, undo, redo);
0711                 QModelIndex index = getIndexFromItem(effect);
0712                 Fun refresh = [effect, index]() {
0713                     Q_EMIT effect->dataChanged(index, QModelIndex(), QVector<int>());
0714                     return true;
0715                 };
0716                 refresh();
0717                 PUSH_LAMBDA(refresh, redo);
0718                 PUSH_LAMBDA(refresh, undo);
0719             } else {
0720                 qDebug() << "// NULL Keyframes---------";
0721             }
0722             if (m_ownerId.type == KdenliveObjectType::TimelineTrack && !hasZone) {
0723                 int oldEffectOut = effect->filter().get_out();
0724                 Fun operation = [effect, out, logUndo]() {
0725                     effect->setParameter(QStringLiteral("out"), out, logUndo);
0726                     return true;
0727                 };
0728                 bool res = operation();
0729                 if (!res) {
0730                     return false;
0731                 }
0732                 if (logUndo) {
0733                     Fun reverse = [effect, oldEffectOut]() {
0734                         effect->setParameter(QStringLiteral("out"), oldEffectOut, true);
0735                         return true;
0736                     };
0737                     PUSH_LAMBDA(operation, redo);
0738                     PUSH_LAMBDA(reverse, undo);
0739                 }
0740             } else if (m_ownerId.type == KdenliveObjectType::TimelineClip && effect->data(QModelIndex(), AssetParameterModel::RequiresInOut).toBool() == true) {
0741                 int oldEffectIn = qMax(0, effect->filter().get_in());
0742                 int oldEffectOut = effect->filter().get_out();
0743                 int newIn = pCore->getItemIn(m_ownerId);
0744                 int newOut = newIn + pCore->getItemDuration(m_ownerId) - 1;
0745                 Fun operation = [effect, newIn, newOut]() {
0746                     effect->filter().set_in_and_out(newIn, newOut);
0747                     qDebug() << "--new effect: " << newIn << "-" << newOut;
0748                     return true;
0749                 };
0750                 bool res = operation();
0751                 if (!res) {
0752                     return false;
0753                 }
0754                 Fun reverse = [effect, oldEffectIn, oldEffectOut]() {
0755                     effect->filter().set_in_and_out(oldEffectIn, oldEffectOut);
0756                     return true;
0757                 };
0758                 PUSH_LAMBDA(operation, redo);
0759                 PUSH_LAMBDA(reverse, undo);
0760             }
0761         }
0762     }
0763     return true;
0764 }
0765 
0766 bool EffectStackModel::adjustFadeLength(int duration, bool fromStart, bool audioFade, bool videoFade, bool logUndo)
0767 {
0768     QWriteLocker locker(&m_lock);
0769     if (fromStart) {
0770         // Fade in
0771         if (m_fadeIns.empty()) {
0772             if (audioFade) {
0773                 appendEffect(QStringLiteral("fadein"));
0774             }
0775             if (videoFade) {
0776                 appendEffect(QStringLiteral("fade_from_black"));
0777             }
0778         }
0779         QList<QModelIndex> indexes;
0780         auto ptr = m_masterService.lock();
0781         int in = 0;
0782         if (ptr) {
0783             in = ptr->get_int("in");
0784         }
0785         int oldDuration = -1;
0786         for (int i = 0; i < rootItem->childCount(); ++i) {
0787             if (m_fadeIns.count(std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) > 0) {
0788                 std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
0789                 if (oldDuration == -1) {
0790                     oldDuration = effect->filter().get_length();
0791                 }
0792                 effect->filter().set("in", in);
0793                 duration = qMin(pCore->getItemDuration(m_ownerId), duration);
0794                 effect->filter().set("out", in + duration);
0795                 indexes << getIndexFromItem(effect);
0796                 if (effect->filter().get("alpha") == QLatin1String("1")) {
0797                     // Adjust level value to match filter end
0798                     effect->filter().set("level", "0=0;-1=1");
0799                 } else if (effect->filter().get("level") == QLatin1String("1")) {
0800                     effect->filter().set("alpha", "0=0;-1=1");
0801                 }
0802             }
0803         }
0804         if (!indexes.isEmpty()) {
0805             Q_EMIT dataChanged(indexes.first(), indexes.last(), QVector<int>());
0806             pCore->updateItemModel(m_ownerId, QStringLiteral("fadein"));
0807             if (videoFade) {
0808                 int min = pCore->getItemPosition(m_ownerId);
0809                 QPair<int, int> range = {min, min + qMax(duration, oldDuration)};
0810                 pCore->refreshProjectRange(range);
0811                 if (logUndo) {
0812                     pCore->invalidateRange(range);
0813                 }
0814             }
0815         }
0816     } else {
0817         // Fade out
0818         if (m_fadeOuts.empty()) {
0819             if (audioFade) {
0820                 appendEffect(QStringLiteral("fadeout"));
0821             }
0822             if (videoFade) {
0823                 appendEffect(QStringLiteral("fade_to_black"));
0824             }
0825         }
0826         int in = 0;
0827         auto ptr = m_masterService.lock();
0828         if (ptr) {
0829             in = ptr->get_int("in");
0830         }
0831         int itemDuration = pCore->getItemDuration(m_ownerId);
0832         int out = in + itemDuration - 1;
0833         int oldDuration = -1;
0834         QList<QModelIndex> indexes;
0835         for (int i = 0; i < rootItem->childCount(); ++i) {
0836             if (m_fadeOuts.count(std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) > 0) {
0837                 std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
0838                 if (oldDuration == -1) {
0839                     oldDuration = effect->filter().get_length();
0840                 }
0841                 effect->filter().set("out", out);
0842                 duration = qMin(itemDuration, duration);
0843                 effect->filter().set("in", out - duration);
0844                 indexes << getIndexFromItem(effect);
0845                 if (effect->filter().get("alpha") == QLatin1String("1")) {
0846                     // Adjust level value to match filter end
0847                     effect->filter().set("level", "0=1;-1=0");
0848                 } else if (effect->filter().get("level") == QLatin1String("1")) {
0849                     effect->filter().set("alpha", "0=1;-1=0");
0850                 }
0851             }
0852         }
0853         if (!indexes.isEmpty()) {
0854             Q_EMIT dataChanged(indexes.first(), indexes.last(), QVector<int>());
0855             pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout"));
0856             if (videoFade) {
0857                 int min = pCore->getItemPosition(m_ownerId);
0858                 QPair<int, int> range = {min + itemDuration - qMax(duration, oldDuration), min + itemDuration};
0859                 pCore->refreshProjectRange(range);
0860                 if (logUndo) {
0861                     pCore->invalidateRange(range);
0862                 }
0863             }
0864         }
0865     }
0866     return true;
0867 }
0868 
0869 int EffectStackModel::getFadePosition(bool fromStart)
0870 {
0871     QWriteLocker locker(&m_lock);
0872     if (fromStart) {
0873         if (m_fadeIns.empty()) {
0874             return 0;
0875         }
0876         for (int i = 0; i < rootItem->childCount(); ++i) {
0877             if (*(m_fadeIns.begin()) == std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) {
0878                 std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
0879                 return effect->filter().get_length() - 1;
0880             }
0881         }
0882     } else {
0883         if (m_fadeOuts.empty()) {
0884             return 0;
0885         }
0886         for (int i = 0; i < rootItem->childCount(); ++i) {
0887             if (*(m_fadeOuts.begin()) == std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) {
0888                 std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
0889                 return effect->filter().get_length() - 1;
0890             }
0891         }
0892     }
0893     return 0;
0894 }
0895 
0896 bool EffectStackModel::removeFade(bool fromStart)
0897 {
0898     QWriteLocker locker(&m_lock);
0899     std::vector<int> toRemove;
0900     for (int i = 0; i < rootItem->childCount(); ++i) {
0901         if ((fromStart && m_fadeIns.count(std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) > 0) ||
0902             (!fromStart && m_fadeOuts.count(std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) > 0)) {
0903             toRemove.push_back(i);
0904         }
0905     }
0906     // Let's put index in reverse order so we don't mess when deleting
0907     std::reverse(toRemove.begin(), toRemove.end());
0908     for (int i : toRemove) {
0909         std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
0910         removeEffect(effect);
0911     }
0912     return true;
0913 }
0914 
0915 void EffectStackModel::moveEffectByRow(int destRow, int srcRow)
0916 {
0917     moveEffect(destRow, getEffectStackRow(srcRow));
0918 }
0919 
0920 void EffectStackModel::moveEffect(int destRow, const std::shared_ptr<AbstractEffectItem> &item)
0921 {
0922     QWriteLocker locker(&m_lock);
0923     Q_ASSERT(m_allItems.count(item->getId()) > 0);
0924     int oldRow = item->row();
0925     Fun undo = moveItem_lambda(item->getId(), oldRow);
0926     Fun redo = moveItem_lambda(item->getId(), destRow);
0927     bool res = redo();
0928     if (res) {
0929         Fun update = [this]() {
0930             Q_EMIT this->dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectNamesRole});
0931             return true;
0932         };
0933         update();
0934         UPDATE_UNDO_REDO(update, update, undo, redo);
0935         auto effectId = std::static_pointer_cast<EffectItemModel>(item)->getAssetId();
0936         PUSH_UNDO(undo, redo, i18n("Move effect %1", EffectsRepository::get()->getName(effectId)));
0937     }
0938 }
0939 
0940 void EffectStackModel::registerItem(const std::shared_ptr<TreeItem> &item)
0941 {
0942     QWriteLocker locker(&m_lock);
0943     // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect";
0944     if (!item->isRoot()) {
0945         auto effectItem = std::static_pointer_cast<EffectItemModel>(item);
0946         if (effectItem->data(QModelIndex(), AssetParameterModel::RequiresInOut).toBool() == true) {
0947             int in = pCore->getItemIn(m_ownerId);
0948             int out = in + pCore->getItemDuration(m_ownerId) - 1;
0949             effectItem->filter().set_in_and_out(in, out);
0950         }
0951         if (!m_loadingExisting) {
0952             // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect in " << m_childServices.size();
0953             effectItem->plant(m_masterService);
0954             // Check if we have an internal effect that needs to stay on top
0955             if (m_ownerId.type == KdenliveObjectType::Master || m_ownerId.type == KdenliveObjectType::TimelineTrack) {
0956                 // check for subtitle effect
0957                 auto ms = m_masterService.lock();
0958                 int ct = ms->filter_count();
0959                 QVector<int> ixToMove;
0960                 for (int i = 0; i < ct; i++) {
0961                     if (ms->filter(i)->get_int("internal_added") > 0) {
0962                         ixToMove << i;
0963                     }
0964                 }
0965                 std::sort(ixToMove.rbegin(), ixToMove.rend());
0966                 for (auto &ix : ixToMove) {
0967                     if (ix < ct - 1) {
0968                         ms->move_filter(ix, ct - 1);
0969                     }
0970                 }
0971             }
0972             for (const auto &service : m_childServices) {
0973                 // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting CLONE effect in " << (void *)service.lock().get();
0974                 effectItem->plantClone(service);
0975             }
0976         }
0977         effectItem->setEffectStackEnabled(m_effectStackEnabled);
0978         const QString &effectId = effectItem->getAssetId();
0979         if (effectId.startsWith(QLatin1String("fadein")) || effectId.startsWith(QLatin1String("fade_from_"))) {
0980             m_fadeIns.insert(effectItem->getId());
0981         } else if (effectId.startsWith(QLatin1String("fadeout")) || effectId.startsWith(QLatin1String("fade_to_"))) {
0982             m_fadeOuts.insert(effectItem->getId());
0983         }
0984         if (!effectItem->isAudio() && !m_loadingExisting) {
0985             pCore->refreshProjectItem(m_ownerId);
0986             pCore->invalidateItem(m_ownerId);
0987         }
0988     }
0989     AbstractTreeModel::registerItem(item);
0990 }
0991 
0992 void EffectStackModel::deregisterItem(int id, TreeItem *item)
0993 {
0994     QWriteLocker locker(&m_lock);
0995     if (!item->isRoot()) {
0996         auto effectItem = static_cast<AbstractEffectItem *>(item);
0997         effectItem->unplant(m_masterService);
0998         for (const auto &service : m_childServices) {
0999             effectItem->unplantClone(service);
1000         }
1001         if (!effectItem->isAudio()) {
1002             pCore->refreshProjectItem(m_ownerId);
1003             pCore->invalidateItem(m_ownerId);
1004         }
1005     }
1006     AbstractTreeModel::deregisterItem(id, item);
1007 }
1008 
1009 void EffectStackModel::setEffectStackEnabled(bool enabled)
1010 {
1011     QWriteLocker locker(&m_lock);
1012     m_effectStackEnabled = enabled;
1013 
1014     QList<QModelIndex> indexes;
1015     // Recursively updates children states
1016     for (int i = 0; i < rootItem->childCount(); ++i) {
1017         std::shared_ptr<AbstractEffectItem> item = std::static_pointer_cast<AbstractEffectItem>(rootItem->child(i));
1018         item->setEffectStackEnabled(enabled);
1019         indexes << getIndexFromItem(item);
1020     }
1021     if (indexes.isEmpty()) {
1022         return;
1023     }
1024     pCore->refreshProjectItem(m_ownerId);
1025     pCore->invalidateItem(m_ownerId);
1026     Q_EMIT dataChanged(indexes.first(), indexes.last(), {TimelineModel::EffectsEnabledRole});
1027     Q_EMIT enabledStateChanged();
1028 }
1029 
1030 std::shared_ptr<AbstractEffectItem> EffectStackModel::getEffectStackRow(int row, const std::shared_ptr<TreeItem> &parentItem)
1031 {
1032     return std::static_pointer_cast<AbstractEffectItem>(parentItem ? parentItem->child(row) : rootItem->child(row));
1033 }
1034 
1035 bool EffectStackModel::importEffects(const std::shared_ptr<EffectStackModel> &sourceStack, PlaylistState::ClipState state)
1036 {
1037     QWriteLocker locker(&m_lock);
1038     // TODO: manage fades, keyframes if clips don't have same size / in point
1039     bool found = false;
1040     bool effectEnabled = rootItem->childCount() > 0;
1041     int imported = 0;
1042     for (int i = 0; i < sourceStack->rowCount(); i++) {
1043         auto item = sourceStack->getEffectStackRow(i);
1044         // NO undo. this should only be used on project opening
1045         if (copyEffect(item, state, false)) {
1046             found = true;
1047             if (item->isEnabled()) {
1048                 effectEnabled = true;
1049             }
1050             imported++;
1051         }
1052     }
1053     if (!effectEnabled && imported == 0) {
1054         effectEnabled = true;
1055     }
1056     m_effectStackEnabled = effectEnabled;
1057     if (!m_effectStackEnabled) {
1058         // Mark all effects as disabled by stack
1059         for (int i = 0; i < rootItem->childCount(); ++i) {
1060             std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
1061             sourceEffect->setEffectStackEnabled(false);
1062             sourceEffect->setEnabled(true);
1063         }
1064     }
1065     if (found) {
1066         Q_EMIT modelChanged();
1067     }
1068     return found;
1069 }
1070 
1071 void EffectStackModel::importEffects(const std::weak_ptr<Mlt::Service> &service, PlaylistState::ClipState state, bool alreadyExist,
1072                                      const QString &originalDecimalPoint, const QUuid &uuid)
1073 {
1074     QWriteLocker locker(&m_lock);
1075     m_loadingExisting = alreadyExist;
1076     bool effectEnabled = true;
1077     if (auto ptr = service.lock()) {
1078         int max = ptr->filter_count();
1079         int imported = 0;
1080         for (int i = 0; i < max; i++) {
1081             std::unique_ptr<Mlt::Filter> filter(ptr->filter(i));
1082             if (filter->get_int("internal_added") > 0 && m_ownerId.type != KdenliveObjectType::TimelineTrack) {
1083                 // Required to load master audio effects
1084                 if (m_ownerId.type == KdenliveObjectType::Master && filter->get("mlt_service") == QLatin1String("avfilter.subtitles")) {
1085                     // A subtitle filter, update project
1086                     QMap<QString, QString> subProperties;
1087                     // subProperties.insert(QStringLiteral("av.filename"), filter->get("av.filename"));
1088                     subProperties.insert(QStringLiteral("disable"), filter->get("disable"));
1089                     subProperties.insert(QStringLiteral("kdenlive:locked"), filter->get("kdenlive:locked"));
1090                     const QString style = filter->get("av.force_style");
1091                     if (!style.isEmpty()) {
1092                         subProperties.insert(QStringLiteral("av.force_style"), style);
1093                     }
1094                     pCore->window()->slotInitSubtitle(subProperties, uuid);
1095                 } else if (auto ms = m_masterService.lock()) {
1096                     ms->attach(*filter.get());
1097                 }
1098                 continue;
1099             }
1100             if (!filter->property_exists("kdenlive_id")) {
1101                 // don't consider internal MLT stuff
1102                 continue;
1103             }
1104             const QString effectId = qstrdup(filter->get("kdenlive_id"));
1105             if (m_ownerId.type == KdenliveObjectType::TimelineClip && EffectsRepository::get()->isUnique(effectId) && hasEffect(effectId)) {
1106                 pCore->displayMessage(i18n("Effect %1 cannot be added twice.", EffectsRepository::get()->getName(effectId)), ErrorMessage);
1107                 continue;
1108             }
1109             if (filter->get_int("disable") == 0) {
1110                 effectEnabled = true;
1111             }
1112             // The MLT filter already exists, use it directly to create the effect
1113             std::shared_ptr<EffectItemModel> effect;
1114             if (alreadyExist) {
1115                 // effect is already plugged in the service
1116                 effect = EffectItemModel::construct(std::move(filter), shared_from_this(), originalDecimalPoint);
1117             } else {
1118                 // duplicate effect
1119                 std::unique_ptr<Mlt::Filter> asset = EffectsRepository::get()->getEffect(effectId);
1120                 asset->inherit(*(filter));
1121                 effect = EffectItemModel::construct(std::move(asset), shared_from_this(), originalDecimalPoint);
1122             }
1123             if (state == PlaylistState::VideoOnly) {
1124                 if (effect->isAudio()) {
1125                     // Don't import effect
1126                     continue;
1127                 }
1128             } else if (state == PlaylistState::AudioOnly) {
1129                 if (!effect->isAudio()) {
1130                     // Don't import effect
1131                     continue;
1132                 }
1133             }
1134             imported++;
1135             connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
1136             connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
1137             connect(effect.get(), &AssetParameterModel::showEffectZone, this, &EffectStackModel::updateEffectZones);
1138             Fun redo = addItem_lambda(effect, rootItem->getId());
1139             int clipIn = ptr->get_int("in");
1140             int clipOut = ptr->get_int("out");
1141             if (clipOut <= clipIn) {
1142                 clipOut = ptr->get_int("length") - 1;
1143             }
1144             effect->prepareKeyframes(clipIn, clipOut);
1145             if (redo()) {
1146                 if (effectId.startsWith(QLatin1String("fadein")) || effectId.startsWith(QLatin1String("fade_from_"))) {
1147                     m_fadeIns.insert(effect->getId());
1148                     if (effect->filter().get_int("in") != clipIn) {
1149                         // Broken fade, fix
1150                         int filterLength = effect->filter().get_length() - 1;
1151                         effect->filter().set("in", clipIn);
1152                         effect->filter().set("out", clipIn + filterLength);
1153                     }
1154                 } else if (effectId.startsWith(QLatin1String("fadeout")) || effectId.startsWith(QLatin1String("fade_to_"))) {
1155                     m_fadeOuts.insert(effect->getId());
1156                     if (effect->filter().get_int("out") != clipOut) {
1157                         // Broken fade, fix
1158                         int filterLength = effect->filter().get_length() - 1;
1159                         effect->filter().set("in", clipOut - filterLength);
1160                         effect->filter().set("out", clipOut);
1161                     }
1162                 }
1163             }
1164         }
1165         if (imported == 0) {
1166             effectEnabled = true;
1167         }
1168     }
1169     m_effectStackEnabled = effectEnabled;
1170     if (!m_effectStackEnabled) {
1171         // Mark all effects as disabled by stack
1172         for (int i = 0; i < rootItem->childCount(); ++i) {
1173             std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
1174             sourceEffect->setEffectStackEnabled(false);
1175             sourceEffect->setEnabled(true);
1176         }
1177     }
1178     m_loadingExisting = false;
1179     Q_EMIT modelChanged();
1180 }
1181 
1182 void EffectStackModel::setActiveEffect(int ix)
1183 {
1184     QWriteLocker locker(&m_lock);
1185     int current = -1;
1186     if (auto ptr = m_masterService.lock()) {
1187         current = ptr->get_int("kdenlive:activeeffect");
1188         ptr->set("kdenlive:activeeffect", ix);
1189     }
1190     // Desactivate previous effect
1191     if (current > -1 && current != ix && current < rootItem->childCount()) {
1192         std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(current));
1193         if (effect) {
1194             effect->setActive(false);
1195             Q_EMIT currentChanged(getIndexFromItem(effect), false);
1196         }
1197     }
1198     // Activate new effect
1199     if (ix > -1 && ix < rootItem->childCount()) {
1200         std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
1201         if (effect) {
1202             effect->setActive(true);
1203             Q_EMIT currentChanged(getIndexFromItem(effect), true);
1204         }
1205     }
1206     pCore->updateItemKeyframes(m_ownerId);
1207 }
1208 
1209 int EffectStackModel::getActiveEffect() const
1210 {
1211     QWriteLocker locker(&m_lock);
1212     if (auto ptr = m_masterService.lock()) {
1213         return ptr->get_int("kdenlive:activeeffect");
1214     }
1215     return 0;
1216 }
1217 
1218 void EffectStackModel::slotCreateGroup(const std::shared_ptr<EffectItemModel> &childEffect)
1219 {
1220     QWriteLocker locker(&m_lock);
1221     auto groupItem = EffectGroupModel::construct(QStringLiteral("group"), shared_from_this());
1222     rootItem->appendChild(groupItem);
1223     groupItem->appendChild(childEffect);
1224 }
1225 
1226 ObjectId EffectStackModel::getOwnerId() const
1227 {
1228     return m_ownerId;
1229 }
1230 
1231 bool EffectStackModel::checkConsistency()
1232 {
1233     if (!AbstractTreeModel::checkConsistency()) {
1234         return false;
1235     }
1236 
1237     std::vector<std::shared_ptr<EffectItemModel>> allFilters;
1238     // We do a DFS on the tree to retrieve all the filters
1239     std::stack<std::shared_ptr<AbstractEffectItem>> stck;
1240     stck.push(std::static_pointer_cast<AbstractEffectItem>(rootItem));
1241 
1242     while (!stck.empty()) {
1243         auto current = stck.top();
1244         stck.pop();
1245 
1246         if (current->effectItemType() == EffectItemType::Effect) {
1247             if (current->childCount() > 0) {
1248                 qDebug() << "ERROR: Found an effect with children";
1249                 return false;
1250             }
1251             allFilters.push_back(std::static_pointer_cast<EffectItemModel>(current));
1252             continue;
1253         }
1254         for (int i = current->childCount() - 1; i >= 0; --i) {
1255             stck.push(std::static_pointer_cast<AbstractEffectItem>(current->child(i)));
1256         }
1257     }
1258 
1259     for (const auto &service : m_childServices) {
1260         auto ptr = service.lock();
1261         if (!ptr) {
1262             qDebug() << "ERROR: unavailable service";
1263             return false;
1264         }
1265         // MLT inserts some default normalizer filters that are not managed by Kdenlive, which explains  why the filter count is not equal
1266         int kdenliveFilterCount = 0;
1267         for (int i = 0; i < ptr->filter_count(); i++) {
1268             std::shared_ptr<Mlt::Filter> filt(ptr->filter(i));
1269             if (filt->property_exists("kdenlive_id")) {
1270                 kdenliveFilterCount++;
1271             }
1272             // qDebug() << "FILTER: "<<i<<" : "<<ptr->filter(i)->get("mlt_service");
1273         }
1274         if (kdenliveFilterCount != int(allFilters.size())) {
1275             qDebug() << "ERROR: Wrong filter count: " << kdenliveFilterCount << " = " << allFilters.size();
1276             return false;
1277         }
1278 
1279         int ct = 0;
1280         for (uint i = 0; i < allFilters.size(); ++i) {
1281             while (ptr->filter(ct)->get("kdenlive_id") == nullptr && ct < ptr->filter_count()) {
1282                 ct++;
1283             }
1284             auto mltFilter = ptr->filter(ct);
1285             auto currentFilter = allFilters[i]->filter();
1286             if (QString(currentFilter.get("mlt_service")) != QLatin1String(mltFilter->get("mlt_service"))) {
1287                 qDebug() << "ERROR: filter " << i << "differ: " << ct << ", " << currentFilter.get("mlt_service") << " = " << mltFilter->get("mlt_service");
1288                 return false;
1289             }
1290             QVector<QPair<QString, QVariant>> params = allFilters[i]->getAllParameters();
1291             for (const auto &val : qAsConst(params)) {
1292                 // Check parameters values
1293                 if (val.second.toString() != QString(mltFilter->get(val.first.toUtf8().constData()))) {
1294                     qDebug() << "ERROR: filter " << i << "PARAMETER MISMATCH: " << val.first << " = " << val.second
1295                              << " != " << mltFilter->get(val.first.toUtf8().constData());
1296                     return false;
1297                 }
1298             }
1299             ct++;
1300         }
1301     }
1302     return true;
1303 }
1304 
1305 void EffectStackModel::adjust(const QString &effectId, const QString &effectName, double value)
1306 {
1307     QWriteLocker locker(&m_lock);
1308     for (int i = 0; i < rootItem->childCount(); ++i) {
1309         std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
1310         if (effectId == sourceEffect->getAssetId()) {
1311             sourceEffect->setParameter(effectName, QString::number(value));
1312             return;
1313         }
1314     }
1315 }
1316 
1317 std::shared_ptr<AssetParameterModel> EffectStackModel::getAssetModelById(const QString &effectId)
1318 {
1319     QWriteLocker locker(&m_lock);
1320     for (int i = 0; i < rootItem->childCount(); ++i) {
1321         std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
1322         if (effectId == sourceEffect->getAssetId()) {
1323             return std::static_pointer_cast<AssetParameterModel>(sourceEffect);
1324         }
1325     }
1326     return nullptr;
1327 }
1328 
1329 bool EffectStackModel::hasFilter(const QString &effectId) const
1330 {
1331     READ_LOCK();
1332     return rootItem->accumulate_const(false, [effectId](bool b, std::shared_ptr<const TreeItem> it) {
1333         if (b) return true;
1334         auto item = std::static_pointer_cast<const AbstractEffectItem>(it);
1335         if (item->effectItemType() == EffectItemType::Group) {
1336             return false;
1337         }
1338         auto sourceEffect = std::static_pointer_cast<const EffectItemModel>(it);
1339         return effectId == sourceEffect->getAssetId();
1340     });
1341 }
1342 
1343 double EffectStackModel::getFilterParam(const QString &effectId, const QString &paramName)
1344 {
1345     READ_LOCK();
1346     for (int i = 0; i < rootItem->childCount(); ++i) {
1347         std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
1348         if (effectId == sourceEffect->getAssetId()) {
1349             return sourceEffect->filter().get_double(paramName.toUtf8().constData());
1350         }
1351     }
1352     return 0.0;
1353 }
1354 
1355 KeyframeModel *EffectStackModel::getEffectKeyframeModel()
1356 {
1357     if (rootItem->childCount() == 0) return nullptr;
1358     int ix = getActiveEffect();
1359     if (ix < 0 || ix >= rootItem->childCount()) {
1360         return nullptr;
1361     }
1362     std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
1363     std::shared_ptr<KeyframeModelList> listModel = sourceEffect->getKeyframeModel();
1364     if (listModel) {
1365         return listModel->getKeyModel();
1366     }
1367     return nullptr;
1368 }
1369 
1370 void EffectStackModel::replugEffect(const std::shared_ptr<AssetParameterModel> &asset)
1371 {
1372     QWriteLocker locker(&m_lock);
1373     auto effectItem = std::static_pointer_cast<EffectItemModel>(asset);
1374     int oldRow = effectItem->row();
1375     int count = rowCount();
1376     for (int ix = oldRow; ix < count; ix++) {
1377         auto item = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
1378         item->unplant(m_masterService);
1379         for (const auto &service : m_childServices) {
1380             item->unplantClone(service);
1381         }
1382     }
1383     std::unique_ptr<Mlt::Properties> effect = EffectsRepository::get()->getEffect(effectItem->getAssetId());
1384     effect->inherit(effectItem->filter());
1385     effectItem->resetAsset(std::move(effect));
1386     for (int ix = oldRow; ix < count; ix++) {
1387         auto item = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
1388         item->plant(m_masterService);
1389         for (const auto &service : m_childServices) {
1390             item->plantClone(service);
1391         }
1392     }
1393 }
1394 
1395 void EffectStackModel::cleanFadeEffects(bool outEffects, Fun &undo, Fun &redo)
1396 {
1397     QWriteLocker locker(&m_lock);
1398     const auto &toDelete = outEffects ? m_fadeOuts : m_fadeIns;
1399     for (int id : toDelete) {
1400         auto effect = std::static_pointer_cast<EffectItemModel>(getItemById(id));
1401         Fun operation = removeItem_lambda(id);
1402         if (operation()) {
1403             Fun reverse = addItem_lambda(effect, rootItem->getId());
1404             UPDATE_UNDO_REDO(operation, reverse, undo, redo);
1405         }
1406     }
1407     if (!toDelete.empty()) {
1408         Fun updateRedo = [this, toDelete, outEffects]() {
1409             for (int id : toDelete) {
1410                 if (outEffects) {
1411                     m_fadeOuts.erase(id);
1412                 } else {
1413                     m_fadeIns.erase(id);
1414                 }
1415             }
1416             QVector<int> roles = {TimelineModel::EffectNamesRole};
1417             roles << (outEffects ? TimelineModel::FadeOutRole : TimelineModel::FadeInRole);
1418             Q_EMIT dataChanged(QModelIndex(), QModelIndex(), roles);
1419             pCore->updateItemKeyframes(m_ownerId);
1420             return true;
1421         };
1422         updateRedo();
1423         PUSH_LAMBDA(updateRedo, redo);
1424     }
1425 }
1426 
1427 const QString EffectStackModel::effectNames() const
1428 {
1429     QStringList effects;
1430     for (int i = 0; i < rootItem->childCount(); ++i) {
1431         effects.append(EffectsRepository::get()->getName(std::static_pointer_cast<EffectItemModel>(rootItem->child(i))->getAssetId()));
1432     }
1433     return effects.join(QLatin1Char('/'));
1434 }
1435 
1436 QStringList EffectStackModel::externalFiles() const
1437 {
1438     QStringList urls;
1439     for (int i = 0; i < rootItem->childCount(); ++i) {
1440         auto filter = std::static_pointer_cast<EffectItemModel>(rootItem->child(i))->filter();
1441         QString url;
1442         if (filter.property_exists("av.file")) {
1443             url = filter.get("av.file");
1444         } else if (filter.property_exists("luma.resource")) {
1445             url = filter.get("luma.resource");
1446         } else if (filter.property_exists("resource")) {
1447             url = filter.get("resource");
1448         }
1449         if (!url.isEmpty()) {
1450             urls << url;
1451         }
1452     }
1453     urls.removeDuplicates();
1454     return urls;
1455 }
1456 
1457 bool EffectStackModel::isStackEnabled() const
1458 {
1459     return m_effectStackEnabled;
1460 }
1461 
1462 bool EffectStackModel::addEffectKeyFrame(int frame, double normalisedVal)
1463 {
1464     if (rootItem->childCount() == 0) return false;
1465     int ix = getActiveEffect();
1466     if (ix < 0) {
1467         return false;
1468     }
1469     std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
1470     std::shared_ptr<KeyframeModelList> listModel = sourceEffect->getKeyframeModel();
1471     if (m_ownerId.type == KdenliveObjectType::TimelineTrack) {
1472         sourceEffect->filter().set("out", pCore->getItemDuration(m_ownerId));
1473     }
1474     return listModel->addKeyframe(frame, normalisedVal);
1475 }
1476 
1477 bool EffectStackModel::removeKeyFrame(int frame)
1478 {
1479     if (rootItem->childCount() == 0) return false;
1480     int ix = getActiveEffect();
1481     if (ix < 0) {
1482         return false;
1483     }
1484     std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
1485     std::shared_ptr<KeyframeModelList> listModel = sourceEffect->getKeyframeModel();
1486     return listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps()));
1487 }
1488 
1489 bool EffectStackModel::updateKeyFrame(int oldFrame, int newFrame, QVariant normalisedVal)
1490 {
1491     if (rootItem->childCount() == 0) return false;
1492     int ix = getActiveEffect();
1493     if (ix < 0) {
1494         return false;
1495     }
1496     std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
1497     std::shared_ptr<KeyframeModelList> listModel = sourceEffect->getKeyframeModel();
1498     if (m_ownerId.type == KdenliveObjectType::TimelineTrack) {
1499         sourceEffect->filter().set("out", pCore->getItemDuration(m_ownerId));
1500     }
1501     return listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), std::move(normalisedVal));
1502 }
1503 
1504 bool EffectStackModel::hasKeyFrame(int frame)
1505 {
1506     if (rootItem->childCount() == 0) return false;
1507     int ix = getActiveEffect();
1508     if (ix < 0) {
1509         return false;
1510     }
1511     std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
1512     std::shared_ptr<KeyframeModelList> listModel = sourceEffect->getKeyframeModel();
1513     return listModel->hasKeyframe(frame);
1514 }
1515 
1516 bool EffectStackModel::hasEffect(const QString &assetId) const
1517 {
1518     for (int i = 0; i < rootItem->childCount(); ++i) {
1519         if (std::static_pointer_cast<EffectItemModel>(rootItem->child(i))->getAssetId() == assetId) {
1520             return true;
1521         }
1522     }
1523     return false;
1524 }
1525 
1526 QVariantList EffectStackModel::getEffectZones() const
1527 {
1528     QVariantList effectZones;
1529     for (int i = 0; i < rootItem->childCount(); ++i) {
1530         auto item = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
1531         if (item->hasForcedInOut()) {
1532             QPair<int, int> z = item->getInOut();
1533             effectZones << QPoint(z.first, z.second);
1534         }
1535     }
1536     return effectZones;
1537 }
1538 
1539 void EffectStackModel::updateEffectZones()
1540 {
1541     Q_EMIT dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectZonesRole});
1542     if (m_ownerId.type == KdenliveObjectType::Master) {
1543         Q_EMIT updateMasterZones();
1544     }
1545 }
1546 
1547 void EffectStackModel::passEffects(Mlt::Producer *producer, const QString &exception)
1548 {
1549     auto ms = m_masterService.lock();
1550     int ct = ms->filter_count();
1551     for (int i = 0; i < ct; i++) {
1552         if (ms->filter(i)->get_int("internal_added") > 0 || !ms->filter(i)->property_exists("kdenlive_id")) {
1553             continue;
1554         }
1555         if (!exception.isEmpty() && QString(ms->filter(i)->get("mlt_service")) == exception) {
1556             continue;
1557         }
1558         auto *filter = new Mlt::Filter(*ms->filter(i));
1559         producer->attach(*filter);
1560     }
1561 }