diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index c2a6a6db7..cbd6c9739 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1427 +1,1442 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectclip.h" #include "bin.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "model/markerlistmodel.hpp" #include "profiles/profilemodel.hpp" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectfolder.h" #include "projectitemmodel.h" #include "projectsubclip.h" #include "timecode.h" #include "timeline2/model/snapmodel.hpp" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" #include #include #include #include "kdenlive_debug.h" +#include "logger.hpp" #include #include #include #include #include #include #include #include #include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wsign-conversion" +#pragma GCC diagnostic ignored "-Wfloat-equal" +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wpedantic" +#include +#pragma GCC diagnostic pop +RTTR_REGISTRATION +{ + using namespace rttr; + registration::class_("ProjectClip"); +} + ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id, producer) , m_thumbsProducer(nullptr) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); m_clipStatus = StatusReady; m_name = clipName(); m_duration = getStringDuration(); m_inPoint = 0; m_date = date; m_description = ClipController::description(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } // Make sure we have a hash for this clip hash(); connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); QString markers = getProducerProperty(QStringLiteral("kdenlive:markers")); if (!markers.isEmpty()) { QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true), Q_ARG(bool, false)); } connectEffectStack(); } // static std::shared_ptr ProjectClip::construct(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer) { std::shared_ptr self(new ProjectClip(id, thumb, model, producer)); baseFinishConstruct(self); self->m_effectStack->importEffects(producer, PlaylistState::Disabled, true); model->loadSubClips(id, self->getPropertiesFromPrefix(QStringLiteral("kdenlive:clipzone."))); return self; } ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) , m_thumbsProducer(nullptr) { m_clipStatus = StatusWaiting; m_thumbnail = thumb; m_markerModel = std::make_shared(m_binId, pCore->projectManager()->undoStack()); if (description.hasAttribute(QStringLiteral("type"))) { m_clipType = (ClipType::ProducerType)description.attribute(QStringLiteral("type")).toInt(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } } m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource")); QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname")); if (!clipName.isEmpty()) { m_name = clipName; } else if (!m_temporaryUrl.isEmpty()) { m_name = QFileInfo(m_temporaryUrl).fileName(); } else { m_name = i18n("Untitled"); } connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); } std::shared_ptr ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) { std::shared_ptr self(new ProjectClip(id, description, thumb, model)); baseFinishConstruct(self); return self; } ProjectClip::~ProjectClip() { // controller is deleted in bincontroller m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); audioFrameCache.clear(); } void ProjectClip::connectEffectStack() { connect(m_effectStack.get(), &EffectStackModel::modelChanged, this, &ProjectClip::updateChildProducers); connect(m_effectStack.get(), &EffectStackModel::dataChanged, this, &ProjectClip::updateChildProducers); connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&]() { if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } }); /*connect(m_effectStack.get(), &EffectStackModel::modelChanged, [&](){ qDebug()<<"/ / / STACK CHANGED"; updateChildProducers(); });*/ } QString ProjectClip::getToolTip() const { return url(); } QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue) { QString value = defaultValue; QDomNodeList props = producer.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) { value = props.at(i).firstChild().nodeValue(); break; } } return value; } void ProjectClip::updateAudioThumbnail(QVariantList audioLevels) { std::swap(audioFrameCache, audioLevels); // avoid second copy m_audioThumbCreated = true; if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshAudioThumbs(m_binId); } updateTimelineClips({TimelineModel::AudioLevelsRole}); } bool ProjectClip::audioThumbCreated() const { return (m_audioThumbCreated); } ClipType::ProducerType ProjectClip::clipType() const { return m_clipType; } bool ProjectClip::hasParent(const QString &id) const { std::shared_ptr par = parent(); while (par) { if (par->clipId() == id) { return true; } par = par->parent(); } return false; } std::shared_ptr ProjectClip::clip(const QString &id) { if (id == m_binId) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } std::shared_ptr ProjectClip::folder(const QString &id) { Q_UNUSED(id) return std::shared_ptr(); } std::shared_ptr ProjectClip::getSubClip(int in, int out) { for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i))->subClip(in, out); if (clip) { return clip; } } return std::shared_ptr(); } QStringList ProjectClip::subClipIds() const { QStringList subIds; for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { subIds << clip->clipId(); } } return subIds; } std::shared_ptr ProjectClip::clipAt(int ix) { if (ix == row()) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } /*bool ProjectClip::isValid() const { return m_controller->isValid(); }*/ bool ProjectClip::hasUrl() const { if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) { return (!clipUrl().isEmpty()); } return false; } const QString ProjectClip::url() const { return clipUrl(); } GenTime ProjectClip::duration() const { return getPlaytime(); } size_t ProjectClip::frameDuration() const { GenTime d = duration(); return (size_t)d.frames(pCore->getCurrentFps()); } void ProjectClip::reloadProducer(bool refreshOnly) { // we find if there are some loading job on that clip int loadjobId = -1; pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::LOADJOB, &loadjobId); QMutexLocker lock(&m_thumbMutex); if (refreshOnly) { // In that case, we only want a new thumbnail. // We thus set up a thumb job. We must make sure that there is no pending LOADJOB // Clear cache first ThumbnailCache::get()->invalidateThumbsForClip(clipId()); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), 150, -1, true, true); } else { // If another load job is running? if (loadjobId > -1) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::LOADJOB); } QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); ThumbnailCache::get()->invalidateThumbsForClip(clipId()); int loadJob = pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), xml); pCore->jobManager()->startJob({clipId()}, loadJob, QString(), 150, -1, true, true); } } } QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta) { getProducerXML(document, includeMeta); QDomElement prod = document.documentElement().firstChildElement(QStringLiteral("producer")); if (m_clipType != ClipType::Unknown) { prod.setAttribute(QStringLiteral("type"), (int)m_clipType); } return prod; } void ProjectClip::setThumbnail(const QImage &img) { QPixmap thumb = roundedPixmap(QPixmap::fromImage(img)); if (hasProxy() && !thumb.isNull()) { // Overlay proxy icon QPainter p(&thumb); QColor c(220, 220, 10, 200); QRect r(0, 0, thumb.height() / 2.5, thumb.height() / 2.5); p.fillRect(r, c); QFont font = p.font(); font.setPixelSize(r.height()); font.setBold(true); p.setFont(font); p.setPen(Qt::black); p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P")); } m_thumbnail = QIcon(thumb); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataThumbnail); } } bool ProjectClip::hasAudioAndVideo() const { return hasAudio() && hasVideo() && m_masterProducer->get_int("set.test_image") == 0 && m_masterProducer->get_int("set.test_audio") == 0; } bool ProjectClip::isCompatible(PlaylistState::ClipState state) const { switch (state) { case PlaylistState::AudioOnly: return hasAudio() && (m_masterProducer->get_int("set.test_audio") == 0); case PlaylistState::VideoOnly: return hasVideo() && (m_masterProducer->get_int("set.test_image") == 0); default: return true; } } QPixmap ProjectClip::thumbnail(int width, int height) { return m_thumbnail.pixmap(width, height); } bool ProjectClip::setProducer(std::shared_ptr producer, bool replaceProducer) { Q_UNUSED(replaceProducer) qDebug() << "################### ProjectClip::setproducer"; QMutexLocker locker(&m_producerMutex); updateProducer(std::move(producer)); m_thumbsProducer.reset(); connectEffectStack(); // Update info if (m_name.isEmpty()) { m_name = clipName(); } m_date = date; m_description = ClipController::description(); m_temporaryUrl.clear(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else if (m_clipType == ClipType::Image) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) < 8 || getProducerIntProperty(QStringLiteral("meta.media.height")) < 8) { KMessageBox::information(QApplication::activeWindow(), i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework.")); } } m_duration = getStringDuration(); m_clipStatus = StatusReady; if (!hasProxy()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->refreshPanel(m_binId); } if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); std::static_pointer_cast(ptr)->updateWatcher(std::static_pointer_cast(shared_from_this())); } // Make sure we have a hash for this clip getFileHash(); // set parent again (some info need to be stored in producer) updateParent(parentItem().lock()); if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() == 1) { QList> clipList; // automatic proxy generation enabled if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { clipList << std::static_pointer_cast(shared_from_this()); } } else if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1 && (m_clipType == ClipType::AV || m_clipType == ClipType::Video) && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { bool skipProducer = false; if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableexternalproxy")).toInt() == 1) { QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';')); // We have a camcorder profile, check if we have opened a proxy clip if (externalParams.count() >= 6) { QFileInfo info(m_path); QDir dir = info.absoluteDir(); dir.cd(externalParams.at(3)); QString fileName = info.fileName(); if (!externalParams.at(2).isEmpty()) { fileName.chop(externalParams.at(2).size()); } fileName.append(externalParams.at(5)); if (dir.exists(fileName)) { setProducerProperty(QStringLiteral("kdenlive:proxy"), m_path); m_path = dir.absoluteFilePath(fileName); setProducerProperty(QStringLiteral("kdenlive:originalurl"), m_path); getFileHash(); skipProducer = true; } } } if (!skipProducer && getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize()) { clipList << std::static_pointer_cast(shared_from_this()); } } if (!clipList.isEmpty()) { pCore->currentDoc()->slotProxyCurrentItem(true, clipList, false); } } pCore->bin()->reloadMonitorIfActive(clipId()); for (auto &p : m_audioProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_videoProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_timewarpProducers) { m_effectStack->removeService(p.second); } // Release audio producers m_audioProducers.clear(); m_videoProducers.clear(); m_timewarpProducers.clear(); emit refreshPropertiesPanel(); replaceInTimeline(); return true; } std::shared_ptr ProjectClip::thumbProducer() { if (m_thumbsProducer) { return m_thumbsProducer; } if (clipType() == ClipType::Unknown) { return nullptr; } QMutexLocker lock(&m_thumbMutex); std::shared_ptr prod = originalProducer(); if (!prod->is_valid()) { return nullptr; } if (KdenliveSettings::gpu_accel()) { // TODO: when the original producer changes, we must reload this thumb producer m_thumbsProducer = softClone(ClipController::getPassPropertiesList()); Mlt::Filter converter(*prod->profile(), "avcolor_space"); m_thumbsProducer->attach(converter); } else { QString mltService = m_masterProducer->get("mlt_service"); const QString mltResource = m_masterProducer->get("resource"); if (mltService == QLatin1String("avformat")) { mltService = QStringLiteral("avformat-novalidate"); } m_thumbsProducer.reset(new Mlt::Producer(*pCore->thumbProfile(), mltService.toUtf8().constData(), mltResource.toUtf8().constData())); if (m_thumbsProducer->is_valid()) { Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(m_thumbsProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList()); Mlt::Filter scaler(*pCore->thumbProfile(), "swscale"); Mlt::Filter padder(*pCore->thumbProfile(), "resize"); Mlt::Filter converter(*pCore->thumbProfile(), "avcolor_space"); m_thumbsProducer->set("audio_index", -1); m_thumbsProducer->attach(scaler); m_thumbsProducer->attach(padder); m_thumbsProducer->attach(converter); } } return m_thumbsProducer; } void ProjectClip::createDisabledMasterProducer() { if (!m_disabledProducer) { m_disabledProducer = cloneProducer(&pCore->getCurrentProfile()->profile()); m_disabledProducer->set("set.test_audio", 1); m_disabledProducer->set("set.test_image", 1); m_effectStack->addService(m_disabledProducer); } } std::shared_ptr ProjectClip::getTimelineProducer(int clipId, PlaylistState::ClipState state, double speed) { if (!m_masterProducer) { return nullptr; } if (qFuzzyCompare(speed, 1.0)) { // we are requesting a normal speed producer // We can first cleen the speed producers we have for the current id if (m_timewarpProducers.count(clipId) > 0) { m_effectStack->removeService(m_timewarpProducers[clipId]); m_timewarpProducers.erase(clipId); } if (state == PlaylistState::AudioOnly) { // We need to get an audio producer, if none exists if (m_audioProducers.count(clipId) == 0) { m_audioProducers[clipId] = cloneProducer(&pCore->getCurrentProfile()->profile(), true); m_audioProducers[clipId]->set("set.test_audio", 0); m_audioProducers[clipId]->set("set.test_image", 1); m_effectStack->addService(m_audioProducers[clipId]); } return std::shared_ptr(m_audioProducers[clipId]->cut()); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } if (state == PlaylistState::VideoOnly) { // we return the video producer // We need to get an audio producer, if none exists if (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text) { int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); - return std::shared_ptr(m_masterProducer->cut(-1, duration > 0 ? duration: -1)); + return std::shared_ptr(m_masterProducer->cut(-1, duration > 0 ? duration : -1)); } if (m_videoProducers.count(clipId) == 0) { m_videoProducers[clipId] = cloneProducer(&pCore->getCurrentProfile()->profile(), true); m_videoProducers[clipId]->set("set.test_audio", 1); m_videoProducers[clipId]->set("set.test_image", 0); m_effectStack->addService(m_videoProducers[clipId]); } int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_videoProducers[clipId]->cut(-1, duration > 0 ? duration : -1)); } if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } Q_ASSERT(state == PlaylistState::Disabled); createDisabledMasterProducer(); int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_disabledProducer->cut(-1, duration > 0 ? duration : -1)); } // in that case, we need to create a warp producer, if we don't have one if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } std::shared_ptr warpProducer; if (m_timewarpProducers.count(clipId) > 0) { // remove in all cases, we add it unconditionally anyways m_effectStack->removeService(m_timewarpProducers[clipId]); if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) { // the producer we have is good, use it ! warpProducer = m_timewarpProducers[clipId]; qDebug() << "Reusing producer!"; } else { m_timewarpProducers.erase(clipId); } } if (!warpProducer) { QLocale locale; QString resource(originalProducer()->get("resource")); if (resource.isEmpty() || resource == QLatin1String("")) { resource = m_service; } QString url = QString("timewarp:%1:%2").arg(locale.toString(speed)).arg(resource); warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), url.toUtf8().constData())); qDebug() << "new producer: " << url; qDebug() << "warp LENGTH before" << warpProducer->get_length(); int original_length = originalProducer()->get_length(); // this is a workaround to cope with Mlt erroneous rounding warpProducer->set("length", double(original_length) / speed); } qDebug() << "warp LENGTH" << warpProducer->get_length(); warpProducer->set("set.test_audio", 1); warpProducer->set("set.test_image", 1); if (state == PlaylistState::AudioOnly) { warpProducer->set("set.test_audio", 0); } if (state == PlaylistState::VideoOnly) { warpProducer->set("set.test_image", 0); } m_timewarpProducers[clipId] = warpProducer; m_effectStack->addService(m_timewarpProducers[clipId]); return std::shared_ptr(warpProducer->cut()); } std::pair, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state) { int in = master->get_in(); int out = master->get_out(); if (master->parent().is_valid()) { // in that case, we have a cut // check whether it's a timewarp double speed = 1.0; bool timeWarp = false; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->parent().get_double("warp_speed"); timeWarp = true; } if (master->parent().get_int("_loaded") == 1) { // we already have a clip that shares the same master if (state != PlaylistState::Disabled || timeWarp) { // In that case, we must create copies std::shared_ptr prod(getTimelineProducer(clipId, state, speed)->cut(in, out)); return {prod, false}; } if (state == PlaylistState::Disabled && !m_disabledProducer) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet"; createDisabledMasterProducer(); return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } if (state == PlaylistState::Disabled && QString::fromUtf8(m_disabledProducer->get("id")) != QString::fromUtf8(master->parent().get("id"))) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but doesn't match ours"; return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } // We have a good id, this clip can be used return {master, true}; } else { master->parent().set("_loaded", 1); if (timeWarp) { m_timewarpProducers[clipId] = std::shared_ptr(new Mlt::Producer(&master->parent())); m_effectStack->loadService(m_timewarpProducers[clipId]); return {master, true}; } if (state == PlaylistState::AudioOnly) { m_audioProducers[clipId] = std::shared_ptr(new Mlt::Producer(&master->parent())); m_effectStack->loadService(m_audioProducers[clipId]); return {master, true}; } if (state == PlaylistState::VideoOnly) { // good, we found a master video producer, and we didn't have any m_videoProducers[clipId] = std::shared_ptr(new Mlt::Producer(&master->parent())); m_effectStack->loadService(m_videoProducers[clipId]); return {master, true}; } if (state == PlaylistState::Disabled && !m_disabledProducer) { // good, we found a master disabled producer, and we didn't have any m_disabledProducer.reset(master->parent().cut()); m_effectStack->loadService(m_disabledProducer); return {master, true}; } qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master"; Q_ASSERT(false); } } else if (master->is_valid()) { // in that case, we have a master qDebug() << "Warning: weird, we received a master clip in lieue of a cut"; exit(1); double speed = 1.0; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->get_double("warp_speed"); } return {getTimelineProducer(clipId, state, speed), false}; } // we have a problem return {std::shared_ptr(ClipController::mediaUnavailable->cut()), false}; } /* std::shared_ptr ProjectClip::timelineProducer(PlaylistState::ClipState state, int track) { if (!m_service.startsWith(QLatin1String("avformat"))) { std::shared_ptr prod(originalProducer()->cut()); int length = getProducerIntProperty(QStringLiteral("kdenlive:duration")); if (length > 0) { prod->set_in_and_out(0, length); } return prod; } if (state == PlaylistState::VideoOnly) { if (m_timelineProducers.count(0) > 0) { return std::shared_ptr(m_timelineProducers.find(0)->second->cut()); } std::shared_ptr videoProd = cloneProducer(); videoProd->set("audio_index", -1); m_timelineProducers[0] = videoProd; return std::shared_ptr(videoProd->cut()); } if (state == PlaylistState::AudioOnly) { if (m_timelineProducers.count(-track) > 0) { return std::shared_ptr(m_timelineProducers.find(-track)->second->cut()); } std::shared_ptr audioProd = cloneProducer(); audioProd->set("video_index", -1); m_timelineProducers[-track] = audioProd; return std::shared_ptr(audioProd->cut()); } if (m_timelineProducers.count(track) > 0) { return std::shared_ptr(m_timelineProducers.find(track)->second->cut()); } std::shared_ptr normalProd = cloneProducer(); m_timelineProducers[track] = normalProd; return std::shared_ptr(normalProd->cut()); }*/ std::shared_ptr ProjectClip::cloneProducer(Mlt::Profile *destProfile, bool removeEffects) { Mlt::Consumer c(*m_masterProducer->profile(), "xml", "string"); Mlt::Service s(m_masterProducer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.start(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod(new Mlt::Producer(destProfile ? *destProfile : *m_masterProducer->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); } if (removeEffects) { int ct = 0; Mlt::Filter *filter = prod->filter(ct); while (filter) { qDebug() << "// EFFECT " << ct << " : " << filter->get("mlt_service"); QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { qDebug() << "/ + + DELTING"; if (prod->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = prod->filter(ct); } } prod->set("id", (char *)nullptr); return prod; } std::shared_ptr ProjectClip::cloneProducer(std::shared_ptr producer) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.start(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod(new Mlt::Producer(*producer->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); } return prod; } std::shared_ptr ProjectClip::softClone(const char *list) { QString service = QString::fromLatin1(m_masterProducer->get("mlt_service")); QString resource = QString::fromLatin1(m_masterProducer->get("resource")); std::shared_ptr clone(new Mlt::Producer(*m_masterProducer->profile(), service.toUtf8().constData(), resource.toUtf8().constData())); Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(clone->get_properties()); cloneProps.pass_list(original, list); return clone; } bool ProjectClip::isReady() const { return m_clipStatus == StatusReady; } /*void ProjectClip::setZone(const QPoint &zone) { m_zone = zone; }*/ QPoint ProjectClip::zone() const { int x = getProducerIntProperty(QStringLiteral("kdenlive:zone_in")); int y = getProducerIntProperty(QStringLiteral("kdenlive:zone_out")); if (y <= x) { y = getFramePlaytime(); } return QPoint(x, y); } const QString ProjectClip::hash() { QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash")); if (!clipHash.isEmpty()) { return clipHash; } return getFileHash(); } const QString ProjectClip::getFileHash() { QByteArray fileData; QByteArray fileHash; switch (m_clipType) { case ClipType::SlideShow: fileData = clipUrl().toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Text: case ClipType::TextTemplate: fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::QText: fileData = getProducerProperty(QStringLiteral("text")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Color: fileData = getProducerProperty(QStringLiteral("resource")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; default: QFile file(clipUrl()); if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 2000000) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(file.size())); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); } break; } if (fileHash.isEmpty()) { qDebug() << "// WARNING EMPTY CLIP HASH: "; return QString(); } QString result = fileHash.toHex(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result); return result; } double ProjectClip::getOriginalFps() const { return originalFps(); } bool ProjectClip::hasProxy() const { QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); return proxy.size() > 2; } void ProjectClip::setProperties(const QMap &properties, bool refreshPanel) { qDebug() << "// SETTING CLIP PROPERTIES: " << properties; QMapIterator i(properties); QMap passProperties; bool refreshAnalysis = false; bool reload = false; bool refreshOnly = true; if (properties.contains(QStringLiteral("templatetext"))) { m_description = properties.value(QStringLiteral("templatetext")); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::ClipStatus); refreshPanel = true; } // Some properties also need to be passed to track producers QStringList timelineProperties{QStringLiteral("force_aspect_ratio"), QStringLiteral("video_index"), QStringLiteral("audio_index"), QStringLiteral("set.force_full_luma"), QStringLiteral("full_luma"), QStringLiteral("threads"), QStringLiteral("force_colorspace"), QStringLiteral("force_tff"), QStringLiteral("force_progressive"), QStringLiteral("video_index"), QStringLiteral("audio_index")}; QStringList forceReloadProperties{QStringLiteral("autorotate"), QStringLiteral("templatetext"), QStringLiteral("resource"), QStringLiteral("force_fps"), QStringLiteral("set.test_image"), QStringLiteral("set.test_audio")}; QStringList keys{QStringLiteral("luma_duration"), QStringLiteral("luma_file"), QStringLiteral("fade"), QStringLiteral("ttl"), QStringLiteral("softness"), QStringLiteral("crop"), QStringLiteral("animation")}; QVector updateRoles; while (i.hasNext()) { i.next(); setProducerProperty(i.key(), i.value()); if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) { reload = true; refreshOnly = false; } if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) { refreshAnalysis = true; } if (timelineProperties.contains(i.key())) { passProperties.insert(i.key(), i.value()); } } if (properties.contains(QStringLiteral("kdenlive:proxy"))) { QString value = properties.value(QStringLiteral("kdenlive:proxy")); // If value is "-", that means user manually disabled proxy on this clip if (value.isEmpty() || value == QLatin1String("-")) { // reset proxy int id; if (pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::PROXYJOB, &id)) { // The proxy clip is being created, abort pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); } else { reload = true; refreshOnly = false; } } else { // A proxy was requested, make sure to keep original url setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); pCore->jobManager()->startJob({clipId()}, -1, QString()); } } else if (!reload) { const QList propKeys = properties.keys(); for (const QString &k : propKeys) { if (forceReloadProperties.contains(k)) { if (m_clipType != ClipType::Color) { reload = true; refreshOnly = false; } else { // Clip resource changed, update thumbnail reload = true; refreshPanel = true; updateRoles << TimelineModel::ResourceRole; } break; } } } if (!reload && (properties.contains(QStringLiteral("xmldata")) || !passProperties.isEmpty())) { reload = true; } if (refreshAnalysis) { emit refreshAnalysisPanel(); } if (properties.contains(QStringLiteral("length")) || properties.contains(QStringLiteral("kdenlive:duration"))) { m_duration = getStringDuration(); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); refreshOnly = false; reload = true; } if (properties.contains(QStringLiteral("kdenlive:clipname"))) { m_name = properties.value(QStringLiteral("kdenlive:clipname")); refreshPanel = true; if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataName); } // update timeline clips updateTimelineClips(QVector() << TimelineModel::NameRole); } if (refreshPanel) { // Some of the clip properties have changed through a command, update properties panel emit refreshPropertiesPanel(); } if (reload) { // producer has changed, refresh monitor and thumbnail if (hasProxy()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); setProducerProperty(QStringLiteral("_overwriteproxy"), 1); pCore->jobManager()->startJob({clipId()}, -1, QString()); } else { reloadProducer(refreshOnly); } if (refreshOnly) { if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshClip(m_binId); } } if (!updateRoles.isEmpty()) { updateTimelineClips(updateRoles); } } if (!passProperties.isEmpty()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->updateTimelineProducers(m_binId, passProperties); } } ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent) { auto ptr = m_model.lock(); Q_ASSERT(ptr); ClipPropertiesController *panel = new ClipPropertiesController(static_cast(this), parent); connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties); connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData); connect(panel, &ClipPropertiesController::requestProxy, [this](bool doProxy) { QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(doProxy, clipList); }); connect(panel, &ClipPropertiesController::deleteProxy, this, &ProjectClip::deleteProxy); return panel; } void ProjectClip::deleteProxy() { // Disable proxy file QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(false, clipList); // Delete bool ok; QDir dir = pCore->currentDoc()->getCacheDir(CacheProxy, &ok); if (ok && proxy.length() > 2) { proxy = QFileInfo(proxy).fileName(); if (dir.exists(proxy)) { dir.remove(proxy); } } } void ProjectClip::updateParent(std::shared_ptr parent) { if (parent) { auto item = std::static_pointer_cast(parent); ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId()); qDebug() << "Setting parent to " << item->clipId(); } AbstractProjectItem::updateParent(parent); } bool ProjectClip::matches(const QString &condition) { // TODO Q_UNUSED(condition) return true; } bool ProjectClip::rename(const QString &name, int column) { QMap newProperites; QMap oldProperites; bool edited = false; switch (column) { case 0: if (m_name == name) { return false; } // Rename clip oldProperites.insert(QStringLiteral("kdenlive:clipname"), m_name); newProperites.insert(QStringLiteral("kdenlive:clipname"), name); m_name = name; edited = true; break; case 2: if (m_description == name) { return false; } // Rename clip if (m_clipType == ClipType::TextTemplate) { oldProperites.insert(QStringLiteral("templatetext"), m_description); newProperites.insert(QStringLiteral("templatetext"), name); } else { oldProperites.insert(QStringLiteral("kdenlive:description"), m_description); newProperites.insert(QStringLiteral("kdenlive:description"), name); } m_description = name; edited = true; break; } if (edited) { pCore->bin()->slotEditClipCommand(m_binId, oldProperites, newProperites); } return edited; } QVariant ProjectClip::getData(DataType type) const { switch (type) { case AbstractProjectItem::IconOverlay: return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant(); default: return AbstractProjectItem::getData(type); } } void ProjectClip::slotExtractImage(const QList &frames) { QMutexLocker lock(&m_thumbMutex); for (int i = 0; i < frames.count(); i++) { if (!m_requestedThumbs.contains(frames.at(i))) { m_requestedThumbs << frames.at(i); } } qSort(m_requestedThumbs); if (!m_thumbThread.isRunning()) { m_thumbThread = QtConcurrent::run(this, &ProjectClip::doExtractImage); } } void ProjectClip::doExtractImage() { // TODO refac: we can probably move that into a ThumbJob std::shared_ptr prod = thumbProducer(); if (prod == nullptr || !prod->is_valid()) { return; } int frameWidth = 150 * prod->profile()->dar() + 0.5; bool ok = false; auto ptr = m_model.lock(); Q_ASSERT(ptr); QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheThumbs, &ok); int max = prod->get_length(); while (!m_requestedThumbs.isEmpty()) { m_thumbMutex.lock(); int pos = m_requestedThumbs.takeFirst(); m_thumbMutex.unlock(); if (ok && thumbFolder.exists(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png"))) { emit thumbReady(pos, QImage(thumbFolder.absoluteFilePath(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png")))); continue; } if (pos >= max) { pos = max - 1; } const QString path = url() + QLatin1Char('_') + QString::number(pos); QImage img; if (ThumbnailCache::get()->hasThumbnail(clipId(), pos, true)) { img = ThumbnailCache::get()->getThumbnail(clipId(), pos, true); } if (!img.isNull()) { emit thumbReady(pos, img); continue; } prod->seek(pos); Mlt::Frame *frame = prod->get_frame(); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (frame->is_valid()) { img = KThumb::getFrame(frame, frameWidth, 150, !qFuzzyCompare(prod->profile()->sar(), 1)); ThumbnailCache::get()->storeThumbnail(clipId(), pos, img, false); emit thumbReady(pos, img); } delete frame; } } int ProjectClip::audioChannels() const { if (!audioInfo()) { return 0; } return audioInfo()->channels(); } void ProjectClip::discardAudioThumb() { QString audioThumbPath = getAudioThumbPath(); if (!audioThumbPath.isEmpty()) { QFile::remove(audioThumbPath); } audioFrameCache.clear(); qCDebug(KDENLIVE_LOG) << "//////////////////// DISCARD AUIIO THUMBNS"; m_audioThumbCreated = false; pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB); } const QString ProjectClip::getAudioThumbPath() { if (audioInfo() == nullptr) { return QString(); } int audioStream = audioInfo()->ffmpeg_audio_index(); QString clipHash = hash(); if (clipHash.isEmpty()) { return QString(); } bool ok = false; QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheAudio, &ok); if (!ok) { return QString(); } QString audioPath = thumbFolder.absoluteFilePath(clipHash); if (audioStream > 0) { audioPath.append(QLatin1Char('_') + QString::number(audioInfo()->audio_index())); } int roundedFps = (int)pCore->getCurrentFps(); audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps)); return audioPath; } QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset) { if (data.isEmpty()) { // Remove data return QStringList() << QString("kdenlive:clipanalysis." + name) << QString(); // m_controller->resetProperty("kdenlive:clipanalysis." + name); } QString current = getProducerProperty("kdenlive:clipanalysis." + name); if (!current.isEmpty()) { if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(), KGuiItem(i18n("Merge")), KGuiItem(i18n("Add"))) == KMessageBox::Yes) { // Merge data auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (newGeometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; geometry.insert(item); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise(); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise()); } // Add data with another name int i = 1; QString previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); while (!previous.isEmpty()) { ++i; previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); } return QStringList() << QString("kdenlive:clipanalysis." + name + QString::number(i)) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name + QLatin1Char(' ') + QString::number(i), geometryWithOffset(data, offset)); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset)); } QMap ProjectClip::analysisData(bool withPrefix) { return getPropertiesFromPrefix(QStringLiteral("kdenlive:clipanalysis."), withPrefix); } const QString ProjectClip::geometryWithOffset(const QString &data, int offset) { if (offset == 0) { return data; } auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newgeometry(nullptr, duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (geometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; newgeometry.insert(item); } return newgeometry.serialise(); } bool ProjectClip::isSplittable() const { return (m_clipType == ClipType::AV || m_clipType == ClipType::Playlist); } void ProjectClip::setBinEffectsEnabled(bool enabled) { ClipController::setBinEffectsEnabled(enabled); } void ProjectClip::registerService(std::weak_ptr timeline, int clipId, std::shared_ptr service, bool forceRegister) { if (!service->is_cut() || forceRegister) { int hasAudio = service->get_int("set.test_audio") == 0; int hasVideo = service->get_int("set.test_image") == 0; if (hasVideo && m_videoProducers.count(clipId) == 0) { // This is an undo producer, register it! m_videoProducers[clipId] = service; m_effectStack->addService(m_videoProducers[clipId]); } else if (hasAudio && m_audioProducers.count(clipId) == 0) { // This is an undo producer, register it! m_audioProducers[clipId] = service; m_effectStack->addService(m_audioProducers[clipId]); } } registerTimelineClip(timeline, clipId); } void ProjectClip::registerTimelineClip(std::weak_ptr timeline, int clipId) { Q_ASSERT(m_registeredClips.count(clipId) == 0); Q_ASSERT(!timeline.expired()); m_registeredClips[clipId] = std::move(timeline); setRefCount((uint)m_registeredClips.size()); } void ProjectClip::deregisterTimelineClip(int clipId) { qDebug() << " ** * DEREGISTERING TIMELINE CLIP: " << clipId; Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.erase(clipId); if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } setRefCount((uint)m_registeredClips.size()); } QList ProjectClip::timelineInstances() const { QList ids; for (std::map>::const_iterator it = m_registeredClips.begin(); it != m_registeredClips.end(); ++it) { ids.push_back(it->first); } return ids; } bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo) { auto toDelete = m_registeredClips; // we cannot use m_registeredClips directly, because it will be modified during loop for (const auto &clip : toDelete) { if (m_registeredClips.count(clip.first) == 0) { // clip already deleted, was probably grouped with another one continue; } if (auto timeline = clip.second.lock()) { timeline->requestItemDeletion(clip.first, undo, redo); } else { qDebug() << "Error while deleting clip: timeline unavailable"; Q_ASSERT(false); return false; } } return AbstractProjectItem::selfSoftDelete(undo, redo); } bool ProjectClip::isIncludedInTimeline() { return m_registeredClips.size() > 0; } void ProjectClip::updateChildProducers() { // TODO refac: the effect should be managed by an effectstack on the master /* // pass effect stack on all child producers QMutexLocker locker(&m_producerMutex); for (const auto &clip : m_timelineProducers) { if (auto producer = clip.second) { Clip clp(producer->parent()); clp.deleteEffects(); clp.replaceEffects(*m_masterProducer); } } */ } void ProjectClip::replaceInTimeline() { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipReload(clip.first); } else { qDebug() << "Error while reloading clip: timeline unavailable"; Q_ASSERT(false); } } } void ProjectClip::updateTimelineClips(QVector roles) { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipUpdate(clip.first, roles); } else { qDebug() << "Error while reloading clip thumb: timeline unavailable"; Q_ASSERT(false); return; } } } diff --git a/src/definitions.cpp b/src/definitions.cpp index bfe9abb98..c248e8076 100644 --- a/src/definitions.cpp +++ b/src/definitions.cpp @@ -1,178 +1,199 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "definitions.h" #include #include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wsign-conversion" +#pragma GCC diagnostic ignored "-Wfloat-equal" +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wpedantic" +#include +#pragma GCC diagnostic pop +RTTR_REGISTRATION +{ + using namespace rttr; + // clang-format off + registration::enumeration("GroupType")( + value("Normal", GroupType::Normal), + value("Selection", GroupType::Selection), + value("AVSplit", GroupType::AVSplit), + value("Leaf", GroupType::Leaf) + ); + // clang-format on +} + QDebug operator<<(QDebug qd, const ItemInfo &info) { qd << "ItemInfo " << &info; qd << "\tTrack" << info.track; qd << "\tStart pos: " << info.startPos.toString(); qd << "\tEnd pos: " << info.endPos.toString(); qd << "\tCrop start: " << info.cropStart.toString(); qd << "\tCrop duration: " << info.cropDuration.toString(); return qd.maybeSpace(); } CommentedTime::CommentedTime() : m_time(GenTime(0)) , m_type(0) { } CommentedTime::CommentedTime(const GenTime &time, const QString &comment, int markerType) : m_time(time) , m_comment(comment) , m_type(markerType) { } CommentedTime::CommentedTime(const QString &hash, const GenTime &time) : m_time(time) , m_comment(hash.section(QLatin1Char(':'), 1)) , m_type(hash.section(QLatin1Char(':'), 0, 0).toInt()) { } QString CommentedTime::comment() const { return (m_comment.isEmpty() ? i18n("Marker") : m_comment); } GenTime CommentedTime::time() const { return m_time; } void CommentedTime::setComment(const QString &comm) { m_comment = comm; } void CommentedTime::setMarkerType(int newtype) { m_type = newtype; } QString CommentedTime::hash() const { return QString::number(m_type) + QLatin1Char(':') + (m_comment.isEmpty() ? i18n("Marker") : m_comment); } int CommentedTime::markerType() const { return m_type; } QColor CommentedTime::markerColor(int type) { switch (type) { case 0: return Qt::red; break; case 1: return Qt::blue; break; case 2: return Qt::green; break; case 3: return Qt::yellow; break; default: return Qt::cyan; break; } } bool CommentedTime::operator>(const CommentedTime &op) const { return m_time > op.time(); } bool CommentedTime::operator<(const CommentedTime &op) const { return m_time < op.time(); } bool CommentedTime::operator>=(const CommentedTime &op) const { return m_time >= op.time(); } bool CommentedTime::operator<=(const CommentedTime &op) const { return m_time <= op.time(); } bool CommentedTime::operator==(const CommentedTime &op) const { return m_time == op.time(); } bool CommentedTime::operator!=(const CommentedTime &op) const { return m_time != op.time(); } const QString groupTypeToStr(GroupType t) { switch (t) { case GroupType::Normal: return QStringLiteral("Normal"); case GroupType::Selection: return QStringLiteral("Selection"); case GroupType::AVSplit: return QStringLiteral("AVSplit"); case GroupType::Leaf: return QStringLiteral("Leaf"); } Q_ASSERT(false); return QString(); } GroupType groupTypeFromStr(const QString &s) { std::vector types{GroupType::Selection, GroupType::Normal, GroupType::AVSplit, GroupType::Leaf}; for (const auto &t : types) { if (s == groupTypeToStr(t)) { return t; } } Q_ASSERT(false); return GroupType::Normal; } std::pair stateToBool(PlaylistState::ClipState state) { return {state == PlaylistState::VideoOnly, state == PlaylistState::AudioOnly}; } PlaylistState::ClipState stateFromBool(std::pair av) { assert(!av.first || !av.second); if (av.first) { return PlaylistState::VideoOnly; } else if (av.second) { return PlaylistState::AudioOnly; } else { return PlaylistState::Disabled; } } diff --git a/src/logger.cpp b/src/logger.cpp index 0c7b32b28..075e05ac8 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -1,199 +1,242 @@ /*************************************************************************** * Copyright (C) 2019 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "logger.hpp" +#include "bin/projectitemmodel.h" #include "timeline2/model/timelinemodel.hpp" #include #include #include #include #include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wsign-conversion" +#pragma GCC diagnostic ignored "-Wfloat-equal" +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wpedantic" +#include +#pragma GCC diagnostic pop thread_local bool Logger::is_executing = false; std::mutex Logger::mut; std::vector Logger::operations; std::vector Logger::invoks; std::unordered_map> Logger::constr; thread_local size_t Logger::result_awaiting = INT_MAX; bool Logger::start_logging() { std::unique_lock lk(mut); if (is_executing) { return false; } is_executing = true; return true; } void Logger::stop_logging() { std::unique_lock lk(mut); is_executing = false; } std::string Logger::get_ptr_name(rttr::variant ptr) { if (ptr.can_convert()) { return "timeline_" + std::to_string(get_id_from_ptr(ptr.convert())); + } else if (ptr.can_convert()) { + return "binModel"; } else { std::cout << "Error: unhandled ptr type " << ptr.get_type().get_name().to_string() << std::endl; } return "unknown"; } void Logger::log_res(rttr::variant result) { std::unique_lock lk(mut); Q_ASSERT(result_awaiting < invoks.size()); invoks[result_awaiting].res = std::move(result); } +void Logger::log_create_producer(const std::string &type, std::vector args) +{ + std::unique_lock lk(mut); + for (auto &a : args) { + // this will rewove shared/weak/unique ptrs + if (a.get_type().is_wrapper()) { + a = a.extract_wrapped_value(); + } + const std::string class_name = a.get_type().get_name().to_string(); + } + constr[type].push_back({type, std::move(args)}); + operations.push_back(ConstrId{type, constr[type].size() - 1}); +} + namespace { bool isIthParamARef(const rttr::method &method, size_t i) { QString sig = QString::fromStdString(method.get_signature().to_string()); int deb = sig.indexOf("("); int end = sig.lastIndexOf(")"); sig = sig.mid(deb + 1, deb - end - 1); QStringList args = sig.split(QStringLiteral(",")); return args[(int)i].contains("&") && !args[(int)i].contains("const &"); } } // namespace void Logger::print_trace() { auto process_args = [&](const std::vector &args, const std::unordered_set &refs = {}) { std::stringstream ss; bool deb = true; size_t i = 0; for (const auto &a : args) { if (deb) { deb = false; } else { ss << ", "; } if (refs.count(i) > 0) { ss << "dummy_" << i; } else if (a.get_type() == rttr::type::get()) { ss << a.convert(); - } else if (a.can_convert()) { + } else if (a.get_type() == rttr::type::get()) { ss << (a.convert() ? "true" : "false"); + } else if (a.get_type().is_enumeration()) { + auto e = a.get_type().get_enumeration(); + ss << e.get_name().to_string() << "::" << a.convert(); } else if (a.can_convert()) { - // Not supported in c++ < 14 - // ss << std::quoted(a.convert().toStdString()); - ss << "\" "<().toStdString()<<" \""; + ss << std::quoted(a.convert().toStdString()); + } else if (a.can_convert()) { + ss << std::quoted(a.convert()); + } else if (a.can_convert>()) { + auto set = a.convert>(); + ss << "{"; + bool beg = true; + for (int s : set) { + if (beg) + beg = false; + else + ss << ", "; + ss << s; + } + ss << "}"; } else if (a.get_type().is_pointer()) { ss << get_ptr_name(a); } else { std::cout << "Error: unhandled arg type " << a.get_type().get_name().to_string() << std::endl; } ++i; } return ss.str(); }; std::ofstream test_file; test_file.open("test_case.cpp"); - // Not supported on GCC < 5.1 - // auto test_file = std::ofstream("test_case.cpp"); test_file << "TEST_CASE(\"Regression\") {" << std::endl; test_file << "auto binModel = pCore->projectItemModel();" << std::endl; test_file << "std::shared_ptr undoStack = std::make_shared(nullptr);" << std::endl; test_file << "std::shared_ptr guideModel = std::make_shared(undoStack);" << std::endl; test_file << "Mock pmMock;" << std::endl; test_file << "When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);" << std::endl; test_file << "ProjectManager &mocked = pmMock.get();" << std::endl; test_file << "pCore->m_projectManager = &mocked;" << std::endl; auto check_consistancy = [&]() { if (constr.count("TimelineModel") > 0) { for (size_t i = 0; i < constr["TimelineModel"].size(); ++i) { test_file << "REQUIRE(timeline_" << i << "->checkConsistency());" << std::endl; } } }; for (const auto &o : operations) { if (o.can_convert()) { InvokId id = o.convert(); Invok &invok = invoks[id.id]; std::unordered_set refs; rttr::method m = invok.ptr.get_type().get_method(invok.method); test_file << "{" << std::endl; for (const auto &a : m.get_parameter_infos()) { if (isIthParamARef(m, a.get_index())) { refs.insert(a.get_index()); test_file << a.get_type().get_name().to_string() << " dummy_" << std::to_string(a.get_index()) << ";" << std::endl; } } if (m.get_return_type() != rttr::type::get()) { test_file << m.get_return_type().get_name().to_string() << " res = "; } test_file << get_ptr_name(invok.ptr) << "->" << invok.method << "(" << process_args(invok.args, refs) << ");" << std::endl; if (m.get_return_type() != rttr::type::get() && invok.res.is_valid()) { test_file << "REQUIRE( res == " << invok.res.to_string() << ");" << std::endl; } test_file << "}" << std::endl; } else if (o.can_convert()) { ConstrId id = o.convert(); if (id.type == "TimelineModel") { test_file << "TimelineItemModel tim_" << id.id << "(new Mlt::Profile(), undoStack);" << std::endl; test_file << "Mock timMock_" << id.id << "(tim_" << id.id << ");" << std::endl; test_file << "auto timeline_" << id.id << " = std::shared_ptr(&timMock_" << id.id << ".get(), [](...) {});" << std::endl; test_file << "TimelineItemModel::finishConstruct(timeline_" << id.id << ", guideModel);" << std::endl; test_file << "Fake(Method(timMock_" << id.id << ", adjustAssetRange));" << std::endl; } else if (id.type == "TrackModel") { std::string params = process_args(constr[id.type][id.id].second); test_file << "TrackModel::construct(" << params << ");" << std::endl; - + } else if (id.type == "test_producer") { + std::string params = process_args(constr[id.type][id.id].second); + test_file << "createProducer(reg_profile, " << params << ");" << std::endl; + } else if (id.type == "test_producer_sound") { + std::string params = process_args(constr[id.type][id.id].second); + test_file << "createProducerWithSound(reg_profile, " << params << ");" << std::endl; } else { std::cout << "Error: unknown constructor " << id.type << std::endl; } } else { std::cout << "Error: unknown operation" << std::endl; } check_consistancy(); test_file << "undoStack->undo();" << std::endl; check_consistancy(); test_file << "undoStack->redo();" << std::endl; check_consistancy(); } test_file << "}" << std::endl; } void Logger::clear() { is_executing = false; invoks.clear(); operations.clear(); } LogGuard::LogGuard() { m_hasGuard = Logger::start_logging(); } LogGuard::~LogGuard() { if (m_hasGuard) { Logger::stop_logging(); } } bool LogGuard::hasGuard() const { return m_hasGuard; } diff --git a/src/logger.hpp b/src/logger.hpp index eaec2a52e..516143d8c 100644 --- a/src/logger.hpp +++ b/src/logger.hpp @@ -1,156 +1,171 @@ /*************************************************************************** * Copyright (C) 2019 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that stdd::it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #pragma once #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop /** @brief This class is meant to provide an easy way to reproduce bugs involving the model. * The idea is to log any modifier function involving a model class, and trace the parameters that were passed, to be able to generate a test-case producing the * same behaviour. Note that many modifier functions of the models are nested. We are only interested in the top-most call, and we must ignore bottom calls. */ class Logger { public: /** @brief Notify the logger that the current thread wants to start logging. * This function returns true if this is a top-level call, meaning that we indeed want to log it. If the function returns false, the caller must not log. */ static bool start_logging(); + + /** @brief This logs the construction of an object of type T, whose new instance is passed. The instance will be kept around in case future calls refer to + * it. The arguments should more or less match the constructor arguments. In general, it's better to call the corresponding macro TRACE_CONSTR */ template static void log_constr(T *inst, std::vector args); + + /** @brief Logs the call to a member function on a given instance of class T. The string contains the method name, and then the vector contains all the + * parameters. In general, the method should be registered in RTTR. It's better to call the corresponding macro TRACE() if appropriate */ template static void log(T *inst, std::string str, std::vector args); + static void log_create_producer(const std::string &type, std::vector args); + /** @brief When the last function logged has a return value, you can log it through this function, by passing the corresponding value. In general, it's + * better to call the macro TRACE_RES */ static void log_res(rttr::variant result); /// @brief Notify that we are done with our function. Must not be called if start_logging returned false. static void stop_logging(); static void print_trace(); static void clear(); protected: - template static size_t get_id_from_ptr(T *ptr); + /** @brief Look amongst the known instances to get the name of a given pointer */ static std::string get_ptr_name(rttr::variant ptr); + template static size_t get_id_from_ptr(T *ptr); struct InvokId { size_t id; }; struct ConstrId { std::string type; size_t id; }; // a construction log contains the pointer as first parameter, and the vector of parameters using Constr = std::pair>; struct Invok { rttr::variant ptr; std::string method; std::vector args; rttr::variant res; }; thread_local static bool is_executing; thread_local static size_t result_awaiting; static std::mutex mut; static std::vector operations; static std::unordered_map> constr; static std::vector invoks; }; /** @brief This class provides a RAII mechanism to log the execution of a function */ class LogGuard { public: LogGuard(); ~LogGuard(); // @brief Returns true if we are the top-level caller. bool hasGuard() const; protected: bool m_hasGuard = false; }; +/// See Logger::log_constr. Note that the macro fills in the ptr instance for you. #define TRACE_CONSTR(ptr, ...) \ LogGuard __guard; \ if (__guard.hasGuard()) { \ Logger::log_constr((ptr), {__VA_ARGS__}); \ } + +/// See Logger::log. Note that the macro fills the ptr instance and the method name for you. #define TRACE(...) \ LogGuard __guard; \ if (__guard.hasGuard()) { \ Logger::log(this, __FUNCTION__, {__VA_ARGS__}); \ } +/// See Logger::log_res #define TRACE_RES(res) \ if (__guard.hasGuard()) { \ Logger::log_res(res); \ } + /******* Implementations ***********/ template void Logger::log_constr(T *inst, std::vector args) { std::unique_lock lk(mut); for (auto &a : args) { // this will rewove shared/weak/unique ptrs if (a.get_type().is_wrapper()) { a = a.extract_wrapped_value(); } } std::string class_name = rttr::type::get().get_name().to_string(); constr[class_name].push_back({inst, std::move(args)}); operations.push_back(ConstrId{class_name, constr[class_name].size() - 1}); } template void Logger::log(T *inst, std::string fctName, std::vector args) { std::unique_lock lk(mut); for (auto &a : args) { // this will rewove shared/weak/unique ptrs if (a.get_type().is_wrapper()) { a = a.extract_wrapped_value(); } } std::string class_name = rttr::type::get().get_name().to_string(); invoks.push_back({inst, std::move(fctName), std::move(args), rttr::variant()}); operations.push_back(InvokId{invoks.size() - 1}); result_awaiting = invoks.size() - 1; } template size_t Logger::get_id_from_ptr(T *ptr) { const std::string class_name = rttr::type::get().get_name().to_string(); for (size_t i = 0; i < constr.at(class_name).size(); ++i) { if (constr.at(class_name)[i].first.convert() == ptr) { return i; } } std::cerr << "Error: ptr of type " << class_name << " not found" << std::endl; return INT_MAX; } diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp index cbf589abf..1795e4549 100644 --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -1,2811 +1,2847 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "effects/effectsrepository.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "logger.hpp" #include "snapmodel.hpp" #include "timelinefunctions.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include #include #ifdef LOGGING #include #include #endif #include "macros.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("TimelineModel") .method("requestClipMove", select_overload(&TimelineModel::requestClipMove)) - .method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion)); + .method("requestClipInsertion", select_overload(&TimelineModel::requestClipInsertion)) + .method("requestItemDeletion", select_overload(&TimelineModel::requestItemDeletion)) + .method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove)) + .method("requestGroupDeletion", select_overload(&TimelineModel::requestGroupDeletion)) + .method("requestItemResize", select_overload(&TimelineModel::requestItemResize)) + .method("requestCompositionMove", select_overload(&TimelineModel::requestCompositionMove)) + .method("requestClipsGroup", select_overload &, bool, GroupType)>(&TimelineModel::requestClipsGroup)) + .method("requestClipUngroup", select_overload(&TimelineModel::requestClipUngroup)) + .method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion)) + .method("requestTrackDeletion", select_overload(&TimelineModel::requestTrackDeletion)); } int TimelineModel::next_id = 0; int TimelineModel::seekDuration = 30000; TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : QAbstractItemModel_shared_from_this() , m_tractor(new Mlt::Tractor(*profile)) , m_snaps(new SnapModel()) , m_undoStack(undo_stack) , m_profile(profile) , m_blackClip(new Mlt::Producer(*profile, "color:black")) , m_lock(QReadWriteLock::Recursive) , m_timelineEffectsEnabled(true) , m_id(getNextId()) , m_temporarySelectionGroup(-1) , m_overlayTrackCount(-1) , m_audioTarget(-1) , m_videoTarget(-1) , m_editMode(TimelineMode::NormalEdit) { // Create black background track m_blackClip->set("id", "black_track"); m_blackClip->set("mlt_type", "producer"); m_blackClip->set("aspect_ratio", 1); m_blackClip->set("length", INT_MAX); m_blackClip->set("set.test_audio", 0); m_blackClip->set("length", INT_MAX); m_blackClip->set_in_and_out(0, TimelineModel::seekDuration); m_tractor->insert_track(*m_blackClip, 0); TRACE_CONSTR(this); #ifdef LOGGING m_logFile = std::ofstream("log.txt"); m_logFile << "TEST_CASE(\"Regression\") {" << std::endl; m_logFile << "Mlt::Profile profile;" << std::endl; m_logFile << "std::shared_ptr undoStack = std::make_shared(nullptr);" << std::endl; m_logFile << "std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), undoStack);" << std::endl; m_logFile << "TimelineModel::next_id = 0;" << std::endl; m_logFile << "int dummy_id;" << std::endl; #endif } TimelineModel::~TimelineModel() { std::vector all_ids; for (auto tracks : m_iteratorTable) { all_ids.push_back(tracks.first); } for (auto tracks : all_ids) { deregisterTrack_lambda(tracks, false)(); } for (const auto &clip : m_allClips) { clip.second->deregisterClipToBin(); } } int TimelineModel::getTracksCount() const { READ_LOCK(); int count = m_tractor->count(); if (m_overlayTrackCount > -1) { count -= m_overlayTrackCount; } Q_ASSERT(count >= 0); // don't count the black background track Q_ASSERT(count - 1 == static_cast(m_allTracks.size())); return count - 1; } int TimelineModel::getTrackIndexFromPosition(int pos) const { Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size()); READ_LOCK(); auto it = m_allTracks.begin(); while (pos > 0) { it++; pos--; } return (*it)->getId(); } int TimelineModel::getClipsCount() const { READ_LOCK(); int size = int(m_allClips.size()); return size; } int TimelineModel::getCompositionsCount() const { READ_LOCK(); int size = int(m_allCompositions.size()); return size; } int TimelineModel::getClipTrackId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getCurrentTrackId(); } int TimelineModel::getCompositionTrackId(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getCurrentTrackId(); } int TimelineModel::getItemTrackId(int itemId) const { READ_LOCK(); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (isComposition(itemId)) { return getCompositionTrackId(itemId); } return getClipTrackId(itemId); } int TimelineModel::getClipPosition(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getPosition(); return pos; } double TimelineModel::getClipSpeed(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->getSpeed(); } int TimelineModel::getClipSplitPartner(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_groups->getSplitPartner(clipId); } int TimelineModel::getClipIn(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getIn(); } PlaylistState::ClipState TimelineModel::getClipState(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->clipState(); } const QString TimelineModel::getClipBinId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); QString id = clip->binId(); return id; } int TimelineModel::getClipPlaytime(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); int playtime = clip->getPlaytime(); return playtime; } QSize TimelineModel::getClipFrameSize(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); return clip->getFrameSize(); } int TimelineModel::getTrackClipsCount(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); int count = getTrackById_const(trackId)->getClipsCount(); return count; } int TimelineModel::getClipByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getClipByPosition(position); } int TimelineModel::getCompositionByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionByPosition(position); } int TimelineModel::getTrackPosition(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_allTracks.begin(); int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId)); return pos; } int TimelineModel::getTrackMltIndex(int trackId) const { READ_LOCK(); // Because of the black track that we insert in first position, the mlt index is the position + 1 return getTrackPosition(trackId) + 1; } int TimelineModel::getTrackSortValue(int trackId, bool separated) const { if (separated) { return getTrackPosition(trackId) + 1; } auto it = m_allTracks.end(); int aCount = 0; int vCount = 0; bool isAudio = false; int trackPos = 0; while (it != m_allTracks.begin()) { --it; bool audioTrack = (*it)->isAudioTrack(); if (audioTrack) { aCount++; } else { vCount++; } if (trackId == (*it)->getId()) { isAudio = audioTrack; trackPos = audioTrack ? aCount : vCount; } } int trackDiff = aCount - vCount; if (trackDiff > 0) { // more audio tracks if (!isAudio) { trackPos -= trackDiff; } else if (trackPos > vCount) { return -trackPos; } } return isAudio ? ((aCount * trackPos) - 1) : (vCount + 1 - trackPos) * 2; } QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); QList results; auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (type == TrackType::AnyTrack) { results << (*it)->getId(); continue; } bool audioTrack = (*it)->isAudioTrack(); if (type == TrackType::AudioTrack && audioTrack) { results << (*it)->getId(); } else if (type == TrackType::VideoTrack && !audioTrack) { results << (*it)->getId(); } } return results; } int TimelineModel::getPreviousVideoTrackIndex(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) { break; } } return it == m_allTracks.begin() ? 0 : (*it)->getId(); } int TimelineModel::getPreviousVideoTrackPos(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) { break; } } return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId()); } int TimelineModel::getMirrorVideoTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if (!(*it)->isAudioTrack()) { // we expected an audio track... return -1; } int count = 0; if (it != m_allTracks.end()) { ++it; } while (it != m_allTracks.end()) { if ((*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } ++it; } if (!(*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } int TimelineModel::getMirrorTrackId(int trackId) const { if (isAudioTrack(trackId)) { return getMirrorVideoTrackId(trackId); } return getMirrorAudioTrackId(trackId); } int TimelineModel::getMirrorAudioTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if ((*it)->isAudioTrack()) { // we expected a video track... return -1; } int count = 0; if (it != m_allTracks.begin()) { --it; } while (it != m_allTracks.begin()) { if (!(*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } --it; } if ((*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } void TimelineModel::setEditMode(TimelineMode::EditMode mode) { m_editMode = mode; } bool TimelineModel::normalEdit() const { return m_editMode == TimelineMode::NormalEdit; } bool TimelineModel::fakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { Q_UNUSED(updateView); Q_UNUSED(invalidateTimeline); Q_UNUSED(undo); Q_UNUSED(redo); Q_ASSERT(isClip(clipId)); m_allClips[clipId]->setFakePosition(position); bool trackChanged = false; if (trackId > -1) { if (trackId != m_allClips[clipId]->getFakeTrackId()) { if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) { m_allClips[clipId]->setFakeTrackId(trackId); trackChanged = true; } } } QModelIndex modelIndex = makeClipIndexFromID(clipId); if (modelIndex.isValid()) { QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } notifyChange(modelIndex, modelIndex, roles); return true; } return false; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView; if (trackId == -1) { return false; } Q_ASSERT(isClip(clipId)); if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) { if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) { return false; } if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) { return false; } } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) { // Move not allowed (audio / video mismatch) qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState(); return false; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool ok = true; int old_trackId = getClipTrackId(clipId); bool notifyViewOnly = false; bool localUpdateView = updateView; // qDebug()<<"MOVING CLIP FROM: "<requestClipMove(" << clipId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", " << (logUndo ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = fakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } return res; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { #ifdef LOGGING m_logFile << "timeline->requestClipMove(" << clipId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", " << (logUndo ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); + TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { + TRACE_RES(true); return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } + TRACE_RES(res); return res; } bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position) { #ifdef LOGGING m_logFile << "timeline->requestClipMove(" << clipId << "," << trackId << " ," << position << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = true; if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false); } else { res = requestClipMove(clipId, trackId, position, false, false, undo, redo); } if (res) { undo(); } return res; } int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance) { if (isClip(itemId)) { return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance); } return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance); } int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool allowViewUpdate) { #ifdef LOGGING m_logFile << "timeline->suggestClipMove(" << clipId << "," << trackId << " ," << position << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isClip(clipId)); Q_ASSERT(isTrack(trackId)); int currentPos = getClipPosition(clipId); int sourceTrackId = getClipTrackId(clipId); if (currentPos == position && sourceTrackId == trackId) { return position; } bool after = position > currentPos; if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; std::unordered_set all_items = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_items = m_groups->getLeaves(groupId); } for (int current_clipId : all_items) { if (getItemTrackId(current_clipId) != -1) { int in = getItemPosition(current_clipId); int out = in + getItemPlaytime(current_clipId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } int snapped = requestBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector(), cursorPosition, snapDistance); // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false) : requestFakeClipMove(clipId, trackId, position, true, false, false); /*} else { possible = requestClipMoveAttempt(clipId, trackId, position); }*/ if (possible) { return position; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Easy // int currentTrackId = getClipTrackId(clipId); // Try same track move qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId; trackId = sourceTrackId; possible = requestClipMove(clipId, trackId, position, true, false, false); if (!possible) { qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position; } else { return position; } int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { position = currentPos + blank_length; } else { position = currentPos - blank_length; } } else { return currentPos; } possible = requestClipMove(clipId, trackId, position, true, false, false); return possible ? position : currentPos; } // find best pos for groups int groupId = m_groups->getRootId(clipId); std::unordered_set all_items = m_groups->getLeaves(groupId); QMap trackPosition; // First pass, sort clips by track and keep only the first / last depending on move direction for (int current_clipId : all_items) { int clipTrack = getItemTrackId(current_clipId); if (clipTrack == -1) { continue; } int in = getItemPosition(current_clipId); if (trackPosition.contains(clipTrack)) { if (after) { // keep only last clip position for track int out = in + getItemPlaytime(current_clipId); if (trackPosition.value(clipTrack) < out) { trackPosition.insert(clipTrack, out); } } else { // keep only first clip position for track if (trackPosition.value(clipTrack) > in) { trackPosition.insert(clipTrack, in); } } } else { trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) : in); } } // Now check space on each track QMapIterator i(trackPosition); int blank_length = -1; while (i.hasNext()) { i.next(); int track_space; if (!after) { // Check space before the position track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1); if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } else { // Check after before the position track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value(); if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } } if (blank_length != 0) { int updatedPos = currentPos + (after ? blank_length : -blank_length); if (allowViewUpdate) { possible = requestClipMove(clipId, trackId, updatedPos, false, false, false); } else { possible = requestClipMoveAttempt(clipId, trackId, updatedPos); } if (possible) { return updatedPos; } } return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance) { #ifdef LOGGING m_logFile << "timeline->suggestCompositionMove(" << compoId << "," << trackId << " ," << position << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); int currentPos = getCompositionPosition(compoId); int currentTrack = getCompositionTrackId(compoId); if (currentPos == position && currentTrack == trackId) { return position; } if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; if (m_groups->isInGroup(compoId)) { int groupId = m_groups->getRootId(compoId); auto all_items = m_groups->getLeaves(groupId); for (int current_compoId : all_items) { // TODO: fix for composition int in = getItemPosition(current_compoId); int out = in + getItemPlaytime(current_compoId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } else { int in = currentPos; int out = in + getCompositionPlaytime(compoId); qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out; ignored_pts.push_back(in); ignored_pts.push_back(out); } int snapped = requestBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance); qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = requestCompositionMove(compoId, trackId, position, true, false); qDebug() << "Original move success" << possible; if (possible) { return position; } /*bool after = position > currentPos; int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { return currentPos + blank_length; } return currentPos - blank_length; } return position;*/ return currentPos; } bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo) { qDebug() << "requestClipCreation " << binClipId; QString bid = binClipId; if (binClipId.contains(QLatin1Char('/'))) { bid = binClipId.section(QLatin1Char('/'), 0, 0); } if (!pCore->projectItemModel()->hasClip(bid)) { qDebug()<<" / / / /MASTER CLIP NOT FOUND"; return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); if (!master->isReady() || !master->isCompatible(state)) { return false; } int clipId = TimelineModel::getNextId(); id = clipId; Fun local_undo = deregisterClip_lambda(clipId); ClipModel::construct(shared_from_this(), bid, clipId, state, speed); auto clip = m_allClips[clipId]; Fun local_redo = [clip, this, state, clipId]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); clip->refreshProducerFromBin(state); return true; }; if (binClipId.contains(QLatin1Char('/'))) { int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt(); int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt(); int initLength = m_allClips[clipId]->getPlaytime(); bool res = true; if (in != 0) { res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo); } res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets) { #ifdef LOGGING m_logFile << "timeline->requestClipInsertion(" << binClipId.toStdString() << "," << trackId << " ," << position << ", dummy_id );" << std::endl; #endif QWriteLocker locker(&m_lock); + TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Clip")); } + TRACE_RES(result); return result; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, Fun &redo) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; qDebug() << "requestClipInsertion " << binClipId << " " << " " << trackId << " " << position; bool res = false; ClipType::ProducerType type = ClipType::Unknown; QString bid = binClipId.section(QLatin1Char('/'), 0, 0); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } if (!pCore->projectItemModel()->hasClip(bid)) { return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); type = master->clipType(); if (useTargets && m_audioTarget == -1 && m_videoTarget == -1) { useTargets = false; } if (dropType == PlaylistState::Disabled && (type == ClipType::AV || type == ClipType::Playlist)) { if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) { // If audio target is set but no video target, only insert audio trackId = m_audioTarget; } bool audioDrop = getTrackById_const(trackId)->isAudioTrack(); res = requestClipCreation(binClipId, id, getTrackById_const(trackId)->trackType(), 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); int target_track = audioDrop ? m_videoTarget : m_audioTarget; qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo(); if (res && (!useTargets || target_track > -1) && master->hasAudioAndVideo()) { if (!useTargets) { target_track = audioDrop ? getMirrorVideoTrackId(trackId) : getMirrorAudioTrackId(trackId); } // QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack); QList possibleTracks; qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets; if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) { possibleTracks << target_track; } if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage); res = false; } else { std::function audio_undo = []() { return true; }; std::function audio_redo = []() { return true; }; int newId; res = requestClipCreation(binClipId, newId, audioDrop ? PlaylistState::VideoOnly : PlaylistState::AudioOnly, 1.0, audio_undo, audio_redo); if (res) { bool move = false; while (!move && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); move = requestClipMove(newId, newTrack, position, true, false, audio_undo, audio_redo); } // use lazy evaluation to group only if move was successful res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit); if (!res || !move) { pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } else { UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo); } } else { pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } } } } else { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid); if (dropType == PlaylistState::Disabled) { dropType = getTrackById_const(trackId)->trackType(); } else if (dropType != getTrackById_const(trackId)->trackType()) { qDebug() << "// INCORRECT DRAG, ABORTING"; return false; } QString normalisedBinId = binClipId; if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) { normalisedBinId.remove(0, 1); } res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestItemDeletion(int clipId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (m_groups->isInGroup(clipId)) { return requestGroupDeletion(clipId, undo, redo); } return requestClipDeletion(clipId, undo, redo); } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestItemDeletion(" << itemId << "); " << std::endl; #endif QWriteLocker locker(&m_lock); + TRACE(itemId, logUndo); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (m_groups->isInGroup(itemId)) { - return requestGroupDeletion(itemId, logUndo); + bool res = requestGroupDeletion(itemId, logUndo); + TRACE_RES(res); + return res; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = false; QString actionLabel; if (isClip(itemId)) { actionLabel = i18n("Delete Clip"); res = requestClipDeletion(itemId, undo, redo); } else { actionLabel = i18n("Delete Composition"); res = requestCompositionDeletion(itemId, undo, redo); } if (res && logUndo) { PUSH_UNDO(undo, redo, actionLabel); } + TRACE_RES(res); return res; } bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo) { int trackId = getClipTrackId(clipId); if (trackId != -1) { bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo); if (!res) { undo(); return false; } } auto operation = deregisterClip_lambda(clipId); auto clip = m_allClips[clipId]; Fun reverse = [this, clip]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); return true; }; if (operation()) { emit removeFromSelection(clipId); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo) { int trackId = getCompositionTrackId(compositionId); if (trackId != -1) { bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo); if (!res) { undo(); return false; } else { unplantComposition(compositionId); } } Fun operation = deregisterComposition_lambda(compositionId); auto composition = m_allCompositions[compositionId]; Fun reverse = [this, composition]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; if (operation()) { emit removeFromSelection(compositionId); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } std::unordered_set TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions) { Q_UNUSED(listCompositions) std::unordered_set allClips; if (trackId == -1) { for (const auto &track : m_allTracks) { std::unordered_set clipTracks = getItemsInRange(track->getId(), start, end, listCompositions); allClips.insert(clipTracks.begin(), clipTracks.end()); } } else { std::unordered_set clipTracks = getTrackById(trackId)->getClipsInRange(start, end); allClips.insert(clipTracks.begin(), clipTracks.end()); if (listCompositions) { std::unordered_set compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end); allClips.insert(compoTracks.begin(), compoTracks.end()); } } return allClips; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } return res; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { Q_UNUSED(updateView); Q_UNUSED(finalMove); Q_UNUSED(undo); Q_UNUSED(redo); Q_UNUSED(allowViewRefresh); QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = []() { return true; }; // Check if there is a track move // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : all_items) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { if (isClip(item)) { old_position[item] = m_allClips[item]->getPosition(); } else { old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } } } // Second step, calculate delta int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } bool trackChanged = false; // Reverse sort. We need to insert from left to right to avoid confusing the view for (int item : all_items) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { qDebug() << "/// SETTING FAKE CLIP: " << target_track << ", POSITION: " << target_position; m_allClips[item]->setFakePosition(target_position); if (m_allClips[item]->getFakeTrackId() != target_track) { trackChanged = true; } m_allClips[item]->setFakeTrackId(target_track); } else { } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } QModelIndex modelIndex; QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } for (int item : all_items) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { + QWriteLocker locker(&m_lock); + TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } + TRACE_RES(res); return res; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { #ifdef LOGGING m_logFile << "timeline->requestGroupMove(" << clipId << "," << groupId << " ," << delta_track << ", " << delta_pos << ", " << (updateView ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Sort clips. We need to delete from right to left to avoid confusing the view std::vector sorted_clips(all_items.begin(), all_items.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this](int clipId1, int clipId2) { int p1 = isClip(clipId1) ? m_allClips[clipId1]->getPosition() : m_allCompositions[clipId1]->getPosition(); int p2 = isClip(clipId2) ? m_allClips[clipId2]->getPosition() : m_allCompositions[clipId2]->getPosition(); return p2 <= p1; }); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = []() { return true; }; // Check if there is a track move bool updatePositionOnly = false; if (delta_track == 0 && updateView) { updateView = false; allowViewRefresh = false; updatePositionOnly = true; update_model = [sorted_clips, this]() { QModelIndex modelIndex; QVector roles{StartRole}; for (int item : sorted_clips) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; }; } // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : sorted_clips) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = allowViewRefresh; if (isClip(item)) { ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo); old_position[item] = m_allClips[item]->getPosition(); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo); old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } } // Second step, reinsert clips at correct positions int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } // Reverse sort. We need to insert from left to right to avoid confusing the view std::reverse(std::begin(sorted_clips), std::end(sorted_clips)); for (int item : sorted_clips) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; bool updateThisView = allowViewRefresh; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { ok = ok && requestClipMove(item, target_track, target_position, updateThisView, finalMove, local_undo, local_redo); } else { ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, finalMove, local_undo, local_redo); } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } if (updatePositionOnly) { update_model(); PUSH_LAMBDA(update_model, local_redo); PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestGroupDeletion(" << clipId << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); + TRACE(clipId, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestGroupDeletion(clipId, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Remove group")); } + TRACE_RES(res); return res; } bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo) { // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves. std::queue group_queue; group_queue.push(m_groups->getRootId(clipId)); std::unordered_set all_items; std::unordered_set all_compositions; while (!group_queue.empty()) { int current_group = group_queue.front(); if (m_temporarySelectionGroup == current_group) { m_temporarySelectionGroup = -1; } group_queue.pop(); Q_ASSERT(isGroup(current_group)); auto children = m_groups->getDirectChildren(current_group); int one_child = -1; // we need the id on any of the indices of the elements of the group for (int c : children) { if (isClip(c)) { all_items.insert(c); one_child = c; } else if (isComposition(c)) { all_compositions.insert(c); one_child = c; } else { Q_ASSERT(isGroup(c)); one_child = c; group_queue.push(c); } } if (one_child != -1) { bool res = m_groups->ungroupItem(one_child, undo, redo); if (!res) { undo(); return false; } } } for (int clip : all_items) { bool res = requestClipDeletion(clip, undo, redo); if (!res) { undo(); return false; } } for (int compo : all_compositions) { bool res = requestCompositionDeletion(compo, undo, redo); if (!res) { undo(); return false; } } return true; } int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) { #ifdef LOGGING m_logFile << "timeline->requestItemResize(" << itemId << "," << size << " ," << (right ? "true" : "false") << ", " << (logUndo ? "true" : "false") << ", " << (snapDistance > 0 ? "true" : "false") << " ); " << std::endl; #endif if (logUndo) { qDebug() << "---------------------\n---------------------\nRESIZE W/UNDO CALLED\n++++++++++++++++\n++++"; } QWriteLocker locker(&m_lock); + TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize); Q_ASSERT(isClip(itemId) || isComposition(itemId)); - if (size <= 0) return -1; + if (size <= 0) { + TRACE_RES(-1); + return -1; + } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); if (snapDistance > 0) { Fun temp_undo = []() { return true; }; Fun temp_redo = []() { return true; }; int proposed_size = m_snaps->proposeSize(in, out, size, right, snapDistance); if (proposed_size > 0) { // only test move if proposed_size is valid bool success = false; if (isClip(itemId)) { success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } else { success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } if (success) { temp_undo(); // undo temp move size = proposed_size; } } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::unordered_set all_items; if (!allowSingleResize && m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); auto items = m_groups->getLeaves(groupId); for (int id : items) { if (id == itemId) { all_items.insert(id); continue; } int start = getItemPosition(id); int end = in + getItemPlaytime(id); if (right) { if (out == end) { all_items.insert(id); } } else if (start == in) { all_items.insert(id); } } } else { all_items.insert(itemId); } bool result = true; for (int id : all_items) { result = result && requestItemResize(id, size, right, logUndo, undo, redo); } if (!result) { bool undone = undo(); Q_ASSERT(undone); + TRACE_RES(-1); return -1; } if (result && logUndo) { if (isClip(itemId)) { PUSH_UNDO(undo, redo, i18n("Resize clip")); } else { PUSH_UNDO(undo, redo, i18n("Resize composition")); } } - return result ? size : -1; + int res = result ? size : -1; + TRACE_RES(res); + return res; } bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; Fun update_model = [itemId, right, logUndo, this]() { Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (getItemTrackId(itemId) != -1) { qDebug() << "++++++++++\nRESIZING ITEM: " << itemId << "\n+++++++"; QModelIndex modelIndex = isClip(itemId) ? makeClipIndexFromID(itemId) : makeCompositionIndexFromID(itemId); notifyChange(modelIndex, modelIndex, !right, true, logUndo); } return true; }; bool result = false; if (isClip(itemId)) { result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } else { Q_ASSERT(isComposition(itemId)); result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } if (result) { if (!blockUndo) { PUSH_LAMBDA(update_model, local_undo); } PUSH_LAMBDA(update_model, local_redo); update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, bool logUndo, GroupType type) { QWriteLocker locker(&m_lock); + TRACE(ids, logUndo, type); Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (m_temporarySelectionGroup > -1) { m_groups->destructGroupItem(m_temporarySelectionGroup); // We don't log in undo the selection changes // int firstChild = *m_groups->getDirectChildren(m_temporarySelectionGroup).begin(); // requestClipUngroup(firstChild, undo, redo); m_temporarySelectionGroup = -1; } int result = requestClipsGroup(ids, undo, redo, type); if (type == GroupType::Selection) { m_temporarySelectionGroup = result; } if (result > -1 && logUndo && type != GroupType::Selection) { PUSH_UNDO(undo, redo, i18n("Group clips")); } + TRACE_RES(result); return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type) { #ifdef LOGGING std::stringstream group; m_logFile << "{" << std::endl; m_logFile << "auto group = {"; bool deb = true; for (int clipId : ids) { if (deb) deb = false; else group << ", "; group << clipId; } m_logFile << group.str() << "};" << std::endl; m_logFile << "timeline->requestClipsGroup(group);" << std::endl; m_logFile << std::endl << "}" << std::endl; #endif QWriteLocker locker(&m_lock); for (int id : ids) { if (isClip(id)) { if (getClipTrackId(id) == -1) { return -1; } } else if (isComposition(id)) { if (getCompositionTrackId(id) == -1) { return -1; } } else if (!isGroup(id)) { return -1; } } int groupId = m_groups->groupItems(ids, undo, redo, type); if (type == GroupType::Selection && *(ids.begin()) == groupId) { // only one element selected, no group created return -1; } return groupId; } bool TimelineModel::requestClipUngroup(int id, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestClipUngroup(" << id << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); + TRACE(id, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; if (id == m_temporarySelectionGroup) { // Delete selection group without undo Fun tmp_undo = []() { return true; }; Fun tmp_redo = []() { return true; }; requestClipUngroup(id, tmp_undo, tmp_redo); m_temporarySelectionGroup = -1; } else { result = requestClipUngroup(id, undo, redo); } if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } + TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int id, Fun &undo, Fun &redo) { return m_groups->ungroupItem(id, undo, redo); } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack) { #ifdef LOGGING m_logFile << "timeline->requestTrackInsertion(" << position << ", dummy_id ); " << std::endl; #endif QWriteLocker locker(&m_lock); TRACE(position, id, trackName, audioTrack); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo); if (result) { PUSH_UNDO(undo, redo, i18n("Insert Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView) { // TODO: make sure we disable overlayTrack before inserting a track if (position == -1) { position = (int)(m_allTracks.size()); } if (position < 0 || position > (int)m_allTracks.size()) { return false; } int trackId = TimelineModel::getNextId(); id = trackId; Fun local_undo = deregisterTrack_lambda(trackId, true); TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack); auto track = getTrackById(trackId); Fun local_redo = [track, position, updateView, this]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, position, updateView); return true; }; UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestTrackDeletion(int trackId) { // TODO: make sure we disable overlayTrack before deleting a track #ifdef LOGGING m_logFile << "timeline->requestTrackDeletion(" << trackId << "); " << std::endl; #endif QWriteLocker locker(&m_lock); + TRACE(trackId); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackDeletion(trackId, undo, redo); if (result) { if (m_videoTarget == trackId) { m_videoTarget = -1; } if (m_audioTarget == trackId) { m_audioTarget = -1; } PUSH_UNDO(undo, redo, i18n("Delete Track")); } + TRACE_RES(result); return result; } bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo) { Q_ASSERT(isTrack(trackId)); std::vector clips_to_delete; for (const auto &it : getTrackById(trackId)->m_allClips) { clips_to_delete.push_back(it.first); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (int clip : clips_to_delete) { bool res = true; while (res && m_groups->isInGroup(clip)) { res = requestClipUngroup(clip, local_undo, local_redo); } if (res) { res = requestClipDeletion(clip, local_undo, local_redo); } if (!res) { bool u = local_undo(); Q_ASSERT(u); return false; } } int old_position = getTrackPosition(trackId); auto operation = deregisterTrack_lambda(trackId, true); std::shared_ptr track = getTrackById(trackId); Fun reverse = [this, track, old_position]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, old_position); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } local_undo(); return false; } void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert, bool reloadView) { qDebug() << "REGISTER TRACK" << track->getId() << pos; int id = track->getId(); if (pos == -1) { pos = static_cast(m_allTracks.size()); } Q_ASSERT(pos >= 0); Q_ASSERT(pos <= static_cast(m_allTracks.size())); // effective insertion (MLT operation), add 1 to account for black background track if (doInsert) { int error = m_tractor->insert_track(*track, pos + 1); Q_ASSERT(error == 0); // we might need better error handling... } // we now insert in the list auto posIt = m_allTracks.begin(); std::advance(posIt, pos); auto it = m_allTracks.insert(posIt, std::move(track)); // it now contains the iterator to the inserted element, we store it Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen) m_iteratorTable[id] = it; if (reloadView) { // don't reload view on each track load on project opening _resetView(); } } void TimelineModel::registerClip(const std::shared_ptr &clip, bool registerProducer) { int id = clip->getId(); qDebug() << " // /REQUEST TL CLP REGSTR: " << id << "\n--------\nCLIPS COUNT: " << m_allClips.size(); Q_ASSERT(m_allClips.count(id) == 0); m_allClips[id] = clip; clip->registerClipToBin(clip->getProducer(), registerProducer); m_groups->createGroupItem(id); clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled); } void TimelineModel::registerGroup(int groupId) { Q_ASSERT(m_allGroups.count(groupId) == 0); m_allGroups.insert(groupId); } Fun TimelineModel::deregisterTrack_lambda(int id, bool updateView) { return [this, id, updateView]() { qDebug() << "DEREGISTER TRACK" << id; auto it = m_iteratorTable[id]; // iterator to the element int index = getTrackPosition(id); // compute index in list m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track // send update to the model m_allTracks.erase(it); // actual deletion of object m_iteratorTable.erase(id); // clean table if (updateView) { QModelIndex root; _resetView(); } return true; }; } Fun TimelineModel::deregisterClip_lambda(int clipId) { return [this, clipId]() { //qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size(); clearAssetView(clipId); Q_ASSERT(m_allClips.count(clipId) > 0); Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point auto clip = m_allClips[clipId]; m_allClips.erase(clipId); clip->deregisterClipToBin(); m_groups->destructGroupItem(clipId); return true; }; } void TimelineModel::deregisterGroup(int id) { Q_ASSERT(m_allGroups.count(id) > 0); m_allGroups.erase(id); } std::shared_ptr TimelineModel::getTrackById(int trackId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable[trackId]; } const std::shared_ptr TimelineModel::getTrackById_const(int trackId) const { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable.at(trackId); } bool TimelineModel::addTrackEffect(int trackId, const QString &effectId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500); return false; } return true; } bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_iteratorTable.count(trackId) > 0 && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) { pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500); return false; } return true; } std::shared_ptr TimelineModel::getClipPtr(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId); } bool TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify) { Q_ASSERT(m_allClips.count(clipId) > 0); bool result = m_allClips.at(clipId)->addEffect(effectId); if (!result && notify) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } return result; } bool TimelineModel::removeFade(int clipId, bool fromStart) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->removeFade(fromStart); } std::shared_ptr TimelineModel::getClipEffectStack(int itemId) { Q_ASSERT(m_allClips.count(itemId)); return m_allClips.at(itemId)->m_effectStack; } bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_allClips.count(clipId) && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); return m_allClips.at(clipId)->copyEffect(effectStack, itemRow); } bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration) { Q_ASSERT(m_allClips.count(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo); if (res && initialDuration > 0) { PUSH_UNDO(undo, redo, i18n("Adjust Fade")); } return res; } std::shared_ptr TimelineModel::getCompositionPtr(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); return m_allCompositions.at(compoId); } int TimelineModel::getNextId() { return TimelineModel::next_id++; } bool TimelineModel::isClip(int id) const { return m_allClips.count(id) > 0; } bool TimelineModel::isComposition(int id) const { return m_allCompositions.count(id) > 0; } bool TimelineModel::isTrack(int id) const { return m_iteratorTable.count(id) > 0; } bool TimelineModel::isGroup(int id) const { return m_allGroups.count(id) > 0; } void TimelineModel::updateDuration() { int current = m_blackClip->get_playtime() - TimelineModel::seekDuration; int duration = 0; for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); duration = qMax(duration, track->trackDuration()); } if (duration != current) { // update black track length m_blackClip->set_in_and_out(0, duration + TimelineModel::seekDuration); emit durationUpdated(); } } int TimelineModel::duration() const { return m_tractor->get_playtime() - TimelineModel::seekDuration; } std::unordered_set TimelineModel::getGroupElements(int clipId) { int groupId = m_groups->getRootId(clipId); return m_groups->getLeaves(groupId); } Mlt::Profile *TimelineModel::getProfile() { return m_profile; } bool TimelineModel::requestReset(Fun &undo, Fun &redo) { std::vector all_ids; for (const auto &track : m_iteratorTable) { all_ids.push_back(track.first); } bool ok = true; for (int trackId : all_ids) { ok = ok && requestTrackDeletion(trackId, undo, redo); } return ok; } void TimelineModel::setUndoStack(std::weak_ptr undo_stack) { m_undoStack = std::move(undo_stack); } int TimelineModel::suggestSnapPoint(int pos, int snapDistance) { int snapped = m_snaps->getClosestPoint(pos); return (qAbs(snapped - pos) < snapDistance ? snapped : pos); } int TimelineModel::requestBestSnapPos(int pos, int length, const std::vector &pts, int cursorPosition, int snapDistance) { if (!pts.empty()) { m_snaps->ignore(pts); } m_snaps->addPoint(cursorPosition); int snapped_start = m_snaps->getClosestPoint(pos); int snapped_end = m_snaps->getClosestPoint(pos + length); m_snaps->unIgnore(); m_snaps->removePoint(cursorPosition); int startDiff = qAbs(pos - snapped_start); int endDiff = qAbs(pos + length - snapped_end); if (startDiff < endDiff && startDiff <= snapDistance) { // snap to start return snapped_start; } if (endDiff <= snapDistance) { // snap to end return snapped_end - length; } return -1; } int TimelineModel::requestNextSnapPos(int pos) { return m_snaps->getNextPoint(pos); } int TimelineModel::requestPreviousSnapPos(int pos) { return m_snaps->getPreviousPoint(pos); } void TimelineModel::addSnap(int pos) { return m_snaps->addPoint(pos); } void TimelineModel::removeSnap(int pos) { return m_snaps->removePoint(pos); } void TimelineModel::registerComposition(const std::shared_ptr &composition) { int id = composition->getId(); Q_ASSERT(m_allCompositions.count(id) == 0); m_allCompositions[id] = composition; m_groups->createGroupItem(id); } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, Mlt::Properties *transProps, int &id, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestCompositionInsertion(\"composite\"," << trackId << " ," << position << "," << length << ", dummy_id );" << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, transProps, id, undo, redo, logUndo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Composition")); } return result; } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, Mlt::Properties *transProps, int &id, Fun &undo, Fun &redo, bool finalMove) { qDebug() << "Inserting compo track" << trackId << "pos" << position << "length" << length; int compositionId = TimelineModel::getNextId(); id = compositionId; Fun local_undo = deregisterComposition_lambda(compositionId); CompositionModel::construct(shared_from_this(), transitionId, compositionId, transProps); auto composition = m_allCompositions[compositionId]; Fun local_redo = [composition, this]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo); qDebug() << "trying to move" << trackId << "pos" << position << "success " << res; if (res) { res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); qDebug() << "trying to resize" << compositionId << "length" << length << "success " << res; } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } Fun TimelineModel::deregisterComposition_lambda(int compoId) { return [this, compoId]() { Q_ASSERT(m_allCompositions.count(compoId) > 0); Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point clearAssetView(compoId); m_allCompositions.erase(compoId); m_groups->destructGroupItem(compoId); return true; }; } int TimelineModel::getCompositionPosition(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getPosition(); } int TimelineModel::getCompositionPlaytime(int compoId) const { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); int playtime = trans->getPlaytime(); return playtime; } int TimelineModel::getItemPosition(int itemId) const { if (isClip(itemId)) { return getClipPosition(itemId); } return getCompositionPosition(itemId); } int TimelineModel::getItemPlaytime(int itemId) const { if (isClip(itemId)) { return getClipPlaytime(itemId); } return getCompositionPlaytime(itemId); } int TimelineModel::getTrackCompositionsCount(int trackId) const { Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionsCount(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestCompositionMove(" << compoId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", " << (logUndo ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) { return true; } if (m_groups->isInGroup(compoId)) { // element is in a group. int groupId = m_groups->getRootId(compoId); int current_trackId = getCompositionTrackId(compoId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allCompositions[compoId]->getPosition(); return requestGroupMove(compoId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; int min = getCompositionPosition(compoId); int max = min + getCompositionPlaytime(compoId); int tk = getCompositionTrackId(compoId); bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo); if (tk > -1) { min = qMin(min, getCompositionPosition(compoId)); max = qMax(max, getCompositionPosition(compoId)); } else { min = getCompositionPosition(compoId); max = min + getCompositionPlaytime(compoId); } if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move composition")); checkRefresh(min, max); } return res; } bool TimelineModel::isAudioTrack(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); return (*it)->isAudioTrack(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) { // qDebug() << "// compo track: " << trackId << ", PREVIOUS TK: " << getPreviousVideoTrackPos(trackId); compositionTrack = getPreviousVideoTrackPos(trackId); } if (compositionTrack == -1) { // it doesn't make sense to insert a composition on the last track qDebug() << "Move failed because of last track"; return false; } qDebug() << "Requesting composition move" << trackId << "," << position << " ( " << compositionTrack << " / " << (compositionTrack > 0 ? getTrackIndexFromPosition(compositionTrack - 1) : 0); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool ok = true; int old_trackId = getCompositionTrackId(compoId); bool notifyViewOnly = false; Fun update_model = []() { return true; }; if (updateView && old_trackId == trackId) { // Move on same track, only send view update updateView = false; notifyViewOnly = true; update_model = [compoId, finalMove, this]() { QModelIndex modelIndex = makeCompositionIndexFromID(compoId); notifyChange(modelIndex, modelIndex, {StartRole}); return true; }; } if (old_trackId != -1) { Fun delete_operation = []() { return true; }; Fun delete_reverse = []() { return true; }; if (old_trackId != trackId) { delete_operation = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; int oldAtrack = m_allCompositions[compoId]->getATrack(); delete_reverse = [this, compoId, oldAtrack, updateView]() { m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack <= 0 ? -1 : getTrackIndexFromPosition(oldAtrack - 1)); return replantCompositions(compoId, updateView); }; } ok = delete_operation(); if (!ok) qDebug() << "Move failed because of first delete operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo); } if (!ok) { qDebug() << "Move failed because of first deletion request"; bool undone = local_undo(); Q_ASSERT(undone); return false; } } ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo); if (!ok) qDebug() << "Move failed because of second insertion request"; if (ok) { Fun insert_operation = []() { return true; }; Fun insert_reverse = []() { return true; }; if (old_trackId != trackId) { insert_operation = [this, compoId, trackId, compositionTrack, updateView]() { qDebug() << "-------------- ATRACK ----------------\n" << compositionTrack << " = " << getTrackIndexFromPosition(compositionTrack); m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack <= 0 ? -1 : getTrackIndexFromPosition(compositionTrack - 1)); return replantCompositions(compoId, updateView); }; insert_reverse = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; } ok = insert_operation(); if (!ok) qDebug() << "Move failed because of second insert operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_redo); } UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::replantCompositions(int currentCompo, bool updateView) { // We ensure that the compositions are planted in a decreasing order of b_track. // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order. std::vector> compos; for (const auto &compo : m_allCompositions) { int trackId = compo.second->getCurrentTrackId(); if (trackId == -1 || compo.second->getATrack() == -1) { continue; } // Note: we need to retrieve the position of the track, that is its melt index. int trackPos = getTrackMltIndex(trackId); compos.push_back({trackPos, compo.first}); if (compo.first != currentCompo) { unplantComposition(compo.first); } } // sort by decreasing b_track std::sort(compos.begin(), compos.end(), [](const std::pair &a, const std::pair &b) { return a.first > b.first; }); // replant QScopedPointer field(m_tractor->field()); field->lock(); // Unplant track compositing mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString resource = mlt_properties_get(properties, "mlt_service"); mlt_service_type mlt_type = mlt_service_identify(nextservice); QList trackCompositions; while (mlt_type == transition_type) { Mlt::Transition transition((mlt_transition)nextservice); nextservice = mlt_service_producer(nextservice); int internal = transition.get_int("internal_added"); if (internal > 0 && resource != QLatin1String("mix")) { trackCompositions << new Mlt::Transition(transition); field->disconnect_service(transition); transition.disconnect_all_producers(); } if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); properties = MLT_SERVICE_PROPERTIES(nextservice); resource = mlt_properties_get(properties, "mlt_service"); } // Sort track compositing std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); for (const auto &compo : compos) { int aTrack = m_allCompositions[compo.second]->getATrack(); Q_ASSERT(aTrack != -1); int ret = field->plant_transition(*m_allCompositions[compo.second].get(), aTrack, compo.first); qDebug() << "Planting composition " << compo.second << "in " << aTrack << "/" << compo.first << "IN = " << m_allCompositions[compo.second]->getIn() << "OUT = " << m_allCompositions[compo.second]->getOut() << "ret=" << ret; Mlt::Transition &transition = *m_allCompositions[compo.second].get(); transition.set_tracks(aTrack, compo.first); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); if (ret != 0) { field->unlock(); return false; } } // Replant last tracks compositing while (!trackCompositions.isEmpty()) { Mlt::Transition *firstTr = trackCompositions.takeFirst(); field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track()); } field->unlock(); if (updateView) { QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo); notifyChange(modelIndex, modelIndex, {ItemATrack}); } return true; } bool TimelineModel::unplantComposition(int compoId) { qDebug() << "Unplanting" << compoId; Mlt::Transition &transition = *m_allCompositions[compoId].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); QScopedPointer field(m_tractor->field()); field->lock(); field->disconnect_service(transition); int ret = transition.disconnect_all_producers(); mlt_service nextservice = mlt_service_get_producer(transition.get_service()); // mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(nextservice == nullptr); // Q_ASSERT(consumer == nullptr); field->unlock(); return ret != 0; } bool TimelineModel::checkConsistency() { for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); // Check parent/children link for tracks if (auto ptr = track->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for track" << tck.first; return false; } } else { qDebug() << "NULL parent for track" << tck.first; return false; } // check consistency of track if (!track->checkConsistency()) { qDebug() << "Consistency check failed for track" << tck.first; return false; } } // We store all in/outs of clips to check snap points std::map snaps; // Check parent/children link for clips for (const auto &cp : m_allClips) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for clip" << cp.first; return false; } } else { qDebug() << "NULL parent for clip" << cp.first; return false; } if (getClipTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } } for (const auto &cp : m_allCompositions) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for compo" << cp.first; return false; } } else { qDebug() << "NULL parent for compo" << cp.first; return false; } if (getCompositionTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } } // Check snaps auto stored_snaps = m_snaps->_snaps(); if (snaps.size() != stored_snaps.size()) { qDebug() << "Wrong number of snaps: " << snaps.size() << " == " << stored_snaps.size(); return false; } for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) { if (*i != *j) { qDebug() << "Wrong snap info at point" << (*i).first; return false; } } // We check consistency with bin model auto binClips = pCore->projectItemModel()->getAllClipIds(); // First step: all clips referenced by the bin model exist and are inserted for (const auto &binClip : binClips) { auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); for (const auto &insertedClip : projClip->m_registeredClips) { if (auto ptr = insertedClip.second.lock()) { if (ptr.get() == this) { // check we are talking of this timeline if (!isClip(insertedClip.first)) { qDebug() << "Bin model registers a bad clip ID" << insertedClip.first; return false; } } } else { qDebug() << "Bin model registers a clip in a NULL timeline" << insertedClip.first; return false; } } } // Second step: all clips are referenced for (const auto &clip : m_allClips) { auto binId = clip.second->m_binClipId; auto projClip = pCore->projectItemModel()->getClipByBinID(binId); if (projClip->m_registeredClips.count(clip.first) == 0) { qDebug() << "Clip " << clip.first << "not registered in bin"; return false; } } // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our // m_allCompositions std::unordered_set remaining_compo; for (const auto &compo : m_allCompositions) { if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) { remaining_compo.insert(compo.first); // check validity of the consumer Mlt::Transition &transition = *m_allCompositions[compo.first].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); } } QScopedPointer field(m_tractor->field()); field->lock(); mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_service_type mlt_type = mlt_service_identify(nextservice); while (nextservice != nullptr) { if (mlt_type == transition_type) { mlt_transition tr = (mlt_transition)nextservice; int currentTrack = mlt_transition_get_b_track(tr); int currentATrack = mlt_transition_get_a_track(tr); int currentIn = (int)mlt_transition_get_in(tr); int currentOut = (int)mlt_transition_get_out(tr); qDebug() << "looking composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; int foundId = -1; // we iterate to try to find a matching compo for (int compoId : remaining_compo) { if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack && m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) { foundId = compoId; break; } } if (foundId == -1) { qDebug() << "Error, we didn't find matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; field->unlock(); return false; } qDebug() << "Found"; remaining_compo.erase(foundId); } nextservice = mlt_service_producer(nextservice); if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); } field->unlock(); if (!remaining_compo.empty()) { qDebug() << "Error: We found less compositions than expected. Compositions that have not been found:"; for (int compoId : remaining_compo) { qDebug() << compoId; } return false; } // We check consistency of groups if (!m_groups->checkConsistency(true, true)) { qDebug() << "== ERROR IN GROUP CONSISTENCY"; return false; } return true; } void TimelineModel::setTimelineEffectsEnabled(bool enabled) { m_timelineEffectsEnabled = enabled; // propagate info to clips for (const auto &clip : m_allClips) { clip.second->setTimelineEffectsEnabled(enabled); } // TODO if we support track effects, they should be disabled here too } Mlt::Producer *TimelineModel::producer() { auto *prod = new Mlt::Producer(tractor()); return prod; } void TimelineModel::checkRefresh(int start, int end) { int currentPos = tractor()->position(); if (currentPos >= start && currentPos < end) { emit requestMonitorRefresh(); } } void TimelineModel::clearAssetView(int itemId) { emit requestClearAssetView(itemId); } std::shared_ptr TimelineModel::getCompositionParameterModel(int compoId) const { READ_LOCK(); Q_ASSERT(isComposition(compoId)); return std::static_pointer_cast(m_allCompositions.at(compoId)); } std::shared_ptr TimelineModel::getClipEffectStackModel(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); return std::static_pointer_cast(m_allClips.at(clipId)->m_effectStack); } std::shared_ptr TimelineModel::getTrackEffectStackModel(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById(trackId)->m_effectStack; } QStringList TimelineModel::extractCompositionLumas() const { QStringList urls; for (const auto &compo : m_allCompositions) { QString luma = compo.second->getProperty(QStringLiteral("resource")); if (!luma.isEmpty()) { urls << QUrl::fromLocalFile(luma).toLocalFile(); } } urls.removeDuplicates(); return urls; } void TimelineModel::adjustAssetRange(int clipId, int in, int out) { Q_UNUSED(clipId) Q_UNUSED(in) Q_UNUSED(out) // pCore->adjustAssetRange(clipId, in, out); } void TimelineModel::requestClipReload(int clipId) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; // in order to make the producer change effective, we need to unplant / replant the clip in int track int old_trackId = getClipTrackId(clipId); int oldPos = getClipPosition(clipId); int oldOut = getClipIn(clipId) + getClipPlaytime(clipId); // Check if clip out is longer than actual producer duration (if user forced duration) std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId)); bool refreshView = oldOut > (int)binClip->frameDuration(); if (old_trackId != -1) { getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo); } m_allClips[clipId]->refreshProducerFromBin(); if (old_trackId != -1) { getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo); } } void TimelineModel::replugClip(int clipId) { int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { getTrackById(old_trackId)->replugClip(clipId); } } void TimelineModel::requestClipUpdate(int clipId, const QVector &roles) { QModelIndex modelIndex = makeClipIndexFromID(clipId); if (roles.contains(TimelineModel::ReloadThumbRole)) { m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload; } notifyChange(modelIndex, modelIndex, roles); } bool TimelineModel::requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed())) { return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; int oldPos = getClipPosition(clipId); // in order to make the producer change effective, we need to unplant / replant the clip in int track bool success = true; int trackId = getClipTrackId(clipId); if (trackId != -1) { success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo); } if (success) { success = m_allClips[clipId]->useTimewarpProducer(speed, local_undo, local_redo); } if (trackId != -1) { success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); } if (!success) { local_undo(); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return success; } bool TimelineModel::requestClipTimeWarp(int clipId, double speed) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Get main clip info int trackId = getClipTrackId(clipId); bool result = true; if (trackId != -1) { // Check if clip has a split partner int splitId = m_groups->getSplitPartner(clipId); if (splitId > -1) { result = requestClipTimeWarp(splitId, speed / 100.0, undo, redo); } if (result) { result = requestClipTimeWarp(clipId, speed / 100.0, undo, redo); } else { pCore->displayMessage(i18n("Change speed failed"), ErrorMessage); undo(); return false; } } else { // If clip is not inserted on a track, we just change the producer m_allClips[clipId]->useTimewarpProducer(speed, undo, redo); } if (result) { PUSH_UNDO(undo, redo, i18n("Change clip speed")); return true; } return false; } const QString TimelineModel::getTrackTagById(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); bool isAudio = getTrackById_const(trackId)->isAudioTrack(); int count = 1; int totalAudio = 2; auto it = m_allTracks.begin(); bool found = false; while ((isAudio || !found) && it != m_allTracks.end()) { if ((*it)->isAudioTrack()) { totalAudio++; if (isAudio && !found) { count++; } } else if (!isAudio) { count++; } if ((*it)->getId() == trackId) { found = true; } it++; } return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1); } void TimelineModel::updateProfile(Mlt::Profile *profile) { m_profile = profile; m_tractor->set_profile(*m_profile); } int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = getClipTrackId(clipId); if (trackId != -1) { return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); } return 0; } int TimelineModel::getPreviousTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.begin() ? trackId : (*it)->getId(); } int TimelineModel::getNextTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.end()) { ++it; if (it != m_allTracks.end() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.end() ? trackId : (*it)->getId(); } diff --git a/tests/modeltest.cpp b/tests/modeltest.cpp index 9d762f964..c86fc6aea 100644 --- a/tests/modeltest.cpp +++ b/tests/modeltest.cpp @@ -1,2130 +1,2129 @@ #include "logger.hpp" #include "test_utils.hpp" using namespace fakeit; std::default_random_engine g(42); Mlt::Profile profile_model; TEST_CASE("Basic creation/deletion of a track", "[TrackModel]") { - Logger::clear(); auto binModel = pCore->projectItemModel(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); Fake(Method(timMock, adjustAssetRange)); // This is faked to allow to count calls Fake(Method(timMock, _resetView)); int id1 = TrackModel::construct(timeline); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 1); REQUIRE(timeline->getTrackPosition(id1) == 0); // In the current implementation, when a track is added/removed, the model is notified with _resetView Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); int id2 = TrackModel::construct(timeline); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 2); REQUIRE(timeline->getTrackPosition(id2) == 1); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); int id3 = TrackModel::construct(timeline); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 3); REQUIRE(timeline->getTrackPosition(id3) == 2); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); int id4; REQUIRE(timeline->requestTrackInsertion(1, id4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 4); REQUIRE(timeline->getTrackPosition(id1) == 0); REQUIRE(timeline->getTrackPosition(id4) == 1); REQUIRE(timeline->getTrackPosition(id2) == 2); REQUIRE(timeline->getTrackPosition(id3) == 3); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); // Test deletion REQUIRE(timeline->requestTrackDeletion(id3)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 3); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); REQUIRE(timeline->requestTrackDeletion(id1)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 2); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); REQUIRE(timeline->requestTrackDeletion(id4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 1); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); REQUIRE(timeline->requestTrackDeletion(id2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTracksCount() == 0); Verify(Method(timMock, _resetView)).Exactly(Once); RESET(timMock); SECTION("Delete a track with groups") { int tid1, tid2; REQUIRE(timeline->requestTrackInsertion(-1, tid1)); REQUIRE(timeline->requestTrackInsertion(-1, tid2)); REQUIRE(timeline->checkConsistency()); QString binId = createProducer(profile_model, "red", binModel); int length = 20; int cid1, cid2, cid3, cid4; REQUIRE(timeline->requestClipInsertion(binId, tid1, 2, cid1)); REQUIRE(timeline->requestClipInsertion(binId, tid2, 0, cid2)); REQUIRE(timeline->requestClipInsertion(binId, tid2, length, cid3)); REQUIRE(timeline->requestClipInsertion(binId, tid2, 2 * length, cid4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 4); REQUIRE(timeline->getTracksCount() == 2); auto g1 = std::unordered_set({cid1, cid3}); auto g2 = std::unordered_set({cid2, cid4}); auto g3 = std::unordered_set({cid1, cid4}); REQUIRE(timeline->requestClipsGroup(g1)); REQUIRE(timeline->requestClipsGroup(g2)); REQUIRE(timeline->requestClipsGroup(g3)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->requestTrackDeletion(tid1)); REQUIRE(timeline->getClipsCount() == 3); REQUIRE(timeline->getTracksCount() == 1); REQUIRE(timeline->checkConsistency()); } binModel->clean(); pCore->m_projectManager = nullptr; Logger::print_trace(); } TEST_CASE("Basic creation/deletion of a clip", "[ClipModel]") { auto binModel = pCore->projectItemModel(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "green", binModel); REQUIRE(timeline->getClipsCount() == 0); int id1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 1); REQUIRE(timeline->checkConsistency()); int id2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 2); REQUIRE(timeline->checkConsistency()); int id3 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 3); REQUIRE(timeline->checkConsistency()); // Test deletion REQUIRE(timeline->requestItemDeletion(id2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 2); REQUIRE(timeline->requestItemDeletion(id3)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 1); REQUIRE(timeline->requestItemDeletion(id1)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipsCount() == 0); binModel->clean(); pCore->m_projectManager = nullptr; } TEST_CASE("Clip manipulation", "[ClipModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); Fake(Method(timMock, adjustAssetRange)); // This is faked to allow to count calls Fake(Method(timMock, _resetView)); Fake(Method(timMock, _beginInsertRows)); Fake(Method(timMock, _beginRemoveRows)); Fake(Method(timMock, _endInsertRows)); Fake(Method(timMock, _endRemoveRows)); QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "blue", binModel); QString binId3 = createProducer(profile_model, "green", binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int tid3 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); int cid4 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); Verify(Method(timMock, _resetView)).Exactly(3_Times); RESET(timMock); // for testing purposes, we make sure the clip will behave as regular clips // (ie their size is fixed, we cannot resize them past their original size) timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; timeline->m_allClips[cid4]->m_endlessResize = false; SECTION("Insert a clip in a track and change track") { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getClipTrackId(cid1) == -1); REQUIRE(timeline->getClipPosition(cid1) == -1); int pos = 10; REQUIRE(timeline->requestClipMove(cid1, tid1, pos)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); // Check that the model was correctly notified CHECK_INSERT(Once); pos = 1; REQUIRE(timeline->requestClipMove(cid1, tid2, pos)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); CHECK_MOVE(Once); // Check conflicts int pos2 = binModel->getClipByBinID(binId)->frameDuration(); REQUIRE(timeline->requestClipMove(cid2, tid1, pos2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); CHECK_INSERT(Once); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, pos2 + 2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); CHECK_MOVE(Once); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, pos2 - 2)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipPosition(cid1) == pos); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); CHECK_MOVE(Once); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == pos2); CHECK_MOVE(Once); } int length = binModel->getClipByBinID(binId)->frameDuration(); SECTION("Insert consecutive clips") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); CHECK_INSERT(Once); REQUIRE(timeline->requestClipMove(cid2, tid1, length)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == length); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); CHECK_INSERT(Once); } SECTION("Resize orphan clip") { REQUIRE(timeline->getClipPlaytime(cid2) == length); REQUIRE(timeline->requestItemResize(cid2, 5, true) == 5); REQUIRE(timeline->checkConsistency()); REQUIRE(binModel->getClipByBinID(binId)->frameDuration() == length); auto inOut = std::pair{0, 4}; REQUIRE(timeline->m_allClips[cid2]->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid2) == 5); REQUIRE(timeline->requestItemResize(cid2, 10, false) == -1); REQUIRE(timeline->requestItemResize(cid2, length + 1, true) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid2) == 5); REQUIRE(timeline->getClipPlaytime(cid2) == 5); REQUIRE(timeline->requestItemResize(cid2, 2, false) == 2); REQUIRE(timeline->checkConsistency()); inOut = std::pair{3, 4}; REQUIRE(timeline->m_allClips[cid2]->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid2) == 2); REQUIRE(timeline->requestItemResize(cid2, length, true) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid2) == 2); CAPTURE(timeline->m_allClips[cid2]->m_producer->get_in()); REQUIRE(timeline->requestItemResize(cid2, length - 2, true) == -1); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->requestItemResize(cid2, length - 3, true) == length - 3); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid2) == length - 3); } SECTION("Resize inserted clips") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); CHECK_INSERT(Once); REQUIRE(timeline->requestItemResize(cid1, 5, true) == 5); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid1) == 5); REQUIRE(timeline->getClipPosition(cid1) == 0); CHECK_RESIZE(Once); REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(binModel->getClipByBinID(binId)->frameDuration() == length); CHECK_INSERT(Once); REQUIRE(timeline->requestItemResize(cid1, 6, true) == -1); REQUIRE(timeline->requestItemResize(cid1, 6, false) == -1); REQUIRE(timeline->checkConsistency()); NO_OTHERS(); REQUIRE(timeline->requestItemResize(cid2, length - 5, false) == length - 5); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPosition(cid2) == 10); CHECK_RESIZE(Once); REQUIRE(timeline->requestItemResize(cid1, 10, true) == 10); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); CHECK_RESIZE(Once); } SECTION("Change track of resized clips") { // // REQUIRE(timeline->allowClipMove(cid2, tid1, 5)); REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); // // REQUIRE(timeline->allowClipMove(cid1, tid2, 10)); REQUIRE(timeline->requestClipMove(cid1, tid2, 10)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->requestItemResize(cid1, 5, false) == 5); REQUIRE(timeline->checkConsistency()); // // REQUIRE(timeline->allowClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); } SECTION("Clip Move") { REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid2) == 5); REQUIRE(timeline->requestClipMove(cid1, tid1, 5 + length)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5 + length); REQUIRE(timeline->getClipPosition(cid2) == 5); }; state(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 3 + length)); state(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0)); state(); REQUIRE(timeline->requestClipMove(cid2, tid1, 0)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5 + length); REQUIRE(timeline->getClipPosition(cid2) == 0); }; state2(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0)); state2(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, length - 5)); state2(); REQUIRE(timeline->requestClipMove(cid1, tid1, length)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length); REQUIRE(timeline->getClipPosition(cid2) == 0); REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length); REQUIRE(timeline->getClipPosition(cid2) == 0); REQUIRE(timeline->requestClipMove(cid1, tid1, length - 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 0); REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 5); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 5); REQUIRE(timeline->requestClipMove(cid2, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPosition(cid1) == length - 5); REQUIRE(timeline->getClipPosition(cid2) == 0); } SECTION("Move and resize") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestItemResize(cid1, length - 2, false) == length - 2); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPlaytime(cid1) == length - 2); }; state(); // try to resize past the left end REQUIRE(timeline->requestItemResize(cid1, length, false) == -1); state(); REQUIRE(timeline->requestItemResize(cid1, length - 4, true) == length - 4); REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4 + 1)); REQUIRE(timeline->requestItemResize(cid2, length - 2, false) == length - 2); REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4 + 1)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPlaytime(cid1) == length - 4); REQUIRE(timeline->getClipPosition(cid2) == length - 4 + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 2); }; state2(); // the gap between the two clips is 1 frame, we try to resize them by 2 frames REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == -1); state2(); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); state2(); REQUIRE(timeline->requestClipMove(cid2, tid1, length - 4)); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPlaytime(cid1) == length - 4); REQUIRE(timeline->getClipPosition(cid2) == length - 4); REQUIRE(timeline->getClipPlaytime(cid2) == length - 2); }; state3(); // Now the gap is 0 frames, the resize should still fail REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == -1); state3(); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); state3(); // We move cid1 out of the way REQUIRE(timeline->requestClipMove(cid1, tid2, 0)); // now resize should work REQUIRE(timeline->requestItemResize(cid1, length - 2, true) == length - 2); REQUIRE(timeline->requestItemResize(cid2, length, false) == length); REQUIRE(timeline->checkConsistency()); } SECTION("Group move") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestClipMove(cid2, tid1, length + 3)); REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 5)); REQUIRE(timeline->requestClipMove(cid4, tid2, 4)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5); REQUIRE(timeline->getClipPosition(cid4) == 4); // check that move is possible without groups REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 3)); REQUIRE(timeline->checkConsistency()); undoStack->undo(); REQUIRE(timeline->checkConsistency()); // check that move is possible without groups REQUIRE(timeline->requestClipMove(cid4, tid2, 9)); REQUIRE(timeline->checkConsistency()); undoStack->undo(); REQUIRE(timeline->checkConsistency()); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5); REQUIRE(timeline->getClipPosition(cid4) == 4); }; state(); // grouping REQUIRE(timeline->requestClipsGroup({cid1, cid3})); REQUIRE(timeline->requestClipsGroup({cid1, cid4})); // move left is now forbidden, because clip1 is at position 0 REQUIRE_FALSE(timeline->requestClipMove(cid3, tid1, 2 * length + 3)); state(); // this move is impossible, because clip1 runs into clip2 REQUIRE_FALSE(timeline->requestClipMove(cid4, tid2, 9)); state(); // this move is possible REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * length + 8)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getTrackClipsCount(tid3) == 0); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 3); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 8); REQUIRE(timeline->getClipPosition(cid4) == 7); }; state1(); // this move is possible REQUIRE(timeline->requestClipMove(cid1, tid2, 8)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 2); REQUIRE(timeline->getTrackClipsCount(tid3) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid2); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid2); REQUIRE(timeline->getClipTrackId(cid4) == tid3); REQUIRE(timeline->getClipPosition(cid1) == 8); REQUIRE(timeline->getClipPosition(cid2) == length + 3); REQUIRE(timeline->getClipPosition(cid3) == 2 * length + 5 + 8); REQUIRE(timeline->getClipPosition(cid4) == 4 + 8); }; state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); REQUIRE(timeline->requestClipMove(cid1, tid1, 3)); state1(); } SECTION("Group move consecutive clips") { REQUIRE(timeline->requestClipMove(cid1, tid1, 7)); REQUIRE(timeline->requestClipMove(cid2, tid1, 7 + length)); REQUIRE(timeline->requestClipMove(cid3, tid1, 7 + 2 * length)); REQUIRE(timeline->requestClipMove(cid4, tid1, 7 + 3 * length)); REQUIRE(timeline->requestClipsGroup({cid1, cid2, cid3, cid4})); auto state = [&](int tid, int start) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid) == 4); int i = 0; for (int cid : std::vector({cid1, cid2, cid3, cid4})) { REQUIRE(timeline->getClipTrackId(cid) == tid); REQUIRE(timeline->getClipPosition(cid) == start + i * length); REQUIRE(timeline->getClipPlaytime(cid) == length); i++; } }; state(tid1, 7); auto check_undo = [&](int target, int tid, int oldTid) { state(tid, target); undoStack->undo(); state(oldTid, 7); undoStack->redo(); state(tid, target); undoStack->undo(); state(oldTid, 7); }; REQUIRE(timeline->requestClipMove(cid1, tid1, 6)); qDebug() << "state1"; state(tid1, 6); undoStack->undo(); state(tid1, 7); undoStack->redo(); state(tid1, 6); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); qDebug() << "state2"; state(tid1, 0); undoStack->undo(); state(tid1, 6); undoStack->redo(); state(tid1, 0); undoStack->undo(); state(tid1, 6); undoStack->undo(); state(tid1, 7); REQUIRE(timeline->requestClipMove(cid3, tid1, 1 + 2 * length)); qDebug() << "state3"; check_undo(1, tid1, tid1); REQUIRE(timeline->requestClipMove(cid4, tid1, 4 + 3 * length)); qDebug() << "state4"; check_undo(4, tid1, tid1); REQUIRE(timeline->requestClipMove(cid4, tid1, 11 + 3 * length)); qDebug() << "state5"; check_undo(11, tid1, tid1); REQUIRE(timeline->requestClipMove(cid2, tid1, 13 + length)); qDebug() << "state6"; check_undo(13, tid1, tid1); REQUIRE(timeline->requestClipMove(cid1, tid1, 20)); qDebug() << "state7"; check_undo(20, tid1, tid1); REQUIRE(timeline->requestClipMove(cid4, tid1, 7 + 4 * length)); qDebug() << "state8"; check_undo(length + 7, tid1, tid1); REQUIRE(timeline->requestClipMove(cid2, tid1, 7 + 2 * length)); qDebug() << "state9"; check_undo(length + 7, tid1, tid1); REQUIRE(timeline->requestClipMove(cid1, tid1, 7 + length)); qDebug() << "state10"; check_undo(length + 7, tid1, tid1); REQUIRE(timeline->requestClipMove(cid2, tid2, 8 + length)); qDebug() << "state11"; check_undo(8, tid2, tid1); } SECTION("Group move to unavailable track") { REQUIRE(timeline->requestClipMove(cid1, tid1, 10)); REQUIRE(timeline->requestClipMove(cid2, tid2, 12)); REQUIRE(timeline->requestClipsGroup({cid1, cid2})); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid2) == tid2); REQUIRE(timeline->getClipPosition(cid1) == 10); REQUIRE(timeline->getClipPosition(cid2) == 12); }; state(); REQUIRE_FALSE(timeline->requestClipMove(cid2, tid1, 10)); state(); REQUIRE_FALSE(timeline->requestClipMove(cid2, tid1, 100)); state(); REQUIRE_FALSE(timeline->requestClipMove(cid1, tid3, 100)); state(); } SECTION("Group move with non-consecutive track ids") { int tid5 = TrackModel::construct(timeline); int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); Q_UNUSED(cid6); int tid6 = TrackModel::construct(timeline); REQUIRE(tid5 + 1 != tid6); REQUIRE(timeline->requestClipMove(cid1, tid5, 10)); REQUIRE(timeline->requestClipMove(cid2, tid5, length + 10)); REQUIRE(timeline->requestClipsGroup({cid1, cid2})); auto state = [&](int t) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(t) == 2); REQUIRE(timeline->getClipTrackId(cid1) == t); REQUIRE(timeline->getClipTrackId(cid2) == t); REQUIRE(timeline->getClipPosition(cid1) == 10); REQUIRE(timeline->getClipPosition(cid2) == 10 + length); }; state(tid5); REQUIRE(timeline->requestClipMove(cid1, tid6, 10)); state(tid6); } SECTION("Movement of AV groups") { int tid6b = TrackModel::construct(timeline, -1, -1, QString(), true); int tid6 = TrackModel::construct(timeline, -1, -1, QString(), true); int tid5 = TrackModel::construct(timeline); int tid5b = TrackModel::construct(timeline); QString binId3 = createProducerWithSound(profile_model, binModel); int cid6 = -1; REQUIRE(timeline->requestClipInsertion(binId3, tid5, 3, cid6, true, true, false)); int cid7 = timeline->m_groups->getSplitPartner(cid6); auto state = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5) == 1); REQUIRE(timeline->getTrackClipsCount(tid6) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid5); REQUIRE(timeline->getClipTrackId(cid7) == tid6); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly); }; state(3); // simple translation on the right REQUIRE(timeline->requestClipMove(cid6, tid5, 10, true, true, true)); state(10); undoStack->undo(); state(3); undoStack->redo(); state(10); // simple translation on the left, moving the audio clip this time REQUIRE(timeline->requestClipMove(cid7, tid6, 1, true, true, true)); state(1); undoStack->undo(); state(10); undoStack->redo(); state(1); // change track, moving video REQUIRE(timeline->requestClipMove(cid6, tid5b, 7, true, true, true)); auto state2 = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5b) == 1); REQUIRE(timeline->getTrackClipsCount(tid6b) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid5b); REQUIRE(timeline->getClipTrackId(cid7) == tid6b); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly); }; state2(7); undoStack->undo(); state(1); undoStack->redo(); state2(7); // change track, moving audio REQUIRE(timeline->requestClipMove(cid7, tid6b, 2, true, true, true)); state2(2); undoStack->undo(); state2(7); undoStack->redo(); state2(2); undoStack->undo(); undoStack->undo(); state(1); // Switching audio and video, going to the extra track REQUIRE(timeline->requestClipMove(cid7, tid5b, 2, true, true, true) == 0); // This test is invalid. AV clips cannot be switched between audio and video clips anymore /*auto state3 = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5b) == 1); REQUIRE(timeline->getTrackClipsCount(tid6b) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid6b); REQUIRE(timeline->getClipTrackId(cid7) == tid5b); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::AudioOnly); }; state3(2); undoStack->undo(); state(1); undoStack->redo(); state3(2); undoStack->undo(); state(1);*/ // Switching audio and video, switching tracks in place REQUIRE(timeline->requestClipMove(cid6, tid6, 1, true, true, true) == 0); /*auto state4 = [&](int pos) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid5) == 1); REQUIRE(timeline->getTrackClipsCount(tid6) == 1); REQUIRE(timeline->getClipTrackId(cid6) == tid6); REQUIRE(timeline->getClipTrackId(cid7) == tid5); REQUIRE(timeline->getClipPosition(cid6) == pos); REQUIRE(timeline->getClipPosition(cid7) == pos); REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::VideoOnly); REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::AudioOnly); }; state4(1); undoStack->undo(); state(1); undoStack->redo(); state4(1);*/ } SECTION("Clip copy") { int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int l = timeline->getClipPlaytime(cid6); REQUIRE(timeline->requestItemResize(cid6, l - 3, true, true, -1) == l - 3); REQUIRE(timeline->requestItemResize(cid6, l - 7, false, true, -1) == l - 7); int newId; std::function undo = []() { return true; }; std::function redo = []() { return true; }; REQUIRE(TimelineFunctions::copyClip(timeline, cid6, newId, PlaylistState::VideoOnly, undo, redo)); REQUIRE(timeline->m_allClips[cid6]->binId() == timeline->m_allClips[newId]->binId()); // TODO check effects } binModel->clean(); pCore->m_projectManager = nullptr; } TEST_CASE("Check id unicity", "[ClipModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel); std::vector track_ids; std::unordered_set all_ids; std::bernoulli_distribution coin(0.5); const int nbr = 20; for (int i = 0; i < nbr; i++) { if (coin(g)) { int tid = TrackModel::construct(timeline); REQUIRE(all_ids.count(tid) == 0); all_ids.insert(tid); track_ids.push_back(tid); REQUIRE(timeline->getTracksCount() == track_ids.size()); } else { int cid = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(all_ids.count(cid) == 0); all_ids.insert(cid); REQUIRE(timeline->getClipsCount() == all_ids.size() - track_ids.size()); } } REQUIRE(timeline->checkConsistency()); REQUIRE(all_ids.size() == nbr); REQUIRE(all_ids.size() != track_ids.size()); binModel->clean(); pCore->m_projectManager = nullptr; } TEST_CASE("Undo and Redo", "[ClipModel]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "blue", binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; int length = 20; int nclips = timeline->m_allClips.size(); SECTION("requestCreateClip") { // an invalid clip id shouldn't get created { int temp; Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE_FALSE(timeline->requestClipCreation("impossible bin id", temp, PlaylistState::VideoOnly, 1., undo, redo)); } auto state0 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips); }; state0(); QString binId3 = createProducer(profile_model, "green", binModel); int cid3; { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(timeline->requestClipCreation(binId3, cid3, PlaylistState::VideoOnly, 1., undo, redo)); pCore->pushUndo(undo, redo, QString()); } auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 1); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == -1); }; state1(); QString binId4 = binId3 + "/1/10"; int cid4; { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(timeline->requestClipCreation(binId4, cid4, PlaylistState::VideoOnly, 1., undo, redo)); pCore->pushUndo(undo, redo, QString()); } auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 2); REQUIRE(timeline->getClipPlaytime(cid4) == 10); REQUIRE(timeline->getClipTrackId(cid4) == -1); auto inOut = std::pair({1, 10}); REQUIRE(timeline->m_allClips.at(cid4)->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == -1); }; state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); } SECTION("requestInsertClip") { auto state0 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips); }; state0(); QString binId3 = createProducer(profile_model, "green", binModel); int cid3; REQUIRE(timeline->requestClipInsertion(binId3, tid1, 12, cid3, true)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 1); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid3) == 12); }; state1(); QString binId4 = binId3 + "/1/10"; int cid4; REQUIRE(timeline->requestClipInsertion(binId4, tid2, 17, cid4, true)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->m_allClips.size() == nclips + 2); REQUIRE(timeline->getClipPlaytime(cid4) == 10); REQUIRE(timeline->getClipTrackId(cid4) == tid2); REQUIRE(timeline->getClipPosition(cid4) == 17); auto inOut = std::pair({1, 10}); REQUIRE(timeline->m_allClips.at(cid4)->getInOut() == inOut); REQUIRE(timeline->getClipPlaytime(cid3) == length); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid3) == 12); }; state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); } int init_index = undoStack->index(); SECTION("Basic move undo") { REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_INSERT(Once); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(undoStack->index() == init_index + 2); // Move on same track does not trigger insert/remove row CHECK_MOVE(0); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_MOVE(0); undoStack->redo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(0); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_MOVE(0); REQUIRE(timeline->requestClipMove(cid1, tid1, 2 * length)); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 2 * length); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(0); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); CHECK_MOVE(0); undoStack->redo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 2 * length); REQUIRE(undoStack->index() == init_index + 2); CHECK_MOVE(0); undoStack->undo(); CHECK_MOVE(0); undoStack->undo(); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getClipTrackId(cid1) == -1); REQUIRE(undoStack->index() == init_index); CHECK_REMOVE(Once); } SECTION("Basic resize orphan clip undo") { REQUIRE(timeline->getClipPlaytime(cid2) == length); REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 5); REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->getClipPlaytime(cid2) == length - 10); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->getClipPlaytime(cid2) == length - 10); undoStack->undo(); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 5); undoStack->redo(); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->getClipPlaytime(cid2) == length - 10); undoStack->undo(); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipPlaytime(cid2) == length - 5); undoStack->undo(); REQUIRE(undoStack->index() == init_index); REQUIRE(timeline->getClipPlaytime(cid2) == length); } SECTION("Basic resize inserted clip undo") { REQUIRE(timeline->getClipPlaytime(cid2) == length); auto check = [&](int pos, int l) { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid2) == tid1); REQUIRE(timeline->getClipPlaytime(cid2) == l); REQUIRE(timeline->getClipPosition(cid2) == pos); }; REQUIRE(timeline->requestClipMove(cid2, tid1, 5)); INFO("Test 1"); check(5, length); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->requestItemResize(cid2, length - 5, true) == length - 5); INFO("Test 2"); check(5, length - 5); REQUIRE(undoStack->index() == init_index + 2); REQUIRE(timeline->requestItemResize(cid2, length - 10, false) == length - 10); INFO("Test 3"); check(10, length - 10); REQUIRE(undoStack->index() == init_index + 3); REQUIRE(timeline->requestItemResize(cid2, length, false) == -1); INFO("Test 4"); check(10, length - 10); REQUIRE(undoStack->index() == init_index + 3); undoStack->undo(); INFO("Test 5"); check(5, length - 5); REQUIRE(undoStack->index() == init_index + 2); undoStack->redo(); INFO("Test 6"); check(10, length - 10); REQUIRE(undoStack->index() == init_index + 3); undoStack->undo(); INFO("Test 7"); check(5, length - 5); REQUIRE(undoStack->index() == init_index + 2); undoStack->undo(); INFO("Test 8"); check(5, length); REQUIRE(undoStack->index() == init_index + 1); } SECTION("Clip Insertion Undo") { QString binId3 = createProducer(profile_model, "red", binModel); REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); }; state1(); int cid3; REQUIRE_FALSE(timeline->requestClipInsertion(binId3, tid1, 5, cid3)); state1(); REQUIRE_FALSE(timeline->requestClipInsertion(binId3, tid1, 6, cid3)); state1(); REQUIRE(timeline->requestClipInsertion(binId3, tid1, 5 + length, cid3)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(cid3) == 5 + length); REQUIRE(timeline->m_allClips[cid3]->isValid()); REQUIRE(undoStack->index() == init_index + 2); }; state2(); REQUIRE(timeline->requestClipMove(cid3, tid1, 10 + length)); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(cid3) == 10 + length); REQUIRE(undoStack->index() == init_index + 3); }; state3(); REQUIRE(timeline->requestItemResize(cid3, 1, true) == 1); auto state4 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 2); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(cid3) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPlaytime(cid3) == 1); REQUIRE(timeline->getClipPosition(cid3) == 10 + length); REQUIRE(undoStack->index() == init_index + 4); }; state4(); undoStack->undo(); state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); undoStack->redo(); state3(); undoStack->redo(); state4(); undoStack->undo(); state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); } SECTION("Clip Deletion undo") { REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); }; state1(); int nbClips = timeline->getClipsCount(); REQUIRE(timeline->requestItemDeletion(cid1)); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getClipsCount() == nbClips - 1); REQUIRE(undoStack->index() == init_index + 2); }; state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); undoStack->undo(); state1(); } SECTION("Track insertion undo") { std::map orig_trackPositions, final_trackPositions; for (const auto &it : timeline->m_iteratorTable) { int track = it.first; int pos = timeline->getTrackPosition(track); orig_trackPositions[track] = pos; if (pos >= 1) pos++; final_trackPositions[track] = pos; } auto checkPositions = [&](const std::map &pos) { for (const auto &p : pos) { REQUIRE(timeline->getTrackPosition(p.first) == p.second); } }; checkPositions(orig_trackPositions); int new_tid; REQUIRE(timeline->requestTrackInsertion(1, new_tid)); checkPositions(final_trackPositions); undoStack->undo(); checkPositions(orig_trackPositions); undoStack->redo(); checkPositions(final_trackPositions); undoStack->undo(); checkPositions(orig_trackPositions); } SECTION("Track deletion undo") { int nb_clips = timeline->getClipsCount(); int nb_tracks = timeline->getTracksCount(); REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); auto state1 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(undoStack->index() == init_index + 1); REQUIRE(timeline->getClipsCount() == nb_clips); REQUIRE(timeline->getTracksCount() == nb_tracks); }; state1(); REQUIRE(timeline->requestTrackDeletion(tid1)); REQUIRE(timeline->getClipsCount() == nb_clips - 1); REQUIRE(timeline->getTracksCount() == nb_tracks - 1); undoStack->undo(); state1(); undoStack->redo(); REQUIRE(timeline->getClipsCount() == nb_clips - 1); REQUIRE(timeline->getTracksCount() == nb_tracks - 1); undoStack->undo(); state1(); } int clipCount = timeline->m_allClips.size(); SECTION("Clip creation and resize") { int cid6; auto state0 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount); REQUIRE(timeline->checkConsistency()); }; state0(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; REQUIRE(timeline->requestClipCreation(binId, cid6, PlaylistState::VideoOnly, 1., undo, redo)); pCore->pushUndo(undo, redo, QString()); } int l = timeline->getClipPlaytime(cid6); auto state1 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == -1); REQUIRE(timeline->getClipPlaytime(cid6) == l); }; state1(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; REQUIRE(timeline->requestItemResize(cid6, l - 5, true, true, undo, redo, false)); pCore->pushUndo(undo, redo, QString()); } auto state2 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == -1); REQUIRE(timeline->getClipPlaytime(cid6) == l - 5); }; state2(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; REQUIRE(timeline->requestClipMove(cid6, tid1, 7, true, true, undo, redo)); pCore->pushUndo(undo, redo, QString()); } auto state3 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == tid1); REQUIRE(timeline->getClipPosition(cid6) == 7); REQUIRE(timeline->getClipPlaytime(cid6) == l - 5); }; state3(); { std::function undo = []() { return true; }; std::function redo = []() { return true; }; REQUIRE(timeline->requestItemResize(cid6, l - 6, false, true, undo, redo, false)); pCore->pushUndo(undo, redo, QString()); } auto state4 = [&]() { REQUIRE(timeline->m_allClips.size() == clipCount + 1); REQUIRE(timeline->isClip(cid6)); REQUIRE(timeline->getClipTrackId(cid6) == tid1); REQUIRE(timeline->getClipPosition(cid6) == 8); REQUIRE(timeline->getClipPlaytime(cid6) == l - 6); }; state4(); undoStack->undo(); state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); undoStack->redo(); state3(); undoStack->redo(); state4(); } binModel->clean(); pCore->m_projectManager = nullptr; } TEST_CASE("Snapping", "[Snapping]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel, 50); QString binId2 = createProducer(profile_model, "blue", binModel); int tid1 = TrackModel::construct(timeline); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid2 = TrackModel::construct(timeline); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; int length = timeline->getClipPlaytime(cid1); int length2 = timeline->getClipPlaytime(cid2); SECTION("getBlankSizeNearClip") { REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 0); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == INT_MAX); REQUIRE(timeline->requestClipMove(cid1, tid1, 10)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 10); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == INT_MAX); REQUIRE(timeline->requestClipMove(cid2, tid1, 25 + length)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 10); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, false) == 15); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == 15); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, true) == INT_MAX); REQUIRE(timeline->requestClipMove(cid2, tid1, 10 + length)); // before REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, false) == 10); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, false) == 0); // after REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid1, true) == 0); REQUIRE(timeline->getTrackById(tid1)->getBlankSizeNearClip(cid2, true) == INT_MAX); } SECTION("Snap move to a single clip") { int beg = 30; // in the absence of other clips, a valid move shouldn't be modified for (int snap = -1; snap <= 5; ++snap) { REQUIRE(timeline->suggestClipMove(cid2, tid2, beg, snap) == beg); REQUIRE(timeline->suggestClipMove(cid2, tid2, beg + length, snap) == beg + length); REQUIRE(timeline->checkConsistency()); } // We add a clip in first track to create snap points REQUIRE(timeline->requestClipMove(cid1, tid1, beg)); // Now a clip in second track should snap to beginning auto check_snap = [&](int pos, int perturb, int snap) { if (snap >= perturb) { REQUIRE(timeline->suggestClipMove(cid2, tid2, pos + perturb, snap) == pos); REQUIRE(timeline->suggestClipMove(cid2, tid2, pos - perturb, snap) == pos); } else { REQUIRE(timeline->suggestClipMove(cid2, tid2, pos + perturb, snap) == pos + perturb); REQUIRE(timeline->suggestClipMove(cid2, tid2, pos - perturb, snap) == pos - perturb); } }; for (int snap = -1; snap <= 5; ++snap) { for (int perturb = 0; perturb <= 6; ++perturb) { // snap to beginning check_snap(beg, perturb, snap); check_snap(beg + length, perturb, snap); // snap to end check_snap(beg - length2, perturb, snap); check_snap(beg + length - length2, perturb, snap); REQUIRE(timeline->checkConsistency()); } } // Same test, but now clip is moved in position 0 first REQUIRE(timeline->requestClipMove(cid2, tid2, 0)); for (int snap = -1; snap <= 5; ++snap) { for (int perturb = 0; perturb <= 6; ++perturb) { // snap to beginning check_snap(beg, perturb, snap); check_snap(beg + length, perturb, snap); // snap to end check_snap(beg - length2, perturb, snap); check_snap(beg + length - length2, perturb, snap); REQUIRE(timeline->checkConsistency()); } } } binModel->clean(); pCore->m_projectManager = nullptr; } TEST_CASE("Advanced trimming operations", "[Trimming]") { auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; // We also mock timeline object to spy few functions and mock others TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); QString binId = createProducer(profile_model, "red", binModel); QString binId2 = createProducer(profile_model, "blue", binModel); QString binId3 = createProducerWithSound(profile_model, binModel); int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int tid3 = TrackModel::construct(timeline); // Add an audio track int tid4 = TrackModel::construct(timeline, -1, -1, QString(), true); int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); int cid3 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid4 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid5 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int cid7 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int audio1 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); int audio2 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); int audio3 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly); timeline->m_allClips[cid1]->m_endlessResize = false; timeline->m_allClips[cid2]->m_endlessResize = false; timeline->m_allClips[cid3]->m_endlessResize = false; timeline->m_allClips[cid4]->m_endlessResize = false; timeline->m_allClips[cid5]->m_endlessResize = false; timeline->m_allClips[cid6]->m_endlessResize = false; timeline->m_allClips[cid7]->m_endlessResize = false; SECTION("Clip splitting") { // Trivial split REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); int l = timeline->getClipPlaytime(cid2); REQUIRE(timeline->requestItemResize(cid2, l - 3, true) == l - 3); REQUIRE(timeline->requestItemResize(cid2, l - 5, false) == l - 5); REQUIRE(timeline->requestClipMove(cid2, tid1, l)); REQUIRE(timeline->requestClipMove(cid3, tid1, l + l - 5)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPlaytime(cid2) == l - 5); REQUIRE(timeline->getClipPlaytime(cid3) == l); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == l); REQUIRE(timeline->getClipPosition(cid3) == l + l - 5); REQUIRE(timeline->getClipPtr(cid2)->getIn() == 2); REQUIRE(timeline->getClipPtr(cid2)->getOut() == l - 4); }; state(); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 0)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 5 * l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l + l - 5)); state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid2, l + 4)); int splitted = timeline->getClipByPosition(tid1, l + 5); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPlaytime(cid2) == 4); REQUIRE(timeline->getClipPlaytime(splitted) == l - 9); REQUIRE(timeline->getClipPlaytime(cid3) == l); REQUIRE(timeline->getClipPosition(cid1) == 0); REQUIRE(timeline->getClipPosition(cid2) == l); REQUIRE(timeline->getClipPosition(splitted) == l + 4); REQUIRE(timeline->getClipPosition(cid3) == l + l - 5); REQUIRE(timeline->getClipPtr(cid2)->getIn() == 2); REQUIRE(timeline->getClipPtr(cid2)->getOut() == 5); REQUIRE(timeline->getClipPtr(splitted)->getIn() == 6); REQUIRE(timeline->getClipPtr(splitted)->getOut() == l - 4); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); } SECTION("Split and resize") { REQUIRE(timeline->requestClipMove(cid1, tid1, 5)); int l = timeline->getClipPlaytime(cid1); timeline->m_allClips[cid1]->m_endlessResize = false; auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPosition(cid1) == 5); }; state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid1, 9)); int splitted = timeline->getClipByPosition(tid1, 10); timeline->m_allClips[splitted]->m_endlessResize = false; auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(splitted) == tid1); REQUIRE(timeline->getClipPlaytime(cid1) == 4); REQUIRE(timeline->getClipPlaytime(splitted) == l - 4); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(splitted) == 9); }; state2(); REQUIRE(timeline->requestClipMove(splitted, tid2, 9, true, true)); REQUIRE(timeline->requestItemResize(splitted, l - 3, true, true) == -1); REQUIRE(timeline->requestItemResize(splitted, l, false, true) == l); REQUIRE(timeline->requestItemResize(cid1, 5, false, true) == -1); REQUIRE(timeline->requestItemResize(cid1, l, true, true) == l); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipTrackId(cid1) == tid1); REQUIRE(timeline->getClipTrackId(splitted) == tid2); REQUIRE(timeline->getClipPlaytime(cid1) == l); REQUIRE(timeline->getClipPlaytime(splitted) == l); REQUIRE(timeline->getClipPosition(cid1) == 5); REQUIRE(timeline->getClipPosition(splitted) == 5); }; state3(); undoStack->undo(); undoStack->undo(); undoStack->undo(); state2(); undoStack->undo(); state(); undoStack->redo(); state2(); undoStack->redo(); undoStack->redo(); undoStack->redo(); state3(); } SECTION("Clip splitting 2") { // More complex group structure split split int l = timeline->getClipPlaytime(cid2); REQUIRE(timeline->requestClipMove(cid1, tid1, 0)); REQUIRE(timeline->requestClipMove(cid2, tid1, l)); REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * l)); REQUIRE(timeline->requestClipMove(cid4, tid2, 0)); REQUIRE(timeline->requestClipMove(cid5, tid2, l)); REQUIRE(timeline->requestClipMove(cid6, tid2, 2 * l)); REQUIRE(timeline->requestClipMove(cid7, tid1, 200)); int gid1 = timeline->requestClipsGroup(std::unordered_set({cid1, cid4}), true, GroupType::Normal); int gid2 = timeline->requestClipsGroup(std::unordered_set({cid2, cid5}), true, GroupType::Normal); int gid3 = timeline->requestClipsGroup(std::unordered_set({cid3, cid6}), true, GroupType::Normal); int gid4 = timeline->requestClipsGroup(std::unordered_set({cid1, cid2, cid3, cid4, cid5, cid6, cid7}), true, GroupType::Normal); auto state = [&]() { REQUIRE(timeline->checkConsistency()); int p = 0; for (int c : std::vector({cid1, cid2, cid3})) { REQUIRE(timeline->getClipPlaytime(c) == l); REQUIRE(timeline->getClipTrackId(c) == tid1); REQUIRE(timeline->getClipPosition(c) == p); p += l; } p = 0; for (int c : std::vector({cid4, cid5, cid6})) { REQUIRE(timeline->getClipPlaytime(c) == l); REQUIRE(timeline->getClipTrackId(c) == tid2); REQUIRE(timeline->getClipPosition(c) == p); p += l; } REQUIRE(timeline->getClipPosition(cid7) == 200); REQUIRE(timeline->getClipTrackId(cid7) == tid1); REQUIRE(timeline->m_groups->getDirectChildren(gid1) == std::unordered_set({cid1, cid4})); REQUIRE(timeline->m_groups->getDirectChildren(gid2) == std::unordered_set({cid2, cid5})); REQUIRE(timeline->m_groups->getDirectChildren(gid3) == std::unordered_set({cid3, cid6})); REQUIRE(timeline->m_groups->getDirectChildren(gid4) == std::unordered_set({gid1, gid2, gid3, cid7})); REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2, cid3, cid4, cid5, cid6, cid7})); }; state(); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 0)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 5 * l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l)); REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 2 * l)); state(); REQUIRE(TimelineFunctions::requestClipCut(timeline, cid2, l + 4)); int splitted = timeline->getClipByPosition(tid1, l + 5); int splitted2 = timeline->getClipByPosition(tid2, l + 5); REQUIRE(splitted != splitted2); auto check_groups = [&]() { REQUIRE(timeline->m_groups->getDirectChildren(gid2) == std::unordered_set({splitted, splitted2})); REQUIRE(timeline->m_groups->getDirectChildren(gid3) == std::unordered_set({cid3, cid6})); REQUIRE(timeline->m_groups->getDirectChildren(gid4) == std::unordered_set({gid2, gid3, cid7})); REQUIRE(timeline->getGroupElements(cid3) == std::unordered_set({splitted, splitted2, cid3, cid6, cid7})); int g1b = timeline->m_groups->m_upLink[cid1]; int g2b = timeline->m_groups->m_upLink[cid2]; int g4b = timeline->m_groups->getRootId(cid1); REQUIRE(timeline->m_groups->getDirectChildren(g1b) == std::unordered_set({cid1, cid4})); REQUIRE(timeline->m_groups->getDirectChildren(g2b) == std::unordered_set({cid2, cid5})); REQUIRE(timeline->m_groups->getDirectChildren(g4b) == std::unordered_set({g1b, g2b})); REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2, cid4, cid5})); }; auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); int p = 0; for (int c : std::vector({cid1, cid2, cid3})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid2 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == tid1); REQUIRE(timeline->getClipPosition(c) == p); p += l; } p = 0; for (int c : std::vector({cid4, cid5, cid6})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid5 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == tid2); REQUIRE(timeline->getClipPosition(c) == p); p += l; } REQUIRE(timeline->getClipPosition(cid7) == 200); REQUIRE(timeline->getClipTrackId(cid7) == tid1); REQUIRE(timeline->getClipPosition(splitted) == l + 4); REQUIRE(timeline->getClipPlaytime(splitted) == l - 4); REQUIRE(timeline->getClipTrackId(splitted) == tid1); REQUIRE(timeline->getClipPosition(splitted2) == l + 4); REQUIRE(timeline->getClipPlaytime(splitted2) == l - 4); REQUIRE(timeline->getClipTrackId(splitted2) == tid2); check_groups(); }; state2(); REQUIRE(timeline->requestClipMove(splitted, tid1, l + 4 + 10, true, true)); REQUIRE(timeline->requestClipMove(cid1, tid2, 10, true, true)); auto state3 = [&]() { REQUIRE(timeline->checkConsistency()); int p = 0; for (int c : std::vector({cid1, cid2, cid3})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid2 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == (c == cid3 ? tid1 : tid2)); REQUIRE(timeline->getClipPosition(c) == p + 10); p += l; } p = 0; for (int c : std::vector({cid4, cid5, cid6})) { REQUIRE(timeline->getClipPlaytime(c) == (c == cid5 ? 4 : l)); REQUIRE(timeline->getClipTrackId(c) == (c == cid6 ? tid2 : tid3)); REQUIRE(timeline->getClipPosition(c) == p + 10); p += l; } REQUIRE(timeline->getClipPosition(cid7) == 210); REQUIRE(timeline->getClipTrackId(cid7) == tid1); REQUIRE(timeline->getClipPosition(splitted) == l + 4 + 10); REQUIRE(timeline->getClipPlaytime(splitted) == l - 4); REQUIRE(timeline->getClipTrackId(splitted) == tid1); REQUIRE(timeline->getClipPosition(splitted2) == l + 4 + 10); REQUIRE(timeline->getClipPlaytime(splitted2) == l - 4); REQUIRE(timeline->getClipTrackId(splitted2) == tid2); check_groups(); }; state3(); undoStack->undo(); undoStack->undo(); state2(); undoStack->undo(); state(); undoStack->redo(); state2(); undoStack->redo(); undoStack->redo(); state3(); } SECTION("Simple audio split") { int l = timeline->getClipPlaytime(audio1); REQUIRE(timeline->requestClipMove(audio1, tid1, 3)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPosition(audio1) == 3); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1})); }; state(); REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid4)); int splitted1 = timeline->getClipByPosition(tid4, 3); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPosition(audio1) == 3); REQUIRE(timeline->getClipPlaytime(splitted1) == l); REQUIRE(timeline->getClipPosition(splitted1) == 3); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getClipTrackId(splitted1) == tid4); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid4) == 1); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, splitted1})); int g1 = timeline->m_groups->getDirectAncestor(audio1); REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({audio1, splitted1})); REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); undoStack->undo(); state(); undoStack->redo(); state2(); // We also make sure that clips that are audio only cannot be further splitted REQUIRE(timeline->requestClipMove(cid1, tid1, l + 30)); // This is a color clip, shouldn't be splittable REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, cid1, tid2)); // Check we cannot split audio on a video track REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid2)); } SECTION("Split audio on a selection") { int l = timeline->getClipPlaytime(audio2); REQUIRE(timeline->requestClipMove(audio1, tid1, 0)); REQUIRE(timeline->requestClipMove(audio2, tid1, l)); REQUIRE(timeline->requestClipMove(audio3, tid1, 2 * l)); std::unordered_set selection{audio1, audio3, audio2}; REQUIRE(timeline->requestClipsGroup(selection, false, GroupType::Selection)); auto state = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPlaytime(audio2) == l); REQUIRE(timeline->getClipPlaytime(audio3) == l); REQUIRE(timeline->getClipPosition(audio1) == 0); REQUIRE(timeline->getClipPosition(audio2) == l); REQUIRE(timeline->getClipPosition(audio3) == l + l); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getClipTrackId(audio2) == tid1); REQUIRE(timeline->getClipTrackId(audio3) == tid1); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid2) == 0); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, audio2, audio3})); int sel = timeline->m_temporarySelectionGroup; // check that selection is preserved REQUIRE(sel != -1); REQUIRE(timeline->m_groups->getType(sel) == GroupType::Selection); }; state(); REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid4)); int splitted1 = timeline->getClipByPosition(tid4, 0); int splitted2 = timeline->getClipByPosition(tid4, l); int splitted3 = timeline->getClipByPosition(tid4, 2 * l); auto state2 = [&]() { REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPlaytime(audio1) == l); REQUIRE(timeline->getClipPlaytime(audio2) == l); REQUIRE(timeline->getClipPlaytime(audio3) == l); REQUIRE(timeline->getClipPosition(audio1) == 0); REQUIRE(timeline->getClipPosition(audio2) == l); REQUIRE(timeline->getClipPosition(audio3) == l + l); REQUIRE(timeline->getClipPlaytime(splitted1) == l); REQUIRE(timeline->getClipPlaytime(splitted2) == l); REQUIRE(timeline->getClipPlaytime(splitted3) == l); REQUIRE(timeline->getClipPosition(splitted1) == 0); REQUIRE(timeline->getClipPosition(splitted2) == l); REQUIRE(timeline->getClipPosition(splitted3) == l + l); REQUIRE(timeline->getClipTrackId(audio1) == tid1); REQUIRE(timeline->getClipTrackId(audio2) == tid1); REQUIRE(timeline->getClipTrackId(audio3) == tid1); REQUIRE(timeline->getClipTrackId(splitted1) == tid4); REQUIRE(timeline->getClipTrackId(splitted2) == tid4); REQUIRE(timeline->getClipTrackId(splitted3) == tid4); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid4) == 3); REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, splitted1, audio2, audio3, splitted2, splitted3})); int sel = timeline->m_temporarySelectionGroup; // check that selection is preserved REQUIRE(sel != -1); REQUIRE(timeline->m_groups->getType(sel) == GroupType::Selection); REQUIRE(timeline->m_groups->getRootId(audio1) == sel); REQUIRE(timeline->m_groups->getDirectChildren(sel).size() == 3); REQUIRE(timeline->m_groups->getLeaves(sel).size() == 6); int g1 = timeline->m_groups->getDirectAncestor(audio1); int g2 = timeline->m_groups->getDirectAncestor(audio2); int g3 = timeline->m_groups->getDirectAncestor(audio3); REQUIRE(timeline->m_groups->getDirectChildren(sel) == std::unordered_set({g1, g2, g3})); REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({audio1, splitted1})); REQUIRE(timeline->m_groups->getDirectChildren(g2) == std::unordered_set({audio2, splitted2})); REQUIRE(timeline->m_groups->getDirectChildren(g3) == std::unordered_set({audio3, splitted3})); REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(g2) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(g3) == GroupType::AVSplit); }; state2(); undoStack->undo(); state(); undoStack->redo(); state2(); } binModel->clean(); pCore->m_projectManager = nullptr; } diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp index 342bb3e17..dcb75ae48 100644 --- a/tests/test_utils.cpp +++ b/tests/test_utils.cpp @@ -1,42 +1,45 @@ #include "test_utils.hpp" +#include "logger.hpp" QString createProducer(Mlt::Profile &prof, std::string color, std::shared_ptr binModel, int length, bool limited) { + Logger::log_create_producer("test_producer", {color, binModel, length, limited}); std::shared_ptr producer = std::make_shared(prof, "color", color.c_str()); producer->set("length", length); producer->set("out", length - 1); REQUIRE(producer->is_valid()); QString binId = QString::number(binModel->getFreeClipId()); auto binClip = ProjectClip::construct(binId, QIcon(), binModel, producer); if (limited) { binClip->forceLimitedDuration(); } Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(binModel->addItem(binClip, binModel->getRootFolder()->clipId(), undo, redo)); return binId; } QString createProducerWithSound(Mlt::Profile &prof, std::shared_ptr binModel) { + Logger::log_create_producer("test_producer_sound", {binModel}); // std::shared_ptr producer = std::make_shared(prof, // QFileInfo("../tests/small.mkv").absoluteFilePath().toStdString().c_str()); // In case the test system does not have avformat support, we can switch to the integrated blipflash producer std::shared_ptr producer = std::make_shared(prof, "blipflash"); producer->set_in_and_out(0, 1); producer->set("kdenlive:duration", 2); REQUIRE(producer->is_valid()); QString binId = QString::number(binModel->getFreeClipId()); auto binClip = ProjectClip::construct(binId, QIcon(), binModel, producer); Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(binModel->addItem(binClip, binModel->getRootFolder()->clipId(), undo, redo)); return binId; }