effectstackmodel.cpp 48 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/***************************************************************************
 *   Copyright (C) 2017 by Nicolas Carion                                  *
 *   This file is part of Kdenlive. See www.kdenlive.org.                  *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) version 3 or any later version accepted by the       *
 *   membership of KDE e.V. (or its successor approved  by the membership  *
 *   of KDE e.V.), which shall act as a proxy defined in Section 14 of     *
 *   version 3 of the license.                                             *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 ***************************************************************************/
#include "effectstackmodel.hpp"
22
#include "assets/keyframes/model/keyframemodellist.hpp"
23
#include "core.h"
24
#include "doc/docundostack.hpp"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
25
26
#include "effectgroupmodel.hpp"
#include "effectitemmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
27
#include "effects/effectsrepository.hpp"
28
#include "macros.hpp"
29
30
#include "timeline2/model/timelinemodel.hpp"
#include <profiles/profilemodel.hpp>
Nicolas Carion's avatar
Nicolas Carion committed
31
#include <stack>
Nicolas Carion's avatar
linting    
Nicolas Carion committed
32
#include <utility>
Nicolas Carion's avatar
Nicolas Carion committed
33
#include <vector>
34

35
EffectStackModel::EffectStackModel(std::weak_ptr<Mlt::Service> service, ObjectId ownerId, std::weak_ptr<DocUndoStack> undo_stack)
36
    : AbstractTreeModel()
37
    , m_effectStackEnabled(true)
Nicolas Carion's avatar
Nicolas Carion committed
38
    , m_ownerId(std::move(ownerId))
Nicolas Carion's avatar
Nicolas Carion committed
39
    , m_undoStack(std::move(undo_stack))
40
    , m_lock(QReadWriteLock::Recursive)
41
    , m_loadingExisting(false)
42
{
43
    m_masterService = std::move(service);
44
45
}

46
std::shared_ptr<EffectStackModel> EffectStackModel::construct(std::weak_ptr<Mlt::Service> service, ObjectId ownerId, std::weak_ptr<DocUndoStack> undo_stack)
47
{
Nicolas Carion's avatar
Nicolas Carion committed
48
    std::shared_ptr<EffectStackModel> self(new EffectStackModel(std::move(service), ownerId, std::move(undo_stack)));
49
    self->rootItem = EffectGroupModel::construct(QStringLiteral("root"), self, true);
50
    return self;
51
52
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
53
54
void EffectStackModel::resetService(std::weak_ptr<Mlt::Service> service)
{
55
    QWriteLocker locker(&m_lock);
56
57
    m_masterService = std::move(service);
    m_childServices.clear();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
58
59
    // replant all effects in new service
    for (int i = 0; i < rootItem->childCount(); ++i) {
60
        std::static_pointer_cast<EffectItemModel>(rootItem->child(i))->plant(m_masterService);
61
62
63
64
65
    }
}

void EffectStackModel::addService(std::weak_ptr<Mlt::Service> service)
{
66
    QWriteLocker locker(&m_lock);
67
68
69
70
71
72
73
74
75
76
    m_childServices.emplace_back(std::move(service));
    for (int i = 0; i < rootItem->childCount(); ++i) {
        std::static_pointer_cast<EffectItemModel>(rootItem->child(i))->plantClone(m_childServices.back());
    }
}

void EffectStackModel::loadService(std::weak_ptr<Mlt::Service> service)
{
    QWriteLocker locker(&m_lock);
    m_childServices.emplace_back(std::move(service));
77
    for (int i = 0; i < rootItem->childCount(); ++i) {
78
        std::static_pointer_cast<EffectItemModel>(rootItem->child(i))->loadClone(m_childServices.back());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
79
80
    }
}
81

Nicolas Carion's avatar
Nicolas Carion committed
82
void EffectStackModel::removeService(const std::shared_ptr<Mlt::Service> &service)
83
{
84
    QWriteLocker locker(&m_lock);
85
    std::vector<int> to_delete;
86
87
88
89
90
91
    for (int i = int(m_childServices.size()) - 1; i >= 0; --i) {
        auto ptr = m_childServices[uint(i)].lock();
        if (service->get_int("_childid") == ptr->get_int("_childid")) {
            for (int j = 0; j < rootItem->childCount(); ++j) {
                std::static_pointer_cast<EffectItemModel>(rootItem->child(j))->unplantClone(ptr);
            }
92
93
94
95
            to_delete.push_back(i);
        }
    }
    for (int i : to_delete) {
96
        m_childServices.erase(m_childServices.begin() + i);
97
98
    }
}
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
99

100
101
102
void EffectStackModel::removeCurrentEffect()
{
    int ix = 0;
103
    if (auto ptr = m_masterService.lock()) {
104
105
106
107
108
109
110
111
112
113
114
        ix = ptr->get_int("kdenlive:activeeffect");
    }
    if (ix < 0) {
        return;
    }
    std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(ix));
    if (effect) {
        removeEffect(effect);
    }
}

