File indexing completed on 2024-04-14 04:46:15

0001 /*
0002 SPDX-FileCopyrightText: 2012 Till Theato <root@ttill.de>
0003 SPDX-FileCopyrightText: 2014 Jean-Baptiste Mardelle <jb@kdenlive.org>
0004 SPDX-FileCopyrightText: 2017 Nicolas Carion
0005 This file is part of Kdenlive. See www.kdenlive.org.
0006 
0007 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0008 */
0009 
0010 #include "projectitemmodel.h"
0011 #include "abstractprojectitem.h"
0012 #include "binplaylist.hpp"
0013 #include "core.h"
0014 #include "doc/kdenlivedoc.h"
0015 #include "filewatcher.hpp"
0016 #include "jobs/audiolevelstask.h"
0017 #include "jobs/cliploadtask.h"
0018 #include "kdenlivesettings.h"
0019 #include "lib/localeHandling.h"
0020 #include "macros.hpp"
0021 #include "profiles/profilemodel.hpp"
0022 #include "project/projectmanager.h"
0023 #include "projectclip.h"
0024 #include "projectfolder.h"
0025 #include "projectsubclip.h"
0026 #include "utils/thumbnailcache.hpp"
0027 #include "xml/xml.hpp"
0028 
0029 #include <KLocalizedString>
0030 
0031 #include <QIcon>
0032 #include <QJsonArray>
0033 #include <QJsonDocument>
0034 #include <QJsonObject>
0035 #include <QMimeData>
0036 #include <QProgressDialog>
0037 
0038 #include <mlt++/Mlt.h>
0039 #include <queue>
0040 #include <qvarlengtharray.h>
0041 #include <utility>
0042 
0043 ProjectItemModel::ProjectItemModel(QObject *parent)
0044     : AbstractTreeModel(parent)
0045     , closing(false)
0046     , m_lock(QReadWriteLock::Recursive)
0047     , m_binPlaylist(nullptr)
0048     , m_fileWatcher(new FileWatcher())
0049     , m_nextId(1)
0050     , m_blankThumb()
0051     , m_dragType(PlaylistState::Disabled)
0052     , m_uuid(QUuid::createUuid())
0053     , m_sequenceFolderId(-1)
0054 {
0055     QPixmap pix(QSize(160, 90));
0056     pix.fill(Qt::lightGray);
0057     m_blankThumb.addPixmap(pix);
0058     connect(m_fileWatcher.get(), &FileWatcher::binClipModified, this, &ProjectItemModel::reloadClip);
0059     connect(m_fileWatcher.get(), &FileWatcher::binClipWaiting, this, &ProjectItemModel::setClipWaiting);
0060     connect(m_fileWatcher.get(), &FileWatcher::binClipMissing, this, &ProjectItemModel::setClipInvalid);
0061     missingClipTimer.setInterval(500);
0062     missingClipTimer.setSingleShot(true);
0063     connect(&missingClipTimer, &QTimer::timeout, this, &ProjectItemModel::slotUpdateInvalidCount);
0064 }
0065 
0066 std::shared_ptr<ProjectItemModel> ProjectItemModel::construct(QObject *parent)
0067 {
0068     std::shared_ptr<ProjectItemModel> self(new ProjectItemModel(parent));
0069     self->rootItem = ProjectFolder::construct(self);
0070     return self;
0071 }
0072 
0073 ProjectItemModel::~ProjectItemModel() = default;
0074 
0075 void ProjectItemModel::buildPlaylist(const QUuid uuid)
0076 {
0077     m_uuid = uuid;
0078     m_fileWatcher->clear();
0079     m_extraPlaylists.clear();
0080     Q_ASSERT(m_projectTractor.use_count() <= 1);
0081     m_projectTractor.reset();
0082     m_binPlaylist.reset(new BinPlaylist(uuid));
0083     m_projectTractor.reset(new Mlt::Tractor(pCore->getProjectProfile()));
0084     m_projectTractor->set("kdenlive:projectTractor", 1);
0085     m_binPlaylist->setRetainIn(m_projectTractor.get());
0086 }
0087 
0088 int ProjectItemModel::mapToColumn(int column) const
0089 {
0090     switch (column) {
0091     case 0:
0092         return AbstractProjectItem::DataName;
0093         break;
0094     case 1:
0095         return AbstractProjectItem::DataDate;
0096         break;
0097     case 2:
0098         return AbstractProjectItem::DataDescription;
0099         break;
0100     case 3:
0101         return AbstractProjectItem::ClipType;
0102         break;
0103     case 4:
0104         return AbstractProjectItem::DataTag;
0105         break;
0106     case 5:
0107         return AbstractProjectItem::DataDuration;
0108         break;
0109     case 6:
0110         return AbstractProjectItem::DataId;
0111         break;
0112     case 7:
0113         return AbstractProjectItem::DataRating;
0114         break;
0115     case 8:
0116         return AbstractProjectItem::UsageCount;
0117         break;
0118     default:
0119         return AbstractProjectItem::DataName;
0120     }
0121 }
0122 
0123 QList<int> ProjectItemModel::mapDataToColumn(AbstractProjectItem::DataType type) const
0124 {
0125     // Some data types are used in several columns, for example usage count has its
0126     // own column for sorting but is also displayed in column 0
0127     switch (type) {
0128     case AbstractProjectItem::DataName:
0129     case AbstractProjectItem::DataThumbnail:
0130     case AbstractProjectItem::IconOverlay:
0131     case AbstractProjectItem::JobProgress:
0132         return {0};
0133         break;
0134     case AbstractProjectItem::DataDate:
0135         return {1};
0136         break;
0137     case AbstractProjectItem::DataDescription:
0138         return {2};
0139         break;
0140     case AbstractProjectItem::ClipType:
0141         return {3};
0142         break;
0143     case AbstractProjectItem::DataTag:
0144         return {0, 4};
0145         break;
0146     case AbstractProjectItem::DataDuration:
0147         return {0, 5};
0148         break;
0149     case AbstractProjectItem::DataId:
0150         return {6};
0151         break;
0152     case AbstractProjectItem::DataRating:
0153         return {7};
0154         break;
0155     case AbstractProjectItem::UsageCount:
0156         return {0, 8};
0157         break;
0158     default:
0159         return {};
0160     }
0161 }
0162 
0163 QVariant ProjectItemModel::data(const QModelIndex &index, int role) const
0164 {
0165     READ_LOCK();
0166     if (!index.isValid()) {
0167         return QVariant();
0168     }
0169     if (role == Qt::DisplayRole || role == Qt::EditRole) {
0170         std::shared_ptr<AbstractProjectItem> item = getBinItemByIndex(index);
0171         auto type = static_cast<AbstractProjectItem::DataType>(mapToColumn(index.column()));
0172         QVariant ret = item->getData(type);
0173         return ret;
0174     }
0175     if (role == Qt::DecorationRole) {
0176         if (index.column() != 0) {
0177             return QVariant();
0178         }
0179         // Data has to be returned as icon to allow the view to scale it
0180         std::shared_ptr<AbstractProjectItem> item = getBinItemByIndex(index);
0181         return item->icon();
0182     }
0183     std::shared_ptr<AbstractProjectItem> item = getBinItemByIndex(index);
0184     return item->getData(static_cast<AbstractProjectItem::DataType>(role));
0185 }
0186 
0187 bool ProjectItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
0188 {
0189     QWriteLocker locker(&m_lock);
0190     std::shared_ptr<AbstractProjectItem> item = getBinItemByIndex(index);
0191     if (item->rename(value.toString(), index.column())) {
0192         Q_EMIT dataChanged(index, index, {role});
0193         return true;
0194     }
0195     // Item name was not changed
0196     return false;
0197 }
0198 
0199 Qt::ItemFlags ProjectItemModel::flags(const QModelIndex &index) const
0200 {
0201     /*return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable;*/
0202     READ_LOCK();
0203     if (!index.isValid()) {
0204         return Qt::ItemIsDropEnabled;
0205     }
0206     std::shared_ptr<AbstractProjectItem> item = getBinItemByIndex(index);
0207     AbstractProjectItem::PROJECTITEMTYPE type = item->itemType();
0208     switch (type) {
0209     case AbstractProjectItem::FolderItem:
0210         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable;
0211         break;
0212     case AbstractProjectItem::ClipItem:
0213         if (!item->statusReady()) {
0214             return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
0215         }
0216         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable;
0217         break;
0218     case AbstractProjectItem::SubClipItem:
0219         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
0220         break;
0221     default:
0222         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
0223     }
0224 }
0225 
0226 // cppcheck-suppress unusedFunction
0227 bool ProjectItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
0228 {
0229     Q_UNUSED(row)
0230     Q_UNUSED(column)
0231     QWriteLocker locker(&m_lock);
0232     if (action == Qt::IgnoreAction) {
0233         return true;
0234     }
0235 
0236     if (data->hasUrls()) {
0237         Q_EMIT urlsDropped(data->urls(), parent);
0238         return true;
0239     }
0240 
0241     if (data->hasFormat(QStringLiteral("text/producerslist"))) {
0242         // Dropping an Bin item
0243         const QStringList ids = QString(data->data(QStringLiteral("text/producerslist"))).split(QLatin1Char(';'));
0244         if (ids.constFirst().contains(QLatin1Char('/'))) {
0245             // subclip zone
0246             QStringList clipData = ids.constFirst().split(QLatin1Char('/'));
0247             if (clipData.length() >= 3) {
0248                 QString bid = clipData.at(0);
0249                 if (bid.startsWith(QLatin1Char('A')) || bid.startsWith(QLatin1Char('V'))) {
0250                     bid.remove(0, 1);
0251                 }
0252                 std::shared_ptr<ProjectClip> masterClip = getClipByBinID(bid);
0253                 std::shared_ptr<ProjectSubClip> sub = masterClip->getSubClip(clipData.at(1).toInt(), clipData.at(2).toInt());
0254                 if (sub != nullptr) {
0255                     // This zone already exists
0256                     return false;
0257                 }
0258                 QString id;
0259                 return requestAddBinSubClip(id, clipData.at(1).toInt(), clipData.at(2).toInt(), {}, bid);
0260             } else {
0261                 // error, malformed clip zone, abort
0262                 return false;
0263             }
0264         } else {
0265             Q_EMIT itemDropped(ids, parent);
0266         }
0267         return true;
0268     }
0269 
0270     if (data->hasFormat(QStringLiteral("kdenlive/effect"))) {
0271         // Dropping effect on a Bin item
0272         QStringList effectData;
0273         effectData << QString::fromUtf8(data->data(QStringLiteral("kdenlive/effect")));
0274         QStringList source = QString::fromUtf8(data->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char(','));
0275         effectData << source;
0276         Q_EMIT effectDropped(effectData, parent);
0277         return true;
0278     }
0279 
0280     if (data->hasFormat(QStringLiteral("kdenlive/clip"))) {
0281         const QStringList list = QString(data->data(QStringLiteral("kdenlive/clip"))).split(QLatin1Char(';'));
0282         QString id;
0283         return requestAddBinSubClip(id, list.at(1).toInt(), list.at(2).toInt(), {}, list.at(0));
0284     }
0285 
0286     if (data->hasFormat(QStringLiteral("kdenlive/tag"))) {
0287         // Dropping effect on a Bin item
0288         QString tag = QString::fromUtf8(data->data(QStringLiteral("kdenlive/tag")));
0289         Q_EMIT addTag(tag, parent);
0290         return true;
0291     }
0292 
0293     return false;
0294 }
0295 
0296 QVariant ProjectItemModel::headerData(int section, Qt::Orientation orientation, int role) const
0297 {
0298     READ_LOCK();
0299     if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
0300         QVariant columnName;
0301         switch (section) {
0302         case 0:
0303             columnName = i18n("Name");
0304             break;
0305         case 1:
0306             columnName = i18n("Date");
0307             break;
0308         case 2:
0309             columnName = i18n("Description");
0310             break;
0311         case 3:
0312             columnName = i18n("Type");
0313             break;
0314         case 4:
0315             columnName = i18n("Tag");
0316             break;
0317         case 5:
0318             columnName = i18n("Duration");
0319             break;
0320         case 6:
0321             columnName = i18n("Id");
0322             break;
0323         case 7:
0324             columnName = i18n("Rating");
0325             break;
0326         case 8:
0327             columnName = i18n("Usage");
0328             break;
0329         default:
0330             columnName = i18n("Unknown");
0331             break;
0332         }
0333         return columnName;
0334     }
0335     return QAbstractItemModel::headerData(section, orientation, role);
0336 }
0337 
0338 int ProjectItemModel::columnCount(const QModelIndex &parent) const
0339 {
0340     READ_LOCK();
0341     if (parent.isValid()) {
0342         return getBinItemByIndex(parent)->supportedDataCount();
0343     }
0344     return std::static_pointer_cast<ProjectFolder>(rootItem)->supportedDataCount();
0345 }
0346 
0347 // cppcheck-suppress unusedFunction
0348 Qt::DropActions ProjectItemModel::supportedDropActions() const
0349 {
0350     return Qt::CopyAction | Qt::MoveAction;
0351 }
0352 
0353 QStringList ProjectItemModel::mimeTypes() const
0354 {
0355     QStringList types{QStringLiteral("text/producerslist"), QStringLiteral("text/uri-list"), QStringLiteral("kdenlive/clip"), QStringLiteral("kdenlive/effect"),
0356                       QStringLiteral("kdenlive/tag")};
0357     return types;
0358 }
0359 
0360 QMimeData *ProjectItemModel::mimeData(const QModelIndexList &indices) const
0361 {
0362     READ_LOCK();
0363     // Mime data is a list of id's separated by ';'.
0364     // Clip ids are represented like:  2 (where 2 is the clip's id)
0365     // Clip zone ids are represented like:  2/10/200 (where 2 is the clip's id, 10 and 200 are in and out points)
0366     // Folder ids are represented like:  #2 (where 2 is the folder's id)
0367     auto *mimeData = new QMimeData();
0368     QStringList list;
0369     QString parentId;
0370     size_t duration = 0;
0371     for (int i = 0; i < indices.count(); i++) {
0372         QModelIndex ix = indices.at(i);
0373         if (!ix.isValid() || ix.column() != 0) {
0374             continue;
0375         }
0376         std::shared_ptr<AbstractProjectItem> item = getBinItemByIndex(ix);
0377         if (!item->statusReady()) {
0378             continue;
0379         }
0380         if (parentId.isEmpty()) {
0381             parentId = ix.parent().data(AbstractProjectItem::DataId).toString();
0382         }
0383         AbstractProjectItem::PROJECTITEMTYPE type = item->itemType();
0384         if (type == AbstractProjectItem::ClipItem) {
0385             ClipType::ProducerType cType = item->clipType();
0386             QString dragId = item->clipId();
0387             if ((cType == ClipType::AV || cType == ClipType::Playlist || cType == ClipType::Timeline)) {
0388                 switch (m_dragType) {
0389                 case PlaylistState::AudioOnly:
0390                     dragId.prepend(QLatin1Char('A'));
0391                     break;
0392                 case PlaylistState::VideoOnly:
0393                     dragId.prepend(QLatin1Char('V'));
0394                     break;
0395                 default:
0396                     break;
0397                 }
0398             }
0399             list << dragId;
0400             duration += (std::static_pointer_cast<ProjectClip>(item))->frameDuration();
0401         } else if (type == AbstractProjectItem::SubClipItem) {
0402             QPoint p = item->zone();
0403             std::shared_ptr<ProjectClip> master = std::static_pointer_cast<ProjectSubClip>(item)->getMasterClip();
0404             QString dragId = master->clipId();
0405             ClipType::ProducerType cType = master->clipType();
0406             if ((cType == ClipType::AV || cType == ClipType::Playlist || cType == ClipType::Timeline)) {
0407                 switch (m_dragType) {
0408                 case PlaylistState::AudioOnly:
0409                     dragId.prepend(QLatin1Char('A'));
0410                     break;
0411                 case PlaylistState::VideoOnly:
0412                     dragId.prepend(QLatin1Char('V'));
0413                     break;
0414                 default:
0415                     break;
0416                 }
0417             }
0418             list << dragId + QLatin1Char('/') + QString::number(p.x()) + QLatin1Char('/') + QString::number(p.y());
0419         } else if (type == AbstractProjectItem::FolderItem) {
0420             list << "#" + item->clipId();
0421         }
0422     }
0423     if (!list.isEmpty()) {
0424         QByteArray data;
0425         data.append(list.join(QLatin1Char(';')).toUtf8());
0426         mimeData->setData(QStringLiteral("text/producerslist"), data);
0427         mimeData->setData(QStringLiteral("text/rootId"), parentId.toLatin1());
0428         mimeData->setData(QStringLiteral("text/dragid"), QUuid::createUuid().toByteArray());
0429         mimeData->setText(QString::number(duration));
0430     }
0431     return mimeData;
0432 }
0433 
0434 void ProjectItemModel::onItemUpdated(const std::shared_ptr<AbstractProjectItem> &item, const QVector<int> &roles)
0435 {
0436     int minColumn = -1;
0437     int maxColumn = -1;
0438     for (auto &r : roles) {
0439         const QList<int> indexes = mapDataToColumn((AbstractProjectItem::DataType)r);
0440         for (auto &ix : indexes) {
0441             if (minColumn == -1 || ix < minColumn) {
0442                 minColumn = ix;
0443             }
0444             if (maxColumn == -1 || ix > maxColumn) {
0445                 maxColumn = ix;
0446             }
0447         }
0448     }
0449     if (minColumn == -1) {
0450         return;
0451     }
0452     QWriteLocker locker(&m_lock);
0453     auto tItem = std::static_pointer_cast<TreeItem>(item);
0454     auto ptr = tItem->parentItem().lock();
0455     if (ptr) {
0456         auto index = getIndexFromItem(tItem, minColumn);
0457         if (minColumn == maxColumn) {
0458             Q_EMIT dataChanged(index, index, roles);
0459         } else {
0460             auto index2 = getIndexFromItem(tItem, maxColumn);
0461             Q_EMIT dataChanged(index, index2, roles);
0462         }
0463     }
0464 }
0465 
0466 void ProjectItemModel::onItemUpdated(const QString &binId, int role)
0467 {
0468     QWriteLocker locker(&m_lock);
0469     std::shared_ptr<AbstractProjectItem> item = getItemByBinId(binId);
0470     if (item) {
0471         onItemUpdated(item, {role});
0472     }
0473 }
0474 
0475 std::shared_ptr<ProjectClip> ProjectItemModel::getClipByBinID(const QString &binId)
0476 {
0477     READ_LOCK();
0478     auto search = m_allClipItems.find(binId.toInt());
0479     if (search != m_allClipItems.end()) {
0480         return search->second;
0481     }
0482     return nullptr;
0483 }
0484 
0485 const QVector<uint8_t> ProjectItemModel::getAudioLevelsByBinID(const QString &binId, int stream)
0486 {
0487     READ_LOCK();
0488     auto search = m_allClipItems.find(binId.toInt());
0489     if (search != m_allClipItems.end()) {
0490         return search->second->audioFrameCache(stream);
0491     }
0492     return QVector<uint8_t>();
0493 }
0494 
0495 double ProjectItemModel::getAudioMaxLevel(const QString &binId, int stream)
0496 {
0497     READ_LOCK();
0498     auto search = m_allClipItems.find(binId.toInt());
0499     if (search != m_allClipItems.end()) {
0500         return search->second->getAudioMax(stream);
0501     }
0502     return 0;
0503 }
0504 
0505 bool ProjectItemModel::hasClip(const QString &binId)
0506 {
0507     READ_LOCK();
0508     return getClipByBinID(binId) != nullptr;
0509 }
0510 
0511 std::shared_ptr<ProjectFolder> ProjectItemModel::getFolderByBinId(const QString &binId)
0512 {
0513     READ_LOCK();
0514     for (const auto &clip : m_allItems) {
0515         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
0516         if (c->itemType() == AbstractProjectItem::FolderItem && c->clipId() == binId) {
0517             return std::static_pointer_cast<ProjectFolder>(c);
0518         }
0519     }
0520     return nullptr;
0521 }
0522 
0523 QList<std::shared_ptr<ProjectFolder>> ProjectItemModel::getFolders()
0524 {
0525     READ_LOCK();
0526     QList<std::shared_ptr<ProjectFolder>> folders;
0527     for (const auto &clip : m_allItems) {
0528         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
0529         if (c->itemType() == AbstractProjectItem::FolderItem) {
0530             folders << std::static_pointer_cast<ProjectFolder>(c);
0531         }
0532     }
0533     return folders;
0534 }
0535 
0536 const QString ProjectItemModel::getFolderIdByName(const QString &folderName)
0537 {
0538     READ_LOCK();
0539     for (const auto &clip : m_allItems) {
0540         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
0541         if (c->itemType() == AbstractProjectItem::FolderItem && c->name() == folderName) {
0542             return c->clipId();
0543         }
0544     }
0545     return QString();
0546 }
0547 
0548 std::shared_ptr<AbstractProjectItem> ProjectItemModel::getItemByBinId(const QString &binId)
0549 {
0550     READ_LOCK();
0551     for (const auto &clip : m_allItems) {
0552         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
0553         if (c->clipId() == binId) {
0554             return c;
0555         }
0556     }
0557     return nullptr;
0558 }
0559 
0560 void ProjectItemModel::setBinEffectsEnabled(bool enabled)
0561 {
0562     QWriteLocker locker(&m_lock);
0563     return std::static_pointer_cast<AbstractProjectItem>(rootItem)->setBinEffectsEnabled(enabled);
0564 }
0565 
0566 QStringList ProjectItemModel::getEnclosingFolderInfo(const QModelIndex &index) const
0567 {
0568     READ_LOCK();
0569     QStringList noInfo;
0570     noInfo << QString::number(-1);
0571     noInfo << QString();
0572     if (!index.isValid()) {
0573         return noInfo;
0574     }
0575 
0576     std::shared_ptr<AbstractProjectItem> currentItem = getBinItemByIndex(index);
0577     auto folder = currentItem->getEnclosingFolder(true);
0578     if ((folder == nullptr) || folder == rootItem) {
0579         return noInfo;
0580     }
0581     QStringList folderInfo;
0582     folderInfo << currentItem->clipId();
0583     folderInfo << currentItem->name();
0584     return folderInfo;
0585 }
0586 
0587 void ProjectItemModel::clean()
0588 {
0589     QWriteLocker locker(&m_lock);
0590     closing = true;
0591     m_extraPlaylists.clear();
0592     std::vector<std::shared_ptr<AbstractProjectItem>> toDelete;
0593     toDelete.reserve(size_t(rootItem->childCount()));
0594     for (int i = 0; i < rootItem->childCount(); ++i) {
0595         qDebug() << "... FOUND CLIP: " << std::static_pointer_cast<AbstractProjectItem>(rootItem->child(i))->clipId() << " = "
0596                  << std::static_pointer_cast<AbstractProjectItem>(rootItem->child(i))->name();
0597         toDelete.push_back(std::static_pointer_cast<AbstractProjectItem>(rootItem->child(i)));
0598     }
0599     Fun undo = []() { return true; };
0600     Fun redo = []() { return true; };
0601     for (const auto &child : toDelete) {
0602         requestBinClipDeletion(child, undo, redo);
0603     }
0604     Q_ASSERT(rootItem->childCount() == 0);
0605     closing = false;
0606     m_nextId = 1;
0607     m_uuid = QUuid::createUuid();
0608     m_sequenceFolderId = -1;
0609     buildPlaylist(m_uuid);
0610     ThumbnailCache::get()->clearCache();
0611 }
0612 
0613 std::shared_ptr<ProjectFolder> ProjectItemModel::getRootFolder() const
0614 {
0615     READ_LOCK();
0616     return std::static_pointer_cast<ProjectFolder>(rootItem);
0617 }
0618 
0619 void ProjectItemModel::loadSubClips(const QString &id, const QString &clipData, bool logUndo)
0620 {
0621     QWriteLocker locker(&m_lock);
0622     Fun undo = []() { return true; };
0623     Fun redo = []() { return true; };
0624     loadSubClips(id, clipData, undo, redo);
0625     if (logUndo) {
0626         Fun update_subs = [this, binId = id]() {
0627             std::shared_ptr<AbstractProjectItem> parentItem = getItemByBinId(binId);
0628             if (parentItem && parentItem->itemType() == AbstractProjectItem::ClipItem) {
0629                 auto clipItem = std::static_pointer_cast<ProjectClip>(parentItem);
0630                 clipItem->updateZones();
0631             }
0632             return true;
0633         };
0634         PUSH_LAMBDA(update_subs, undo);
0635         PUSH_LAMBDA(update_subs, redo);
0636         update_subs();
0637         pCore->pushUndo(undo, redo, i18n("Add sub clips"));
0638     }
0639 }
0640 
0641 void ProjectItemModel::loadSubClips(const QString &id, const QString &dataMap, Fun &undo, Fun &redo)
0642 {
0643     if (dataMap.isEmpty()) {
0644         return;
0645     }
0646     QWriteLocker locker(&m_lock);
0647     std::shared_ptr<ProjectClip> clip = getClipByBinID(id);
0648     if (!clip) {
0649         qWarning() << "Clip not loaded";
0650         return;
0651     }
0652     auto json = QJsonDocument::fromJson(dataMap.toUtf8());
0653     if (!json.isArray()) {
0654         qWarning() << "Error loading zones: no json array";
0655         return;
0656     }
0657     int maxFrame = clip->duration().frames(pCore->getCurrentFps()) - 1;
0658     auto list = json.array();
0659     for (const auto &entry : qAsConst(list)) {
0660         if (!entry.isObject()) {
0661             qWarning() << "Skipping invalid marker data";
0662             continue;
0663         }
0664         auto entryObj = entry.toObject();
0665         if (!entryObj.contains(QLatin1String("name"))) {
0666             qWarning() << "Skipping invalid zone (does not contain name)";
0667             continue;
0668         }
0669         int in = entryObj[QLatin1String("in")].toInt();
0670         int out = entryObj[QLatin1String("out")].toInt();
0671         QMap<QString, QString> zoneProperties;
0672         zoneProperties.insert(QStringLiteral("name"), entryObj[QLatin1String("name")].toString(i18n("Zone")));
0673         zoneProperties.insert(QStringLiteral("rating"), QString::number(entryObj[QLatin1String("rating")].toInt()));
0674         zoneProperties.insert(QStringLiteral("tags"), entryObj[QLatin1String("tags")].toString(QString()));
0675         if (in >= out) {
0676             qWarning() << "Invalid zone" << zoneProperties.value("name") << in << out;
0677             continue;
0678         }
0679         if (maxFrame > 0) {
0680             out = qMin(out, maxFrame);
0681         }
0682         QString subId;
0683         requestAddBinSubClip(subId, in, out, zoneProperties, id, undo, redo);
0684     }
0685 }
0686 
0687 std::shared_ptr<AbstractProjectItem> ProjectItemModel::getBinItemByIndex(const QModelIndex &index) const
0688 {
0689     READ_LOCK();
0690     return std::static_pointer_cast<AbstractProjectItem>(getItemById(int(index.internalId())));
0691 }
0692 
0693 bool ProjectItemModel::requestBinClipDeletion(const std::shared_ptr<AbstractProjectItem> &clip, Fun &undo, Fun &redo)
0694 {
0695     QWriteLocker locker(&m_lock);
0696     Q_ASSERT(clip);
0697     if (!clip) return false;
0698     int parentId = -1;
0699     QString parentBinId;
0700     int binId = clip->clipId().toInt();
0701     if (binId == m_sequenceFolderId) {
0702         setSequencesFolder(-1);
0703     }
0704     if (auto ptr = clip->parent()) {
0705         parentId = ptr->getId();
0706         parentBinId = ptr->clipId();
0707     }
0708     bool isSubClip = clip->itemType() == AbstractProjectItem::SubClipItem;
0709     if (!clip->selfSoftDelete(undo, redo)) {
0710         return false;
0711     }
0712     int id = clip->getId();
0713     Fun operation = removeProjectItem_lambda(binId, id);
0714     Fun reverse = addItem_lambda(clip, parentId);
0715     bool res = operation();
0716     if (res) {
0717         if (isSubClip) {
0718             Fun update_doc = [this, parentBinId]() {
0719                 std::shared_ptr<AbstractProjectItem> parentItem = getItemByBinId(parentBinId);
0720                 if (parentItem && parentItem->itemType() == AbstractProjectItem::ClipItem) {
0721                     auto clipItem = std::static_pointer_cast<ProjectClip>(parentItem);
0722                     clipItem->updateZones();
0723                 }
0724                 return true;
0725             };
0726             update_doc();
0727             PUSH_LAMBDA(update_doc, operation);
0728             PUSH_LAMBDA(update_doc, reverse);
0729         } else {
0730             Fun checkAudio = clip->getAudio_lambda();
0731             PUSH_LAMBDA(checkAudio, reverse);
0732         }
0733         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
0734     }
0735     return res;
0736 }
0737 
0738 void ProjectItemModel::registerItem(const std::shared_ptr<TreeItem> &item)
0739 {
0740     QWriteLocker locker(&m_lock);
0741     auto clip = std::static_pointer_cast<AbstractProjectItem>(item);
0742     m_binPlaylist->manageBinItemInsertion(clip);
0743     AbstractTreeModel::registerItem(item);
0744     m_allIds.append(clip->clipId().toInt());
0745     if (clip->itemType() == AbstractProjectItem::ClipItem) {
0746         auto clipItem = std::static_pointer_cast<ProjectClip>(clip);
0747         m_allClipItems[clip->clipId().toInt()] = clipItem;
0748         updateWatcher(clipItem);
0749         if (clipItem->clipType() == ClipType::Timeline && clipItem->statusReady()) {
0750             const QString uuid = clipItem->getSequenceUuid().toString();
0751             std::shared_ptr<Mlt::Tractor> trac(new Mlt::Tractor(clipItem->originalProducer()->parent()));
0752             storeSequence(uuid, trac, false);
0753         }
0754     }
0755 }
0756 
0757 void ProjectItemModel::deregisterItem(int id, TreeItem *item)
0758 {
0759     QWriteLocker locker(&m_lock);
0760     auto clip = static_cast<AbstractProjectItem *>(item);
0761     m_allIds.removeAll(clip->clipId().toInt());
0762     m_allClipItems.erase(clip->clipId().toInt());
0763     m_binPlaylist->manageBinItemDeletion(clip);
0764     // TODO : here, we should suspend jobs belonging to the item we delete. They can be restarted if the item is reinserted by undo
0765     AbstractTreeModel::deregisterItem(id, item);
0766     if (clip->itemType() == AbstractProjectItem::ClipItem) {
0767         auto clipItem = static_cast<ProjectClip *>(clip);
0768         m_fileWatcher->removeFile(clipItem->clipId());
0769         if (clipItem->clipType() == ClipType::Timeline) {
0770             const QString uuid = clipItem->getSequenceUuid().toString();
0771             if (m_extraPlaylists.count(uuid) > 0) {
0772                 m_extraPlaylists.erase(uuid);
0773             }
0774         }
0775     }
0776 }
0777 
0778 bool ProjectItemModel::hasSequenceId(const QUuid &uuid) const
0779 {
0780     return m_binPlaylist->hasSequenceId(uuid);
0781 }
0782 
0783 std::shared_ptr<ProjectClip> ProjectItemModel::getSequenceClip(const QUuid &uuid)
0784 {
0785     const QString binId = getSequenceId(uuid);
0786     return getClipByBinID(binId);
0787 }
0788 
0789 QMap<QUuid, QString> ProjectItemModel::getAllSequenceClips() const
0790 {
0791     return m_binPlaylist->getAllSequenceClips();
0792 }
0793 
0794 const QString ProjectItemModel::getSequenceId(const QUuid &uuid)
0795 {
0796     return m_binPlaylist->getSequenceId(uuid);
0797 }
0798 
0799 int ProjectItemModel::getFreeFolderId()
0800 {
0801     while (!isIdFree(QString::number(++m_nextId))) {
0802     };
0803     return m_nextId;
0804 }
0805 
0806 int ProjectItemModel::getFreeClipId()
0807 {
0808     while (!isIdFree(QString::number(++m_nextId))) {
0809     };
0810     return m_nextId;
0811 }
0812 
0813 bool ProjectItemModel::addItem(const std::shared_ptr<AbstractProjectItem> &item, const QString &parentId, Fun &undo, Fun &redo)
0814 {
0815     QWriteLocker locker(&m_lock);
0816     std::shared_ptr<AbstractProjectItem> parentItem = getItemByBinId(parentId);
0817     if (!parentItem) {
0818         qCDebug(KDENLIVE_LOG) << "  / / ERROR IN PARENT FOLDER";
0819         return false;
0820     }
0821     if (item->itemType() == AbstractProjectItem::ClipItem && parentItem->itemType() != AbstractProjectItem::FolderItem) {
0822         qCDebug(KDENLIVE_LOG) << "  / / ERROR when inserting clip: a clip should be inserted in a folder";
0823         return false;
0824     }
0825     if (item->itemType() == AbstractProjectItem::SubClipItem && parentItem->itemType() != AbstractProjectItem::ClipItem) {
0826         qCDebug(KDENLIVE_LOG) << "  / / ERROR when inserting subclip: a subclip should be inserted in a clip";
0827         return false;
0828     }
0829     Fun operation = addItem_lambda(item, parentItem->getId());
0830 
0831     int itemId = item->getId();
0832     int binId = item->clipId().toInt();
0833     Fun reverse = removeProjectItem_lambda(binId, itemId);
0834     bool res = operation();
0835     Q_ASSERT(item->isInModel());
0836     if (res) {
0837         Fun checkAudio = item->getAudio_lambda();
0838         PUSH_LAMBDA(checkAudio, operation);
0839         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
0840     }
0841     return res;
0842 }
0843 
0844 bool ProjectItemModel::requestAddFolder(QString &id, const QString &name, const QString &parentId, Fun &undo, Fun &redo)
0845 {
0846     QWriteLocker locker(&m_lock);
0847     if (!id.isEmpty() && !isIdFree(id)) {
0848         id.clear();
0849     }
0850     if (id.isEmpty()) {
0851         id = QString::number(getFreeFolderId());
0852     }
0853     std::shared_ptr<ProjectFolder> new_folder = ProjectFolder::construct(id, name, std::static_pointer_cast<ProjectItemModel>(shared_from_this()));
0854     return addItem(new_folder, parentId, undo, redo);
0855 }
0856 
0857 bool ProjectItemModel::requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, Fun &undo, Fun &redo,
0858                                          const std::function<void(const QString &)> &readyCallBack)
0859 {
0860     if (id.isEmpty()) {
0861         id = Xml::getXmlProperty(description, QStringLiteral("kdenlive:id"), QStringLiteral("-1"));
0862         if (id == QStringLiteral("-1") || !isIdFree(id)) {
0863             id = QString::number(getFreeClipId());
0864         }
0865     }
0866     Q_ASSERT(isIdFree(id));
0867     QWriteLocker locker(&m_lock);
0868     std::shared_ptr<ProjectClip> new_clip =
0869         ProjectClip::construct(id, description, m_blankThumb, std::static_pointer_cast<ProjectItemModel>(shared_from_this()));
0870     locker.unlock();
0871     bool res = addItem(new_clip, parentId, undo, redo);
0872     if (res) {
0873         ClipLoadTask::start(ObjectId(KdenliveObjectType::BinClip, id.toInt(), QUuid()), description, false, -1, -1, this, false, std::bind(readyCallBack, id));
0874     }
0875     return res;
0876 }
0877 
0878 bool ProjectItemModel::requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, const QString &undoText,
0879                                          const std::function<void(const QString &)> &readyCallBack)
0880 {
0881     QWriteLocker locker(&m_lock);
0882     Fun undo = []() { return true; };
0883     Fun redo = []() { return true; };
0884     bool res = requestAddBinClip(id, description, parentId, undo, redo, readyCallBack);
0885     if (res) {
0886         pCore->pushUndo(undo, redo, undoText.isEmpty() ? i18n("Add bin clip") : undoText);
0887     }
0888     return res;
0889 }
0890 
0891 bool ProjectItemModel::requestAddBinClip(QString &id, std::shared_ptr<Mlt::Producer> &producer, const QString &parentId, Fun &undo, Fun &redo,
0892                                          const std::function<void(const QString &)> &readyCallBack)
0893 {
0894     QWriteLocker locker(&m_lock);
0895     if (id.isEmpty()) {
0896         if (producer->property_exists("kdenlive:id")) {
0897             id = QString::number(producer->get_int("kdenlive:id"));
0898         }
0899         if (!isIdFree(id)) {
0900             id = QString::number(getFreeClipId());
0901         }
0902     }
0903     Q_ASSERT(isIdFree(id));
0904     std::shared_ptr<ProjectClip> new_clip = ProjectClip::construct(id, m_blankThumb, std::static_pointer_cast<ProjectItemModel>(shared_from_this()), producer);
0905     bool res = addItem(new_clip, parentId, undo, redo);
0906     if (res) {
0907         new_clip->importEffects(producer);
0908         const std::function<void()> readyCallBackExec = std::bind(readyCallBack, id);
0909         QMetaObject::invokeMethod(qApp, [readyCallBackExec] { readyCallBackExec(); });
0910     }
0911     return res;
0912 }
0913 
0914 bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, const QMap<QString, QString> &zoneProperties, const QString &parentId, Fun &undo,
0915                                             Fun &redo)
0916 {
0917     QWriteLocker locker(&m_lock);
0918     if (id.isEmpty()) {
0919         id = QString::number(getFreeClipId());
0920     }
0921     Q_ASSERT(isIdFree(id));
0922     QString subId = parentId;
0923     if (subId.startsWith(QLatin1Char('A')) || subId.startsWith(QLatin1Char('V'))) {
0924         subId.remove(0, 1);
0925     }
0926     auto clip = getClipByBinID(subId);
0927     Q_ASSERT(clip->itemType() == AbstractProjectItem::ClipItem);
0928     auto tc = pCore->currentDoc()->timecode().getDisplayTimecodeFromFrames(in, KdenliveSettings::frametimecode());
0929     std::shared_ptr<ProjectSubClip> new_clip =
0930         ProjectSubClip::construct(id, clip, std::static_pointer_cast<ProjectItemModel>(shared_from_this()), in, out, tc, zoneProperties);
0931     bool res = addItem(new_clip, subId, undo, redo);
0932     return res;
0933 }
0934 bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, const QMap<QString, QString> &zoneProperties, const QString &parentId)
0935 {
0936     QWriteLocker locker(&m_lock);
0937     Fun undo = []() { return true; };
0938     Fun redo = []() { return true; };
0939     bool res = requestAddBinSubClip(id, in, out, zoneProperties, parentId, undo, redo);
0940     if (res) {
0941         Fun update_doc = [this, parentId]() {
0942             std::shared_ptr<AbstractProjectItem> parentItem = getItemByBinId(parentId);
0943             if (parentItem && parentItem->itemType() == AbstractProjectItem::ClipItem) {
0944                 auto clipItem = std::static_pointer_cast<ProjectClip>(parentItem);
0945                 clipItem->updateZones();
0946             }
0947             return true;
0948         };
0949         update_doc();
0950         PUSH_LAMBDA(update_doc, undo);
0951         PUSH_LAMBDA(update_doc, redo);
0952         pCore->pushUndo(undo, redo, i18n("Add a sub clip"));
0953     }
0954     return res;
0955 }
0956 
0957 Fun ProjectItemModel::requestRenameFolder_lambda(const std::shared_ptr<AbstractProjectItem> &folder, const QString &newName)
0958 {
0959     int id = folder->getId();
0960     return [this, id, newName]() {
0961         auto currentFolder = std::static_pointer_cast<AbstractProjectItem>(m_allItems[id].lock());
0962         if (!currentFolder) {
0963             return false;
0964         }
0965         currentFolder->setName(newName);
0966         m_binPlaylist->manageBinFolderRename(currentFolder);
0967         auto index = getIndexFromItem(currentFolder);
0968         Q_EMIT dataChanged(index, index, {AbstractProjectItem::DataName});
0969         return true;
0970     };
0971 }
0972 
0973 bool ProjectItemModel::requestRenameFolder(const std::shared_ptr<AbstractProjectItem> &folder, const QString &name, Fun &undo, Fun &redo)
0974 {
0975     QWriteLocker locker(&m_lock);
0976     QString oldName = folder->name();
0977     auto operation = requestRenameFolder_lambda(folder, name);
0978     if (operation()) {
0979         auto reverse = requestRenameFolder_lambda(folder, oldName);
0980         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
0981         return true;
0982     }
0983     return false;
0984 }
0985 
0986 bool ProjectItemModel::requestRenameFolder(std::shared_ptr<AbstractProjectItem> folder, const QString &name)
0987 {
0988     QWriteLocker locker(&m_lock);
0989     Fun undo = []() { return true; };
0990     Fun redo = []() { return true; };
0991     bool res = requestRenameFolder(std::move(folder), name, undo, redo);
0992     if (res) {
0993         pCore->pushUndo(undo, redo, i18n("Rename Folder"));
0994     }
0995     return res;
0996 }
0997 
0998 bool ProjectItemModel::requestCleanupUnused()
0999 {
1000     QWriteLocker locker(&m_lock);
1001     Fun undo = []() { return true; };
1002     Fun redo = []() { return true; };
1003     std::vector<std::shared_ptr<AbstractProjectItem>> to_delete;
1004     // Iterate to find clips that are not in timeline
1005     for (const auto &clip : m_allItems) {
1006         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
1007         if (c->itemType() == AbstractProjectItem::ClipItem && !c->isIncludedInTimeline() && c->clipType() != ClipType::Timeline) {
1008             to_delete.push_back(c);
1009         }
1010     }
1011     // it is important to execute deletion in a separate loop, because otherwise
1012     // the iterators of m_allItems get messed up
1013     for (const auto &c : to_delete) {
1014         bool res = requestBinClipDeletion(c, undo, redo);
1015         if (!res) {
1016             bool undone = undo();
1017             Q_ASSERT(undone);
1018             return false;
1019         }
1020     }
1021     pCore->pushUndo(undo, redo, i18n("Clean Project"));
1022     return true;
1023 }
1024 
1025 bool ProjectItemModel::requestTrashClips(QStringList &ids, QStringList &urls)
1026 {
1027     QWriteLocker locker(&m_lock);
1028     Fun undo = []() { return true; };
1029     Fun redo = []() { return true; };
1030     std::vector<std::shared_ptr<AbstractProjectItem>> to_delete;
1031     // Iterate to find clips that are not in timeline
1032     for (const auto &clip : m_allItems) {
1033         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
1034         if (ids.contains(c->clipId())) {
1035             to_delete.push_back(c);
1036         }
1037     }
1038 
1039     // it is important to execute deletion in a separate loop, because otherwise
1040     // the iterators of m_allItems get messed up
1041     for (const auto &c : to_delete) {
1042         bool res = requestBinClipDeletion(c, undo, redo);
1043         if (!res) {
1044             bool undone = undo();
1045             Q_ASSERT(undone);
1046             return false;
1047         }
1048     }
1049     for (const auto &url : urls) {
1050         QFile::remove(url);
1051     }
1052     // don't push undo/redo: the files are deleted we can't redo/undo
1053     return true;
1054 }
1055 
1056 std::vector<QString> ProjectItemModel::getAllClipIds() const
1057 {
1058     READ_LOCK();
1059     std::vector<QString> result;
1060     for (const auto &clip : m_allItems) {
1061         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
1062         if (c->itemType() == AbstractProjectItem::ClipItem) {
1063             result.push_back(c->clipId());
1064         }
1065     }
1066     return result;
1067 }
1068 
1069 void ProjectItemModel::updateCacheThumbnail(std::unordered_map<QString, std::vector<int>> &thumbData)
1070 {
1071     READ_LOCK();
1072     for (const auto &clip : m_allItems) {
1073         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
1074         if (c->itemType() == AbstractProjectItem::ClipItem) {
1075             int frameNumber = qMax(0, std::static_pointer_cast<ProjectClip>(c)->getThumbFrame());
1076             thumbData[c->clipId()].push_back(frameNumber);
1077         } else if (c->itemType() == AbstractProjectItem::SubClipItem) {
1078             QPoint p = c->zone();
1079             thumbData[std::static_pointer_cast<ProjectSubClip>(c)->getMasterClip()->clipId()].push_back(p.x());
1080         }
1081     }
1082 }
1083 
1084 QStringList ProjectItemModel::getClipByUrl(const QFileInfo &url) const
1085 {
1086     READ_LOCK();
1087     QStringList result;
1088     if (url.filePath().isEmpty()) {
1089         // Invalid url
1090         return result;
1091     }
1092     for (const auto &clip : m_allItems) {
1093         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
1094         if (c->itemType() == AbstractProjectItem::ClipItem) {
1095             if (QFileInfo(std::static_pointer_cast<ProjectClip>(c)->clipUrl()) == url) {
1096                 result << c->clipId();
1097             }
1098         }
1099     }
1100     return result;
1101 }
1102 
1103 bool ProjectItemModel::loadFolders(Mlt::Properties &folders, std::unordered_map<QString, QString> &binIdCorresp)
1104 {
1105     QWriteLocker locker(&m_lock);
1106     // At this point, we expect the folders properties to have a name of the form "x.y" where x is the id of the parent folder and y the id of the child.
1107     // Note that for root folder, x = -1
1108     // The value of the property is the name of the child folder
1109 
1110     std::unordered_map<int, std::vector<int>> downLinks; // key are parents, value are children
1111     std::unordered_map<int, int> upLinks;                // key are children, value are parent
1112     std::unordered_map<int, QString> newIds;             // we store the correspondence to the new ids
1113     std::unordered_map<int, QString> folderNames;
1114     newIds[-1] = getRootFolder()->clipId();
1115 
1116     if (folders.count() == 0) return true;
1117 
1118     for (int i = 0; i < folders.count(); i++) {
1119         QString folderName = folders.get(i);
1120         QString id = folders.get_name(i);
1121 
1122         int parentId = id.section(QLatin1Char('.'), 0, 0).toInt();
1123         int folderId = id.section(QLatin1Char('.'), 1, 1).toInt();
1124         downLinks[parentId].push_back(folderId);
1125         upLinks[folderId] = parentId;
1126         folderNames[folderId] = folderName;
1127     }
1128 
1129     // In case there are some non-existent parent, we fall back to root
1130     for (const auto &f : downLinks) {
1131         if (upLinks.count(f.first) == 0) {
1132             upLinks[f.first] = -1;
1133         }
1134         if (f.first != -1 && downLinks.count(upLinks[f.first]) == 0) {
1135             qWarning() << f.first << "has invalid parent folder" << upLinks[f.first] << "it will be placed in top directory";
1136             upLinks[f.first] = -1;
1137         }
1138     }
1139 
1140     // We now do a BFS to construct the folders in order
1141     Q_ASSERT(downLinks.count(-1) > 0);
1142     Fun undo = []() { return true; };
1143     Fun redo = []() { return true; };
1144     std::queue<int> queue;
1145     std::unordered_set<int> seen;
1146     queue.push(-1);
1147     while (!queue.empty()) {
1148         int current = queue.front();
1149         seen.insert(current);
1150         queue.pop();
1151         if (current != -1) {
1152             QString id = QString::number(current);
1153             bool res = requestAddFolder(id, folderNames[current], newIds[upLinks[current]], undo, redo);
1154             if (!res) {
1155                 bool undone = undo();
1156                 Q_ASSERT(undone);
1157                 return false;
1158             }
1159             binIdCorresp[QString::number(current)] = id;
1160             newIds[current] = id;
1161         }
1162         for (int c : downLinks[current]) {
1163             queue.push(c);
1164         }
1165     }
1166     return true;
1167 }
1168 
1169 bool ProjectItemModel::isIdFree(const QString &id) const
1170 {
1171     READ_LOCK();
1172     if (id.isEmpty()) {
1173         return false;
1174     }
1175     return !m_allIds.contains(id.toInt());
1176 }
1177 
1178 QList<QUuid> ProjectItemModel::loadBinPlaylist(Mlt::Service *documentTractor, std::unordered_map<QString, QString> &binIdCorresp, QStringList &expandedFolders,
1179                                                const QUuid &activeUuid, int &zoomLevel)
1180 {
1181     QWriteLocker locker(&m_lock);
1182     clean();
1183     QList<QUuid> brokenSequences;
1184     QList<QUuid> foundSequences;
1185     QList<int> foundIds;
1186     Mlt::Properties retainList(mlt_properties(documentTractor->get_data("xml_retain")));
1187     if (retainList.is_valid()) {
1188         Mlt::Playlist playlist(mlt_playlist(retainList.get_data(BinPlaylist::binPlaylistId.toUtf8().constData())));
1189         if (playlist.is_valid() && playlist.type() == mlt_service_playlist_type) {
1190             // Load folders
1191             Mlt::Properties folderProperties;
1192             Mlt::Properties playlistProps(playlist.get_properties());
1193             expandedFolders = QString(playlistProps.get("kdenlive:expandedFolders")).split(QLatin1Char(';'));
1194             folderProperties.pass_values(playlistProps, "kdenlive:folder.");
1195             loadFolders(folderProperties, binIdCorresp);
1196             m_sequenceFolderId = -1;
1197             if (playlistProps.property_exists("kdenlive:sequenceFolder")) {
1198                 int sequenceFolder = playlistProps.get_int("kdenlive:sequenceFolder");
1199                 if (sequenceFolder > -1 && binIdCorresp.count(QString::number(sequenceFolder)) > 0) {
1200                     setSequencesFolder(binIdCorresp.at(QString::number(sequenceFolder)).toInt());
1201                 }
1202             }
1203 
1204             m_audioCaptureFolderId = -1;
1205             if (playlistProps.property_exists("kdenlive:audioCaptureFolder")) {
1206                 int audioCaptureFolder = playlistProps.get_int("kdenlive:audioCaptureFolder");
1207                 if (audioCaptureFolder > -1 && binIdCorresp.count(QString::number(audioCaptureFolder)) > 0) {
1208                     setAudioCaptureFolder(binIdCorresp.at(QString::number(audioCaptureFolder)).toInt());
1209                 }
1210             }
1211 
1212             // Load Zoom level
1213             if (playlistProps.property_exists("kdenlive:binZoom")) {
1214                 zoomLevel = playlistProps.get_int("kdenlive:binZoom");
1215             }
1216 
1217             // Read notes
1218             QString notes = playlistProps.get("kdenlive:documentnotes");
1219             pCore->projectManager()->setDocumentNotes(notes);
1220 
1221             Fun undo = []() { return true; };
1222             Fun redo = []() { return true; };
1223             int max = playlist.count();
1224             if (max > 0) {
1225                 Q_EMIT pCore->loadingMessageNewStage(i18n("Reading project clips…"), max);
1226             }
1227             QMap<int, std::shared_ptr<Mlt::Producer>> binProducers;
1228             for (int i = 0; i < max; i++) {
1229                 Q_EMIT pCore->loadingMessageIncrease();
1230                 qApp->processEvents();
1231                 QScopedPointer<Mlt::Producer> prod(playlist.get_clip(i));
1232                 if (prod->is_blank() || !prod->is_valid() || prod->parent().property_exists("kdenlive:remove")) {
1233                     qDebug() << "==== IGNORING BIN PRODUCER: " << prod->parent().get("kdenlive:id");
1234                     continue;
1235                 }
1236                 std::shared_ptr<Mlt::Producer> producer;
1237                 if (prod->parent().property_exists("kdenlive:uuid")) {
1238                     const QUuid uuid(prod->parent().get("kdenlive:uuid"));
1239                     if (foundSequences.contains(uuid)) {
1240                         qDebug() << "FOUND DUPLICATED SEQUENCE: " << uuid.toString();
1241                         Q_ASSERT(false);
1242                     }
1243                     if (prod->parent().type() == mlt_service_tractor_type) {
1244                         // Load sequence properties
1245                         foundSequences << uuid;
1246                         Mlt::Properties sequenceProps;
1247                         sequenceProps.pass_values(prod->parent(), "kdenlive:sequenceproperties.");
1248                         pCore->currentDoc()->loadSequenceProperties(uuid, sequenceProps);
1249 
1250                         std::shared_ptr<Mlt::Tractor> trac = std::make_shared<Mlt::Tractor>(prod->parent());
1251                         int id(prod->parent().get_int("kdenlive:id"));
1252                         trac->set("kdenlive:id", id);
1253                         trac->set("kdenlive:uuid", uuid.toString().toUtf8().constData());
1254                         trac->set("length", prod->parent().get("length"));
1255                         trac->set("out", prod->parent().get("out"));
1256                         trac->set("kdenlive:clipname", prod->parent().get("kdenlive:clipname"));
1257                         trac->set("kdenlive:description", prod->parent().get("kdenlive:description"));
1258                         trac->set("kdenlive:folderid", prod->parent().get("kdenlive:folderid"));
1259                         trac->set("kdenlive:duration", prod->parent().get("kdenlive:duration"));
1260                         trac->set("kdenlive:producer_type", ClipType::Timeline);
1261                         trac->set("kdenlive:maxduration", prod->parent().get_int("kdenlive:maxduration"));
1262                         trac->set("_kdenlive_processed", 1);
1263                         std::shared_ptr<Mlt::Producer> prod2(trac->cut());
1264 
1265                         prod2->set("kdenlive:id", id);
1266                         prod2->set("kdenlive:uuid", uuid.toString().toUtf8().constData());
1267                         prod2->set("length", prod->parent().get("length"));
1268                         prod2->set("out", prod->parent().get("out"));
1269                         prod2->set("kdenlive:clipname", prod->parent().get("kdenlive:clipname"));
1270                         prod2->set("kdenlive:description", prod->parent().get("kdenlive:description"));
1271                         prod2->set("kdenlive:folderid", prod->parent().get("kdenlive:folderid"));
1272                         prod2->set("kdenlive:duration", prod->parent().get("kdenlive:duration"));
1273                         prod2->set("kdenlive:producer_type", ClipType::Timeline);
1274                         prod2->set("kdenlive:maxduration", prod->parent().get_int("kdenlive:maxduration"));
1275                         prod2->set("_kdenlive_processed", 1);
1276                         if (foundIds.contains(id)) {
1277                             qWarning() << "ERROR, several Sequence Clips using the same id: " << id << ", UUID: " << uuid.toString()
1278                                        << "\n____________________";
1279                             brokenSequences << uuid;
1280                         }
1281                         foundIds << id;
1282                         binProducers.insert(id, prod2);
1283                     } else {
1284                         const QString resource = prod->parent().get("resource");
1285                         qDebug() << "/// INCORRECT SEQUENCE FOUND IN PROJECT BIN: " << resource;
1286                         if (resource.endsWith(QLatin1String("<tractor>"))) {
1287                             // Buggy internal xml producer, drop
1288                             qDebug() << "/// AARGH INCORRECT SEQUENCE CLIP IN PROJECT BIN... TRY TO RECOVER";
1289                             brokenSequences.append(QUuid(uuid));
1290                         }
1291                     }
1292                     continue;
1293                 }
1294                 producer.reset(new Mlt::Producer(prod->parent()));
1295                 int id = producer->get_int("kdenlive:id");
1296                 if (!id) {
1297                     qDebug() << "WARNING THIS SHOULD NOT HAPPEN, BIN CLIP WITHOUT ID: " << producer->parent().get("resource")
1298                              << ", ID: " << producer->parent().get("id") << "\n\nFZFZFZFZFZFZFZFZFZFZFZFZFZ";
1299                     // Using a temporary negative reference so we don't mess with yet unloaded clips
1300                     id = -getFreeClipId();
1301                 } else {
1302                     if (foundIds.contains(id)) {
1303                         qWarning() << "ERROR, several Bin Clips using the same id: " << id << "\n____________________";
1304                         Q_ASSERT(false);
1305                     }
1306                     foundIds << id;
1307                 }
1308                 binProducers.insert(id, producer);
1309             }
1310             // Ensure active playlist is in the project bin
1311             if (!foundSequences.contains(activeUuid)) {
1312                 // Project corruption, try to restore sequence from the ProjectTractor
1313                 Mlt::Tractor tractor(*documentTractor);
1314                 std::shared_ptr<Mlt::Producer> tk(tractor.track(0));
1315                 std::shared_ptr<Mlt::Producer> producer(new Mlt::Producer(tk->parent()));
1316                 const QUuid uuid(producer->get("kdenlive:uuid"));
1317                 if (!uuid.isNull() && uuid == activeUuid) {
1318                     // TODO: Show user info about the recovered corruption
1319                     qWarning() << "Recovering corrupted sequence: " << uuid.toString();
1320                     int id = producer->get_int("kdenlive:id");
1321                     binProducers.insert(id, std::move(producer));
1322                     foundSequences << activeUuid;
1323                 } else {
1324                     brokenSequences << activeUuid;
1325                 }
1326             }
1327             // Do the real insertion
1328             QList<int> binIds = binProducers.keys();
1329 
1330             if (binIds.length() > 0) {
1331                 Q_EMIT pCore->loadingMessageNewStage(i18n("Loading project clips…"), binIds.length());
1332             }
1333 
1334             while (!binProducers.isEmpty()) {
1335                 Q_EMIT pCore->loadingMessageIncrease();
1336                 qApp->processEvents();
1337                 int bid = binIds.takeFirst();
1338                 std::shared_ptr<Mlt::Producer> prod = binProducers.take(bid);
1339                 QString newId = QString::number(getFreeClipId());
1340                 QString parentId = qstrdup(prod->get("kdenlive:folderid"));
1341                 if (parentId.isEmpty()) {
1342                     parentId = QStringLiteral("-1");
1343                 } else {
1344                     if (binIdCorresp.count(parentId.section(QLatin1Char('.'), -1)) == 0) {
1345                         // Error, folder was lost
1346                         parentId = QStringLiteral("-1");
1347                     }
1348                 }
1349                 prod->set("_kdenlive_processed", 1);
1350                 requestAddBinClip(newId, prod, parentId, undo, redo);
1351                 qApp->processEvents();
1352                 binIdCorresp[QString::number(bid)] = newId;
1353             }
1354         }
1355     } else {
1356         qDebug() << "HHHHHHHHHHHH\nINVALID BIN PLAYLIST...";
1357     }
1358     return brokenSequences;
1359 }
1360 
1361 void ProjectItemModel::loadTractorPlaylist(Mlt::Tractor documentTractor, std::unordered_map<QString, QString> &binIdCorresp)
1362 {
1363     QWriteLocker locker(&m_lock);
1364     QList<int> processedIds;
1365     QMap<int, std::shared_ptr<Mlt::Producer>> binProducers;
1366     for (int i = 1; i < documentTractor.count(); i++) {
1367         std::unique_ptr<Mlt::Producer> track(documentTractor.track(i));
1368         Mlt::Service service(track->get_service());
1369         Mlt::Tractor tractor(service);
1370         if (tractor.count() < 2) {
1371             qDebug() << "// TRACTOR PROBLEM";
1372             return;
1373         }
1374         for (int j = 0; j < tractor.count(); j++) {
1375             std::unique_ptr<Mlt::Producer> trackProducer(tractor.track(j));
1376             Mlt::Playlist trackPlaylist(mlt_playlist(trackProducer->get_service()));
1377             for (int k = 0; k < trackPlaylist.count(); k++) {
1378                 if (trackPlaylist.is_blank(k)) {
1379                     continue;
1380                 }
1381                 std::unique_ptr<Mlt::Producer> clip(trackPlaylist.get_clip(k));
1382                 if (clip->parent().property_exists("kdenlive:id")) {
1383                     int cid = clip->parent().get_int("kdenlive:id");
1384                     if (processedIds.contains(cid)) {
1385                         continue;
1386                     }
1387                     const QString service = clip->parent().get("mlt_service");
1388                     const QString resource = clip->parent().get("resource");
1389                     const QString hash = clip->parent().get("kdenlive:file_hash");
1390                     ClipType::ProducerType matchType;
1391                     if (service.startsWith(QLatin1String("avformat"))) {
1392                         int cType = clip->parent().get_int("kdenlive:clip_type");
1393                         switch (cType) {
1394                         case 1:
1395                             matchType = ClipType::Audio;
1396                             break;
1397                         case 2:
1398                             matchType = ClipType::Video;
1399                             break;
1400                         default:
1401                             matchType = ClipType::AV;
1402                             break;
1403                         }
1404                     } else {
1405                         matchType = ClipLoadTask::getTypeForService(service, resource);
1406                     }
1407                     qDebug() << ":::: LOOKING FOR A MATCHING ITEM TYPE: " << matchType;
1408                     // Try to find matching clip
1409                     bool found = false;
1410                     for (const auto &clip : m_allItems) {
1411                         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
1412                         if (c->itemType() == AbstractProjectItem::ClipItem) {
1413                             if (c->clipType() == matchType) {
1414                                 std::shared_ptr<ProjectClip> pClip = std::static_pointer_cast<ProjectClip>(c);
1415                                 qDebug() << "::::::\nTRYING TO MATCH: " << pClip->getProducerProperty(QStringLiteral("resource")) << " = " << resource;
1416                                 if (pClip->getProducerProperty(QStringLiteral("kdenlive:file_hash")) == hash) {
1417                                     // Found a match
1418                                     binIdCorresp[QString::number(cid)] = pClip->clipId();
1419                                     found = true;
1420                                     break;
1421                                 }
1422                             }
1423                         }
1424                     }
1425                     if (!found) {
1426                         std::shared_ptr<Mlt::Producer> clipProd(new Mlt::Producer(clip->parent()));
1427                         binProducers.insert(cid, clipProd);
1428                     }
1429                     processedIds << cid;
1430                 }
1431                 qDebug() << ":::: FOUND CLIPS IN PLAYLIST: " << i << ": " << trackPlaylist.count();
1432             }
1433         }
1434     }
1435     Fun undo = []() { return true; };
1436     Fun redo = []() { return true; };
1437     // Do the real insertion
1438     QList<int> binIds = binProducers.keys();
1439     while (!binProducers.isEmpty()) {
1440         int bid = binIds.takeFirst();
1441         std::shared_ptr<Mlt::Producer> prod = binProducers.take(bid);
1442         QString newId = QString::number(getFreeClipId());
1443         QString parentId = qstrdup(prod->get("kdenlive:folderid"));
1444         if (parentId.isEmpty()) {
1445             parentId = QStringLiteral("-1");
1446         } else {
1447             if (binIdCorresp.count(parentId.section(QLatin1Char('.'), -1)) == 0) {
1448                 // Error, folder was lost
1449                 parentId = QStringLiteral("-1");
1450             }
1451         }
1452         prod->set("_kdenlive_processed", 1);
1453         qDebug() << "======\nADDING NEW CLIP: " << newId << " = " << prod->is_valid();
1454         requestAddBinClip(newId, prod, parentId, undo, redo);
1455         binIdCorresp[QString::number(bid)] = newId;
1456     }
1457 }
1458 
1459 void ProjectItemModel::storeSequence(const QString uuid, std::shared_ptr<Mlt::Tractor> tractor, bool internalSave)
1460 {
1461     if (m_extraPlaylists.count(uuid) > 0) {
1462         m_extraPlaylists.erase(uuid);
1463     }
1464     m_extraPlaylists.insert({uuid, std::move(tractor)});
1465     if (internalSave) {
1466         // Ensure we never use the mapped ids when re-opening an already opened sequence
1467         setExtraTimelineSaved(uuid);
1468     }
1469 }
1470 
1471 int ProjectItemModel::sequenceCount() const
1472 {
1473     return m_extraPlaylists.size();
1474 }
1475 
1476 std::shared_ptr<Mlt::Tractor> ProjectItemModel::projectTractor()
1477 {
1478     return m_projectTractor;
1479 }
1480 
1481 const QString ProjectItemModel::sceneList(const QString &root, const QString &fullPath, const QString &filterData, Mlt::Tractor *activeTractor, int duration)
1482 {
1483     LocaleHandling::resetLocale();
1484     QString playlist;
1485     Mlt::Consumer xmlConsumer(pCore->getProjectProfile(), "xml", fullPath.isEmpty() ? "kdenlive_playlist" : fullPath.toUtf8().constData());
1486     if (!root.isEmpty()) {
1487         xmlConsumer.set("root", root.toUtf8().constData());
1488     }
1489     if (!xmlConsumer.is_valid()) {
1490         return QString();
1491     }
1492     xmlConsumer.set("store", "kdenlive");
1493     xmlConsumer.set("time_format", "clock");
1494     // Disabling meta creates cleaner files, but then we don't have access to metadata on the fly (meta channels, etc)
1495     // And we must use "avformat" instead of "avformat-novalidate" on project loading which causes a big delay on project opening
1496     // xmlConsumer.set("no_meta", 1);
1497 
1498     // Add active timeline as playlist of the main tractor so that when played through melt, the .kdenlive file reads the playlist
1499     if (m_projectTractor->count() > 0) {
1500         m_projectTractor->remove_track(0);
1501     }
1502     m_projectTractor->insert_track(*activeTractor->cut(0, duration), 0);
1503 
1504     Mlt::Service s(m_projectTractor->get_service());
1505     std::unique_ptr<Mlt::Filter> filter = nullptr;
1506     if (!filterData.isEmpty()) {
1507         filter = std::make_unique<Mlt::Filter>(pCore->getProjectProfile(), QString("dynamictext:%1").arg(filterData).toUtf8().constData());
1508         filter->set("fgcolour", "#ffffff");
1509         filter->set("bgcolour", "#bb333333");
1510         s.attach(*filter.get());
1511     }
1512     xmlConsumer.connect(s);
1513     xmlConsumer.run();
1514     if (filter) {
1515         s.detach(*filter.get());
1516     }
1517     playlist = fullPath.isEmpty() ? QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")) : fullPath;
1518     return playlist;
1519 }
1520 
1521 std::shared_ptr<Mlt::Tractor> ProjectItemModel::getExtraTimeline(const QString &uuid)
1522 {
1523     if (m_extraPlaylists.count(uuid) > 0) {
1524         return m_extraPlaylists.at(uuid);
1525     }
1526     return nullptr;
1527 }
1528 
1529 void ProjectItemModel::setExtraTimelineSaved(const QString &uuid)
1530 {
1531     if (m_extraPlaylists.count(uuid) > 0) {
1532         m_extraPlaylists.at(uuid)->set("_dontmapids", 1);
1533     }
1534 }
1535 
1536 void ProjectItemModel::removeReferencedClips(const QUuid &uuid, bool onDeletion)
1537 {
1538     QList<std::shared_ptr<ProjectClip>> clipList = getRootFolder()->childClips();
1539     for (const std::shared_ptr<ProjectClip> &clip : qAsConst(clipList)) {
1540         if (clip->refCount() > 0) {
1541             clip->purgeReferences(uuid, onDeletion);
1542         }
1543     }
1544 }
1545 
1546 /** @brief Save document properties in MLT's bin playlist */
1547 void ProjectItemModel::saveDocumentProperties(const QMap<QString, QString> &props, const QMap<QString, QString> &metadata)
1548 {
1549     m_binPlaylist->saveDocumentProperties(props, metadata);
1550 }
1551 
1552 void ProjectItemModel::saveProperty(const QString &name, const QString &value)
1553 {
1554     m_binPlaylist->saveProperty(name, value);
1555 }
1556 
1557 QMap<QString, QString> ProjectItemModel::getProxies(const QString &root)
1558 {
1559     READ_LOCK();
1560     return m_binPlaylist->getProxies(root);
1561 }
1562 
1563 void ProjectItemModel::reloadClip(const QString &binId)
1564 {
1565     QWriteLocker locker(&m_lock);
1566     std::shared_ptr<ProjectClip> clip = getClipByBinID(binId);
1567     if (clip) {
1568         clip->reloadProducer();
1569     }
1570 }
1571 
1572 void ProjectItemModel::setClipWaiting(const QString &binId)
1573 {
1574     QWriteLocker locker(&m_lock);
1575     std::shared_ptr<ProjectClip> clip = getClipByBinID(binId);
1576     if (clip) {
1577         clip->setClipStatus(FileStatus::StatusWaiting);
1578     }
1579 }
1580 
1581 void ProjectItemModel::setClipInvalid(const QString &binId)
1582 {
1583     QWriteLocker locker(&m_lock);
1584     std::shared_ptr<ProjectClip> clip = getClipByBinID(binId);
1585     if (clip) {
1586         clip->setClipStatus(FileStatus::StatusMissing);
1587         // TODO: set producer as blank invalid
1588     }
1589 }
1590 
1591 void ProjectItemModel::slotUpdateInvalidCount()
1592 {
1593     READ_LOCK();
1594     int missingCount = 0;
1595     int missingUsed = 0;
1596     for (const auto &clip : m_allItems) {
1597         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
1598         if (c->clipStatus() == FileStatus::StatusMissing) {
1599             int usage = c->getData(AbstractProjectItem::UsageCount).toInt();
1600             if (usage > 0) {
1601                 missingUsed++;
1602             }
1603             missingCount++;
1604         }
1605     }
1606     Q_EMIT pCore->gotMissingClipsCount(missingCount, missingUsed);
1607 }
1608 
1609 void ProjectItemModel::updateWatcher(const std::shared_ptr<ProjectClip> &clipItem)
1610 {
1611     QWriteLocker locker(&m_lock);
1612     ClipType::ProducerType type = clipItem->clipType();
1613     if (type == ClipType::AV || type == ClipType::Audio || type == ClipType::Image || type == ClipType::Video || type == ClipType::Playlist ||
1614         type == ClipType::TextTemplate || type == ClipType::Animation) {
1615         m_fileWatcher->removeFile(clipItem->clipId());
1616         QFileInfo check_file(clipItem->clipUrl());
1617         // check if file exists and if yes: Is it really a file and no directory?
1618         if ((check_file.exists() && check_file.isFile()) || clipItem->clipStatus() == FileStatus::StatusMissing) {
1619             m_fileWatcher->addFile(clipItem->clipId(), clipItem->clipUrl());
1620         }
1621     }
1622 }
1623 
1624 void ProjectItemModel::setDragType(PlaylistState::ClipState type)
1625 {
1626     QWriteLocker locker(&m_lock);
1627     m_dragType = type;
1628 }
1629 
1630 int ProjectItemModel::clipsCount() const
1631 {
1632     READ_LOCK();
1633     return m_binPlaylist->count();
1634 }
1635 
1636 bool ProjectItemModel::hasProxies() const
1637 {
1638     READ_LOCK();
1639     for (const auto &clip : m_allItems) {
1640         auto c = std::static_pointer_cast<AbstractProjectItem>(clip.second.lock());
1641         if (c->itemType() == AbstractProjectItem::ClipItem) {
1642             if (std::static_pointer_cast<ProjectClip>(c)->hasProxy()) {
1643                 return true;
1644             }
1645         }
1646     }
1647     return false;
1648 }
1649 
1650 bool ProjectItemModel::validateClip(const QString &binId, const QString &clipHash)
1651 {
1652     QWriteLocker locker(&m_lock);
1653     std::shared_ptr<ProjectClip> clip = getClipByBinID(binId);
1654     if (clip) {
1655         return clip->hash() == clipHash;
1656     }
1657     return false;
1658 }
1659 
1660 QString ProjectItemModel::validateClipInFolder(const QString &folderId, const QString &clipHash)
1661 {
1662     QWriteLocker locker(&m_lock);
1663     std::shared_ptr<ProjectFolder> folder = getFolderByBinId(folderId);
1664     if (folder) {
1665         return folder->childByHash(clipHash);
1666     }
1667     return QString();
1668 }
1669 
1670 bool ProjectItemModel::urlExists(const QString &path) const
1671 {
1672     return m_fileWatcher->contains(path);
1673 }
1674 
1675 bool ProjectItemModel::canBeEmbeded(const QUuid destUuid, const QUuid srcUuid)
1676 {
1677     const QString srcId = getSequenceId(destUuid);
1678     QList<QUuid> checkedUuids;
1679     std::shared_ptr<ProjectClip> clip = getClipByBinID(srcId);
1680     if (clip) {
1681         // Get a list of all dependencies
1682         QList<QUuid> updatedUUids = clip->registeredUuids();
1683         while (!updatedUUids.isEmpty()) {
1684             QUuid uuid = updatedUUids.takeFirst();
1685             if (!checkedUuids.contains(uuid)) {
1686                 checkedUuids.append(uuid);
1687                 const QString secId = getSequenceId(uuid);
1688                 std::shared_ptr<ProjectClip> subclip = getClipByBinID(secId);
1689                 updatedUUids.append(subclip->registeredUuids());
1690             }
1691         }
1692         if (checkedUuids.contains(srcUuid)) {
1693             return false;
1694         }
1695     } else {
1696         qDebug() << "::: CLIP NOT FOUND FOR : " << srcId;
1697     }
1698     return true;
1699 }
1700 
1701 int ProjectItemModel::defaultSequencesFolder() const
1702 {
1703     return m_sequenceFolderId;
1704 }
1705 
1706 void ProjectItemModel::setSequencesFolder(int id)
1707 {
1708     m_sequenceFolderId = id;
1709     saveProperty(QStringLiteral("kdenlive:sequenceFolder"), QString::number(id));
1710     if (id > -1) {
1711         Q_ASSERT(getFolderByBinId(QString::number(id)) != nullptr);
1712         onItemUpdated(QString::number(id), Qt::DecorationRole);
1713     }
1714 }
1715 
1716 int ProjectItemModel::defaultAudioCaptureFolder() const
1717 {
1718     return m_audioCaptureFolderId;
1719 }
1720 
1721 void ProjectItemModel::setAudioCaptureFolder(int id)
1722 {
1723     m_audioCaptureFolderId = id;
1724     saveProperty(QStringLiteral("kdenlive:audioCaptureFolder"), QString::number(id));
1725 }
1726 
1727 Fun ProjectItemModel::removeProjectItem_lambda(int binId, int id)
1728 {
1729     return [this, binId, id]() {
1730         if (binId > -1) {
1731             Q_EMIT pCore->binClipDeleted(binId);
1732         }
1733         Fun operation = removeItem_lambda(id);
1734         bool result = operation();
1735         return result;
1736     };
1737 }
1738 
1739 void ProjectItemModel::checkSequenceIntegrity(const QString activeSequenceId)
1740 {
1741     QStringList sequencesIds = pCore->currentDoc()->getTimelinesIds();
1742     Q_ASSERT(sequencesIds.contains(activeSequenceId));
1743     QStringList allMltIds = m_binPlaylist->getAllMltIds();
1744     for (auto &i : sequencesIds) {
1745         Q_ASSERT(allMltIds.contains(i));
1746     }
1747 }
1748 
1749 std::shared_ptr<EffectStackModel> ProjectItemModel::getClipEffectStack(int itemId)
1750 {
1751     std::shared_ptr<ProjectClip> clip = getClipByBinID(QString::number(itemId));
1752     Q_ASSERT(clip != nullptr);
1753     return clip->getEffectStack();
1754 }