File indexing completed on 2024-04-28 04:52:30

0001 /*
0002     SPDX-FileCopyrightText: 2017 Nicolas Carion
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "timelineitemmodel.hpp"
0007 #include "assets/keyframes/model/keyframemodel.hpp"
0008 #include "audiomixer/mixermanager.hpp"
0009 #include "bin/model/markerlistmodel.hpp"
0010 #include "bin/model/subtitlemodel.hpp"
0011 #include "clipmodel.hpp"
0012 #include "compositionmodel.hpp"
0013 #include "core.h"
0014 #include "doc/docundostack.hpp"
0015 #include "doc/kdenlivedoc.h"
0016 #include "groupsmodel.hpp"
0017 #include "kdenlivesettings.h"
0018 #include "macros.hpp"
0019 #include "snapmodel.hpp"
0020 #include "timeline2/view/previewmanager.h"
0021 #include "trackmodel.hpp"
0022 #include "transitions/transitionsrepository.hpp"
0023 #include <QDebug>
0024 #include <QFileInfo>
0025 #include <mlt++/MltField.h>
0026 #include <mlt++/MltProfile.h>
0027 #include <mlt++/MltTractor.h>
0028 #include <mlt++/MltTransition.h>
0029 
0030 #ifdef CRASH_AUTO_TEST
0031 #pragma GCC diagnostic push
0032 #pragma GCC diagnostic ignored "-Wunused-parameter"
0033 #pragma GCC diagnostic ignored "-Wsign-conversion"
0034 #pragma GCC diagnostic ignored "-Wfloat-equal"
0035 #pragma GCC diagnostic ignored "-Wshadow"
0036 #pragma GCC diagnostic ignored "-Wpedantic"
0037 #include <rttr/registration>
0038 #pragma GCC diagnostic pop
0039 RTTR_REGISTRATION
0040 {
0041     using namespace rttr;
0042     registration::class_<TimelineItemModel>("TimelineItemModel");
0043 }
0044 #endif
0045 
0046 TimelineItemModel::TimelineItemModel(const QUuid &uuid, std::weak_ptr<DocUndoStack> undo_stack)
0047     : TimelineModel(uuid, std::move(undo_stack))
0048 {
0049 }
0050 
0051 void TimelineItemModel::finishConstruct(const std::shared_ptr<TimelineItemModel> &ptr)
0052 {
0053     ptr->weak_this_ = ptr;
0054     ptr->m_groups = std::make_unique<GroupsModel>(ptr);
0055 }
0056 
0057 std::shared_ptr<TimelineItemModel> TimelineItemModel::construct(const QUuid &uuid, std::weak_ptr<DocUndoStack> undo_stack)
0058 {
0059     std::shared_ptr<TimelineItemModel> ptr(new TimelineItemModel(uuid, std::move(undo_stack)));
0060     finishConstruct(ptr);
0061     return ptr;
0062 }
0063 
0064 TimelineItemModel::~TimelineItemModel() = default;
0065 
0066 QModelIndex TimelineItemModel::index(int row, int column, const QModelIndex &parent) const
0067 {
0068     READ_LOCK();
0069     QModelIndex result;
0070     if (parent.isValid()) {
0071         auto trackId = int(parent.internalId());
0072         Q_ASSERT(isTrack(trackId));
0073         int clipId = getTrackById_const(trackId)->getClipByRow(row);
0074         if (clipId != -1) {
0075             result = createIndex(row, 0, quintptr(clipId));
0076         } else if (row < getTrackClipsCount(trackId) + getTrackCompositionsCount(trackId)) {
0077             int compoId = getTrackById_const(trackId)->getCompositionByRow(row);
0078             if (compoId != -1) {
0079                 result = createIndex(row, 0, quintptr(compoId));
0080             }
0081         } else {
0082             // Invalid index requested
0083             Q_ASSERT(false);
0084         }
0085     } else if (row < int(m_allTracks.size()) && row >= 0) {
0086         // Get sort order
0087         // row = getTracksCount() - 1 - row;
0088         auto it = m_allTracks.cbegin();
0089         std::advance(it, row);
0090         int trackId = (*it)->getId();
0091         result = createIndex(row, column, quintptr(trackId));
0092     }
0093     return result;
0094 }
0095 
0096 /*QModelIndex TimelineItemModel::makeIndex(int trackIndex, int clipIndex) const
0097 {
0098     return index(clipIndex, 0, index(trackIndex));
0099 }*/
0100 
0101 bool TimelineItemModel::addTracksAtPosition(int position, int tracksCount, QString &trackName, bool addAudioTrack, bool addAVTrack, bool addRecTrack)
0102 {
0103     Fun undo = []() { return true; };
0104     Fun redo = []() { return true; };
0105     bool result = true;
0106 
0107     int insertionIndex = position;
0108 
0109     for (int ix = 0; ix < tracksCount; ++ix) {
0110         int newTid;
0111         result = requestTrackInsertion(insertionIndex, newTid, trackName, addAudioTrack, undo, redo);
0112         // bump up insertion index so that the next new track goes after this one
0113         insertionIndex++;
0114         if (result) {
0115             if (addAVTrack) {
0116                 int newTid2;
0117                 int mirrorPos = 0;
0118                 int mirrorId = getMirrorAudioTrackId(newTid);
0119                 if (mirrorId > -1) {
0120                     mirrorPos = getTrackMltIndex(mirrorId);
0121                 }
0122                 result = requestTrackInsertion(mirrorPos, newTid2, trackName, true, undo, redo);
0123                 // because we also added an audio track, we need to put the next
0124                 // new track's index is 1 further
0125                 insertionIndex++;
0126             }
0127             if (addRecTrack) {
0128                 setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1"));
0129             }
0130         } else {
0131             break;
0132         }
0133     }
0134     if (result) {
0135         pCore->pushUndo(undo, redo, addAVTrack || tracksCount > 1 ? i18nc("@action", "Insert Tracks") : i18nc("@action", "Insert Track"));
0136         return true;
0137     } else {
0138         undo();
0139         return false;
0140     }
0141 }
0142 
0143 QModelIndex TimelineItemModel::makeClipIndexFromID(int clipId) const
0144 {
0145     Q_ASSERT(m_allClips.count(clipId) > 0);
0146     int trackId = m_allClips.at(clipId)->getCurrentTrackId();
0147     if (trackId == -1) {
0148         // Clip is not inserted in a track
0149         qDebug() << "/// WARNING; INVALID CLIP INDEX REQUESTED: " << clipId << "\n________________";
0150         return {};
0151     }
0152     int row = getTrackById_const(trackId)->getRowfromClip(clipId);
0153     return index(row, 0, makeTrackIndexFromID(trackId));
0154 }
0155 
0156 QModelIndex TimelineItemModel::makeCompositionIndexFromID(int compoId) const
0157 {
0158     Q_ASSERT(m_allCompositions.count(compoId) > 0);
0159     int trackId = m_allCompositions.at(compoId)->getCurrentTrackId();
0160     return index(getTrackById_const(trackId)->getRowfromComposition(compoId), 0, makeTrackIndexFromID(trackId));
0161 }
0162 
0163 void TimelineItemModel::subtitleChanged(int subId, const QVector<int> &roles)
0164 {
0165     if (m_closing) {
0166         return;
0167     }
0168     Q_ASSERT(m_subtitleModel != nullptr);
0169     Q_ASSERT(m_allSubtitles.count(subId) > 0);
0170     m_subtitleModel->updateSub(subId, roles);
0171 }
0172 
0173 QModelIndex TimelineItemModel::makeTrackIndexFromID(int trackId) const
0174 {
0175     // we retrieve iterator
0176     Q_ASSERT(m_iteratorTable.count(trackId) > 0);
0177     auto it = m_iteratorTable.at(trackId);
0178     int ind = int(std::distance<decltype(m_allTracks.cbegin())>(m_allTracks.begin(), it));
0179     // Get sort order
0180     // ind = getTracksCount() - 1 - ind;
0181     return index(ind);
0182 }
0183 
0184 QModelIndex TimelineItemModel::parent(const QModelIndex &index) const
0185 {
0186     READ_LOCK();
0187     // qDebug() << "TimelineItemModel::parent"<< index;
0188     if (index == QModelIndex()) {
0189         return index;
0190     }
0191     const int id = static_cast<int>(index.internalId());
0192     if (!index.isValid() || isTrack(id)) {
0193         return QModelIndex();
0194     }
0195     if (isClip(id)) {
0196         const int trackId = getClipTrackId(id);
0197         return makeTrackIndexFromID(trackId);
0198     }
0199     if (isComposition(id)) {
0200         const int trackId = getCompositionTrackId(id);
0201         return makeTrackIndexFromID(trackId);
0202     }
0203     return {};
0204 }
0205 
0206 int TimelineItemModel::rowCount(const QModelIndex &parent) const
0207 {
0208     READ_LOCK();
0209     if (parent.isValid()) {
0210         const int id = int(parent.internalId());
0211         if (!isTrack(id)) {
0212             // clips don't have children
0213             // if it is not a track, it is something invalid
0214             return 0;
0215         }
0216         return getTrackClipsCount(id) + getTrackCompositionsCount(id);
0217     }
0218     return int(m_allTracks.size());
0219 }
0220 
0221 int TimelineItemModel::columnCount(const QModelIndex &parent) const
0222 {
0223     Q_UNUSED(parent);
0224     return 1;
0225 }
0226 
0227 QHash<int, QByteArray> TimelineItemModel::roleNames() const
0228 {
0229     QHash<int, QByteArray> roles;
0230     roles[NameRole] = "name";
0231     roles[ResourceRole] = "resource";
0232     roles[ServiceRole] = "mlt_service";
0233     roles[BinIdRole] = "binId";
0234     roles[TrackIdRole] = "trackId";
0235     roles[TagRole] = "tag";
0236     roles[FakeTrackIdRole] = "fakeTrackId";
0237     roles[FakePositionRole] = "fakePosition";
0238     roles[StartRole] = "start";
0239     roles[MixRole] = "mixDuration";
0240     roles[MixCutRole] = "mixCut";
0241     roles[DurationRole] = "duration";
0242     roles[MaxDurationRole] = "maxDuration";
0243     roles[MarkersRole] = "markers";
0244     roles[KeyframesRole] = "keyframeModel";
0245     roles[ShowKeyframesRole] = "showKeyframes";
0246     roles[PlaylistStateRole] = "clipState";
0247     roles[StatusRole] = "clipStatus";
0248     roles[TypeRole] = "clipType";
0249     roles[InPointRole] = "in";
0250     roles[OutPointRole] = "out";
0251     roles[FramerateRole] = "fps";
0252     roles[GroupedRole] = "grouped";
0253     roles[IsDisabledRole] = "disabled";
0254     roles[IsAudioRole] = "audio";
0255     roles[AudioChannelsRole] = "audioChannels";
0256     roles[AudioStreamRole] = "audioStream";
0257     roles[AudioMultiStreamRole] = "multiStream";
0258     roles[AudioStreamIndexRole] = "audioStreamIndex";
0259     roles[IsCompositeRole] = "composite";
0260     roles[IsLockedRole] = "locked";
0261     roles[FadeInRole] = "fadeIn";
0262     roles[FadeOutRole] = "fadeOut";
0263     roles[FileHashRole] = "hash";
0264     roles[SpeedRole] = "speed";
0265     roles[TimeRemapRole] = "timeremap";
0266     roles[HeightRole] = "trackHeight";
0267     roles[TrackTagRole] = "trackTag";
0268     roles[ItemIdRole] = "item";
0269     roles[ItemATrack] = "a_track";
0270     roles[HasAudio] = "hasAudio";
0271     roles[CanBeAudioRole] = "canBeAudio";
0272     roles[CanBeVideoRole] = "canBeVideo";
0273     roles[ClipThumbRole] = "clipThumbId";
0274     roles[ReloadAudioThumbRole] = "reloadAudioThumb";
0275     roles[PositionOffsetRole] = "positionOffset";
0276     roles[ThumbsFormatRole] = "thumbsFormat";
0277     roles[AudioRecordRole] = "audioRecord";
0278     roles[TrackActiveRole] = "trackActive";
0279     roles[EffectNamesRole] = "effectNames";
0280     roles[EffectsEnabledRole] = "isStackEnabled";
0281     roles[EffectZonesRole] = "effectZones";
0282     roles[GrabbedRole] = "isGrabbed";
0283     roles[SelectedRole] = "selected";
0284     return roles;
0285 }
0286 
0287 QVariant TimelineItemModel::data(const QModelIndex &index, int role) const
0288 {
0289     READ_LOCK();
0290     if (!m_tractor || !index.isValid()) {
0291         // qDebug() << "DATA abort. Index validity="<<index.isValid();
0292         return QVariant();
0293     }
0294     const int id = int(index.internalId());
0295     if (role == ItemIdRole) {
0296         return id;
0297     }
0298     if (role == SortRole) {
0299         if (isTrack(id)) {
0300             return getTrackSortValue(id, KdenliveSettings::audiotracksbelow());
0301         }
0302         return QVariant();
0303     }
0304     if (isClip(id)) {
0305         // qDebug() << "REQUESTING DATA "<<roleNames()[role]<<index;
0306         std::shared_ptr<ClipModel> clip = m_allClips.at(id);
0307         // Get data for a clip
0308         switch (role) {
0309         // TODO
0310         case NameRole:
0311         case Qt::DisplayRole: {
0312             return clip->clipName();
0313         }
0314         case ResourceRole: {
0315             QString result = clip->getProperty("resource");
0316             if (result == QLatin1String("<producer>")) {
0317                 result = clip->getProperty("mlt_service");
0318             }
0319             return result;
0320         }
0321         case StatusRole: {
0322             return clip->clipStatus();
0323         }
0324         case FakeTrackIdRole:
0325             return clip->getFakeTrackId();
0326         case FakePositionRole:
0327             return clip->getFakePosition();
0328         case BinIdRole:
0329             return clip->binId();
0330         case TrackIdRole:
0331             return clip->getCurrentTrackId();
0332         case ServiceRole:
0333             return clip->getProperty("mlt_service");
0334             break;
0335         case AudioChannelsRole:
0336             return clip->audioChannels();
0337         case AudioStreamRole:
0338             return clip->audioStream();
0339         case AudioMultiStreamRole:
0340             return clip->audioMultiStream();
0341         case AudioStreamIndexRole:
0342             return clip->audioStreamIndex();
0343         case HasAudio:
0344             return clip->audioEnabled();
0345         case IsAudioRole:
0346             return clip->isAudioOnly();
0347         case CanBeAudioRole:
0348             return clip->canBeAudio();
0349         case CanBeVideoRole:
0350             return clip->canBeVideo();
0351         case MarkersRole: {
0352             return QVariant::fromValue<MarkerListModel *>(clip->getMarkerModel().get());
0353         }
0354         case KeyframesRole: {
0355             return QVariant::fromValue<KeyframeModel *>(clip->getKeyframeModel());
0356         }
0357         case PlaylistStateRole:
0358             return QVariant::fromValue(clip->clipState());
0359         case TypeRole:
0360             return QVariant::fromValue(clip->clipType());
0361         case StartRole:
0362             return clip->getPosition();
0363         case DurationRole:
0364             return clip->getPlaytime();
0365         case MaxDurationRole:
0366             return clip->getMaxDuration();
0367         case GroupedRole:
0368             return m_groups->isInGroup(id);
0369         case EffectNamesRole:
0370             return clip->effectNames();
0371         case EffectsEnabledRole:
0372             return clip->stackEnabled();
0373         case InPointRole:
0374             return clip->getIn();
0375         case OutPointRole:
0376             return clip->getOut();
0377         case ShowKeyframesRole:
0378             return clip->showKeyframes();
0379         case FadeInRole:
0380             return clip->fadeIn();
0381         case FadeOutRole:
0382             return clip->fadeOut();
0383         case MixRole:
0384             return clip->getMixDuration();
0385         case MixCutRole:
0386             return clip->getMixCutPosition();
0387         case ClipThumbRole:
0388             return clip->clipThumbPath();
0389         case ReloadAudioThumbRole:
0390             return clip->forceThumbReload;
0391         case PositionOffsetRole:
0392             return clip->getOffset();
0393         case SpeedRole:
0394             return clip->getSpeed();
0395         case GrabbedRole:
0396             return clip->isGrabbed();
0397         case SelectedRole:
0398             return clip->selected;
0399         case TagRole:
0400             return clip->clipTag();
0401         case TimeRemapRole:
0402             return clip->hasTimeRemap();
0403         default:
0404             break;
0405         }
0406     } else if (isTrack(id)) {
0407         // qDebug() << "DATA REQUESTED FOR TRACK "<< id;
0408         switch (role) {
0409         case NameRole:
0410         case Qt::DisplayRole: {
0411             return getTrackById_const(id)->getProperty("kdenlive:track_name").toString();
0412         }
0413         case TypeRole:
0414             return QVariant::fromValue(ClipType::ProducerType::Track);
0415         case DurationRole:
0416             // qDebug() << "DATA yielding duration" << m_tractor->get_playtime();
0417             return getTrackById_const(id)->trackDuration();
0418         case IsDisabledRole:
0419             // qDebug() << "DATA yielding mute" << 0;
0420             return getTrackById_const(id)->isAudioTrack() ? getTrackById_const(id)->isMute() : getTrackById_const(id)->isHidden();
0421         case IsAudioRole:
0422             return getTrackById_const(id)->isAudioTrack();
0423         case TrackTagRole:
0424             return getTrackTagById(id);
0425         case IsLockedRole:
0426             return getTrackById_const(id)->isLocked();
0427         case HeightRole: {
0428             int collapsed = getTrackById_const(id)->getProperty("kdenlive:collapsed").toInt();
0429             if (collapsed > 0) {
0430                 return collapsed;
0431             }
0432             int height = getTrackById_const(id)->getProperty("kdenlive:trackheight").toInt();
0433             // qDebug() << "DATA yielding height" << height;
0434             return (height > 0 ? height : KdenliveSettings::trackheight());
0435         }
0436         case ThumbsFormatRole:
0437             return getTrackById_const(id)->getProperty("kdenlive:thumbs_format").toInt();
0438         case IsCompositeRole: {
0439         case AudioRecordRole:
0440             return getTrackById_const(id)->getProperty("kdenlive:audio_rec").toInt();
0441         }
0442         case TrackActiveRole: {
0443             return getTrackById_const(id)->isTimelineActive();
0444         }
0445         case EffectNamesRole: {
0446             return getTrackById_const(id)->effectNames();
0447         }
0448         case EffectsEnabledRole: {
0449             return getTrackById_const(id)->stackEnabled();
0450         }
0451         case EffectZonesRole: {
0452             return getTrackById_const(id)->stackZones();
0453         }
0454         default:
0455             break;
0456         }
0457     } else if (isComposition(id)) {
0458         std::shared_ptr<CompositionModel> compo = m_allCompositions.at(id);
0459         switch (role) {
0460         case NameRole:
0461         case Qt::DisplayRole:
0462         case ResourceRole:
0463         case ServiceRole:
0464             return compo->displayName();
0465             break;
0466         case TypeRole:
0467             return QVariant::fromValue(ClipType::ProducerType::Composition);
0468         case StartRole:
0469             return compo->getPosition();
0470         case TrackIdRole:
0471             return compo->getCurrentTrackId();
0472         case DurationRole:
0473             return compo->getPlaytime();
0474         case GroupedRole:
0475             return m_groups->isInGroup(id);
0476         case InPointRole:
0477             return 0;
0478         case OutPointRole:
0479             return 100;
0480         case BinIdRole:
0481             return 5;
0482         case KeyframesRole: {
0483             return QVariant::fromValue<KeyframeModel *>(compo->getEffectKeyframeModel());
0484         }
0485         case ShowKeyframesRole:
0486             return compo->showKeyframes();
0487         case ItemATrack:
0488             return compo->getForcedTrack();
0489         case MarkersRole: {
0490             QVariantList markersList;
0491             return markersList;
0492         }
0493         case GrabbedRole:
0494             return compo->isGrabbed();
0495         case SelectedRole:
0496             return compo->selected;
0497         default:
0498             break;
0499         }
0500     } else {
0501         qDebug() << "UNKNOWN DATA requested " << index << roleNames()[role];
0502     }
0503     return QVariant();
0504 }
0505 
0506 void TimelineItemModel::setTrackName(int trackId, const QString &text)
0507 {
0508     QWriteLocker locker(&m_lock);
0509     const QString &currentName = getTrackProperty(trackId, QStringLiteral("kdenlive:track_name")).toString();
0510     if (text == currentName) {
0511         return;
0512     }
0513     Fun undo_lambda = [this, trackId, currentName]() {
0514         setTrackProperty(trackId, QStringLiteral("kdenlive:track_name"), currentName);
0515         return true;
0516     };
0517     Fun redo_lambda = [this, trackId, text]() {
0518         setTrackProperty(trackId, QStringLiteral("kdenlive:track_name"), text);
0519         return true;
0520     };
0521     redo_lambda();
0522     PUSH_UNDO(undo_lambda, redo_lambda, i18n("Rename Track"));
0523 }
0524 
0525 void TimelineItemModel::setTrackProperty(int trackId, const QString &name, const QString &value)
0526 {
0527     std::shared_ptr<TrackModel> track = getTrackById(trackId);
0528     track->setProperty(name, value);
0529     QVector<int> roles;
0530     bool updateMultiTrack = false;
0531     if (name == QLatin1String("kdenlive:track_name")) {
0532         roles.push_back(NameRole);
0533         if (!track->isAudioTrack()) {
0534             updateMultiTrack = true;
0535         }
0536     } else if (name == QLatin1String("kdenlive:locked_track")) {
0537         roles.push_back(IsLockedRole);
0538     } else if (name == QLatin1String("hide")) {
0539         roles.push_back(IsDisabledRole);
0540         if (!track->isAudioTrack() && !isLoading) {
0541             pCore->invalidateItem(ObjectId(KdenliveObjectType::TimelineTrack, trackId, m_uuid));
0542             pCore->refreshProjectMonitorOnce();
0543             updateMultiTrack = true;
0544         }
0545     } else if (name == QLatin1String("kdenlive:timeline_active")) {
0546         roles.push_back(TrackActiveRole);
0547     } else if (name == QLatin1String("kdenlive:thumbs_format")) {
0548         roles.push_back(ThumbsFormatRole);
0549     } else if (name == QLatin1String("kdenlive:collapsed")) {
0550         roles.push_back(HeightRole);
0551     } else if (name == QLatin1String("kdenlive:audio_rec")) {
0552         roles.push_back(AudioRecordRole);
0553     }
0554     if (!roles.isEmpty()) {
0555         QModelIndex ix = makeTrackIndexFromID(trackId);
0556         Q_EMIT dataChanged(ix, ix, roles);
0557         if (updateMultiTrack) {
0558             Q_EMIT trackVisibilityChanged();
0559         }
0560     }
0561 }
0562 
0563 void TimelineItemModel::setTrackStackEnabled(int tid, bool enable)
0564 {
0565     std::shared_ptr<TrackModel> track = getTrackById(tid);
0566     track->setEffectStackEnabled(enable);
0567     QModelIndex ix = makeTrackIndexFromID(tid);
0568     Q_EMIT dataChanged(ix, ix, {TimelineModel::EffectsEnabledRole});
0569 }
0570 
0571 void TimelineItemModel::importTrackEffects(int tid, std::weak_ptr<Mlt::Service> service)
0572 {
0573     std::shared_ptr<TrackModel> track = getTrackById(tid);
0574     Mlt::Tractor *destination = track->getTrackService();
0575     // Audio mixer effects are attached to the Tractor service, while track effects are attached to first playlist service
0576     if (auto ptr = service.lock()) {
0577         for (int i = 0; i < ptr->filter_count(); i++) {
0578             std::unique_ptr<Mlt::Filter> filter(ptr->filter(i));
0579             if (filter->get_int("internal_added") > 0) {
0580                 destination->attach(*filter.get());
0581             }
0582         }
0583     }
0584 
0585     track->importEffects(std::move(service));
0586 }
0587 
0588 QVariant TimelineItemModel::getTrackProperty(int tid, const QString &name) const
0589 {
0590     return getTrackById_const(tid)->getProperty(name);
0591 }
0592 
0593 int TimelineItemModel::getFirstVideoTrackIndex() const
0594 {
0595     int trackId = -1;
0596     auto it = m_allTracks.cbegin();
0597     while (it != m_allTracks.cend()) {
0598         trackId = (*it)->getId();
0599         if (!(*it)->isAudioTrack()) {
0600             break;
0601         }
0602         ++it;
0603     }
0604     return trackId;
0605 }
0606 
0607 int TimelineItemModel::getFirstAudioTrackIndex() const
0608 {
0609     int trackId = -1;
0610     auto it = m_allTracks.cbegin();
0611     while (it != m_allTracks.cend()) {
0612         if ((*it)->isAudioTrack()) {
0613             trackId = (*it)->getId();
0614         }
0615         ++it;
0616     }
0617     return trackId;
0618 }
0619 
0620 std::shared_ptr<SubtitleModel> TimelineItemModel::createSubtitleModel()
0621 {
0622     if (m_subtitleModel == nullptr) {
0623         // Initialize the subtitle model and load file if any
0624         m_subtitleModel.reset(
0625             new SubtitleModel(std::static_pointer_cast<TimelineItemModel>(shared_from_this()), std::static_pointer_cast<SnapInterface>(m_snaps), this));
0626         Q_EMIT subtitleModelInitialized();
0627     }
0628     return m_subtitleModel;
0629 }
0630 
0631 const QString TimelineItemModel::getTrackFullName(int tid) const
0632 {
0633     QString tag = getTrackTagById(tid);
0634     QString trackName = getTrackById_const(tid)->getProperty(QStringLiteral("kdenlive:track_name")).toString();
0635     return trackName.isEmpty() ? tag : tag + QStringLiteral(" - ") + trackName;
0636 }
0637 
0638 const QString TimelineItemModel::groupsData()
0639 {
0640     return m_groups->toJson();
0641 }
0642 
0643 bool TimelineItemModel::loadGroups(const QString &groupsData)
0644 {
0645     return m_groups->fromJson(groupsData);
0646 }
0647 
0648 void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb)
0649 {
0650     QVector<int> roles;
0651     if (start) {
0652         roles.push_back(TimelineModel::StartRole);
0653         if (updateThumb) {
0654             roles.push_back(TimelineModel::InPointRole);
0655         }
0656     }
0657     if (duration) {
0658         roles.push_back(TimelineModel::DurationRole);
0659         if (updateThumb) {
0660             roles.push_back(TimelineModel::OutPointRole);
0661         }
0662     }
0663     Q_EMIT dataChanged(topleft, bottomright, roles);
0664 }
0665 
0666 void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector<int> &roles)
0667 {
0668     Q_EMIT dataChanged(topleft, bottomright, roles);
0669 }
0670 
0671 void TimelineItemModel::rebuildMixer()
0672 {
0673     if (pCore->mixer() == nullptr) {
0674         return;
0675     }
0676     pCore->mixer()->cleanup();
0677     pCore->mixer()->setModel(std::static_pointer_cast<TimelineItemModel>(shared_from_this()));
0678     auto it = m_allTracks.cbegin();
0679     while (it != m_allTracks.cend()) {
0680         if ((*it)->isAudioTrack()) {
0681             pCore->mixer()->registerTrack((*it)->getId(), (*it)->getTrackService(), getTrackTagById((*it)->getId()),
0682                                           (*it)->getProperty(QStringLiteral("kdenlive:track_name")).toString());
0683             connect(pCore->mixer(), &MixerManager::showEffectStack, this, &TimelineItemModel::showTrackEffectStack);
0684         }
0685         ++it;
0686     }
0687 }
0688 
0689 bool TimelineItemModel::copyClipEffect(int clipId, const QString sourceId)
0690 {
0691     QStringList source = sourceId.split(QLatin1Char(','));
0692     Q_ASSERT(m_allClips.count(clipId) && source.count() == 4);
0693     int itemType = source.at(0).toInt();
0694     int itemId = source.at(1).toInt();
0695     int itemRow = source.at(2).toInt();
0696     const QUuid uuid(source.at(3));
0697     std::shared_ptr<EffectStackModel> effectStack = pCore->getItemEffectStack(uuid, itemType, itemId);
0698     if (m_singleSelectionMode && m_currentSelection.count(clipId)) {
0699         // only operate on the selected item
0700         Fun undo = []() { return true; };
0701         Fun redo = []() { return true; };
0702         for (auto &s : m_currentSelection) {
0703             if (isClip(s)) {
0704                 m_allClips.at(s)->copyEffectWithUndo(uuid, effectStack, itemRow, undo, redo);
0705             }
0706         }
0707         pCore->pushUndo(undo, redo, i18n("Copy effect"));
0708         return true;
0709     } else if (m_groups->isInGroup(clipId)) {
0710         int parentGroup = m_groups->getRootId(clipId);
0711         if (parentGroup > -1) {
0712             Fun undo = []() { return true; };
0713             Fun redo = []() { return true; };
0714             std::unordered_set<int> sub = m_groups->getLeaves(parentGroup);
0715             for (auto &s : sub) {
0716                 if (isClip(s)) {
0717                     m_allClips.at(s)->copyEffectWithUndo(uuid, effectStack, itemRow, undo, redo);
0718                 }
0719             }
0720             pCore->pushUndo(undo, redo, i18n("Copy effect"));
0721             return true;
0722         }
0723     }
0724     return m_allClips.at(clipId)->copyEffect(uuid, effectStack, itemRow);
0725 }
0726 
0727 void TimelineItemModel::buildTrackCompositing(bool rebuild)
0728 {
0729     bool isMultiTrack = pCore->enableMultiTrack(false);
0730     auto it = m_allTracks.cbegin();
0731     QScopedPointer<Mlt::Service> service(m_tractor->field());
0732     QScopedPointer<Mlt::Field> field(m_tractor->field());
0733     field->lock();
0734     // Make sure all previous track compositing is removed
0735     if (rebuild) {
0736         while (service != nullptr && service->is_valid()) {
0737             if (service->type() == mlt_service_transition_type) {
0738                 Mlt::Transition t(mlt_transition(service->get_service()));
0739                 service.reset(service->producer());
0740                 if (t.get_int("internal_added") == 237) {
0741                     // remove all compositing transitions
0742                     field->disconnect_service(t);
0743                     t.disconnect_all_producers();
0744                 }
0745             } else {
0746                 service.reset(service->producer());
0747             }
0748         }
0749     }
0750     QString composite = TransitionsRepository::get()->getCompositingTransition();
0751     bool hasMixer = pCore->mixer() != nullptr;
0752     if (hasMixer) {
0753         pCore->mixer()->cleanup();
0754     }
0755     int videoTracks = 0;
0756     int audioTracks = 0;
0757     while (it != m_allTracks.cend()) {
0758         int trackPos = getTrackMltIndex((*it)->getId());
0759         if (!composite.isEmpty() && !(*it)->isAudioTrack()) {
0760             // video track, add composition
0761             std::unique_ptr<Mlt::Transition> transition = TransitionsRepository::get()->getTransition(composite);
0762             transition->set("internal_added", 237);
0763             transition->set("always_active", 1);
0764             transition->set_tracks(0, trackPos);
0765             field->plant_transition(*transition.get(), 0, trackPos);
0766             videoTracks++;
0767         } else if ((*it)->isAudioTrack()) {
0768             // audio mix
0769             std::unique_ptr<Mlt::Transition> transition = TransitionsRepository::get()->getTransition(QStringLiteral("mix"));
0770             transition->set("internal_added", 237);
0771             transition->set("always_active", 1);
0772             transition->set("accepts_blanks", 1);
0773             transition->set("sum", 1);
0774             transition->set_tracks(0, trackPos);
0775             field->plant_transition(*transition.get(), 0, trackPos);
0776             audioTracks++;
0777             if (hasMixer) {
0778                 pCore->mixer()->registerTrack((*it)->getId(), (*it)->getTrackService(), getTrackTagById((*it)->getId()),
0779                                               (*it)->getProperty(QStringLiteral("kdenlive:track_name")).toString());
0780                 connect(pCore->mixer(), &MixerManager::showEffectStack, this, &TimelineItemModel::showTrackEffectStack);
0781             }
0782         }
0783         ++it;
0784     }
0785     field->unlock();
0786     // Update sequence clip's AV status
0787     int currentClipType = m_tractor->get_int("kdenlive:clip_type");
0788     int newClipType = audioTracks > 0 ? (videoTracks > 0 ? 0 : 1) : 2;
0789     if (currentClipType != newClipType) {
0790         m_tractor->set("kdenlive:sequenceproperties.hasAudio", audioTracks > 0 ? 1 : 0);
0791         m_tractor->set("kdenlive:sequenceproperties.hasVideo", videoTracks > 0 ? 1 : 0);
0792         m_tractor->set("kdenlive:clip_type", newClipType);
0793     }
0794     pCore->updateSequenceAVType(m_uuid, audioTracks + videoTracks);
0795     if (isMultiTrack) {
0796         pCore->enableMultiTrack(true);
0797     }
0798     if (composite.isEmpty()) {
0799         pCore->displayMessage(i18n("Could not setup track compositing, check your install"), MessageType::ErrorMessage);
0800     }
0801 }
0802 
0803 void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role)
0804 {
0805     Q_EMIT dataChanged(topleft, bottomright, {role});
0806 }
0807 
0808 void TimelineItemModel::_beginRemoveRows(const QModelIndex &i, int j, int k)
0809 {
0810     // qDebug()<<"FORWARDING beginRemoveRows"<<i<<j<<k;
0811     beginRemoveRows(i, j, k);
0812 }
0813 void TimelineItemModel::_beginInsertRows(const QModelIndex &i, int j, int k)
0814 {
0815     // qDebug()<<"FORWARDING beginInsertRows"<<i<<j<<k;
0816     beginInsertRows(i, j, k);
0817 }
0818 void TimelineItemModel::_endRemoveRows()
0819 {
0820     // qDebug()<<"FORWARDING endRemoveRows";
0821     endRemoveRows();
0822 }
0823 void TimelineItemModel::_endInsertRows()
0824 {
0825     // qDebug()<<"FORWARDING endinsertRows";
0826     endInsertRows();
0827 }
0828 
0829 void TimelineItemModel::_resetView()
0830 {
0831     beginResetModel();
0832     endResetModel();
0833 }
0834 
0835 void TimelineItemModel::passSequenceProperties(const QMap<QString, QString> baseProperties)
0836 {
0837     QMapIterator<QString, QString> i(baseProperties);
0838     while (i.hasNext()) {
0839         i.next();
0840         tractor()->set(QString("kdenlive:sequenceproperties.%1").arg(i.key()).toUtf8().constData(), i.value().toUtf8().constData());
0841     }
0842     // Store groups data
0843     tractor()->set("kdenlive:sequenceproperties.groups", groupsData().toUtf8().constData());
0844     tractor()->set("kdenlive:sequenceproperties.documentuuid", pCore->currentDoc()->uuid().toString().toUtf8().constData());
0845     // Save timeline guides
0846     const QString guidesData = getGuideModel()->toJson();
0847     tractor()->set("kdenlive:sequenceproperties.guides", guidesData.toUtf8().constData());
0848     QPair<int, int> tracks = getAVtracksCount();
0849     tractor()->set("kdenlive:sequenceproperties.hasAudio", tracks.first > 0 ? 1 : 0);
0850     tractor()->set("kdenlive:sequenceproperties.hasVideo", tracks.second > 0 ? 1 : 0);
0851     tractor()->set("kdenlive:sequenceproperties.tracksCount", tracks.first + tracks.second);
0852 
0853     tractor()->set("kdenlive:sequenceproperties.position", pCore->getMonitorPosition());
0854 
0855     if (hasTimelinePreview()) {
0856         QPair<QStringList, QStringList> chunks = previewManager()->previewChunks();
0857         tractor()->set("kdenlive:sequenceproperties.previewchunks", chunks.first.join(QLatin1Char(',')).toUtf8().constData());
0858         tractor()->set("kdenlive:sequenceproperties.dirtypreviewchunks", chunks.second.join(QLatin1Char(',')).toUtf8().constData());
0859     }
0860 }
0861 
0862 void TimelineItemModel::processTimelineReplacement(QList<int> instances, const QString &originalId, const QString &replacementId, int maxDuration,
0863                                                    bool replaceAudio, bool replaceVideo)
0864 {
0865     // Check for locked tracks
0866     QList<int> lockedTracks;
0867     for (const auto &track : m_allTracks) {
0868         if (track->isLocked()) {
0869             lockedTracks << track->getId();
0870         }
0871     }
0872     // Check if some clips are longer than our replacement clip
0873     QList<int> notReplacedIds;
0874     for (auto &id : instances) {
0875         Q_ASSERT(m_allClips.count(id) > 0);
0876         if ((replaceAudio && m_allClips.at(id)->isAudioOnly()) || (replaceVideo && m_allClips.at(id)->clipState() == PlaylistState::VideoOnly)) {
0877             // Match, replace
0878             std::shared_ptr<ClipModel> clip = m_allClips.at(id);
0879             if (clip->getOut() > maxDuration || lockedTracks.contains(clip->getCurrentTrackId())) {
0880                 notReplacedIds << id;
0881             }
0882         }
0883     }
0884     Fun local_redo = [this, instances, replacementId, replaceAudio, replaceVideo, notReplacedIds]() {
0885         int replaced = 0;
0886         for (auto &id : instances) {
0887             if (notReplacedIds.contains(id)) {
0888                 continue;
0889             }
0890             Q_ASSERT(m_allClips.count(id) > 0);
0891             if ((replaceAudio && m_allClips.at(id)->isAudioOnly()) || (replaceVideo && m_allClips.at(id)->clipState() == PlaylistState::VideoOnly)) {
0892                 // Match, replace
0893                 std::shared_ptr<ClipModel> clip = m_allClips.at(id);
0894                 clip->switchBinReference(replacementId, m_uuid);
0895                 replaced++;
0896                 QModelIndex ix = makeClipIndexFromID(id);
0897                 Q_EMIT dataChanged(ix, ix, {NameRole});
0898             }
0899         }
0900         if (!notReplacedIds.isEmpty()) {
0901             pCore->displayMessage(i18n("Clips replaced: %1. <b>Clips not replaced: %2</b>", replaced, notReplacedIds.size()), MessageType::ErrorMessage);
0902         } else {
0903             pCore->displayMessage(i18np("One clip replaced", "%1 clips replaced", replaced), MessageType::InformationMessage);
0904         }
0905         return replaced > 0;
0906     };
0907     if (local_redo()) {
0908         Fun local_undo = [this, instances, originalId, replaceAudio, replaceVideo, notReplacedIds]() {
0909             int replaced = 0;
0910             for (auto &id : instances) {
0911                 if (notReplacedIds.contains(id)) {
0912                     continue;
0913                 }
0914                 Q_ASSERT(m_allClips.count(id) > 0);
0915                 if ((replaceAudio && m_allClips.at(id)->isAudioOnly()) || (replaceVideo && m_allClips.at(id)->clipState() == PlaylistState::VideoOnly)) {
0916                     // Match, replace
0917                     std::shared_ptr<ClipModel> clip = m_allClips.at(id);
0918                     clip->switchBinReference(originalId, m_uuid);
0919                     replaced++;
0920                     QModelIndex ix = makeClipIndexFromID(id);
0921                     Q_EMIT dataChanged(ix, ix, {NameRole});
0922                 }
0923             }
0924             if (!notReplacedIds.isEmpty()) {
0925                 pCore->displayMessage(i18n("Clips replaced: %1, Clips too long: %2", replaced, notReplacedIds.size()), MessageType::InformationMessage);
0926             } else {
0927                 pCore->displayMessage(i18np("One clip replaced", "%1 clips replaced", replaced), MessageType::InformationMessage);
0928             }
0929             return replaced > 0;
0930         };
0931         pCore->pushUndo(local_undo, local_redo, replaceAudio ? (replaceVideo ? i18n("Replace clip") : i18n("Replace audio")) : i18n("Replace video"));
0932     }
0933 }