Nicolas Carion's avatar
Nicolas Carion committed
115
void EffectStackModel::removeEffect(const std::shared_ptr<EffectItemModel> &effect)
116
{
117
    qDebug() << "* * ** REMOVING EFFECT FROM STACK!!!\n!!!!!!!!!";
118
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
Nicolas Carion committed
119
    Q_ASSERT(m_allItems.count(effect->getId()) > 0);
120
121
    int parentId = -1;
    if (auto ptr = effect->parentItem().lock()) parentId = ptr->getId();
122
    int current = 0;
123
124
125
126
    if (auto srv = m_masterService.lock()) {
        current = srv->get_int("kdenlive:activeeffect");
        if (current >= rootItem->childCount() - 1) {
            srv->set("kdenlive:activeeffect", --current);
127
128
        }
    }
129
    int currentRow = effect->row();
130
    Fun undo = addItem_lambda(effect, parentId);
131
132
133
134
    if (currentRow != rowCount() - 1) {
        Fun move = moveItem_lambda(effect->getId(), currentRow, true);
        PUSH_LAMBDA(move, undo);
    }
135
136
137
    Fun redo = removeItem_lambda(effect->getId());
    bool res = redo();
    if (res) {
Nicolas Carion's avatar
Nicolas Carion committed
138
139
140
141
142
143
        int inFades = int(m_fadeIns.size());
        int outFades = int(m_fadeOuts.size());
        m_fadeIns.erase(effect->getId());
        m_fadeOuts.erase(effect->getId());
        inFades = int(m_fadeIns.size()) - inFades;
        outFades = int(m_fadeOuts.size()) - outFades;
144
        QString effectName = EffectsRepository::get()->getName(effect->getAssetId());
145
        Fun update = [this, current, inFades, outFades]() {
146
            // Required to build the effect view
147
148
149
150
151
152
153
            if (current < 0 || rowCount() == 0) {
                // Stack is now empty
                emit dataChanged(QModelIndex(), QModelIndex(), {});
            } else {
                QVector<int> roles = {TimelineModel::EffectNamesRole};
                if (inFades < 0) {
                    roles << TimelineModel::FadeInRole;
154
                }
155
156
157
                if (outFades < 0) {
                    roles << TimelineModel::FadeOutRole;
                }
158
                qDebug() << "// EMITTING UNDO DATA CHANGE: " << roles;
159
                emit dataChanged(QModelIndex(), QModelIndex(), roles);
160
            }
161
            // TODO: only update if effect is fade or keyframe
162
            /*if (inFades < 0) {
163
                pCore->updateItemModel(m_ownerId, QStringLiteral("fadein"));
164
            } else if (outFades < 0) {
165
                pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout"));
166
            }*/
167
168
169
            pCore->updateItemKeyframes(m_ownerId);
            return true;
        };
170
        Fun update2 = [this, inFades, outFades]() {
171
            // Required to build the effect view
172
            QVector<int> roles = {TimelineModel::EffectNamesRole};
173
174
            // TODO: only update if effect is fade or keyframe
            if (inFades < 0) {
175
                roles << TimelineModel::FadeInRole;
176
            } else if (outFades < 0) {
177
                roles << TimelineModel::FadeOutRole;
178
            }
179
            qDebug() << "// EMITTING REDO DATA CHANGE: " << roles;
180
            emit dataChanged(QModelIndex(), QModelIndex(), roles);
181
182
183
            pCore->updateItemKeyframes(m_ownerId);
            return true;
        };
184
185
        update();
        PUSH_LAMBDA(update, redo);
186
        PUSH_LAMBDA(update2, undo);
187
        PUSH_UNDO(undo, redo, i18n("Delete effect %1", effectName));
188
    } else {
189
        qDebug() << "..........FAILED EFFECT DELETION";
190
    }
191
192
}

Nicolas Carion's avatar
Nicolas Carion committed
193
bool EffectStackModel::copyEffect(const std::shared_ptr<AbstractEffectItem> &sourceItem, PlaylistState::ClipState state, bool logUndo)
194
195
196
197
198
199
200
{
    std::function<bool(void)> undo = []() { return true; };
    std::function<bool(void)> redo = []() { return true; };
    bool result = copyEffect(sourceItem, state, undo, redo);
    if (result && logUndo) {
        std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(sourceItem);
        QString effectName = EffectsRepository::get()->getName(sourceEffect->getAssetId());
Pino Toscano's avatar
Pino Toscano committed
201
        PUSH_UNDO(undo, redo, i18n("Copy effect %1", effectName));
202
203
204
205
    }
    return result;
}

206
207
QDomElement EffectStackModel::toXml(QDomDocument &document)
{
208
209
    QDomElement container = document.createElement(QStringLiteral("effects"));
    for (int i = 0; i < rootItem->childCount(); ++i) {
210
211
212
        std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
        QDomElement sub = document.createElement(QStringLiteral("effect"));
        sub.setAttribute(QStringLiteral("id"), sourceEffect->getAssetId());
213
214
215
216
217
218
        int filterIn = sourceEffect->filter().get_int("in");
        int filterOut = sourceEffect->filter().get_int("out");
        if (filterOut > filterIn) {
            sub.setAttribute(QStringLiteral("in"), filterIn);
            sub.setAttribute(QStringLiteral("out"), filterOut);
        }
219
        QVector<QPair<QString, QVariant>> params = sourceEffect->getAllParameters();
220
        QLocale locale;
Nicolas Carion's avatar
Nicolas Carion committed
221
        for (const auto &param : params) {
222
223
224
225
226
227
228
            if (param.second.type() == QVariant::Double) {
                Xml::setXmlProperty(sub, param.first, locale.toString(param.second.toDouble()));
            } else {
                Xml::setXmlProperty(sub, param.first, param.second.toString());
            }
        }
        container.appendChild(sub);
229
230
    }
    return container;
231
232
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
233
bool EffectStackModel::fromXml(const QDomElement &effectsXml, Fun &undo, Fun &redo)
234
235
{
    QDomNodeList nodeList = effectsXml.elementsByTagName(QStringLiteral("effect"));
236
237
    int parentIn = effectsXml.attribute(QStringLiteral("parentIn")).toInt();
    int currentIn = pCore->getItemIn(m_ownerId);
238
    PlaylistState::ClipState state = pCore->getItemState(m_ownerId);
239
240
241
    for (int i = 0; i < nodeList.count(); ++i) {
        QDomElement node = nodeList.item(i).toElement();
        const QString effectId = node.attribute(QStringLiteral("id"));
242
243
244
245
246
247
248
249
        bool isAudioEffect = EffectsRepository::get()->getType(effectId) == EffectType::Audio;
        if (isAudioEffect) {
            if (state != PlaylistState::AudioOnly) {
                continue;
            }
        } else if (state != PlaylistState::VideoOnly) {
            continue;
        }
250
        auto effect = EffectItemModel::construct(effectId, shared_from_this());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
251
252
253
254
255
        const QString in = node.attribute(QStringLiteral("in"));
        const QString out = node.attribute(QStringLiteral("out"));
        if (!out.isEmpty()) {
            effect->filter().set("in", in.toUtf8().constData());
            effect->filter().set("out", out.toUtf8().constData());
256
        }
257
        QStringList keyframeParams = effect->getKeyframableParameters();
258
259
260
261
        QVector<QPair<QString, QVariant>> parameters;
        QDomNodeList params = node.elementsByTagName(QStringLiteral("property"));
        for (int j = 0; j < params.count(); j++) {
            QDomElement pnode = params.item(j).toElement();
262
            const QString pName = pnode.attribute(QStringLiteral("name"));
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
263
264
265
            if (pName == QLatin1String("in") || pName == QLatin1String("out")) {
                continue;
            }
266
267
268
269
270
271
272
            if (keyframeParams.contains(pName)) {
                // This is a keyframable parameter, fix offest
                QString pValue = KeyframeModel::getAnimationStringWithOffset(effect, pnode.text(), currentIn - parentIn);
                parameters.append(QPair<QString, QVariant>(pName, QVariant(pValue)));
            } else {
                parameters.append(QPair<QString, QVariant>(pName, QVariant(pnode.text())));
            }
273
274
275
276
277
        }
        effect->setParameters(parameters);
        Fun local_undo = removeItem_lambda(effect->getId());
        // TODO the parent should probably not always be the root
        Fun local_redo = addItem_lambda(effect, rootItem->getId());
278
        effect->prepareKeyframes();
279
280
281
        connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
        connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
        if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
Nicolas Carion's avatar
Nicolas Carion committed
282
            m_fadeIns.insert(effect->getId());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
283
284
285
            int duration = effect->filter().get_length() - 1;
            effect->filter().set("in", currentIn);
            effect->filter().set("out", currentIn + duration);
286
        } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
Nicolas Carion's avatar
Nicolas Carion committed
287
            m_fadeOuts.insert(effect->getId());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
288
289
290
291
            int duration = effect->filter().get_length() - 1;
            int filterOut = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1;
            effect->filter().set("in", filterOut - duration);
            effect->filter().set("out", filterOut);
292
293
294
295
296
297
        }
        local_redo();
        UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
    }
    if (true) {
        Fun update = [this]() {
298
            emit dataChanged(QModelIndex(), QModelIndex(), {});
299
300
301
302
303
304
            return true;
        };
        update();
        PUSH_LAMBDA(update, redo);
        PUSH_LAMBDA(update, undo);
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
305
    return true;
306
307
}

Nicolas Carion's avatar
Nicolas Carion committed
308
bool EffectStackModel::copyEffect(const std::shared_ptr<AbstractEffectItem> &sourceItem, PlaylistState::ClipState state, Fun &undo, Fun &redo)
309
{
310
    QWriteLocker locker(&m_lock);
311
    if (sourceItem->childCount() > 0) {
Nicolas Carion's avatar
linting    
Nicolas Carion committed
312
        // TODO: group
313
314
315
316
317
318
319
320
321
322
        return false;
    }
    bool audioEffect = sourceItem->isAudio();
    if (audioEffect) {
        if (state == PlaylistState::VideoOnly) {
            // This effect cannot be used
            return false;
        }
    } else if (state == PlaylistState::AudioOnly) {
        return false;
323
324
    }
    std::shared_ptr<EffectItemModel> sourceEffect = std::static_pointer_cast<EffectItemModel>(sourceItem);
325
    const QString effectId = sourceEffect->getAssetId();
326
    auto effect = EffectItemModel::construct(effectId, shared_from_this());
327
    effect->setParameters(sourceEffect->getAllParameters());
328
329
    effect->filter().set("in", sourceEffect->filter().get_int("in"));
    effect->filter().set("out", sourceEffect->filter().get_int("out"));
330
    Fun local_undo = removeItem_lambda(effect->getId());
331
    // TODO the parent should probably not always be the root
332
    Fun local_redo = addItem_lambda(effect, rootItem->getId());
333
    effect->prepareKeyframes();
334
    connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
335
    connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
336
    QVector<int> roles = {TimelineModel::EffectNamesRole};
337
    if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
Nicolas Carion's avatar
Nicolas Carion committed
338
        m_fadeIns.insert(effect->getId());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
339
340
341
342
        int duration = effect->filter().get_length() - 1;
        int in = pCore->getItemIn(m_ownerId);
        effect->filter().set("in", in);
        effect->filter().set("out", in + duration);
343
        roles << TimelineModel::FadeInRole;
344
    } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
Nicolas Carion's avatar
Nicolas Carion committed
345
        m_fadeOuts.insert(effect->getId());
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
346
347
348
349
        int duration = effect->filter().get_length() - 1;
        int out = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1;
        effect->filter().set("in", out - duration);
        effect->filter().set("out", out);
350
        roles << TimelineModel::FadeOutRole;
351
    }
352
353
    bool res = local_redo();
    if (res) {
354
355
        Fun update = [this, roles]() {
            emit dataChanged(QModelIndex(), QModelIndex(), roles);
356
357
358
359
360
361
            return true;
        };
        update();
        PUSH_LAMBDA(update, local_redo);
        PUSH_LAMBDA(update, local_undo);
        UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
362
    }
363
    return res;
364
365
}

366
bool EffectStackModel::appendEffect(const QString &effectId, bool makeCurrent)
367
{
368
    QWriteLocker locker(&m_lock);
369
    auto effect = EffectItemModel::construct(effectId, shared_from_this());
370
371
372
373
374
375
376
377
378
379
    PlaylistState::ClipState state = pCore->getItemState(m_ownerId);
    if (effect->isAudio()) {
        if (state == PlaylistState::VideoOnly) {
            // Cannot add effect to this clip
            return false;
        }
    } else if (state == PlaylistState::AudioOnly) {
        // Cannot add effect to this clip
        return false;
    }
380
381
382
    Fun undo = removeItem_lambda(effect->getId());
    // TODO the parent should probably not always be the root
    Fun redo = addItem_lambda(effect, rootItem->getId());
383
    effect->prepareKeyframes();
384
    connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged);
385
    connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection);
386
387
    int currentActive = getActiveEffect();
    if (makeCurrent) {
388
389
        if (auto srvPtr = m_masterService.lock()) {
            srvPtr->set("kdenlive:activeeffect", rowCount());
390
391
        }
    }
392
393
    bool res = redo();
    if (res) {
394
395
        int inFades = 0;
        int outFades = 0;
396
        if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
397
398
399
400
            int duration = effect->filter().get_length() - 1;
            int in = pCore->getItemIn(m_ownerId);
            effect->filter().set("in", in);
            effect->filter().set("out", in + duration);
401
            inFades++;
402
        } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
403
404
405
406
            /*int duration = effect->filter().get_length() - 1;
            int out = pCore->getItemIn(m_ownerId) + pCore->getItemDuration(m_ownerId) - 1;
            effect->filter().set("in", out - duration);
            effect->filter().set("out", out);*/
407
            outFades++;
408
        }
409
        QString effectName = EffectsRepository::get()->getName(effectId);
410
        Fun update = [this, inFades, outFades]() {
411
            // TODO: only update if effect is fade or keyframe
412
            QVector<int> roles = {TimelineModel::EffectNamesRole};
413
            if (inFades > 0) {
414
                roles << TimelineModel::FadeInRole;
415
            } else if (outFades > 0) {
416
                roles << TimelineModel::FadeOutRole;
417
            }
418
            pCore->updateItemKeyframes(m_ownerId);
419
            emit dataChanged(QModelIndex(), QModelIndex(), roles);
420
421
            return true;
        };
422
        update();
423
424
        PUSH_LAMBDA(update, redo);
        PUSH_LAMBDA(update, undo);
425
        PUSH_UNDO(undo, redo, i18n("Add effect %1", effectName));
426
    } else if (makeCurrent) {
427
428
        if (auto srvPtr = m_masterService.lock()) {
            srvPtr->set("kdenlive:activeeffect", currentActive);
429
        }
430
    }
431
    return res;
432
433
}

434
435
bool EffectStackModel::adjustStackLength(bool adjustFromEnd, int oldIn, int oldDuration, int newIn, int duration, int offset, Fun &undo, Fun &redo,
                                         bool logUndo)
436
{
437
    QWriteLocker locker(&m_lock);
438
439
440
    const int fadeInDuration = getFadePosition(true);
    const int fadeOutDuration = getFadePosition(false);
    int out = newIn + duration;
441
442
443
444
445
446
447
    for (const auto &leaf : rootItem->getLeaves()) {
        std::shared_ptr<AbstractEffectItem> item = std::static_pointer_cast<AbstractEffectItem>(leaf);
        if (item->effectItemType() == EffectItemType::Group) {
            // probably an empty group, ignore
            continue;
        }
        std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(leaf);
Nicolas Carion's avatar
Nicolas Carion committed
448
        if (fadeInDuration > 0 && m_fadeIns.count(leaf->getId()) > 0) {
449
450
            int oldEffectIn = qMax(0, effect->filter().get_in());
            int oldEffectOut = effect->filter().get_out();
451
            qDebug() << "--previous effect: " << oldEffectIn << "-" << oldEffectOut;
452
453
            int effectDuration = qMin(effect->filter().get_length() - 1, duration);
            if (!adjustFromEnd && (oldIn != newIn || duration != oldDuration)) {
454
                // Clip start was resized, adjust effect in / out
455
                Fun operation = [effect, newIn, effectDuration, logUndo]() {
456
                    effect->setParameter(QStringLiteral("in"), newIn, false);
457
                    effect->setParameter(QStringLiteral("out"), newIn + effectDuration, logUndo);
458
                    qDebug() << "--new effect: " << newIn << "-" << newIn + effectDuration;
459
460
                    return true;
                };
461
462
463
464
                bool res = operation();
                if (!res) {
                    return false;
                }
465
                Fun reverse = [effect, oldEffectIn, oldEffectOut, logUndo]() {
466
                    effect->setParameter(QStringLiteral("in"), oldEffectIn, false);
467
                    effect->setParameter(QStringLiteral("out"), oldEffectOut, logUndo);
468
469
470
471
472
473
474
475
476
477
478
                    return true;
                };
                PUSH_LAMBDA(operation, redo);
                PUSH_LAMBDA(reverse, undo);
            } else if (effectDuration < oldEffectOut - oldEffectIn || (logUndo && effect->filter().get_int("_refout") > 0)) {
                // Clip length changed, shorter than effect length so resize
                int referenceEffectOut = effect->filter().get_int("_refout");
                if (referenceEffectOut <= 0) {
                    referenceEffectOut = oldEffectOut;
                    effect->filter().set("_refout", referenceEffectOut);
                }
479
                Fun operation = [effect, oldEffectIn, effectDuration, logUndo]() {
480
                    effect->setParameter(QStringLiteral("out"), oldEffectIn + effectDuration, logUndo);
481
482
                    return true;
                };
483
484
485
486
487
                bool res = operation();
                if (!res) {
                    return false;
                }
                if (logUndo) {
488
                    Fun reverse = [effect, referenceEffectOut]() {
489
                        effect->setParameter(QStringLiteral("out"), referenceEffectOut, true);
490
                        effect->filter().set("_refout", (char *)nullptr);
491
492
493
494
495
496
                        return true;
                    };
                    PUSH_LAMBDA(operation, redo);
                    PUSH_LAMBDA(reverse, undo);
                }
            }
Nicolas Carion's avatar
Nicolas Carion committed
497
        } else if (fadeOutDuration > 0 && m_fadeOuts.count(leaf->getId()) > 0) {
498
            int effectDuration = qMin(fadeOutDuration, duration);
Vincent Pinon's avatar
Vincent Pinon committed
499
500
            int newFadeIn = out - effectDuration;
            int oldFadeIn = effect->filter().get_int("in");
501
502
503
            int oldOut = effect->filter().get_int("out");
            int referenceEffectIn = effect->filter().get_int("_refin");
            if (referenceEffectIn <= 0) {
Vincent Pinon's avatar
Vincent Pinon committed
504
                referenceEffectIn = oldFadeIn;
505
506
                effect->filter().set("_refin", referenceEffectIn);
            }
507
            Fun operation = [effect, newFadeIn, out, logUndo]() {
Vincent Pinon's avatar
Vincent Pinon committed
508
                effect->setParameter(QStringLiteral("in"), newFadeIn, false);
509
                effect->setParameter(QStringLiteral("out"), out, logUndo);
510
511
                return true;
            };
512
513
514
515
516
            bool res = operation();
            if (!res) {
                return false;
            }
            if (logUndo) {
517
                Fun reverse = [effect, referenceEffectIn, oldOut]() {
518
                    effect->setParameter(QStringLiteral("in"), referenceEffectIn, false);
519
                    effect->setParameter(QStringLiteral("out"), oldOut, true);
520
                    effect->filter().set("_refin", (char *)nullptr);
521
522
523
524
525
                    return true;
                };
                PUSH_LAMBDA(operation, redo);
                PUSH_LAMBDA(reverse, undo);
            }
526
527
        } else {
            // Not a fade effect, check for keyframes
528
529
            std::shared_ptr<KeyframeModelList> keyframes = effect->getKeyframeModel();
            if (keyframes != nullptr) {
530
                // Effect has keyframes, update these
531
                keyframes->resizeKeyframes(oldIn, oldIn + oldDuration - 1, newIn, out - 1, offset, adjustFromEnd, undo, redo);
532
                QModelIndex index = getIndexFromItem(effect);
533
                Fun refresh = [effect, index]() {
534
535
536
537
538
539
                    effect->dataChanged(index, index, QVector<int>());
                    return true;
                };
                refresh();
                PUSH_LAMBDA(refresh, redo);
                PUSH_LAMBDA(refresh, undo);
540
            } else {
541
                qDebug() << "// NULL Keyframes---------";
542
            }
543
544
545
        }
    }
    return true;
546
547
}

548
bool EffectStackModel::adjustFadeLength(int duration, bool fromStart, bool audioFade, bool videoFade, bool logUndo)
549
{
550
    QWriteLocker locker(&m_lock);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
551
552
    if (fromStart) {
        // Fade in
Nicolas Carion's avatar
Nicolas Carion committed
553
        if (m_fadeIns.empty()) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
554
555
556
557
558
559
560
            if (audioFade) {
                appendEffect(QStringLiteral("fadein"));
            }
            if (videoFade) {
                appendEffect(QStringLiteral("fade_from_black"));
            }
        }
Nicolas Carion's avatar
Nicolas Carion committed
561
        QList<QModelIndex> indexes;
562
        auto ptr = m_masterService.lock();
563
564
565
566
        int in = 0;
        if (ptr) {
            in = ptr->get_int("in");
        }
567
        int oldDuration = -1;
568
        for (int i = 0; i < rootItem->childCount(); ++i) {
Nicolas Carion's avatar
Nicolas Carion committed
569
            if (m_fadeIns.count(std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) > 0) {
570
                std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
571
572
573
                if (oldDuration == -1) {
                    oldDuration = effect->filter().get_length();
                }
574
                effect->filter().set("in", in);
575
                duration = qMin(pCore->getItemDuration(m_ownerId), duration);
576
                effect->filter().set("out", in + duration);
577
578
                indexes << getIndexFromItem(effect);
            }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
579
        }
580
581
        if (!indexes.isEmpty()) {
            emit dataChanged(indexes.first(), indexes.last(), QVector<int>());
582
            pCore->updateItemModel(m_ownerId, QStringLiteral("fadein"));
583
584
585
586
587
588
589
590
            if (videoFade) {
                int min = pCore->getItemPosition(m_ownerId);
                QSize range(min, min + qMax(duration, oldDuration));
                pCore->refreshProjectRange(range);
                if (logUndo) {
                    pCore->invalidateRange(range);
                }
            }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
591
592
593
        }
    } else {
        // Fade out
Nicolas Carion's avatar
Nicolas Carion committed
594
        if (m_fadeOuts.empty()) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
595
596
597
598
599
600
            if (audioFade) {
                appendEffect(QStringLiteral("fadeout"));
            }
            if (videoFade) {
                appendEffect(QStringLiteral("fade_to_black"));
            }
601
        }
602
        int in = 0;
603
        auto ptr = m_masterService.lock();
604
605
606
        if (ptr) {
            in = ptr->get_int("in");
        }
607
608
609
        int itemDuration = pCore->getItemDuration(m_ownerId);
        int out = in + itemDuration;
        int oldDuration = -1;
Nicolas Carion's avatar
Nicolas Carion committed
610
        QList<QModelIndex> indexes;
611
        for (int i = 0; i < rootItem->childCount(); ++i) {
Nicolas Carion's avatar
Nicolas Carion committed
612
            if (m_fadeOuts.count(std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) > 0) {
613
                std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
614
615
616
                if (oldDuration == -1) {
                    oldDuration = effect->filter().get_length();
                }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
617
                effect->filter().set("out", out - 1);
618
                duration = qMin(itemDuration, duration);
619
620
621
                effect->filter().set("in", out - duration);
                indexes << getIndexFromItem(effect);
            }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
622
        }
623
624
        if (!indexes.isEmpty()) {
            emit dataChanged(indexes.first(), indexes.last(), QVector<int>());
625
            pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout"));
626
627
628
629
630
631
632
633
            if (videoFade) {
                int min = pCore->getItemPosition(m_ownerId);
                QSize range(min + itemDuration - qMax(duration, oldDuration), min + itemDuration);
                pCore->refreshProjectRange(range);
                if (logUndo) {
                    pCore->invalidateRange(range);
                }
            }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
634
        }
635
636
637
638
    }
    return true;
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
639
int EffectStackModel::getFadePosition(bool fromStart)
640
{
641
    QWriteLocker locker(&m_lock);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
642
    if (fromStart) {
Nicolas Carion's avatar
Nicolas Carion committed
643
        if (m_fadeIns.empty()) {
644
645
646
            return 0;
        }
        for (int i = 0; i < rootItem->childCount(); ++i) {
Nicolas Carion's avatar
Nicolas Carion committed
647
            if (*(m_fadeIns.begin()) == std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) {
648
                std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
649
                return effect->filter().get_length() - 1;
650
            }
651
        }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
652
    } else {
Nicolas Carion's avatar
Nicolas Carion committed
653
        if (m_fadeOuts.empty()) {
654
655
656
            return 0;
        }
        for (int i = 0; i < rootItem->childCount(); ++i) {
Nicolas Carion's avatar
Nicolas Carion committed
657
            if (*(m_fadeOuts.begin()) == std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) {
658
                std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
659
                return effect->filter().get_length() - 1;
660
            }
661
662
663
664
665
        }
    }
    return 0;
}

666
bool EffectStackModel::removeFade(bool fromStart)
667
{
668
    QWriteLocker locker(&m_lock);
669
    std::vector<int> toRemove;
670
    for (int i = 0; i < rootItem->childCount(); ++i) {
Nicolas Carion's avatar
Nicolas Carion committed
671
672
        if ((fromStart && m_fadeIns.count(std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) > 0) ||
            (!fromStart && m_fadeOuts.count(std::static_pointer_cast<TreeItem>(rootItem->child(i))->getId()) > 0)) {
673
            toRemove.push_back(i);
674
675
        }
    }
676
    for (int i : toRemove) {
677
678
        std::shared_ptr<EffectItemModel> effect = std::static_pointer_cast<EffectItemModel>(rootItem->child(i));
        removeEffect(effect);
679
680
681
682
    }
    return true;
}

Nicolas Carion's avatar
Nicolas Carion committed
683
void EffectStackModel::moveEffect(int destRow, const std::shared_ptr<AbstractEffectItem> &item)
684
{
685
    QWriteLocker locker(&m_lock);
686
687
688
689
690
691
    Q_ASSERT(m_allItems.count(item->getId()) > 0);
    int oldRow = item->row();
    Fun undo = moveItem_lambda(item->getId(), oldRow);
    Fun redo = moveItem_lambda(item->getId(), destRow);
    bool res = redo();
    if (res) {
692
        Fun update = [this]() {
693
            this->dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectNamesRole});
694
695
696
697
            return true;
        };
        update();
        UPDATE_UNDO_REDO(update, update, undo, redo);
698
699
700
        auto effectId = std::static_pointer_cast<EffectItemModel>(item)->getAssetId();
        QString effectName = EffectsRepository::get()->getName(effectId);
        PUSH_UNDO(undo, redo, i18n("Move effect %1", effectName));
701
702
703
    }
}

704
void EffectStackModel::registerItem(const std::shared_ptr<TreeItem> &item)
705
{
706
    QWriteLocker locker(&m_lock);
707
    // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect";
708
    QModelIndex ix;
709
    if (!item->isRoot()) {
710
        auto effectItem = std::static_pointer_cast<EffectItemModel>(item);
711
        if (!m_loadingExisting) {
712
            // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect in " << m_childServices.size();
713
714
            effectItem->plant(m_masterService);
            for (const auto &service : m_childServices) {
715
                // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting CLONE effect in " << (void *)service.lock().get();
716
                effectItem->plantClone(service);
717
            }
718
        }
719
        effectItem->setEffectStackEnabled(m_effectStackEnabled);
720
721
        const QString &effectId = effectItem->getAssetId();
        if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
Nicolas Carion's avatar
Nicolas Carion committed
722
            m_fadeIns.insert(effectItem->getId());
723
        } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
Nicolas Carion's avatar
Nicolas Carion committed
724
            m_fadeOuts.insert(effectItem->getId());
725
        }
726
        ix = getIndexFromItem(effectItem);
727
        if (!effectItem->isAudio() && !m_loadingExisting) {
728
            pCore->refreshProjectItem(m_ownerId);
729
            pCore->invalidateItem(m_ownerId);
730
        }
731
    }
732
    AbstractTreeModel::registerItem(item);
733
}
734

735
736
void EffectStackModel::deregisterItem(int id, TreeItem *item)
{
737
    QWriteLocker locker(&m_lock);
738
    if (!item->isRoot()) {
Nicolas Carion's avatar
Nicolas Carion committed
739
        auto effectItem = static_cast<AbstractEffectItem *>(item);
740
741
742
        effectItem->unplant(m_masterService);
        for (const auto &service : m_childServices) {
            effectItem->unplantClone(service);
743
        }
744
745
        if (!effectItem->isAudio()) {
            pCore->refreshProjectItem(m_ownerId);
746
            pCore->invalidateItem(m_ownerId);
747
        }
748
749
    }
    AbstractTreeModel::deregisterItem(id, item);
750
}
751

752
void EffectStackModel::setEffectStackEnabled(bool enabled)
753
{
754
    QWriteLocker locker(&m_lock);
755
    m_effectStackEnabled = enabled;
756

Nicolas Carion's avatar
Nicolas Carion committed
757
    // Recursively updates children states
758
    for (int i = 0; i < rootItem->childCount(); ++i) {
759
        std::static_pointer_cast<AbstractEffectItem>(rootItem->child(i))->setEffectStackEnabled(enabled);
760
    }
761
    emit dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectsEnabledRole});
762
    emit enabledStateChanged();
763
}
764

Nicolas Carion's avatar
Nicolas Carion committed
765
std::shared_ptr<AbstractEffectItem> EffectStackModel::getEffectStackRow(int row, const std::shared_ptr<TreeItem> &parentItem)
766
{
767
    return std::static_pointer_cast<AbstractEffectItem>(parentItem ? parentItem->child(row) : rootItem->child(row));
768
769
}

Nicolas Carion's avatar
Nicolas Carion committed
770
bool EffectStackModel::importEffects(const std::shared_ptr<EffectStackModel> &sourceStack, PlaylistState::ClipState state)
771
{
772
    QWriteLocker locker(&m_lock);
Nicolas Carion's avatar
linting    
Nicolas Carion committed
773
    // TODO: manage fades, keyframes if clips don't have same size / in point
774
    bool found = false;
775
    for (int i = 0; i < sourceStack->rowCount(); i++) {
776
        auto item = sourceStack->getEffectStackRow(i);
777
        // NO undo. this should only be used on project opening
778
779
780
        if (copyEffect(item, state, false)) {
            found = true;
        }