diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index f5504310e..b67acbb1d 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1017 +1,1144 @@ /* 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/thumbjob.hpp" #include "jobs/loadjob.hpp" -#include +#include "jobs/thumbjob.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/clip.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/KoIconUtils.h" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" #include +#include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include 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_date = date; m_description = ClipController::description(); if (m_clipType == ClipType::Audio) { m_thumbnail = KoIconUtils::themedIcon(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } - if (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Image || m_clipType == ClipType::Video || m_clipType == ClipType::Playlist || m_clipType == ClipType::TextTemplate) { + if (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Image || m_clipType == ClipType::Video || + m_clipType == ClipType::Playlist || m_clipType == ClipType::TextTemplate) { pCore->bin()->addWatchFile(id, clipUrl()); } // 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); 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)description.attribute(QStringLiteral("type")).toInt(); if (m_clipType == ClipType::Audio) { m_thumbnail = KoIconUtils::themedIcon(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 - if (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Image || m_clipType == ClipType::Video || m_clipType == ClipType::Playlist || m_clipType == ClipType::TextTemplate) { + if (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Image || m_clipType == ClipType::Video || + m_clipType == ClipType::Playlist || m_clipType == ClipType::TextTemplate) { pCore->bin()->removeWatchFile(clipId(), clipUrl()); } m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); audioFrameCache.clear(); - // delete all timeline producers - std::map>::iterator itr = m_timelineProducers.begin(); - while (itr != m_timelineProducers.end()) { - itr = m_timelineProducers.erase(itr); - } } 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::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 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(); } int ProjectClip::frameDuration() const { GenTime d = duration(); return 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); 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 m_thumbsProducer.reset(); ThumbnailCache::get()->invalidateThumbsForClip(clipId()); pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), 150, -1, true, true); } else { - //TODO: check if another load job is running? + // TODO: check if another load job is running? QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { 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); + std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), + AbstractProjectItem::DataThumbnail); } } 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)); 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 = KoIconUtils::themedIcon(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)->onItemUpdated(std::static_pointer_cast(shared_from_this()), + AbstractProjectItem::DataDuration); } // Make sure we have a hash for this clip getFileHash(); - if (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Image || m_clipType == ClipType::Video || m_clipType == ClipType::Playlist || m_clipType == ClipType::TextTemplate) { + if (m_clipType == ClipType::AV || m_clipType == ClipType::Audio || m_clipType == ClipType::Image || m_clipType == ClipType::Video || + m_clipType == ClipType::Playlist || m_clipType == ClipType::TextTemplate) { pCore->bin()->addWatchFile(clipId(), clipUrl()); } // set parent again (some info need to be stored in producer) updateParent(parentItem().lock()); return true; } std::shared_ptr ProjectClip::thumbProducer() { QMutexLocker locker(&m_producerMutex); if (m_thumbsProducer) { return m_thumbsProducer; } if (clipType() == ClipType::Unknown) { return nullptr; } 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 + // TODO: when the original producer changes, we must reload this thumb producer Clip clip(*prod.get()); m_thumbsProducer = std::make_shared(clip.softClone(ClipController::getPassPropertiesList())); Mlt::Filter scaler(*prod->profile(), "swscale"); Mlt::Filter converter(*prod->profile(), "avcolor_space"); m_thumbsProducer->attach(scaler); m_thumbsProducer->attach(converter); } else { m_thumbsProducer = cloneProducer(pCore->thumbProfile()); } return m_thumbsProducer; } +void ProjectClip::createVideoMasterProducer() +{ + if (!m_videoProducer) { + m_videoProducer = cloneProducer(&pCore->getCurrentProfile()->profile()); + // disable audio but activate video + m_videoProducer->set("set.test_audio", 1); + m_videoProducer->set("set.test_image", 0); + } +} +std::shared_ptr ProjectClip::getTimelineProducer(int clipId, PlaylistState state, double speed) +{ + 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 + 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()); + m_audioProducers[clipId]->set("set.test_audio", 0); + m_audioProducers[clipId]->set("set.test_image", 1); + } + return std::shared_ptr(m_audioProducers[clipId]->cut()); + } + // we return the video producer + m_audioProducers.erase(clipId); + createVideoMasterProducer(); + return std::shared_ptr(m_videoProducer->cut()); + } + + // in that case, we need to create a warp producer, if we don't have one + m_audioProducers.erase(clipId); + + std::shared_ptr warpProducer; + if (m_timewarpProducers.count(clipId) > 0) { + if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) { + // the producer we have is good, use it ! + warpProducer = m_timewarpProducers[clipId]; + } + } + if (!warpProducer) { + QString resource = QString("timewarp:%1:%2").arg(speed).arg(originalProducer()->get("resource")); + warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), resource.toUtf8().constData())); + } + + 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; + return warpProducer; +} + +std::pair, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, + PlaylistState 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; + double 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::AudioOnly || timeWarp) { + // In that case, we must create copies + return {getTimelineProducer(clipId, state, speed), false}; + } + // if it's a video or disabled clip, we must make sure that its master clip matches our video master + if (!m_videoProducer) { + qDebug() << "Warning: weird, we found a video clip whose master is already loaded but we don't have any yet"; + createVideoMasterProducer(); + return {std::shared_ptr(m_videoProducer->cut(in, out)), false}; + } + if (QString::fromUtf8(m_videoProducer->get("id")) != QString::fromUtf8(master->parent().get("id"))) { + qDebug() << "Warning: weird, we found a video clip whose master is already loaded but doesn't match ours"; + return {std::shared_ptr(m_videoProducer->cut(in, out)), false}; + } + // We have a good id, this clip can be used + return {master, true}; + } else { + master->parent().set("loaded", 1); + if (state == PlaylistState::AudioOnly) { + m_audioProducers[clipId] = std::shared_ptr(&master->parent()); + return {master, true}; + } + if (timeWarp) { + m_timewarpProducers[clipId] = std::shared_ptr(&master->parent()); + return {master, true}; + } + if (!m_videoProducer) { + // good, we found a master video producer, and we didn't have any + m_videoProducer.reset(&master->parent()); + return {master, true}; + } + qDebug() << "Warning: weird, we found a video clip whose master is not loaded but we already have a master"; + return {std::shared_ptr(m_videoProducer->cut(in, out)), 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"; + double speed = 1.0; + if (QString::fromUtf8(master->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) { 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"); } return prod; } 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() - 1; } 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: "; + 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) { QMapIterator i(properties); QMap passProperties; bool refreshAnalysis = false; bool reload = false; bool refreshOnly = true; // Some properties also need to be passed to track producers QStringList timelineProperties; 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); + std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), + AbstractProjectItem::ClipStatus); refreshPanel = true; } 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("force_fps"); QStringList keys; keys << QStringLiteral("luma_duration") << QStringLiteral("luma_file") << QStringLiteral("fade") << QStringLiteral("ttl") << QStringLiteral("softness") << QStringLiteral("crop") << QStringLiteral("animation"); 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 pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); reloadProducer(); } else { // A proxy was requested, make sure to keep original url setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); pCore->jobManager()->startJob({clipId()}, -1, QString()); } } else if (properties.contains(QStringLiteral("resource")) || properties.contains(QStringLiteral("templatetext")) || properties.contains(QStringLiteral("autorotate"))) { // Clip resource changed, update thumbnail if (m_clipType != ClipType::Color) { reloadProducer(); } else { reload = true; } } if (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); + 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); + 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 reloadProducer(refreshOnly); if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->refreshClip(m_binId); } 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); return panel; } 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 hasEffects() ? QVariant("kdenlive-track_has_effect") : QVariant(); break; default: break; } 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, 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; } bool ProjectClip::isTransparent() const { if (m_clipType == ClipType::Text) { return true; } return m_clipType == ClipType::Image && getProducerIntProperty(QStringLiteral("kdenlive:transparency")) == 1; } 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::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) { Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.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/bin/projectclip.h b/src/bin/projectclip.h index 45210c2da..a46b438f5 100644 --- a/src/bin/projectclip.h +++ b/src/bin/projectclip.h @@ -1,250 +1,277 @@ /* 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 . */ #ifndef PROJECTCLIP_H #define PROJECTCLIP_H #include "abstractprojectitem.h" #include "definitions.h" #include "mltcontroller/clipcontroller.h" #include "timeline2/model/timelinemodel.hpp" #include #include #include #include class AudioStreamInfo; class ClipPropertiesController; class MarkerListModel; class ProjectFolder; class ProjectSubClip; class QDomElement; class QUndoCommand; namespace Mlt { class Producer; class Properties; } // namespace Mlt /** * @class ProjectClip * @brief Represents a clip in the project (not timeline). - * + * It will be displayed as a bin item that can be dragged onto the timeline. + * A single bin clip can be inserted several times on the timeline, and the ProjectClip + * keeps track of all the ids of the corresponding ClipModel. + * Note that because of a limitation in melt and AvFilter, it is currently difficult to + * mix the audio of two producers that are cut from the same master producer + * (that produces small but noticeable clicking artifacts) + * To workaround this, we need to have a master clip for each instance of the audio clip in the timeline. This class is tracking them all. This track also holds + * a master clip for each clip where the timewarp producer has been applied */ class ProjectClip : public AbstractProjectItem, public ClipController { Q_OBJECT public: friend class Bin; friend bool TimelineModel::checkConsistency(); // for testing /** * @brief Constructor; used when loading a project and the producer is already available. */ static std::shared_ptr construct(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer); /** * @brief Constructor. * @param description element describing the clip; the "kdenlive:id" attribute and "resource" property are used */ static std::shared_ptr construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model); protected: ProjectClip(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer); ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model); public: virtual ~ProjectClip(); void reloadProducer(bool refreshOnly = false); /** @brief Returns a unique hash identifier used to store clip thumbnails. */ // virtual void hash() = 0; /** @brief Returns this if @param id matches the clip's id or nullptr otherwise. */ std::shared_ptr clip(const QString &id) override; std::shared_ptr folder(const QString &id) override; std::shared_ptr getSubClip(int in, int out); /** @brief Returns this if @param ix matches the clip's index or nullptr otherwise. */ std::shared_ptr clipAt(int ix) override; /** @brief Returns the clip type as defined in definitions.h */ ClipType clipType() const; bool selfSoftDelete(Fun &undo, Fun &redo) override; /** @brief Check if clip has a parent folder with id id */ bool hasParent(const QString &id) const; ClipPropertiesController *buildProperties(QWidget *parent); QPoint zone() const override; /** @brief Returns true if we want to add an affine transition in timeline when dropping this clip. */ bool isTransparent() const; /** @brief Returns whether this clip has a url (=describes a file) or not. */ bool hasUrl() const; /** @brief Returns the clip's url. */ const QString url() const; /** @brief Returns the clip's duration. */ GenTime duration() const; int frameDuration() const; /** @brief Returns the original clip's fps. */ double getOriginalFps() const; bool rename(const QString &name, int column) override; QDomElement toXml(QDomDocument &document, bool includeMeta = false) override; // QVariant getData(DataType type) const override; /** @brief Sets thumbnail for this clip. */ void setThumbnail(const QImage &); QPixmap thumbnail(int width, int height); /** @brief Sets the MLT producer associated with this clip * @param producer The producer * @param replaceProducer If true, we replace existing producer with this one * @returns true if producer was changed * . */ bool setProducer(std::shared_ptr producer, bool replaceProducer); /** @brief Returns true if this clip already has a producer. */ bool isReady() const; /** @brief Returns this clip's producer. */ - std::shared_ptr< Mlt::Producer > thumbProducer(); + std::shared_ptr thumbProducer(); /** @brief Recursively disable/enable bin effects. */ void setBinEffectsEnabled(bool enabled) override; /** @brief Set properties on this clip. TODO: should we store all in MLT or use extra m_properties ?. */ void setProperties(const QMap &properties, bool refreshPanel = false); /** @brief Get an XML property from MLT produced xml. */ static QString getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue = QString()); QString getToolTip() const override; /** @brief The clip hash created from the clip's resource. */ const QString hash(); /** @brief Returns true if we are using a proxy for this clip. */ bool hasProxy() const; /** Cache for every audio Frame with 10 Bytes */ /** format is frame -> channel ->bytes */ QVariantList audioFrameCache; bool audioThumbCreated() const; void setWaitingStatus(const QString &id); /** @brief Returns true if the clip matched a condition, for example vcodec=mpeg1video. */ bool matches(const QString &condition); /** @brief Returns the number of audio channels. */ int audioChannels() const; /** @brief get data analysis value. */ QStringList updatedAnalysisData(const QString &name, const QString &data, int offset); QMap analysisData(bool withPrefix = false); /** @brief Returns the list of this clip's subclip's ids. */ QStringList subClipIds() const; /** @brief Delete cached audio thumb - needs to be recreated */ void discardAudioThumb(); /** @brief Get path for this clip's audio thumbnail */ const QString getAudioThumbPath(); /** @brief Returns true if this producer has audio and can be splitted on timeline*/ bool isSplittable() const; /** @brief Returns true if a clip corresponding to this bin is inserted in a timeline. Note that this function does not account for children, use TreeItem::accumulate if you want to get that information as well. */ bool isIncludedInTimeline() override; /** @brief Returns a list of all timeline clip ids for this bin clip */ QList timelineInstances() const; - std::shared_ptr timelineProducer(PlaylistState::ClipState state = PlaylistState::Original, int track = 1); + /** @brief This function returns a cut to the master producer associated to the timeline clip with given ID. + Each clip must have a different master producer (see comment of the class) + */ + std::shared_ptr getTimelineProducer(int clipId, PlaylistState st, double speed = 1.0); + + /* @brief This function should only be used at loading. It takes a producer that was read from mlt, and checks whether the master producer is already in + use. If yes, then we must create a new one, because of the mixing bug. In any case, we return a cut of the master that can be used in the timeline The + bool returned has the following sementic: + - if true, then the returned cut still possibly has effect on it. You need to rebuild the effectStack based on this + - if false, the the returned cut don't have effects anymore (it's a fresh one), so you need to reload effects from the old producer + */ + std::pair, bool> giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState state); + std::shared_ptr cloneProducer(Mlt::Profile *destProfile = nullptr); void updateTimelineClips(QVector roles); protected: friend class ClipModel; /** @brief This is a call-back called by a ClipModel when it is created @param timeline ptr to the pointer in which this ClipModel is inserted @param clipId id of the inserted clip */ void registerTimelineClip(std::weak_ptr timeline, int clipId); /* @brief update the producer to reflect new parent folder */ void updateParent(std::shared_ptr parent) override; /** @brief This is a call-back called by a ClipModel when it is deleted @param clipId id of the deleted clip */ void deregisterTimelineClip(int clipId); void emitProducerChanged(const QString &id, const std::shared_ptr &producer) override { emit producerChanged(id, producer); }; /** @brief Replace instance of this clip in timeline */ void updateChildProducers(); void replaceInTimeline(); void connectEffectStack(); public slots: /* @brief Store the audio thumbnails once computed. Note that the parameter is a value and not a reference, fill free to use it as a sink (use std::move to * avoid copy). */ void updateAudioThumbnail(QVariantList audioLevels); /** @brief Extract image thumbnails for timeline. */ void slotExtractImage(const QList &frames); private: /** @brief Generate and store file hash if not available. */ const QString getFileHash(); /** @brief Store clip url temporarily while the clip controller has not been created. */ QString m_temporaryUrl; std::shared_ptr m_thumbsProducer; QMutex m_producerMutex; QMutex m_thumbMutex; QFuture m_thumbThread; QList m_requestedThumbs; const QString geometryWithOffset(const QString &data, int offset); void doExtractImage(); + // This is a helper function that creates the video producer. This is a clone of the original one, with audio disabled + void createVideoMasterProducer(); + std::map> m_registeredClips; - std::map> m_timelineProducers; + + // the following holds a producer for each audio clip in the timeline + // keys are the id of the clips in the timeline, values are their values + std::unordered_map> m_audioProducers; + std::unordered_map> m_timewarpProducers; + std::shared_ptr m_videoProducer; signals: void producerChanged(const QString &, const std::shared_ptr &); void refreshPropertiesPanel(); void refreshAnalysisPanel(); void refreshClipDisplay(); void thumbReady(int, const QImage &); /** @brief Clip is ready, load properties. */ void loadPropertiesPanel(); }; #endif diff --git a/src/definitions.cpp b/src/definitions.cpp index 1736abb30..bf55e8768 100644 --- a/src/definitions.cpp +++ b/src/definitions.cpp @@ -1,160 +1,176 @@ /*************************************************************************** * 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 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 state) +{ + return {state == PlaylistState::VideoOnly, state == PlaylistState::AudioOnly}; +} +PlaylistState 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/definitions.h b/src/definitions.h index ea53f6777..7158b1c20 100644 --- a/src/definitions.h +++ b/src/definitions.h @@ -1,322 +1,324 @@ /*************************************************************************** * 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 * ***************************************************************************/ #ifndef DEFINITIONS_H #define DEFINITIONS_H #include "effectslist/effectslist.h" #include "gentime.h" #include "kdenlive_debug.h" #include #include #include #include #include #include const int MAXCLIPDURATION = 15000; namespace Kdenlive { enum MonitorId { NoMonitor = 0x01, ClipMonitor = 0x02, ProjectMonitor = 0x04, RecordMonitor = 0x08, StopMotionMonitor = 0x10, DvdMonitor = 0x20 }; const int DefaultThumbHeight = 100; } // namespace Kdenlive enum class GroupType { Normal, Selection, // in that case, the group is used to emulate a selection AVSplit, // in that case, the group links the audio and video of the same clip Leaf // This is a leaf (clip or composition) }; const QString groupTypeToStr(GroupType t); GroupType groupTypeFromStr(const QString &s); enum class ObjectType { TimelineClip, TimelineComposition, TimelineTrack, BinClip, NoItem }; using ObjectId = std::pair; enum OperationType { None = 0, WaitingForConfirm, MoveOperation, ResizeStart, ResizeEnd, RollingStart, RollingEnd, RippleStart, RippleEnd, FadeIn, FadeOut, TransitionStart, TransitionEnd, MoveGuide, KeyFrame, Seek, Spacer, RubberSelection, ScrollTimeline, ZoomTimeline }; -namespace PlaylistState { +enum class PlaylistState { VideoOnly = 1, AudioOnly = 2, Disabled = 3 }; +Q_DECLARE_METATYPE(PlaylistState) -enum ClipState { Original = 0, VideoOnly = 1, AudioOnly = 2, Disabled = 3 }; -} +// returns a pair corresponding to (video, audio) +std::pair stateToBool(PlaylistState state); +PlaylistState stateFromBool(std::pair av); namespace TimelineMode { enum EditMode { NormalEdit = 0, OverwriteEdit = 1, InsertEdit = 2 }; } enum class ClipType { Unknown = 0, Audio = 1, Video = 2, AV = 3, Color = 4, Image = 5, Text = 6, SlideShow = 7, Virtual = 8, Playlist = 9, WebVfx = 10, TextTemplate = 11, QText }; enum ProjectItemType { ProjectClipType = 0, ProjectFolderType, ProjectSubclipType }; enum GraphicsRectItem { AVWidget = 70000, LabelWidget, TransitionWidget, GroupWidget }; enum ProjectTool { SelectTool = 0, RazorTool = 1, SpacerTool = 2 }; enum MonitorSceneType { MonitorSceneNone = 0, MonitorSceneDefault, MonitorSceneGeometry, MonitorSceneCorners, MonitorSceneRoto, MonitorSceneSplit, MonitorSceneRipple }; enum MessageType { DefaultMessage, ProcessingJobMessage, OperationCompletedMessage, InformationMessage, ErrorMessage, MltError }; enum TrackType { AudioTrack = 0, VideoTrack = 1, AnyTrack = 2 }; enum CacheType { SystemCacheRoot = -1, CacheRoot = 0, CacheBase = 1, CachePreview = 2, CacheProxy = 3, CacheAudio = 4, CacheThumbs = 5 }; enum TrimMode { NormalTrim, RippleTrim, RollingTrim, SlipTrim, SlideTrim }; class TrackInfo { public: TrackType type; QString trackName; bool isMute; bool isBlind; bool isLocked; int duration; EffectsList effectsList; TrackInfo() : type(VideoTrack) , isMute(0) , isBlind(0) , isLocked(0) , duration(0) , effectsList(true) { } }; struct requestClipInfo { QDomElement xml; QString clipId; int imageHeight; bool replaceProducer; bool operator==(const requestClipInfo &a) const { return clipId == a.clipId; } }; typedef QMap stringMap; typedef QMap> audioByteArray; typedef QVector audioShortVector; class ItemInfo { public: /** startPos is the position where the clip starts on the track */ GenTime startPos; /** endPos is the duration where the clip ends on the track */ GenTime endPos; /** cropStart is the position where the sub-clip starts, relative to the clip's 0 position */ GenTime cropStart; /** cropDuration is the duration of the clip */ GenTime cropDuration; /** Track number */ int track; ItemInfo() : track(0) { } bool isValid() const { return startPos != endPos; } bool contains(GenTime pos) const { if (startPos == endPos) { return true; } return (pos <= endPos && pos >= startPos); } bool operator==(const ItemInfo &a) const { return startPos == a.startPos && endPos == a.endPos && track == a.track && cropStart == a.cropStart; } }; class TransitionInfo { public: /** startPos is the position where the clip starts on the track */ GenTime startPos; /** endPos is the duration where the clip ends on the track */ GenTime endPos; /** the track on which the transition is (b_track)*/ int b_track; /** the track on which the transition is applied (a_track)*/ int a_track; /** Does the user request for a special a_track */ bool forceTrack; TransitionInfo() : b_track(0) , a_track(0) , forceTrack(0) { } }; class CommentedTime { public: CommentedTime(); CommentedTime(const GenTime &time, const QString &comment, int markerType = 0); CommentedTime(const QString &hash, const GenTime &time); QString comment() const; GenTime time() const; /** @brief Returns a string containing infos needed to store marker info. string equals marker type + QLatin1Char(':') + marker comment */ QString hash() const; void setComment(const QString &comm); void setMarkerType(int t); int markerType() const; static QColor markerColor(int type); /* Implementation of > operator; Works identically as with basic types. */ bool operator>(const CommentedTime &op) const; /* Implementation of < operator; Works identically as with basic types. */ bool operator<(const CommentedTime &op) const; /* Implementation of >= operator; Works identically as with basic types. */ bool operator>=(const CommentedTime &op) const; /* Implementation of <= operator; Works identically as with basic types. */ bool operator<=(const CommentedTime &op) const; /* Implementation of == operator; Works identically as with basic types. */ bool operator==(const CommentedTime &op) const; /* Implementation of != operator; Works identically as with basic types. */ bool operator!=(const CommentedTime &op) const; private: GenTime m_time; QString m_comment; int m_type; }; QDebug operator<<(QDebug qd, const ItemInfo &info); // we provide hash function for qstring and QPersistentModelIndex namespace std { template <> struct hash { std::size_t operator()(const QString &k) const { return qHash(k); } }; template <> struct hash { std::size_t operator()(const QPersistentModelIndex &k) const { return qHash(k); } }; } // namespace std // The following is a hack that allows to use shared_from_this in the case of a multiple inheritance. // Credit: https://stackoverflow.com/questions/14939190/boost-shared-from-this-and-multiple-inheritance template struct enable_shared_from_this_virtual; class enable_shared_from_this_virtual_base : public std::enable_shared_from_this { typedef std::enable_shared_from_this base_type; template friend struct enable_shared_from_this_virtual; std::shared_ptr shared_from_this() { return base_type::shared_from_this(); } std::shared_ptr shared_from_this() const { return base_type::shared_from_this(); } }; template struct enable_shared_from_this_virtual : virtual enable_shared_from_this_virtual_base { typedef enable_shared_from_this_virtual_base base_type; public: std::shared_ptr shared_from_this() { std::shared_ptr result(base_type::shared_from_this(), static_cast(this)); return result; } std::shared_ptr shared_from_this() const { std::shared_ptr result(base_type::shared_from_this(), static_cast(this)); return result; } }; // This is a small trick to have a QAbstractItemModel with shared_from_this enabled without multiple inheritance // Be careful, if you use this class, you have to make sure to init weak_this_ when you construct a shared_ptr to your object template class QAbstractItemModel_shared_from_this : public QAbstractItemModel { protected: QAbstractItemModel_shared_from_this() : QAbstractItemModel() { } public: std::shared_ptr shared_from_this() { std::shared_ptr p(weak_this_); assert(p.get() == this); return p; } std::shared_ptr shared_from_this() const { std::shared_ptr p(weak_this_); assert(p.get() == this); return p; } public: // actually private, but avoids compiler template friendship issues mutable std::weak_ptr weak_this_; }; #endif diff --git a/src/effects/effectstack/model/effectstackmodel.cpp b/src/effects/effectstack/model/effectstackmodel.cpp index dc968ed0c..0e2f1ef06 100644 --- a/src/effects/effectstack/model/effectstackmodel.cpp +++ b/src/effects/effectstack/model/effectstackmodel.cpp @@ -1,705 +1,677 @@ /*************************************************************************** * 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 "effectstackmodel.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "effectgroupmodel.hpp" #include "effectitemmodel.hpp" #include "effects/effectsrepository.hpp" #include "macros.hpp" #include #include #include EffectStackModel::EffectStackModel(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack) : AbstractTreeModel() , m_service(std::move(service)) , m_effectStackEnabled(true) , m_ownerId(ownerId) , m_undoStack(undo_stack) , m_loadingExisting(false) { } std::shared_ptr EffectStackModel::construct(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack) { std::shared_ptr self(new EffectStackModel(std::move(service), ownerId, undo_stack)); self->rootItem = EffectGroupModel::construct(QStringLiteral("root"), self, true); return self; } -void EffectStackModel::loadEffects() -{ - auto ptr = m_service.lock(); - m_loadingExisting = true; - if (ptr) { - for (int i = 0; i < ptr->filter_count(); i++) { - if (ptr->filter(i)->get("kdenlive_id") == nullptr) { - // don't consider internal MLT stuff - continue; - } - QString effectId = ptr->filter(i)->get("kdenlive_id"); - auto effect = EffectItemModel::construct(ptr->filter(i), shared_from_this()); - // effect->setParameters - Fun redo = addItem_lambda(effect, rootItem->getId()); - redo(); - if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { - fadeIns << effect->getId(); - } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { - fadeOuts << effect->getId(); - } - connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); - connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); - } - } else { - qDebug() << "// CANNOT LOCK CLIP SEEVCE"; - } - this->modelChanged(); - m_loadingExisting = false; -} - void EffectStackModel::resetService(std::weak_ptr service) { m_service = std::move(service); // replant all effects in new service for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->plant(m_service); } } void EffectStackModel::removeEffect(std::shared_ptr effect) { Q_ASSERT(m_allItems.count(effect->getId()) > 0); int parentId = -1; if (auto ptr = effect->parentItem().lock()) parentId = ptr->getId(); int current = 0; bool currentChanged = false; if (auto srv = m_service.lock()) { current = srv->get_int("kdenlive:activeeffect"); if (current >= rootItem->childCount() - 1) { currentChanged = true; srv->set("kdenlive:activeeffect", --current); } } int currentRow = effect->row(); Fun undo = addItem_lambda(effect, parentId); if (currentRow != rowCount() - 1) { Fun move = moveItem_lambda(effect->getId(), currentRow, true); PUSH_LAMBDA(move, undo); } Fun redo = removeItem_lambda(effect->getId()); bool res = redo(); if (res) { int inFades = fadeIns.size(); int outFades = fadeOuts.size(); - fadeIns.removeAll(effect->getId()); - fadeOuts.removeAll(effect->getId()); + fadeIns.erase(effect->getId()); + fadeOuts.erase(effect->getId()); inFades = fadeIns.size() - inFades; outFades = fadeOuts.size() - outFades; QString effectName = EffectsRepository::get()->getName(effect->getAssetId()); Fun update = [this, current, currentChanged, inFades, outFades]() { // Required to build the effect view if (currentChanged) { if (current < 0 || rowCount() == 0) { // Stack is now empty emit dataChanged(QModelIndex(), QModelIndex(), QVector()); } else { std::shared_ptr effectItem = std::static_pointer_cast(rootItem->child(current)); QModelIndex ix = getIndexFromItem(effectItem); emit dataChanged(ix, ix, QVector()); } } - //TODO: only update if effect is fade or keyframe + // TODO: only update if effect is fade or keyframe if (inFades < 0) { pCore->updateItemModel(m_ownerId, QStringLiteral("fadein")); - } - else if (outFades < 0) { + } else if (outFades < 0) { pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout")); } pCore->updateItemKeyframes(m_ownerId); return true; }; update(); PUSH_LAMBDA(update, redo); PUSH_LAMBDA(update, undo); PUSH_UNDO(undo, redo, i18n("Delete effect %1", effectName)); } } -void EffectStackModel::copyEffect(std::shared_ptr sourceItem) +void EffectStackModel::copyEffect(std::shared_ptr sourceItem, bool logUndo) { if (sourceItem->childCount() > 0) { // TODO: group return; } std::shared_ptr sourceEffect = std::static_pointer_cast(sourceItem); - QString effectId = sourceEffect->getAssetId(); + const QString effectId = sourceEffect->getAssetId(); auto effect = EffectItemModel::construct(effectId, shared_from_this()); effect->setParameters(sourceEffect->getAllParameters()); + effect->filter().set("in", sourceEffect->filter().get_int("in")); + effect->filter().set("out", sourceEffect->filter().get_int("out")); Fun undo = removeItem_lambda(effect->getId()); // TODO the parent should probably not always be the root Fun redo = addItem_lambda(effect, rootItem->getId()); connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); + if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { + fadeIns.insert(effect->getId()); + } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { + fadeOuts.insert(effect->getId()); + } bool res = redo(); - if (res) { + if (res && logUndo) { QString effectName = EffectsRepository::get()->getName(effectId); PUSH_UNDO(undo, redo, i18n("copy effect %1", effectName)); } } void EffectStackModel::appendEffect(const QString &effectId, bool makeCurrent) { auto effect = EffectItemModel::construct(effectId, shared_from_this()); Fun undo = removeItem_lambda(effect->getId()); // TODO the parent should probably not always be the root Fun redo = addItem_lambda(effect, rootItem->getId()); connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); int currentActive = getActiveEffect(); if (makeCurrent) { auto srvPtr = m_service.lock(); if (srvPtr) { srvPtr->set("kdenlive:activeeffect", rowCount()); } } bool res = redo(); if (res) { int inFades = 0; int outFades = 0; if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { - fadeIns << effect->getId(); + fadeIns.insert(effect->getId()); inFades++; } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { - fadeOuts << effect->getId(); - outFades ++; + fadeOuts.insert(effect->getId()); + outFades++; } QString effectName = EffectsRepository::get()->getName(effectId); Fun update = [this, inFades, outFades]() { - //TODO: only update if effect is fade or keyframe + // TODO: only update if effect is fade or keyframe if (inFades > 0) { pCore->updateItemModel(m_ownerId, QStringLiteral("fadein")); - } - else if (outFades > 0) { + } else if (outFades > 0) { pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout")); } pCore->updateItemKeyframes(m_ownerId); emit dataChanged(QModelIndex(), QModelIndex(), QVector()); return true; }; update(); PUSH_LAMBDA(update, redo); PUSH_LAMBDA(update, undo); PUSH_UNDO(undo, redo, i18n("Add effect %1", effectName)); } else if (makeCurrent) { auto srvPtr = m_service.lock(); if (srvPtr) { srvPtr->set("kdenlive:activeeffect", currentActive); } } } bool EffectStackModel::adjustStackLength(bool adjustFromEnd, int oldIn, int oldDuration, int newIn, int duration, Fun &undo, Fun &redo, bool logUndo) { const int fadeInDuration = getFadePosition(true); const int fadeOutDuration = getFadePosition(false); QList indexes; auto ptr = m_service.lock(); int out = newIn + duration; for (int i = 0; i < rootItem->childCount(); ++i) { - if (fadeInDuration > 0 && fadeIns.contains(std::static_pointer_cast(rootItem->child(i))->getId())) { + if (fadeInDuration > 0 && fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); int oldEffectIn = qMax(0, effect->filter().get_in()); int oldEffectOut = effect->filter().get_out(); - qDebug()<<"--previous effect: "<filter().get_length() - 1, duration); indexes << getIndexFromItem(effect); if (!adjustFromEnd && (oldIn != newIn || duration != oldDuration)) { // Clip start was resized, adjust effect in / out Fun operation = [this, effect, newIn, effectDuration, logUndo]() { effect->setParameter(QStringLiteral("in"), newIn, false); effect->setParameter(QStringLiteral("out"), newIn + effectDuration, logUndo); - qDebug()<<"--new effect: "<setParameter(QStringLiteral("in"), oldEffectIn, false); effect->setParameter(QStringLiteral("out"), oldEffectOut, logUndo); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } else if (effectDuration < oldEffectOut - oldEffectIn || (logUndo && effect->filter().get_int("_refout") > 0)) { // Clip length changed, shorter than effect length so resize int referenceEffectOut = effect->filter().get_int("_refout"); if (referenceEffectOut <= 0) { referenceEffectOut = oldEffectOut; effect->filter().set("_refout", referenceEffectOut); } Fun operation = [this, effect, oldEffectIn, effectDuration, logUndo]() { effect->setParameter(QStringLiteral("out"), oldEffectIn + effectDuration, logUndo); return true; }; if (operation() && logUndo) { Fun reverse = [this, effect, referenceEffectOut]() { effect->setParameter(QStringLiteral("out"), referenceEffectOut, true); - effect->filter().set("_refout", (char*) nullptr); + effect->filter().set("_refout", (char *)nullptr); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } } - } else if (fadeOutDuration > 0 && fadeOuts.contains(std::static_pointer_cast(rootItem->child(i))->getId())) { + } else if (fadeOutDuration > 0 && fadeOuts.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); int effectDuration = qMin(fadeOutDuration, duration); int newFadeIn = out - effectDuration; int oldFadeIn = effect->filter().get_int("in"); int oldOut = effect->filter().get_int("out"); int referenceEffectIn = effect->filter().get_int("_refin"); if (referenceEffectIn <= 0) { referenceEffectIn = oldFadeIn; effect->filter().set("_refin", referenceEffectIn); } Fun operation = [this, effect, newFadeIn, out, logUndo]() { effect->setParameter(QStringLiteral("in"), newFadeIn, false); effect->setParameter(QStringLiteral("out"), out, logUndo); return true; }; if (operation() && logUndo) { Fun reverse = [this, effect, referenceEffectIn, oldOut]() { effect->setParameter(QStringLiteral("in"), referenceEffectIn, false); effect->setParameter(QStringLiteral("out"), oldOut, true); - effect->filter().set("_refin", (char*) nullptr); + effect->filter().set("_refin", (char *)nullptr); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } indexes << getIndexFromItem(effect); } } /*Fun checkLength = [this, newIn, duration, out]() { pCore->adjustAssetRange(m_ownerId.second, newIn, newIn + duration); return true; }; Fun checkOldLength = [this, oldIn, duration, out]() { pCore->adjustAssetRange(m_ownerId.second, oldIn, oldIn + duration); return true; }; if (logUndo && checkLength()) { PUSH_LAMBDA(checkOldLength, redo); PUSH_LAMBDA(checkLength, undo); }*/ /*if (!indexes.isEmpty()) { emit dataChanged(indexes.first(), indexes.last(), QVector()); }*/ return true; } bool EffectStackModel::adjustFadeLength(int duration, bool fromStart, bool audioFade, bool videoFade) { if (fromStart) { // Fade in - if (fadeIns.isEmpty()) { + if (fadeIns.empty()) { if (audioFade) { appendEffect(QStringLiteral("fadein")); } if (videoFade) { appendEffect(QStringLiteral("fade_from_black")); } } QList indexes; auto ptr = m_service.lock(); int in = 0; if (ptr) { in = ptr->get_int("in"); } - qDebug()<<"//// SETTING CLIP FADIN: "<childCount(); ++i) { - if (fadeIns.contains(std::static_pointer_cast(rootItem->child(i))->getId())) { + if (fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); effect->filter().set("in", in); duration = qMin(pCore->getItemDuration(m_ownerId), duration); effect->filter().set("out", in + duration); indexes << getIndexFromItem(effect); } } if (!indexes.isEmpty()) { emit dataChanged(indexes.first(), indexes.last(), QVector()); pCore->updateItemModel(m_ownerId, QStringLiteral("fadein")); } } else { // Fade out - if (fadeOuts.isEmpty()) { + if (fadeOuts.empty()) { if (audioFade) { appendEffect(QStringLiteral("fadeout")); } if (videoFade) { appendEffect(QStringLiteral("fade_to_black")); } } int in = 0; auto ptr = m_service.lock(); if (ptr) { in = ptr->get_int("in"); } int out = in + pCore->getItemDuration(m_ownerId); QList indexes; for (int i = 0; i < rootItem->childCount(); ++i) { - if (fadeOuts.contains(std::static_pointer_cast(rootItem->child(i))->getId())) { + if (fadeOuts.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); effect->filter().set("out", out); duration = qMin(pCore->getItemDuration(m_ownerId), duration); effect->filter().set("in", out - duration); indexes << getIndexFromItem(effect); } } if (!indexes.isEmpty()) { - qDebug()<<"// UPDATING DATA INDEXES 2!!!"; + qDebug() << "// UPDATING DATA INDEXES 2!!!"; emit dataChanged(indexes.first(), indexes.last(), QVector()); pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout")); } } return true; } int EffectStackModel::getFadePosition(bool fromStart) { if (fromStart) { - if (fadeIns.isEmpty()) { + if (fadeIns.empty()) { return 0; } for (int i = 0; i < rootItem->childCount(); ++i) { - if (fadeIns.first() == std::static_pointer_cast(rootItem->child(i))->getId()) { + if (*(fadeIns.begin()) == std::static_pointer_cast(rootItem->child(i))->getId()) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); return effect->filter().get_length(); } } } else { - if (fadeOuts.isEmpty()) { + if (fadeOuts.empty()) { return 0; } for (int i = 0; i < rootItem->childCount(); ++i) { - if (fadeOuts.first() == std::static_pointer_cast(rootItem->child(i))->getId()) { + if (*(fadeOuts.begin()) == std::static_pointer_cast(rootItem->child(i))->getId()) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); return effect->filter().get_length(); } } } return 0; } bool EffectStackModel::removeFade(bool fromStart) { - QList toRemove; + std::vector toRemove; for (int i = 0; i < rootItem->childCount(); ++i) { - if ((fromStart && fadeIns.contains(std::static_pointer_cast(rootItem->child(i))->getId())) || - (!fromStart && fadeOuts.contains(std::static_pointer_cast(rootItem->child(i))->getId()))) { - toRemove << i; + if ((fromStart && fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) || + (!fromStart && fadeOuts.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0)) { + toRemove.push_back(i); } } - qSort(toRemove.begin(), toRemove.end(), qGreater()); - foreach (int i, toRemove) { + for (int i : toRemove) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); if (fromStart) { - fadeIns.removeAll(rootItem->child(i)->getId()); + fadeIns.erase(rootItem->child(i)->getId()); } else { - fadeOuts.removeAll(rootItem->child(i)->getId()); + fadeOuts.erase(rootItem->child(i)->getId()); } removeEffect(effect); } return true; } void EffectStackModel::moveEffect(int destRow, std::shared_ptr item) { Q_ASSERT(m_allItems.count(item->getId()) > 0); int oldRow = item->row(); Fun undo = moveItem_lambda(item->getId(), oldRow); Fun redo = moveItem_lambda(item->getId(), destRow); bool res = redo(); if (res) { Fun update = [this]() { this->dataChanged(QModelIndex(), QModelIndex(), {}); return true; }; update(); UPDATE_UNDO_REDO(update, update, undo, redo); auto effectId = std::static_pointer_cast(item)->getAssetId(); QString effectName = EffectsRepository::get()->getName(effectId); PUSH_UNDO(undo, redo, i18n("Move effect %1", effectName)); } } void EffectStackModel::registerItem(const std::shared_ptr &item) { QModelIndex ix; if (!item->isRoot()) { auto effectItem = std::static_pointer_cast(item); if (!m_loadingExisting) { effectItem->plant(m_service); } effectItem->setEffectStackEnabled(m_effectStackEnabled); ix = getIndexFromItem(effectItem); if (!effectItem->isAudio() && !m_loadingExisting) { pCore->refreshProjectItem(m_ownerId); pCore->invalidateItem(m_ownerId); } } AbstractTreeModel::registerItem(item); if (ix.isValid()) { // Required to build the effect view emit dataChanged(ix, ix, QVector()); } } void EffectStackModel::deregisterItem(int id, TreeItem *item) { if (!item->isRoot()) { auto effectItem = static_cast(item); effectItem->unplant(this->m_service); if (!effectItem->isAudio()) { pCore->refreshProjectItem(m_ownerId); pCore->invalidateItem(m_ownerId); } } AbstractTreeModel::deregisterItem(id, item); } void EffectStackModel::setEffectStackEnabled(bool enabled) { m_effectStackEnabled = enabled; // Recursively updates children states for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->setEffectStackEnabled(enabled); } } std::shared_ptr EffectStackModel::getEffectStackRow(int row, std::shared_ptr parentItem) { return std::static_pointer_cast(parentItem ? rootItem->child(row) : rootItem->child(row)); } void EffectStackModel::importEffects(std::shared_ptr sourceStack) { // TODO: manage fades, keyframes if clips don't have same size / in point for (int i = 0; i < sourceStack->rowCount(); i++) { auto item = sourceStack->getEffectStackRow(i); - if (item->childCount() > 0) { - // TODO: group - continue; - } - std::shared_ptr effect = std::static_pointer_cast(item); - auto clone = EffectItemModel::construct(effect->getAssetId(), shared_from_this()); - rootItem->appendChild(clone); - clone->setParameters(effect->getAllParameters()); - clone->filter().set("in", effect->filter().get_int("in")); - clone->filter().set("out", effect->filter().get_int("out")); - const QString effectId = effect->getAssetId(); - if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { - fadeIns << clone->getId(); - } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { - fadeOuts << clone->getId(); + copyEffect(item, false); + } + modelChanged(); +} + +void EffectStackModel::importEffects(std::weak_ptr service, bool alreadyExist) +{ + m_loadingExisting = alreadyExist; + if (auto ptr = service.lock()) { + for (int i = 0; i < ptr->filter_count(); i++) { + if (ptr->filter(i)->get("kdenlive_id") == nullptr) { + // don't consider internal MLT stuff + continue; + } + QString effectId = ptr->filter(i)->get("kdenlive_id"); + auto effect = EffectItemModel::construct(ptr->filter(i), shared_from_this()); + copyEffect(effect, false); } - // TODO parent should not always be root - Fun redo = addItem_lambda(clone, rootItem->getId()); - connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); - connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); - redo(); } + m_loadingExisting = false; + modelChanged(); } void EffectStackModel::setActiveEffect(int ix) { auto ptr = m_service.lock(); if (ptr) { ptr->set("kdenlive:activeeffect", ix); } pCore->updateItemKeyframes(m_ownerId); } int EffectStackModel::getActiveEffect() const { auto ptr = m_service.lock(); if (ptr) { return ptr->get_int("kdenlive:activeeffect"); } return 0; } void EffectStackModel::slotCreateGroup(std::shared_ptr childEffect) { auto groupItem = EffectGroupModel::construct(QStringLiteral("group"), shared_from_this()); rootItem->appendChild(groupItem); groupItem->appendChild(childEffect); } ObjectId EffectStackModel::getOwnerId() const { return m_ownerId; } bool EffectStackModel::checkConsistency() { if (!AbstractTreeModel::checkConsistency()) { return false; } std::vector> allFilters; // We do a DFS on the tree to retrieve all the filters std::stack> stck; stck.push(std::static_pointer_cast(rootItem)); while (!stck.empty()) { auto current = stck.top(); stck.pop(); if (current->effectItemType() == EffectItemType::Effect) { if (current->childCount() > 0) { qDebug() << "ERROR: Found an effect with children"; return false; } allFilters.push_back(std::static_pointer_cast(current)); continue; } for (int i = current->childCount() - 1; i >= 0; --i) { stck.push(std::static_pointer_cast(current->child(i))); } } auto ptr = m_service.lock(); if (!ptr) { qDebug() << "ERROR: unavailable service"; return false; } if (ptr->filter_count() != (int)allFilters.size()) { qDebug() << "ERROR: Wrong filter count"; return false; } for (uint i = 0; i < allFilters.size(); ++i) { auto mltFilter = ptr->filter((int)i)->get_filter(); auto currentFilter = allFilters[i]->filter().get_filter(); if (mltFilter != currentFilter) { qDebug() << "ERROR: filter " << i << "differ"; return false; } } return true; } void EffectStackModel::adjust(const QString &effectId, const QString &effectName, double value) { for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); if (effectId == sourceEffect->getAssetId()) { sourceEffect->setParameter(effectName, QString::number(value)); return; } } } bool EffectStackModel::hasFilter(const QString &effectId) { for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); if (effectId == sourceEffect->getAssetId()) { return true; } } return false; } double EffectStackModel::getFilter(const QString &effectId, const QString ¶mName) { for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); if (effectId == sourceEffect->getAssetId()) { return sourceEffect->filter().get_double(paramName.toUtf8().constData()); } } return 0.0; } KeyframeModel *EffectStackModel::getEffectKeyframeModel() { if (rootItem->childCount() == 0) return nullptr; auto ptr = m_service.lock(); int ix = 0; if (ptr) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return nullptr; } std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix)); std::shared_ptr listModel = sourceEffect->getKeyframeModel(); if (listModel) { return listModel->getKeyModel(); } return nullptr; } void EffectStackModel::replugEffect(std::shared_ptr asset) { auto effectItem = std::static_pointer_cast(asset); int oldRow = effectItem->row(); int count = rowCount(); for (int ix = oldRow; ix < count; ix++) { auto item = std::static_pointer_cast(rootItem->child(ix)); item->unplant(this->m_service); } Mlt::Properties *effect = EffectsRepository::get()->getEffect(effectItem->getAssetId()); effect->inherit(effectItem->filter()); effectItem->resetAsset(effect); for (int ix = oldRow; ix < count; ix++) { auto item = std::static_pointer_cast(rootItem->child(ix)); item->plant(this->m_service); } } void EffectStackModel::cleanFadeEffects(bool outEffects, Fun &undo, Fun &redo) { - QList toDelete = outEffects ? fadeOuts : fadeIns; + const auto &toDelete = outEffects ? fadeOuts : fadeIns; for (int id : toDelete) { auto effect = std::static_pointer_cast(getItemById(id)); Fun operation = removeItem_lambda(id); if (operation()) { Fun reverse = addItem_lambda(effect, rootItem->getId()); - PUSH_LAMBDA(operation, redo); - PUSH_LAMBDA(reverse, undo); + UPDATE_UNDO_REDO(operation, reverse, undo, redo); } } - if (!toDelete.isEmpty()) { + if (!toDelete.empty()) { Fun update = [this]() { - //TODO: only update if effect is fade or keyframe + // TODO: only update if effect is fade or keyframe pCore->updateItemKeyframes(m_ownerId); return true; }; update(); PUSH_LAMBDA(update, redo); } } diff --git a/src/effects/effectstack/model/effectstackmodel.hpp b/src/effects/effectstack/model/effectstackmodel.hpp index 0e81ddaa9..8a4f215e0 100644 --- a/src/effects/effectstack/model/effectstackmodel.hpp +++ b/src/effects/effectstack/model/effectstackmodel.hpp @@ -1,141 +1,148 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef EFFECTSTACKMODEL_H #define EFFECTSTACKMODEL_H #include "abstractmodel/abstracttreemodel.hpp" #include "definitions.h" #include "undohelper.hpp" #include #include #include +#include /* @brief This class an effect stack as viewed by the back-end. It is responsible for planting and managing effects into the producer it holds a pointer to. */ class AbstractEffectItem; class AssetParameterModel; class DocUndoStack; class EffectItemModel; class TreeItem; class KeyframeModel; class EffectStackModel : public AbstractTreeModel { Q_OBJECT public: /* @brief Constructs an effect stack and returns a shared ptr to the constucted object @param service is the mlt object on which we will plant the effects @param ownerId is some information about the actual object to which the effects are applied */ static std::shared_ptr construct(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack); void resetService(std::weak_ptr service); - void loadEffects(); protected: EffectStackModel(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack); public: /* @brief Add an effect at the bottom of the stack */ void appendEffect(const QString &effectId, bool makeCurrent = false); - /* @brief Copy an existing effect and append it at the bottom of the stack */ - void copyEffect(std::shared_ptr sourceItem); + /* @brief Copy an existing effect and append it at the bottom of the stack + @param logUndo: if true, an undo/redo is created + */ + void copyEffect(std::shared_ptr sourceItem, bool logUndo = true); /* @brief Import all effects from the given effect stack */ void importEffects(std::shared_ptr sourceStack); + /* @brief Import all effects attached to a given service + @param alreadyExist: if true, the effect should be already attached to the service owned by this effectstack (it means we are in the process of loading). + In that case, we need to build the stack but not replant the effects + */ + void importEffects(std::weak_ptr service, bool alreadyExist = false); bool removeFade(bool fromStart); /* @brief This function change the global (timeline-wise) enabled state of the effects */ void setEffectStackEnabled(bool enabled); /* @brief Returns an effect or group from the stack (at the given row) */ std::shared_ptr getEffectStackRow(int row, std::shared_ptr parentItem = nullptr); /* @brief Move an effect in the stack */ void moveEffect(int destRow, std::shared_ptr item); /* @brief Set effect in row as current one */ void setActiveEffect(int ix); /* @brief Get currently active effect row */ int getActiveEffect() const; /* @brief Adjust an effect duration (useful for fades) */ bool adjustFadeLength(int duration, bool fromStart, bool audioFade, bool videoFade); bool adjustStackLength(bool adjustFromEnd, int oldIn, int oldDuration, int newIn, int duration, Fun &undo, Fun &redo, bool logUndo); void slotCreateGroup(std::shared_ptr childEffect); /* @brief Returns the id of the owner of the stack */ ObjectId getOwnerId() const; int getFadePosition(bool fromStart); Q_INVOKABLE void adjust(const QString &effectId, const QString &effectName, double value); Q_INVOKABLE bool hasFilter(const QString &effectId); Q_INVOKABLE double getFilter(const QString &effectId, const QString ¶mName); /** get the active effect's keyframe model */ Q_INVOKABLE KeyframeModel *getEffectKeyframeModel(); /** Remove unwanted fade effects, mostly after a cut operation */ void cleanFadeEffects(bool outEffects, Fun &undo, Fun &redo); public slots: /* @brief Delete an effect from the stack */ void removeEffect(std::shared_ptr effect); protected: /* @brief Register the existence of a new element */ void registerItem(const std::shared_ptr &item) override; /* @brief Deregister the existence of a new element*/ void deregisterItem(int id, TreeItem *item) override; /* @brief This is a convenience function that helps check if the tree is in a valid state */ bool checkConsistency() override; std::weak_ptr m_service; bool m_effectStackEnabled; ObjectId m_ownerId; std::weak_ptr m_undoStack; private: mutable QReadWriteLock m_lock; - QList fadeIns; - QList fadeOuts; + std::unordered_set fadeIns; + std::unordered_set fadeOuts; + /** @brief: When loading a project, we load filters/effects that are already planted * in the producer, so we shouldn't plant them again. Setting this value to * true will prevent planting in the producer */ bool m_loadingExisting; - private slots: /** @brief: Some effects do not support dynamic changes like sox, and need to be unplugged / replugged on each param change */ void replugEffect(std::shared_ptr asset); signals: /** @brief: This signal is connected to the project clip for bin clips and activates the reload of effects on child (timeline) producers */ void modelChanged(); }; #endif diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d7b2584c7..dd5aef928 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,4151 +1,4145 @@ /*************************************************************************** * 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 "mainwindow.h" #include "assets/assetpanel.hpp" #include "bin/clipcreator.hpp" #include "bin/generators/generators.h" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/clipcreationdialog.h" #include "dialogs/kdenlivesettingsdialog.h" #include "dialogs/renderwidget.h" #include "dialogs/wizard.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectlist/view/effectlistwidget.hpp" #include "effectslist/effectbasket.h" #include "effectslist/effectslistview.h" #include "effectslist/effectslistwidget.h" #include "effectslist/initeffects.h" #include "hidetitlebars.h" #include "jobs/jobmanager.h" #include "jobs/scenesplitjob.hpp" #include "jobs/speedjob.hpp" #include "jobs/stabilizejob.hpp" #include "kdenlivesettings.h" #include "layoutmanagement.h" #include "library/librarywidget.h" #include "mainwindowadaptor.h" #include "mltconnection.h" #include "mltcontroller/bincontroller.h" #include "mltcontroller/clipcontroller.h" #include "monitor/monitor.h" #include "monitor/monitormanager.h" #include "monitor/scopes/audiographspectrum.h" #include "profiles/profilemodel.hpp" #include "project/clipmanager.h" #include "project/cliptranscode.h" #include "project/dialogs/archivewidget.h" #include "project/dialogs/projectsettings.h" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "renderer.h" #include "scopes/scopemanager.h" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinetabs.hpp" #include "timeline2/view/timelinewidget.h" #include "titler/titlewidget.h" #include "transitions/transitionlist/view/transitionlistwidget.hpp" #include "transitions/transitionsrepository.hpp" #include "utils/resourcewidget.h" #include "utils/thememanager.h" #include "effectslist/effectslistwidget.h" #include "profiles/profilerepository.hpp" #include "widgets/progressbutton.h" #include #include "project/dialogs/temporarydata.h" #include "utils/KoIconUtils.h" #ifdef USE_JOGSHUTTLE #include "jogshuttle/jogmanager.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const char version[] = KDENLIVE_VERSION; namespace Mlt { class Producer; } EffectsList MainWindow::videoEffects; EffectsList MainWindow::audioEffects; EffectsList MainWindow::customEffects; EffectsList MainWindow::transitions; QMap MainWindow::m_lumacache; QMap MainWindow::m_lumaFiles; /*static bool sortByNames(const QPair &a, const QPair &b) { return a.first < b.first; }*/ // determine the default KDE style as defined BY THE USER // (as opposed to whatever style KDE considers default) static QString defaultStyle(const char *fallback = nullptr) { KSharedConfigPtr kdeGlobals = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::NoGlobals); KConfigGroup cg(kdeGlobals, "KDE"); return cg.readEntry("widgetStyle", fallback); } MainWindow::MainWindow(QWidget *parent) : KXmlGuiWindow(parent) , m_exitCode(EXIT_SUCCESS) , m_effectList(nullptr) , m_transitionList(nullptr) , m_assetPanel(nullptr) , m_clipMonitor(nullptr) , m_projectMonitor(nullptr) , m_timelineTabs(nullptr) , m_renderWidget(nullptr) , m_messageLabel(nullptr) , m_themeInitialized(false) , m_isDarkTheme(false) { } void MainWindow::init() { QString desktopStyle = QApplication::style()->objectName(); // Init color theme KActionMenu *themeAction = new KActionMenu(i18n("Theme"), this); ThemeManager::instance()->setThemeMenuAction(themeAction); connect(ThemeManager::instance(), &ThemeManager::signalThemeChanged, this, &MainWindow::slotThemeChanged, Qt::DirectConnection); ThemeManager::instance()->setCurrentTheme(KdenliveSettings::colortheme()); if (!KdenliveSettings::widgetstyle().isEmpty() && QString::compare(desktopStyle, KdenliveSettings::widgetstyle(), Qt::CaseInsensitive) != 0) { // User wants a custom widget style, init doChangeStyle(); } else { ThemeManager::instance()->slotChangePalette(); } // Widget themes for non KDE users KActionMenu *stylesAction = new KActionMenu(i18n("Style"), this); auto *stylesGroup = new QActionGroup(stylesAction); // GTK theme does not work well with Kdenlive, and does not support color theming, so avoid it QStringList availableStyles = QStyleFactory::keys(); if (KdenliveSettings::widgetstyle().isEmpty()) { // First run QStringList incompatibleStyles; incompatibleStyles << QStringLiteral("GTK+") << QStringLiteral("windowsvista") << QStringLiteral("windowsxp"); if (incompatibleStyles.contains(desktopStyle, Qt::CaseInsensitive)) { if (availableStyles.contains(QStringLiteral("breeze"), Qt::CaseInsensitive)) { // Auto switch to Breeze theme KdenliveSettings::setWidgetstyle(QStringLiteral("Breeze")); } else if (availableStyles.contains(QStringLiteral("fusion"), Qt::CaseInsensitive)) { KdenliveSettings::setWidgetstyle(QStringLiteral("Fusion")); } } else { KdenliveSettings::setWidgetstyle(QStringLiteral("Default")); } } // Add default style action QAction *defaultStyle = new QAction(i18n("Default"), stylesGroup); defaultStyle->setData(QStringLiteral("Default")); defaultStyle->setCheckable(true); stylesAction->addAction(defaultStyle); if (KdenliveSettings::widgetstyle() == QLatin1String("Default") || KdenliveSettings::widgetstyle().isEmpty()) { defaultStyle->setChecked(true); } for (const QString &style : availableStyles) { auto *a = new QAction(style, stylesGroup); a->setCheckable(true); a->setData(style); if (KdenliveSettings::widgetstyle() == style) { a->setChecked(true); } stylesAction->addAction(a); } connect(stylesGroup, &QActionGroup::triggered, this, &MainWindow::slotChangeStyle); // QIcon::setThemeSearchPaths(QStringList() <setCurrentProfile(defaultProfile.isEmpty() ? ProjectManager::getDefaultProjectFormat() : defaultProfile); m_commandStack = new QUndoGroup(); // If using a custom profile, make sure the file exists or fallback to default QString currentProfilePath = pCore->getCurrentProfile()->path(); if (currentProfilePath.startsWith(QLatin1Char('/')) && !QFile::exists(currentProfilePath)) { KMessageBox::sorry(this, i18n("Cannot find your default profile, switching to ATSC 1080p 25")); pCore->setCurrentProfile(QStringLiteral("atsc_1080p_25")); KdenliveSettings::setDefault_profile(QStringLiteral("atsc_1080p_25")); } m_gpuAllowed = initEffects::parseEffectFiles(pCore->getMltRepository()); // initEffects::parseCustomEffectsFile(); m_shortcutRemoveFocus = new QShortcut(QKeySequence(QStringLiteral("Esc")), this); connect(m_shortcutRemoveFocus, &QShortcut::activated, this, &MainWindow::slotRemoveFocus); /// Add Widgets setDockOptions(dockOptions() | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); setDockOptions(dockOptions() | QMainWindow::GroupedDragging); setTabPosition(Qt::AllDockWidgetAreas, (QTabWidget::TabPosition)KdenliveSettings::tabposition()); m_timelineToolBar = toolBar(QStringLiteral("timelineToolBar")); m_timelineToolBarContainer = new QWidget(this); auto *ctnLay = new QVBoxLayout; ctnLay->setSpacing(0); ctnLay->setContentsMargins(0, 0, 0, 0); m_timelineToolBarContainer->setLayout(ctnLay); ctnLay->addWidget(m_timelineToolBar); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup mainConfig(config, QStringLiteral("MainWindow")); KConfigGroup tbGroup(&mainConfig, QStringLiteral("Toolbar timelineToolBar")); m_timelineToolBar->applySettings(tbGroup); QFrame *fr = new QFrame(this); fr->setFrameShape(QFrame::HLine); fr->setMaximumHeight(1); fr->setLineWidth(1); ctnLay->addWidget(fr); setCentralWidget(m_timelineToolBarContainer); setupActions(); QDockWidget *libraryDock = addDock(i18n("Library"), QStringLiteral("library"), pCore->library()); m_clipMonitor = new Monitor(Kdenlive::ClipMonitor, pCore->monitorManager(), this); pCore->bin()->setMonitor(m_clipMonitor); connect(m_clipMonitor, &Monitor::showConfigDialog, this, &MainWindow::slotPreferences); connect(m_clipMonitor, &Monitor::addMarker, this, &MainWindow::slotAddMarkerGuideQuickly); connect(m_clipMonitor, &Monitor::deleteMarker, this, &MainWindow::slotDeleteClipMarker); connect(m_clipMonitor, &Monitor::seekToPreviousSnap, this, &MainWindow::slotSnapRewind); connect(m_clipMonitor, &Monitor::seekToNextSnap, this, &MainWindow::slotSnapForward); connect(pCore->bin(), &Bin::findInTimeline, this, &MainWindow::slotClipInTimeline); // TODO deprecated, replace with Bin methods if necessary /*connect(m_projectList, SIGNAL(loadingIsOver()), this, SLOT(slotElapsedTime())); connect(m_projectList, SIGNAL(updateRenderStatus()), this, SLOT(slotCheckRenderStatus())); connect(m_projectList, SIGNAL(updateProfile(QString)), this, SLOT(slotUpdateProjectProfile(QString))); connect(m_projectList, SIGNAL(refreshClip(QString,bool)), pCore->monitorManager(), SLOT(slotRefreshCurrentMonitor(QString))); connect(m_clipMonitor, SIGNAL(zoneUpdated(QPoint)), m_projectList, SLOT(slotUpdateClipCut(QPoint)));*/ // TODO refac : reimplement ? // connect(m_clipMonitor, &Monitor::extractZone, pCore->bin(), &Bin::slotStartCutJob); connect(m_clipMonitor, &Monitor::passKeyPress, this, &MainWindow::triggerKey); m_projectMonitor = new Monitor(Kdenlive::ProjectMonitor, pCore->monitorManager(), this); connect(m_projectMonitor, &Monitor::passKeyPress, this, &MainWindow::triggerKey); connect(m_projectMonitor, &Monitor::addMarker, this, &MainWindow::slotAddMarkerGuideQuickly); connect(m_projectMonitor, &Monitor::deleteMarker, this, &MainWindow::slotDeleteClipMarker); connect(m_projectMonitor, &Monitor::seekToPreviousSnap, this, &MainWindow::slotSnapRewind); connect(m_projectMonitor, &Monitor::seekToNextSnap, this, &MainWindow::slotSnapForward); connect(m_loopClip, &QAction::triggered, m_projectMonitor, &Monitor::slotLoopClip); pCore->monitorManager()->initMonitors(m_clipMonitor, m_projectMonitor); connect(m_clipMonitor, &Monitor::addMasterEffect, pCore->bin(), &Bin::slotAddEffect); m_timelineTabs = new TimelineTabs(this); ctnLay->addWidget(m_timelineTabs); // Audio spectrum scope m_audioSpectrum = new AudioGraphSpectrum(pCore->monitorManager()); QDockWidget *spectrumDock = addDock(i18n("Audio Spectrum"), QStringLiteral("audiospectrum"), m_audioSpectrum); connect(this, &MainWindow::reloadTheme, m_audioSpectrum, &AudioGraphSpectrum::refreshPixmap); // Close library and audiospectrum on first run libraryDock->close(); spectrumDock->close(); m_projectBinDock = addDock(i18n("Project Bin"), QStringLiteral("project_bin"), pCore->bin()); m_assetPanel = new AssetPanel(this); connect(m_assetPanel, &AssetPanel::doSplitEffect, m_projectMonitor, &Monitor::slotSwitchCompare); connect(m_assetPanel, &AssetPanel::doSplitBinEffect, m_clipMonitor, &Monitor::slotSwitchCompare); connect(m_assetPanel, &AssetPanel::changeSpeed, this, &MainWindow::slotChangeSpeed); connect(m_timelineTabs, &TimelineTabs::showTransitionModel, m_assetPanel, &AssetPanel::showTransition); connect(m_timelineTabs, &TimelineTabs::showItemEffectStack, m_assetPanel, &AssetPanel::showEffectStack); connect(pCore->bin(), &Bin::requestShowEffectStack, m_assetPanel, &AssetPanel::showEffectStack); connect(this, &MainWindow::clearAssetPanel, m_assetPanel, &AssetPanel::clearAssetPanel); connect(m_assetPanel, &AssetPanel::seekToPos, [this](int pos) { ObjectId oId = m_assetPanel->effectStackOwner(); switch (oId.first) { case ObjectType::TimelineTrack: case ObjectType::TimelineClip: case ObjectType::TimelineComposition: getCurrentTimeline()->controller()->setPosition(pos); break; case ObjectType::BinClip: m_clipMonitor->requestSeek(pos); break; default: qDebug()<<"ERROR unhandled object type"; break; } }); m_effectStackDock = addDock(i18n("Properties"), QStringLiteral("effect_stack"), m_assetPanel); m_effectList = new EffectsListView(); // m_effectListDock = addDock(i18n("Effects"), QStringLiteral("effect_list"), m_effectList); m_effectList2 = new EffectListWidget(this); connect(m_effectList2, &EffectListWidget::activateAsset, pCore->projectManager(), &ProjectManager::activateAsset); m_effectListDock = addDock(i18n("Effects"), QStringLiteral("effect_list"), m_effectList2); m_transitionList = new EffectsListView(EffectsListView::TransitionMode); m_transitionList2 = new TransitionListWidget(this); // m_transitionListDock = addDock(i18n("Transitions"), QStringLiteral("transition_list"), m_transitionList); m_transitionListDock = addDock(i18n("Transitions"), QStringLiteral("transition_list"), m_transitionList2); // Add monitors here to keep them at the right of the window m_clipMonitorDock = addDock(i18n("Clip Monitor"), QStringLiteral("clip_monitor"), m_clipMonitor); m_projectMonitorDock = addDock(i18n("Project Monitor"), QStringLiteral("project_monitor"), m_projectMonitor); m_undoView = new QUndoView(); m_undoView->setCleanIcon(KoIconUtils::themedIcon(QStringLiteral("edit-clear"))); m_undoView->setEmptyLabel(i18n("Clean")); m_undoView->setGroup(m_commandStack); m_undoViewDock = addDock(i18n("Undo History"), QStringLiteral("undo_history"), m_undoView); // Color and icon theme stuff addAction(QStringLiteral("themes_menu"), themeAction); connect(m_commandStack, &QUndoGroup::cleanChanged, m_saveAction, &QAction::setDisabled); addAction(QStringLiteral("styles_menu"), stylesAction); QAction *iconAction = new QAction(i18n("Force Breeze Icon Theme"), this); iconAction->setCheckable(true); iconAction->setChecked(KdenliveSettings::force_breeze()); addAction(QStringLiteral("force_icon_theme"), iconAction); connect(iconAction, &QAction::triggered, this, &MainWindow::forceIconSet); // Close non-general docks for the initial layout // only show important ones m_undoViewDock->close(); /// Tabify Widgets tabifyDockWidget(m_transitionListDock, m_effectListDock); tabifyDockWidget(m_effectStackDock, pCore->bin()->clipPropertiesDock()); // tabifyDockWidget(m_effectListDock, m_effectStackDock); tabifyDockWidget(m_clipMonitorDock, m_projectMonitorDock); bool firstRun = readOptions(); // Monitor Record action addAction(QStringLiteral("switch_monitor_rec"), m_clipMonitor->recAction()); // Build effects menu m_effectsMenu = new QMenu(i18n("Add Effect"), this); m_effectActions = new KActionCategory(i18n("Effects"), actionCollection()); m_effectList->reloadEffectList(m_effectsMenu, m_effectActions); m_transitionsMenu = new QMenu(i18n("Add Transition"), this); m_transitionActions = new KActionCategory(i18n("Transitions"), actionCollection()); m_transitionList->reloadEffectList(m_transitionsMenu, m_transitionActions); auto *scmanager = new ScopeManager(this); new LayoutManagement(this); new HideTitleBars(this); m_extraFactory = new KXMLGUIClient(this); buildDynamicActions(); // Create Effect Basket (dropdown list of favorites) m_effectBasket = new EffectBasket(m_effectList); connect(m_effectBasket, SIGNAL(addEffect(QDomElement)), this, SLOT(slotAddEffect(QDomElement))); auto *widgetlist = new QWidgetAction(this); widgetlist->setDefaultWidget(m_effectBasket); // widgetlist->setText(i18n("Favorite Effects")); widgetlist->setToolTip(i18n("Favorite Effects")); widgetlist->setIcon(KoIconUtils::themedIcon(QStringLiteral("favorite"))); auto *menu = new QMenu(this); menu->addAction(widgetlist); auto *basketButton = new QToolButton(this); basketButton->setMenu(menu); basketButton->setToolButtonStyle(toolBar()->toolButtonStyle()); basketButton->setDefaultAction(widgetlist); basketButton->setPopupMode(QToolButton::InstantPopup); // basketButton->setText(i18n("Favorite Effects")); basketButton->setToolTip(i18n("Favorite Effects")); basketButton->setIcon(KoIconUtils::themedIcon(QStringLiteral("favorite"))); auto *toolButtonAction = new QWidgetAction(this); toolButtonAction->setText(i18n("Favorite Effects")); toolButtonAction->setIcon(KoIconUtils::themedIcon(QStringLiteral("favorite"))); toolButtonAction->setDefaultWidget(basketButton); addAction(QStringLiteral("favorite_effects"), toolButtonAction); connect(toolButtonAction, &QAction::triggered, basketButton, &QToolButton::showMenu); // Render button ProgressButton *timelineRender = new ProgressButton(i18n("Render"), 100, this); auto *tlrMenu = new QMenu(this); timelineRender->setMenu(tlrMenu); connect(this, &MainWindow::setRenderProgress, timelineRender, &ProgressButton::setProgress); auto *renderButtonAction = new QWidgetAction(this); renderButtonAction->setText(i18n("Render Button")); renderButtonAction->setIcon(KoIconUtils::themedIcon(QStringLiteral("media-record"))); renderButtonAction->setDefaultWidget(timelineRender); addAction(QStringLiteral("project_render_button"), renderButtonAction); // Timeline preview button ProgressButton *timelinePreview = new ProgressButton(i18n("Rendering preview"), 1000, this); auto *tlMenu = new QMenu(this); timelinePreview->setMenu(tlMenu); connect(this, &MainWindow::setPreviewProgress, timelinePreview, &ProgressButton::setProgress); auto *previewButtonAction = new QWidgetAction(this); previewButtonAction->setText(i18n("Timeline Preview")); previewButtonAction->setIcon(KoIconUtils::themedIcon(QStringLiteral("preview-render-on"))); previewButtonAction->setDefaultWidget(timelinePreview); addAction(QStringLiteral("timeline_preview_button"), previewButtonAction); setupGUI(); if (firstRun) { QScreen *current = QApplication::primaryScreen(); if (current) { if (current->availableSize().height() < 1000) { resize(current->availableSize()); } else { resize(current->availableSize() / 1.5); } } } updateActionsToolTip(); m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); m_timelineToolBar->setProperty("otherToolbar", true); timelinePreview->setToolButtonStyle(m_timelineToolBar->toolButtonStyle()); connect(m_timelineToolBar, &QToolBar::toolButtonStyleChanged, timelinePreview, &ProgressButton::setToolButtonStyle); timelineRender->setToolButtonStyle(toolBar()->toolButtonStyle()); /*ScriptingPart* sp = new ScriptingPart(this, QStringList()); guiFactory()->addClient(sp);*/ loadGenerators(); loadDockActions(); loadClipActions(); // Connect monitor overlay info menu. QMenu *monitorOverlay = static_cast(factory()->container(QStringLiteral("monitor_config_overlay"), this)); connect(monitorOverlay, &QMenu::triggered, this, &MainWindow::slotSwitchMonitorOverlay); m_projectMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, nullptr, m_loopClip); m_clipMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); QMenu *clipInTimeline = static_cast(factory()->container(QStringLiteral("clip_in_timeline"), this)); clipInTimeline->setIcon(KoIconUtils::themedIcon(QStringLiteral("go-jump"))); pCore->bin()->setupGeneratorMenu(); connect(pCore->monitorManager(), &MonitorManager::updateOverlayInfos, this, &MainWindow::slotUpdateMonitorOverlays); // Setup and fill effects and transitions menus. QMenu *m = static_cast(factory()->container(QStringLiteral("video_effects_menu"), this)); connect(m, &QMenu::triggered, this, &MainWindow::slotAddVideoEffect); connect(m_effectsMenu, &QMenu::triggered, this, &MainWindow::slotAddVideoEffect); connect(m_transitionsMenu, &QMenu::triggered, this, &MainWindow::slotAddTransition); m_timelineContextMenu = new QMenu(this); m_timelineContextClipMenu = new QMenu(this); m_timelineContextTransitionMenu = new QMenu(this); m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("insert_space"))); m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("delete_space"))); m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("delete_space_all_tracks"))); m_timelineContextMenu->addAction(actionCollection()->action(KStandardAction::name(KStandardAction::Paste))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("clip_in_project_tree"))); // m_timelineContextClipMenu->addAction(actionCollection()->action("clip_to_project_tree")); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("delete_timeline_clip"))); m_timelineContextClipMenu->addSeparator(); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("group_clip"))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("ungroup_clip"))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("split_audio"))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("set_audio_align_ref"))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("align_audio"))); m_timelineContextClipMenu->addSeparator(); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("cut_timeline_clip"))); m_timelineContextClipMenu->addAction(actionCollection()->action(KStandardAction::name(KStandardAction::Copy))); m_timelineContextClipMenu->addAction(actionCollection()->action(QStringLiteral("paste_effects"))); m_timelineContextClipMenu->addSeparator(); QMenu *markersMenu = static_cast(factory()->container(QStringLiteral("marker_menu"), this)); m_timelineContextClipMenu->addMenu(markersMenu); m_timelineContextClipMenu->addSeparator(); m_timelineContextClipMenu->addMenu(m_transitionsMenu); m_timelineContextClipMenu->addMenu(m_effectsMenu); m_timelineContextTransitionMenu->addAction(actionCollection()->action(QStringLiteral("delete_timeline_clip"))); m_timelineContextTransitionMenu->addAction(actionCollection()->action(KStandardAction::name(KStandardAction::Copy))); m_timelineContextTransitionMenu->addAction(actionCollection()->action(QStringLiteral("auto_transition"))); connect(m_effectList, &EffectsListView::addEffect, this, &MainWindow::slotAddEffect); connect(m_effectList, &EffectsListView::reloadEffects, this, &MainWindow::slotReloadEffects); slotConnectMonitors(); m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); // TODO: let user select timeline toolbar toolbutton style // connect(toolBar(), &QToolBar::iconSizeChanged, m_timelineToolBar, &QToolBar::setToolButtonStyle); m_timelineToolBar->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_timelineToolBar, &QWidget::customContextMenuRequested, this, &MainWindow::showTimelineToolbarMenu); QAction *prevRender = actionCollection()->action(QStringLiteral("prerender_timeline_zone")); QAction *stopPrevRender = actionCollection()->action(QStringLiteral("stop_prerender_timeline")); tlMenu->addAction(stopPrevRender); tlMenu->addAction(actionCollection()->action(QStringLiteral("set_render_timeline_zone"))); tlMenu->addAction(actionCollection()->action(QStringLiteral("unset_render_timeline_zone"))); tlMenu->addAction(actionCollection()->action(QStringLiteral("clear_render_timeline_zone"))); // Automatic timeline preview action QAction *autoRender = new QAction(KoIconUtils::themedIcon(QStringLiteral("view-refresh")), i18n("Automatic Preview"), this); autoRender->setCheckable(true); autoRender->setChecked(KdenliveSettings::autopreview()); connect(autoRender, &QAction::triggered, this, &MainWindow::slotToggleAutoPreview); tlMenu->addAction(autoRender); tlMenu->addSeparator(); tlMenu->addAction(actionCollection()->action(QStringLiteral("disable_preview"))); tlMenu->addAction(actionCollection()->action(QStringLiteral("manage_cache"))); timelinePreview->defineDefaultAction(prevRender, stopPrevRender); timelinePreview->setAutoRaise(true); QAction *showRender = actionCollection()->action(QStringLiteral("project_render")); tlrMenu->addAction(showRender); tlrMenu->addAction(actionCollection()->action(QStringLiteral("stop_project_render"))); timelineRender->defineDefaultAction(showRender, showRender); timelineRender->setAutoRaise(true); // Populate encoding profiles KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); if (KdenliveSettings::proxyparams().isEmpty() || KdenliveSettings::proxyextension().isEmpty()) { KConfigGroup group(&conf, "proxy"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString proxystring = i.value(); KdenliveSettings::setProxyparams(proxystring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setProxyextension(proxystring.section(QLatin1Char(';'), 1, 1)); } } if (KdenliveSettings::v4l_parameters().isEmpty() || KdenliveSettings::v4l_extension().isEmpty()) { KConfigGroup group(&conf, "video4linux"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString v4lstring = i.value(); KdenliveSettings::setV4l_parameters(v4lstring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setV4l_extension(v4lstring.section(QLatin1Char(';'), 1, 1)); } } if (KdenliveSettings::grab_parameters().isEmpty() || KdenliveSettings::grab_extension().isEmpty()) { KConfigGroup group(&conf, "screengrab"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString grabstring = i.value(); KdenliveSettings::setGrab_parameters(grabstring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setGrab_extension(grabstring.section(QLatin1Char(';'), 1, 1)); } } if (KdenliveSettings::decklink_parameters().isEmpty() || KdenliveSettings::decklink_extension().isEmpty()) { KConfigGroup group(&conf, "decklink"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString decklinkstring = i.value(); KdenliveSettings::setDecklink_parameters(decklinkstring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setDecklink_extension(decklinkstring.section(QLatin1Char(';'), 1, 1)); } } if (!QDir(KdenliveSettings::currenttmpfolder()).isReadable()) KdenliveSettings::setCurrenttmpfolder(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); QTimer::singleShot(0, this, &MainWindow::GUISetupDone); connect(this, &MainWindow::reloadTheme, this, &MainWindow::slotReloadTheme, Qt::UniqueConnection); #ifdef USE_JOGSHUTTLE new JogManager(this); #endif scmanager->slotCheckActiveScopes(); // m_messageLabel->setMessage(QStringLiteral("This is a beta version. Always backup your data"), MltError); } void MainWindow::slotThemeChanged(const QString &theme) { disconnect(this, &MainWindow::reloadTheme, this, &MainWindow::slotReloadTheme); KSharedConfigPtr config = KSharedConfig::openConfig(theme); QPalette plt = KColorScheme::createApplicationPalette(config); setPalette(plt); qApp->setPalette(palette()); // Required for qml palette change QGuiApplication::setPalette(plt); KdenliveSettings::setColortheme(theme); QColor background = plt.window().color(); bool useDarkIcons = background.value() < 100; if (KdenliveSettings::force_breeze() && useDarkIcons != KdenliveSettings::use_dark_breeze()) { // We need to reload icon theme KdenliveSettings::setUse_dark_breeze(useDarkIcons); if (KMessageBox::warningContinueCancel(this, i18n("Kdenlive needs to be restarted to apply color theme change. Restart now ?")) == KMessageBox::Continue) { slotRestart(); } } if (m_assetPanel) { m_assetPanel->updatePalette(); } if (m_effectList) { m_effectList->updatePalette(); } if (m_transitionList) { m_transitionList->updatePalette(); } if (m_clipMonitor) { m_clipMonitor->setPalette(plt); } if (m_projectMonitor) { m_projectMonitor->setPalette(plt); } if (m_timelineTabs) { m_timelineTabs->setPalette(plt); } #if KXMLGUI_VERSION_MINOR < 23 && KXMLGUI_VERSION_MAJOR == 5 // Not required anymore with auto colored icons since KF5 5.23 QColor background = plt.window().color(); bool useDarkIcons = background.value() < 100; if (m_themeInitialized && useDarkIcons != m_isDarkTheme) { if (pCore->bin()) { pCore->bin()->refreshIcons(); } if (m_clipMonitor) { m_clipMonitor->refreshIcons(); } if (m_projectMonitor) { m_projectMonitor->refreshIcons(); } if (pCore->monitorManager()) { pCore->monitorManager()->refreshIcons(); } if (m_effectList) { m_effectList->refreshIcons(); } for (QAction *action : actionCollection()->actions()) { QIcon icon = action->icon(); if (icon.isNull()) { continue; } QString iconName = icon.name(); QIcon newIcon = KoIconUtils::themedIcon(iconName); if (newIcon.isNull()) { continue; } action->setIcon(newIcon); } } m_themeInitialized = true; m_isDarkTheme = useDarkIcons; #endif connect(this, &MainWindow::reloadTheme, this, &MainWindow::slotReloadTheme, Qt::UniqueConnection); } bool MainWindow::event(QEvent *e) { switch (e->type()) { case QEvent::ApplicationPaletteChange: emit reloadTheme(); e->accept(); break; default: break; } return KXmlGuiWindow::event(e); } void MainWindow::updateActionsToolTip() { // Add shortcut to action tooltips QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); for (QAction *tempAction : coll->actions()) { // find the shortcut pattern and delete (note the preceding space in the RegEx) QString strippedTooltip = tempAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); // append shortcut if it exists for action if (tempAction->shortcut() == QKeySequence(0)) { tempAction->setToolTip(strippedTooltip); } else { tempAction->setToolTip(strippedTooltip + QStringLiteral(" (") + tempAction->shortcut().toString() + QLatin1Char(')')); } connect(tempAction, &QAction::changed, this, &MainWindow::updateAction); } } } void MainWindow::updateAction() { QAction *action = qobject_cast(sender()); QString toolTip = KLocalizedString::removeAcceleratorMarker(action->toolTip()); QString strippedTooltip = toolTip.remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); action->setToolTip(i18nc("@info:tooltip Tooltip of toolbar button", "%1 (%2)", strippedTooltip, action->shortcut().toString())); } void MainWindow::slotReloadTheme() { ThemeManager::instance()->slotSettingsChanged(); } MainWindow::~MainWindow() { pCore->prepareShutdown(); m_timelineTabs->disconnectTimeline(getMainTimeline()); delete m_audioSpectrum; if (m_projectMonitor) { m_projectMonitor->stop(); } if (m_clipMonitor) { m_clipMonitor->stop(); } ClipController::mediaUnavailable.reset(); delete m_projectMonitor; delete m_clipMonitor; delete m_shortcutRemoveFocus; delete m_effectList2; delete m_transitionList2; qDeleteAll(m_transitions); // Mlt::Factory::close(); } // virtual bool MainWindow::queryClose() { if (m_renderWidget) { int waitingJobs = m_renderWidget->waitingJobsCount(); if (waitingJobs > 0) { switch (KMessageBox::warningYesNoCancel(this, i18np("You have 1 rendering job waiting in the queue.\nWhat do you want to do with this job?", "You have %1 rendering jobs waiting in the queue.\nWhat do you want to do with these jobs?", waitingJobs), QString(), KGuiItem(i18n("Start them now")), KGuiItem(i18n("Delete them")))) { case KMessageBox::Yes: // create script with waiting jobs and start it if (!m_renderWidget->startWaitingRenderJobs()) { return false; } break; case KMessageBox::No: // Don't do anything, jobs will be deleted break; default: return false; } } } saveOptions(); // WARNING: According to KMainWindow::queryClose documentation we are not supposed to close the document here? return pCore->projectManager()->closeCurrentDocument(true, true); } void MainWindow::loadGenerators() { QMenu *addMenu = static_cast(factory()->container(QStringLiteral("generators"), this)); Generators::getGenerators(KdenliveSettings::producerslist(), addMenu); connect(addMenu, &QMenu::triggered, this, &MainWindow::buildGenerator); } void MainWindow::buildGenerator(QAction *action) { Generators gen(m_clipMonitor, action->data().toString(), this); if (gen.exec() == QDialog::Accepted) { pCore->bin()->slotAddClipToProject(gen.getSavedClip()); } } void MainWindow::saveProperties(KConfigGroup &config) { // save properties here KXmlGuiWindow::saveProperties(config); // TODO: fix session management if (qApp->isSavingSession() && pCore->projectManager()) { if (pCore->currentDoc() && !pCore->currentDoc()->url().isEmpty()) { config.writeEntry("kdenlive_lastUrl", pCore->currentDoc()->url().toLocalFile()); } } } void MainWindow::readProperties(const KConfigGroup &config) { // read properties here KXmlGuiWindow::readProperties(config); // TODO: fix session management /*if (qApp->isSessionRestored()) { pCore->projectManager()->openFile(QUrl::fromLocalFile(config.readEntry("kdenlive_lastUrl", QString()))); }*/ } void MainWindow::saveNewToolbarConfig() { KXmlGuiWindow::saveNewToolbarConfig(); // TODO for some reason all dynamically inserted actions are removed by the save toolbar // So we currently re-add them manually.... loadDockActions(); loadClipActions(); pCore->bin()->rebuildMenu(); QMenu *monitorOverlay = static_cast(factory()->container(QStringLiteral("monitor_config_overlay"), this)); if (monitorOverlay) { m_projectMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, nullptr, m_loopClip); m_clipMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); } } void MainWindow::slotReloadEffects() { initEffects::parseCustomEffectsFile(); m_effectList->reloadEffectList(m_effectsMenu, m_effectActions); } void MainWindow::configureNotifications() { KNotifyConfigWidget::configure(this); } void MainWindow::slotFullScreen() { KToggleFullScreenAction::setFullScreen(this, actionCollection()->action(QStringLiteral("fullscreen"))->isChecked()); } void MainWindow::slotAddEffect(const QDomElement &effect) { Q_UNUSED(effect) // TODO refac : reimplement /* if (effect.isNull()) { qCDebug(KDENLIVE_LOG) << "--- ERROR, TRYING TO APPEND nullptr EFFECT"; return; } QDomElement effectToAdd = effect.cloneNode().toElement(); EFFECTMODE status = m_effectStack->effectStatus(); switch (status) { case TIMELINE_TRACK: pCore->projectManager()->currentTimeline()->projectView()->slotAddTrackEffect(effectToAdd, m_effectStack->trackIndex()); break; case TIMELINE_CLIP: pCore->projectManager()->currentTimeline()->projectView()->slotAddEffectToCurrentItem(effectToAdd); break; case MASTER_CLIP: // TODO refac reimplement this. // pCore->bin()->slotEffectDropped(QString(), effectToAdd); break; default: // No clip selected m_messageLabel->setMessage(i18n("Select a clip if you want to apply an effect"), ErrorMessage); } */ } void MainWindow::slotConnectMonitors() { // connect(m_projectList, SIGNAL(deleteProjectClips(QStringList,QMap)), this, // SLOT(slotDeleteProjectClips(QStringList,QMap))); connect(m_clipMonitor, &Monitor::refreshClipThumbnail, pCore->bin(), &Bin::slotRefreshClipThumbnail); connect(m_projectMonitor, &Monitor::requestFrameForAnalysis, this, &MainWindow::slotMonitorRequestRenderFrame); connect(m_projectMonitor, &Monitor::createSplitOverlay, this, &MainWindow::createSplitOverlay); connect(m_projectMonitor, &Monitor::removeSplitOverlay, this, &MainWindow::removeSplitOverlay); } void MainWindow::createSplitOverlay(Mlt::Filter *filter) { getMainTimeline()->controller()->createSplitOverlay(filter); m_projectMonitor->activateSplit(); } void MainWindow::removeSplitOverlay() { getMainTimeline()->controller()->removeSplitOverlay(); } void MainWindow::addAction(const QString &name, QAction *action) { m_actionNames.append(name); actionCollection()->addAction(name, action); actionCollection()->setDefaultShortcut(action, action->shortcut()); // Fix warning about setDefaultShortcut } QAction *MainWindow::addAction(const QString &name, const QString &text, const QObject *receiver, const char *member, const QIcon &icon, const QKeySequence &shortcut) { auto *action = new QAction(text, this); if (!icon.isNull()) { action->setIcon(icon); } if (!shortcut.isEmpty()) { action->setShortcut(shortcut); } addAction(name, action); connect(action, SIGNAL(triggered(bool)), receiver, member); return action; } void MainWindow::setupActions() { // create edit mode buttons m_normalEditTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-normal-edit")), i18n("Normal mode"), this); m_normalEditTool->setShortcut(i18nc("Normal editing", "n")); m_normalEditTool->setCheckable(true); m_normalEditTool->setChecked(true); m_overwriteEditTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-overwrite-edit")), i18n("Overwrite mode"), this); m_overwriteEditTool->setCheckable(true); m_overwriteEditTool->setChecked(false); m_insertEditTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-insert-edit")), i18n("Insert mode"), this); m_insertEditTool->setCheckable(true); m_insertEditTool->setChecked(false); KSelectAction *sceneMode = new KSelectAction(i18n("Timeline Edit Mode"), this); sceneMode->addAction(m_normalEditTool); sceneMode->addAction(m_overwriteEditTool); sceneMode->addAction(m_insertEditTool); sceneMode->setCurrentItem(0); connect(sceneMode, static_cast(&KSelectAction::triggered), this, &MainWindow::slotChangeEdit); addAction(QStringLiteral("timeline_mode"), sceneMode); KDualAction *ac = new KDualAction(i18n("Don't Use Timeline Zone for Insert"), i18n("Use Timeline Zone for Insert"), this); ac->setActiveIcon(KoIconUtils::themedIcon(QStringLiteral("timeline-use-zone-on"))); ac->setInactiveIcon(KoIconUtils::themedIcon(QStringLiteral("timeline-use-zone-off"))); ac->setShortcut(Qt::Key_G); ac->setActive(KdenliveSettings::useTimelineZoneToEdit()); ac->setAutoToggle(true); connect(ac, &KDualAction::activeChangedByUser, this, &MainWindow::slotSwitchTimelineZone); addAction(QStringLiteral("use_timeline_zone_in_edit"), ac); m_compositeAction = new KSelectAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-off")), i18n("Track compositing"), this); m_compositeAction->setToolTip(i18n("Track compositing")); QAction *noComposite = new QAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-off")), i18n("None"), this); noComposite->setCheckable(true); noComposite->setData(0); m_compositeAction->addAction(noComposite); QString compose = TransitionsRepository::get()->getCompositingTransition(); if (compose == QStringLiteral("movit.overlay")) { // Movit, do not show "preview" option since movit is faster QAction *hqComposite = new QAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-on")), i18n("High Quality"), this); hqComposite->setCheckable(true); hqComposite->setData(2); m_compositeAction->addAction(hqComposite); m_compositeAction->setCurrentAction(hqComposite); } else { QAction *previewComposite = new QAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-preview")), i18n("Preview"), this); previewComposite->setCheckable(true); previewComposite->setData(1); m_compositeAction->addAction(previewComposite); if (compose != QStringLiteral("composite")) { QAction *hqComposite = new QAction(KoIconUtils::themedIcon(QStringLiteral("composite-track-on")), i18n("High Quality"), this); hqComposite->setData(2); hqComposite->setCheckable(true); m_compositeAction->addAction(hqComposite); m_compositeAction->setCurrentAction(hqComposite); } else { m_compositeAction->setCurrentAction(previewComposite); } } connect(m_compositeAction, static_cast(&KSelectAction::triggered), this, &MainWindow::slotUpdateCompositing); addAction(QStringLiteral("timeline_compositing"), m_compositeAction); m_timeFormatButton = new KSelectAction(QStringLiteral("00:00:00:00 / 00:00:00:00"), this); m_timeFormatButton->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_timeFormatButton->addAction(i18n("hh:mm:ss:ff")); m_timeFormatButton->addAction(i18n("Frames")); if (KdenliveSettings::frametimecode()) { m_timeFormatButton->setCurrentItem(1); } else { m_timeFormatButton->setCurrentItem(0); } connect(m_timeFormatButton, static_cast(&KSelectAction::triggered), this, &MainWindow::slotUpdateTimecodeFormat); m_timeFormatButton->setToolBarMode(KSelectAction::MenuMode); m_timeFormatButton->setToolButtonPopupMode(QToolButton::InstantPopup); addAction(QStringLiteral("timeline_timecode"), m_timeFormatButton); // create tools buttons m_buttonSelectTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("cursor-arrow")), i18n("Selection tool"), this); m_buttonSelectTool->setShortcut(i18nc("Selection tool shortcut", "s")); // toolbar->addAction(m_buttonSelectTool); m_buttonSelectTool->setCheckable(true); m_buttonSelectTool->setChecked(true); m_buttonRazorTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("edit-cut")), i18n("Razor tool"), this); m_buttonRazorTool->setShortcut(i18nc("Razor tool shortcut", "x")); // toolbar->addAction(m_buttonRazorTool); m_buttonRazorTool->setCheckable(true); m_buttonRazorTool->setChecked(false); m_buttonSpacerTool = new QAction(KoIconUtils::themedIcon(QStringLiteral("distribute-horizontal-x")), i18n("Spacer tool"), this); m_buttonSpacerTool->setShortcut(i18nc("Spacer tool shortcut", "m")); // toolbar->addAction(m_buttonSpacerTool); m_buttonSpacerTool->setCheckable(true); m_buttonSpacerTool->setChecked(false); auto *toolGroup = new QActionGroup(this); toolGroup->addAction(m_buttonSelectTool); toolGroup->addAction(m_buttonRazorTool); toolGroup->addAction(m_buttonSpacerTool); toolGroup->setExclusive(true); // toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); /*QWidget * actionWidget; int max = toolbar->iconSizeDefault() + 2; actionWidget = toolbar->widgetForAction(m_normalEditTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_insertEditTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_overwriteEditTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonSelectTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonRazorTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonSpacerTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4);*/ connect(toolGroup, &QActionGroup::triggered, this, &MainWindow::slotChangeTool); m_buttonVideoThumbs = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-show-videothumb")), i18n("Show video thumbnails"), this); m_buttonVideoThumbs->setCheckable(true); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); connect(m_buttonVideoThumbs, &QAction::triggered, this, &MainWindow::slotSwitchVideoThumbs); m_buttonAudioThumbs = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-show-audiothumb")), i18n("Show audio thumbnails"), this); m_buttonAudioThumbs->setCheckable(true); m_buttonAudioThumbs->setChecked(KdenliveSettings::audiothumbnails()); connect(m_buttonAudioThumbs, &QAction::triggered, this, &MainWindow::slotSwitchAudioThumbs); m_buttonShowMarkers = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-show-markers")), i18n("Show markers comments"), this); m_buttonShowMarkers->setCheckable(true); m_buttonShowMarkers->setChecked(KdenliveSettings::showmarkers()); connect(m_buttonShowMarkers, &QAction::triggered, this, &MainWindow::slotSwitchMarkersComments); m_buttonSnap = new QAction(KoIconUtils::themedIcon(QStringLiteral("kdenlive-snap")), i18n("Snap"), this); m_buttonSnap->setCheckable(true); m_buttonSnap->setChecked(KdenliveSettings::snaptopoints()); connect(m_buttonSnap, &QAction::triggered, this, &MainWindow::slotSwitchSnap); m_buttonAutomaticTransition = new QAction(KoIconUtils::themedIcon(QStringLiteral("auto-transition")), i18n("Automatic transitions"), this); m_buttonAutomaticTransition->setCheckable(true); m_buttonAutomaticTransition->setChecked(KdenliveSettings::automatictransitions()); connect(m_buttonAutomaticTransition, &QAction::triggered, this, &MainWindow::slotSwitchAutomaticTransition); m_buttonFitZoom = new QAction(KoIconUtils::themedIcon(QStringLiteral("zoom-fit-best")), i18n("Fit zoom to project"), this); m_buttonFitZoom->setCheckable(false); m_zoomOut = new QAction(KoIconUtils::themedIcon(QStringLiteral("zoom-out")), i18n("Zoom Out"), this); m_zoomOut->setShortcut(Qt::CTRL + Qt::Key_Minus); m_zoomSlider = new QSlider(Qt::Horizontal, this); m_zoomSlider->setMaximum(13); m_zoomSlider->setPageStep(1); m_zoomSlider->setInvertedAppearance(true); m_zoomSlider->setInvertedControls(true); m_zoomSlider->setMaximumWidth(150); m_zoomSlider->setMinimumWidth(100); m_zoomIn = new QAction(KoIconUtils::themedIcon(QStringLiteral("zoom-in")), i18n("Zoom In"), this); m_zoomIn->setShortcut(Qt::CTRL + Qt::Key_Plus); /*actionWidget = toolbar->widgetForAction(m_buttonFitZoom); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget->setStyleSheet(styleBorderless); actionWidget = toolbar->widgetForAction(m_zoomIn); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget->setStyleSheet(styleBorderless); actionWidget = toolbar->widgetForAction(m_zoomOut); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget->setStyleSheet(styleBorderless);*/ connect(m_zoomSlider, SIGNAL(valueChanged(int)), this, SLOT(slotSetZoom(int))); connect(m_zoomSlider, &QAbstractSlider::sliderMoved, this, &MainWindow::slotShowZoomSliderToolTip); connect(m_buttonFitZoom, &QAction::triggered, this, &MainWindow::slotFitZoom); connect(m_zoomIn, &QAction::triggered, this, &MainWindow::slotZoomIn); connect(m_zoomOut, &QAction::triggered, this, &MainWindow::slotZoomOut); m_trimLabel = new QLabel(QStringLiteral(" "), this); m_trimLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); // m_trimLabel->setAutoFillBackground(true); m_trimLabel->setAlignment(Qt::AlignHCenter); m_trimLabel->setStyleSheet(QStringLiteral("QLabel { background-color :red; }")); KToolBar *toolbar = new KToolBar(QStringLiteral("statusToolBar"), this, Qt::BottomToolBarArea); toolbar->setMovable(false); toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); /*QString styleBorderless = QStringLiteral("QToolButton { border-width: 0px;margin: 1px 3px 0px;padding: 0px;}");*/ toolbar->addWidget(m_trimLabel); toolbar->addAction(m_buttonAutomaticTransition); toolbar->addAction(m_buttonVideoThumbs); toolbar->addAction(m_buttonAudioThumbs); toolbar->addAction(m_buttonShowMarkers); toolbar->addAction(m_buttonSnap); toolbar->addSeparator(); toolbar->addAction(m_buttonFitZoom); toolbar->addAction(m_zoomOut); toolbar->addWidget(m_zoomSlider); toolbar->addAction(m_zoomIn); int small = style()->pixelMetric(QStyle::PM_SmallIconSize); statusBar()->setMaximumHeight(2 * small); m_messageLabel = new StatusBarMessageLabel(this); m_messageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding); connect(this, &MainWindow::displayMessage, m_messageLabel, &StatusBarMessageLabel::setMessage); statusBar()->addWidget(m_messageLabel, 0); QWidget *spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); statusBar()->addWidget(spacer, 1); statusBar()->addPermanentWidget(toolbar); toolbar->setIconSize(QSize(small, small)); toolbar->layout()->setContentsMargins(0, 0, 0, 0); statusBar()->setContentsMargins(0, 0, 0, 0); addAction(QStringLiteral("normal_mode"), m_normalEditTool); addAction(QStringLiteral("overwrite_mode"), m_overwriteEditTool); addAction(QStringLiteral("insert_mode"), m_insertEditTool); addAction(QStringLiteral("select_tool"), m_buttonSelectTool); addAction(QStringLiteral("razor_tool"), m_buttonRazorTool); addAction(QStringLiteral("spacer_tool"), m_buttonSpacerTool); addAction(QStringLiteral("automatic_transition"), m_buttonAutomaticTransition); addAction(QStringLiteral("show_video_thumbs"), m_buttonVideoThumbs); addAction(QStringLiteral("show_audio_thumbs"), m_buttonAudioThumbs); addAction(QStringLiteral("show_markers"), m_buttonShowMarkers); addAction(QStringLiteral("snap"), m_buttonSnap); addAction(QStringLiteral("zoom_fit"), m_buttonFitZoom); addAction(QStringLiteral("zoom_in"), m_zoomIn); addAction(QStringLiteral("zoom_out"), m_zoomOut); KNS3::standardAction(i18n("Download New Wipes..."), this, SLOT(slotGetNewLumaStuff()), actionCollection(), "get_new_lumas"); KNS3::standardAction(i18n("Download New Keyboard Schemes..."), this, SLOT(slotGetNewKeyboardStuff()), actionCollection(), "get_new_keyboardschemes"); KNS3::standardAction(i18n("Download New Render Profiles..."), this, SLOT(slotGetNewRenderStuff()), actionCollection(), "get_new_profiles"); KNS3::standardAction(i18n("Download New Title Templates..."), this, SLOT(slotGetNewTitleStuff()), actionCollection(), "get_new_titles"); addAction(QStringLiteral("run_wizard"), i18n("Run Config Wizard"), this, SLOT(slotRunWizard()), KoIconUtils::themedIcon(QStringLiteral("tools-wizard"))); addAction(QStringLiteral("project_settings"), i18n("Project Settings"), this, SLOT(slotEditProjectSettings()), KoIconUtils::themedIcon(QStringLiteral("configure"))); addAction(QStringLiteral("project_render"), i18n("Render"), this, SLOT(slotRenderProject()), KoIconUtils::themedIcon(QStringLiteral("media-record")), Qt::CTRL + Qt::Key_Return); addAction(QStringLiteral("stop_project_render"), i18n("Stop Render"), this, SLOT(slotStopRenderProject()), KoIconUtils::themedIcon(QStringLiteral("media-record"))); addAction(QStringLiteral("project_clean"), i18n("Clean Project"), this, SLOT(slotCleanProject()), KoIconUtils::themedIcon(QStringLiteral("edit-clear"))); /*QAction *timelineZone = new QAction(KoIconUtils::themedIcon(QStringLiteral("insert-horizontal-rule")), i18n("Use Timeline Zone in Edit"), this); timelineZone->setCheckable(true); timelineZone->setChecked(KdenliveSettings::useTimelineZoneToEdit()); addAction(QStringLiteral("use_timeline_zone_in_edit"), timelineZone); connect(timelineZone, &QAction::toggled, this, &MainWindow::slotSwitchTimelineZone);*/ // TODO // addAction("project_adjust_profile", i18n("Adjust Profile to Current Clip"), pCore->bin(), SLOT(adjustProjectProfileToItem())); m_playZone = addAction(QStringLiteral("monitor_play_zone"), i18n("Play Zone"), pCore->monitorManager(), SLOT(slotPlayZone()), KoIconUtils::themedIcon(QStringLiteral("media-playback-start")), Qt::CTRL + Qt::Key_Space); m_loopZone = addAction(QStringLiteral("monitor_loop_zone"), i18n("Loop Zone"), pCore->monitorManager(), SLOT(slotLoopZone()), KoIconUtils::themedIcon(QStringLiteral("media-playback-start")), Qt::ALT + Qt::Key_Space); m_loopClip = new QAction(KoIconUtils::themedIcon(QStringLiteral("media-playback-start")), i18n("Loop selected clip"), this); addAction(QStringLiteral("monitor_loop_clip"), m_loopClip); m_loopClip->setEnabled(false); addAction(QStringLiteral("dvd_wizard"), i18n("DVD Wizard"), this, SLOT(slotDvdWizard()), KoIconUtils::themedIcon(QStringLiteral("media-optical"))); addAction(QStringLiteral("transcode_clip"), i18n("Transcode Clips"), this, SLOT(slotTranscodeClip()), KoIconUtils::themedIcon(QStringLiteral("edit-copy"))); addAction(QStringLiteral("archive_project"), i18n("Archive Project"), this, SLOT(slotArchiveProject()), KoIconUtils::themedIcon(QStringLiteral("document-save-all"))); addAction(QStringLiteral("switch_monitor"), i18n("Switch monitor"), this, SLOT(slotSwitchMonitors()), QIcon(), Qt::Key_T); addAction(QStringLiteral("expand_timeline_clip"), i18n("Expand Clip"), pCore->projectManager(), SLOT(slotExpandClip()), KoIconUtils::themedIcon(QStringLiteral("document-open"))); QAction *overlayInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Info Overlay"), this); addAction(QStringLiteral("monitor_overlay"), overlayInfo); overlayInfo->setCheckable(true); overlayInfo->setData(0x01); QAction *overlayTCInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Timecode"), this); addAction(QStringLiteral("monitor_overlay_tc"), overlayTCInfo); overlayTCInfo->setCheckable(true); overlayTCInfo->setData(0x02); QAction *overlayFpsInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Playback Fps"), this); addAction(QStringLiteral("monitor_overlay_fps"), overlayFpsInfo); overlayFpsInfo->setCheckable(true); overlayFpsInfo->setData(0x20); QAction *overlayMarkerInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Markers"), this); addAction(QStringLiteral("monitor_overlay_markers"), overlayMarkerInfo); overlayMarkerInfo->setCheckable(true); overlayMarkerInfo->setData(0x04); QAction *overlaySafeInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Safe Zones"), this); addAction(QStringLiteral("monitor_overlay_safezone"), overlaySafeInfo); overlaySafeInfo->setCheckable(true); overlaySafeInfo->setData(0x08); QAction *overlayAudioInfo = new QAction(KoIconUtils::themedIcon(QStringLiteral("help-hint")), i18n("Monitor Overlay Audio Waveform"), this); addAction(QStringLiteral("monitor_overlay_audiothumb"), overlayAudioInfo); overlayAudioInfo->setCheckable(true); overlayAudioInfo->setData(0x10); QAction *dropFrames = new QAction(QIcon(), i18n("Real Time (drop frames)"), this); dropFrames->setCheckable(true); dropFrames->setChecked(KdenliveSettings::monitor_dropframes()); addAction(QStringLiteral("mlt_realtime"), dropFrames); connect(dropFrames, &QAction::toggled, this, &MainWindow::slotSwitchDropFrames); KSelectAction *monitorGamma = new KSelectAction(i18n("Monitor Gamma"), this); monitorGamma->addAction(i18n("sRGB (computer)")); monitorGamma->addAction(i18n("Rec. 709 (TV)")); addAction(QStringLiteral("mlt_gamma"), monitorGamma); monitorGamma->setCurrentItem(KdenliveSettings::monitor_gamma()); connect(monitorGamma, static_cast(&KSelectAction::triggered), this, &MainWindow::slotSetMonitorGamma); addAction(QStringLiteral("switch_trim"), i18n("Trim Mode"), this, SLOT(slotSwitchTrimMode()), KoIconUtils::themedIcon(QStringLiteral("cursor-arrow"))); // disable shortcut until fully working, Qt::CTRL + Qt::Key_T); addAction(QStringLiteral("insert_project_tree"), i18n("Insert Zone in Project Bin"), this, SLOT(slotInsertZoneToTree()), QIcon(), Qt::CTRL + Qt::Key_I); addAction(QStringLiteral("insert_timeline"), i18n("Insert Zone in Timeline"), this, SLOT(slotInsertZoneToTimeline()), QIcon(), Qt::SHIFT + Qt::CTRL + Qt::Key_I); QAction *resizeStart = new QAction(QIcon(), i18n("Resize Item Start"), this); addAction(QStringLiteral("resize_timeline_clip_start"), resizeStart); resizeStart->setShortcut(Qt::Key_1); connect(resizeStart, &QAction::triggered, this, &MainWindow::slotResizeItemStart); QAction *resizeEnd = new QAction(QIcon(), i18n("Resize Item End"), this); addAction(QStringLiteral("resize_timeline_clip_end"), resizeEnd); resizeEnd->setShortcut(Qt::Key_2); connect(resizeEnd, &QAction::triggered, this, &MainWindow::slotResizeItemEnd); addAction(QStringLiteral("monitor_seek_snap_backward"), i18n("Go to Previous Snap Point"), this, SLOT(slotSnapRewind()), KoIconUtils::themedIcon(QStringLiteral("media-seek-backward")), Qt::ALT + Qt::Key_Left); addAction(QStringLiteral("seek_clip_start"), i18n("Go to Clip Start"), this, SLOT(slotClipStart()), KoIconUtils::themedIcon(QStringLiteral("media-seek-backward")), Qt::Key_Home); addAction(QStringLiteral("seek_clip_end"), i18n("Go to Clip End"), this, SLOT(slotClipEnd()), KoIconUtils::themedIcon(QStringLiteral("media-seek-forward")), Qt::Key_End); addAction(QStringLiteral("monitor_seek_snap_forward"), i18n("Go to Next Snap Point"), this, SLOT(slotSnapForward()), KoIconUtils::themedIcon(QStringLiteral("media-seek-forward")), Qt::ALT + Qt::Key_Right); addAction(QStringLiteral("delete_timeline_clip"), i18n("Delete Selected Item"), this, SLOT(slotDeleteItem()), KoIconUtils::themedIcon(QStringLiteral("edit-delete")), Qt::Key_Delete); addAction(QStringLiteral("align_playhead"), i18n("Align Playhead to Mouse Position"), this, SLOT(slotAlignPlayheadToMousePos()), QIcon(), Qt::Key_P); QAction *stickTransition = new QAction(i18n("Automatic Transition"), this); stickTransition->setData(QStringLiteral("auto")); stickTransition->setCheckable(true); stickTransition->setEnabled(false); addAction(QStringLiteral("auto_transition"), stickTransition); connect(stickTransition, &QAction::triggered, this, &MainWindow::slotAutoTransition); addAction(QStringLiteral("group_clip"), i18n("Group Clips"), this, SLOT(slotGroupClips()), KoIconUtils::themedIcon(QStringLiteral("object-group")), Qt::CTRL + Qt::Key_G); QAction *ungroupClip = addAction(QStringLiteral("ungroup_clip"), i18n("Ungroup Clips"), this, SLOT(slotUnGroupClips()), KoIconUtils::themedIcon(QStringLiteral("object-ungroup")), Qt::CTRL + Qt::SHIFT + Qt::Key_G); ungroupClip->setData("ungroup_clip"); addAction(QStringLiteral("edit_item_duration"), i18n("Edit Duration"), this, SLOT(slotEditItemDuration()), KoIconUtils::themedIcon(QStringLiteral("measure"))); addAction(QStringLiteral("clip_in_project_tree"), i18n("Clip in Project Bin"), this, SLOT(slotClipInProjectTree()), KoIconUtils::themedIcon(QStringLiteral("go-jump-definition"))); addAction(QStringLiteral("overwrite_to_in_point"), i18n("Overwrite Clip Zone in Timeline"), this, SLOT(slotInsertClipOverwrite()), KoIconUtils::themedIcon(QStringLiteral("timeline-overwrite")), Qt::Key_B); addAction(QStringLiteral("insert_to_in_point"), i18n("Insert Clip Zone in Timeline"), this, SLOT(slotInsertClipInsert()), KoIconUtils::themedIcon(QStringLiteral("timeline-insert")), Qt::Key_V); addAction(QStringLiteral("remove_extract"), i18n("Extract Timeline Zone"), this, SLOT(slotExtractZone()), KoIconUtils::themedIcon(QStringLiteral("timeline-extract")), Qt::SHIFT + Qt::Key_X); addAction(QStringLiteral("remove_lift"), i18n("Lift Timeline Zone"), this, SLOT(slotLiftZone()), KoIconUtils::themedIcon(QStringLiteral("timeline-lift")), Qt::Key_Z); addAction(QStringLiteral("set_render_timeline_zone"), i18n("Add Preview Zone"), this, SLOT(slotDefinePreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-add-zone"))); addAction(QStringLiteral("unset_render_timeline_zone"), i18n("Remove Preview Zone"), this, SLOT(slotRemovePreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-remove-zone"))); addAction(QStringLiteral("clear_render_timeline_zone"), i18n("Remove All Preview Zones"), this, SLOT(slotClearPreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-remove-all"))); addAction(QStringLiteral("prerender_timeline_zone"), i18n("Start Preview Render"), this, SLOT(slotPreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-render-on")), QKeySequence(Qt::SHIFT + Qt::Key_Return)); addAction(QStringLiteral("stop_prerender_timeline"), i18n("Stop Preview Render"), this, SLOT(slotStopPreviewRender()), KoIconUtils::themedIcon(QStringLiteral("preview-render-off"))); addAction(QStringLiteral("select_timeline_clip"), i18n("Select Clip"), this, SLOT(slotSelectTimelineClip()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::Key_Plus); addAction(QStringLiteral("deselect_timeline_clip"), i18n("Deselect Clip"), this, SLOT(slotDeselectTimelineClip()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::Key_Minus); addAction(QStringLiteral("select_add_timeline_clip"), i18n("Add Clip To Selection"), this, SLOT(slotSelectAddTimelineClip()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::ALT + Qt::Key_Plus); addAction(QStringLiteral("select_timeline_transition"), i18n("Select Transition"), this, SLOT(slotSelectTimelineTransition()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::SHIFT + Qt::Key_Plus); addAction(QStringLiteral("deselect_timeline_transition"), i18n("Deselect Transition"), this, SLOT(slotDeselectTimelineTransition()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::SHIFT + Qt::Key_Minus); addAction(QStringLiteral("select_add_timeline_transition"), i18n("Add Transition To Selection"), this, SLOT(slotSelectAddTimelineTransition()), KoIconUtils::themedIcon(QStringLiteral("edit-select")), Qt::ALT + Qt::SHIFT + Qt::Key_Plus); addAction(QStringLiteral("cut_timeline_clip"), i18n("Cut Clip"), this, SLOT(slotCutTimelineClip()), KoIconUtils::themedIcon(QStringLiteral("edit-cut")), Qt::SHIFT + Qt::Key_R); addAction(QStringLiteral("add_clip_marker"), i18n("Add Marker"), this, SLOT(slotAddClipMarker()), KoIconUtils::themedIcon(QStringLiteral("bookmark-new"))); addAction(QStringLiteral("delete_clip_marker"), i18n("Delete Marker"), this, SLOT(slotDeleteClipMarker()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); addAction(QStringLiteral("delete_all_clip_markers"), i18n("Delete All Markers"), this, SLOT(slotDeleteAllClipMarkers()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); QAction *editClipMarker = addAction(QStringLiteral("edit_clip_marker"), i18n("Edit Marker"), this, SLOT(slotEditClipMarker()), KoIconUtils::themedIcon(QStringLiteral("document-properties"))); editClipMarker->setData(QStringLiteral("edit_marker")); addAction(QStringLiteral("add_marker_guide_quickly"), i18n("Add Marker/Guide quickly"), this, SLOT(slotAddMarkerGuideQuickly()), KoIconUtils::themedIcon(QStringLiteral("bookmark-new")), Qt::Key_Asterisk); QAction *splitAudio = addAction(QStringLiteral("split_audio"), i18n("Split Audio"), this, SLOT(slotSplitAudio()), KoIconUtils::themedIcon(QStringLiteral("document-new"))); // "A+V" as data means this action should only be available for clips with audio AND video splitAudio->setData("A+V"); QAction *setAudioAlignReference = addAction(QStringLiteral("set_audio_align_ref"), i18n("Set Audio Reference"), this, SLOT(slotSetAudioAlignReference())); // "A" as data means this action should only be available for clips with audio setAudioAlignReference->setData("A"); QAction *alignAudio = addAction(QStringLiteral("align_audio"), i18n("Align Audio to Reference"), this, SLOT(slotAlignAudio()), QIcon()); // "A" as data means this action should only be available for clips with audio alignAudio->setData("A"); QAction *audioOnly = new QAction(KoIconUtils::themedIcon(QStringLiteral("document-new")), i18n("Audio Only"), this); addAction(QStringLiteral("clip_audio_only"), audioOnly); - audioOnly->setData(PlaylistState::AudioOnly); + audioOnly->setData(QVariant::fromValue(PlaylistState::AudioOnly)); audioOnly->setCheckable(true); QAction *videoOnly = new QAction(KoIconUtils::themedIcon(QStringLiteral("document-new")), i18n("Video Only"), this); addAction(QStringLiteral("clip_video_only"), videoOnly); - videoOnly->setData(PlaylistState::VideoOnly); + videoOnly->setData(QVariant::fromValue(PlaylistState::VideoOnly)); videoOnly->setCheckable(true); - QAction *audioAndVideo = new QAction(KoIconUtils::themedIcon(QStringLiteral("document-new")), i18n("Audio and Video"), this); - addAction(QStringLiteral("clip_audio_and_video"), audioAndVideo); - audioAndVideo->setData(PlaylistState::Original); - audioAndVideo->setCheckable(true); - m_clipTypeGroup = new QActionGroup(this); m_clipTypeGroup->addAction(audioOnly); m_clipTypeGroup->addAction(videoOnly); - m_clipTypeGroup->addAction(audioAndVideo); connect(m_clipTypeGroup, &QActionGroup::triggered, this, &MainWindow::slotUpdateClipType); m_clipTypeGroup->setEnabled(false); addAction(QStringLiteral("insert_space"), i18n("Insert Space"), this, SLOT(slotInsertSpace())); addAction(QStringLiteral("delete_space"), i18n("Remove Space"), this, SLOT(slotRemoveSpace())); addAction(QStringLiteral("delete_space_all_tracks"), i18n("Remove Space In All Tracks"), this, SLOT(slotRemoveAllSpace())); KActionCategory *timelineActions = new KActionCategory(i18n("Tracks"), actionCollection()); QAction *insertTrack = new QAction(QIcon(), i18n("Insert Track"), this); connect(insertTrack, &QAction::triggered, this, &MainWindow::slotInsertTrack); timelineActions->addAction(QStringLiteral("insert_track"), insertTrack); QAction *deleteTrack = new QAction(QIcon(), i18n("Delete Track"), this); connect(deleteTrack, &QAction::triggered, this, &MainWindow::slotDeleteTrack); timelineActions->addAction(QStringLiteral("delete_track"), deleteTrack); deleteTrack->setData("delete_track"); QAction *configTracks = new QAction(KoIconUtils::themedIcon(QStringLiteral("configure")), i18n("Configure Tracks"), this); connect(configTracks, &QAction::triggered, this, &MainWindow::slotConfigTrack); timelineActions->addAction(QStringLiteral("config_tracks"), configTracks); QAction *selectTrack = new QAction(QIcon(), i18n("Select All in Current Track"), this); connect(selectTrack, &QAction::triggered, this, &MainWindow::slotSelectTrack); timelineActions->addAction(QStringLiteral("select_track"), selectTrack); QAction *selectAll = KStandardAction::selectAll(this, SLOT(slotSelectAllTracks()), this); selectAll->setIcon(KoIconUtils::themedIcon(QStringLiteral("kdenlive-select-all"))); selectAll->setShortcutContext(Qt::WidgetWithChildrenShortcut); timelineActions->addAction(QStringLiteral("select_all_tracks"), selectAll); QAction *unselectAll = KStandardAction::deselect(this, SLOT(slotUnselectAllTracks()), this); unselectAll->setIcon(KoIconUtils::themedIcon(QStringLiteral("kdenlive-unselect-all"))); unselectAll->setShortcutContext(Qt::WidgetWithChildrenShortcut); timelineActions->addAction(QStringLiteral("unselect_all_tracks"), unselectAll); kdenliveCategoryMap.insert(QStringLiteral("timeline"), timelineActions); // Cached data management addAction(QStringLiteral("manage_cache"), i18n("Manage Cached Data"), this, SLOT(slotManageCache()), KoIconUtils::themedIcon(QStringLiteral("network-server-database"))); QAction *disablePreview = new QAction(i18n("Disable Timeline Preview"), this); disablePreview->setCheckable(true); addAction(QStringLiteral("disable_preview"), disablePreview); addAction(QStringLiteral("add_guide"), i18n("Add Guide"), this, SLOT(slotAddGuide()), KoIconUtils::themedIcon(QStringLiteral("list-add"))); addAction(QStringLiteral("delete_guide"), i18n("Delete Guide"), this, SLOT(slotDeleteGuide()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); addAction(QStringLiteral("edit_guide"), i18n("Edit Guide"), this, SLOT(slotEditGuide()), KoIconUtils::themedIcon(QStringLiteral("document-properties"))); addAction(QStringLiteral("delete_all_guides"), i18n("Delete All Guides"), this, SLOT(slotDeleteAllGuides()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); QAction *pasteEffects = addAction(QStringLiteral("paste_effects"), i18n("Paste Effects"), this, SLOT(slotPasteEffects()), KoIconUtils::themedIcon(QStringLiteral("edit-paste"))); pasteEffects->setData("paste_effects"); m_saveAction = KStandardAction::save(pCore->projectManager(), SLOT(saveFile()), actionCollection()); m_saveAction->setIcon(KoIconUtils::themedIcon(QStringLiteral("document-save"))); addAction(QStringLiteral("save_selection"), i18n("Save Selection"), pCore->projectManager(), SLOT(slotSaveSelection()), KoIconUtils::themedIcon(QStringLiteral("document-save"))); QAction *sentToLibrary = addAction(QStringLiteral("send_library"), i18n("Add Timeline Selection to Library"), pCore->library(), SLOT(slotAddToLibrary()), KoIconUtils::themedIcon(QStringLiteral("bookmark-new"))); pCore->library()->setupActions(QList() << sentToLibrary); KStandardAction::showMenubar(this, SLOT(showMenuBar(bool)), actionCollection()); QAction *a = KStandardAction::quit(this, SLOT(close()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("application-exit"))); // TODO: make the following connection to slotEditKeys work // KStandardAction::keyBindings(this, SLOT(slotEditKeys()), actionCollection()); a = KStandardAction::preferences(this, SLOT(slotPreferences()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("configure"))); a = KStandardAction::configureNotifications(this, SLOT(configureNotifications()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("configure"))); a = KStandardAction::copy(this, SLOT(slotCopy()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("edit-copy"))); a = KStandardAction::paste(this, SLOT(slotPaste()), actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("edit-paste"))); a = KStandardAction::fullScreen(this, SLOT(slotFullScreen()), this, actionCollection()); a->setIcon(KoIconUtils::themedIcon(QStringLiteral("view-fullscreen"))); QAction *undo = KStandardAction::undo(m_commandStack, SLOT(undo()), actionCollection()); undo->setIcon(KoIconUtils::themedIcon(QStringLiteral("edit-undo"))); undo->setEnabled(false); connect(m_commandStack, &QUndoGroup::canUndoChanged, undo, &QAction::setEnabled); QAction *redo = KStandardAction::redo(m_commandStack, SLOT(redo()), actionCollection()); redo->setIcon(KoIconUtils::themedIcon(QStringLiteral("edit-redo"))); redo->setEnabled(false); connect(m_commandStack, &QUndoGroup::canRedoChanged, redo, &QAction::setEnabled); auto *addClips = new QMenu(this); QAction *addClip = addAction(QStringLiteral("add_clip"), i18n("Add Clip"), pCore->bin(), SLOT(slotAddClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-clip"))); addClips->addAction(addClip); QAction *action = addAction(QStringLiteral("add_color_clip"), i18n("Add Color Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-color-clip"))); action->setData((int)ClipType::Color); addClips->addAction(action); action = addAction(QStringLiteral("add_slide_clip"), i18n("Add Slideshow Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-slide-clip"))); action->setData((int)ClipType::SlideShow); addClips->addAction(action); action = addAction(QStringLiteral("add_text_clip"), i18n("Add Title Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-text-clip"))); action->setData((int)ClipType::Text); addClips->addAction(action); action = addAction(QStringLiteral("add_text_template_clip"), i18n("Add Template Title"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-text-clip"))); action->setData((int)ClipType::TextTemplate); addClips->addAction(action); /*action = addAction(QStringLiteral("add_qtext_clip"), i18n("Add Simple Text Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), KoIconUtils::themedIcon(QStringLiteral("kdenlive-add-text-clip"))); action->setData((int) QText); addClips->addAction(action);*/ QAction *addFolder = addAction(QStringLiteral("add_folder"), i18n("Create Folder"), pCore->bin(), SLOT(slotAddFolder()), KoIconUtils::themedIcon(QStringLiteral("folder-new"))); addClips->addAction(addAction(QStringLiteral("download_resource"), i18n("Online Resources"), this, SLOT(slotDownloadResources()), KoIconUtils::themedIcon(QStringLiteral("edit-download")))); QAction *clipProperties = addAction(QStringLiteral("clip_properties"), i18n("Clip Properties"), pCore->bin(), SLOT(slotSwitchClipProperties()), KoIconUtils::themedIcon(QStringLiteral("document-edit"))); clipProperties->setData("clip_properties"); QAction *openClip = addAction(QStringLiteral("edit_clip"), i18n("Edit Clip"), pCore->bin(), SLOT(slotOpenClip()), KoIconUtils::themedIcon(QStringLiteral("document-open"))); openClip->setData("edit_clip"); openClip->setEnabled(false); QAction *deleteClip = addAction(QStringLiteral("delete_clip"), i18n("Delete Clip"), pCore->bin(), SLOT(slotDeleteClip()), KoIconUtils::themedIcon(QStringLiteral("edit-delete"))); deleteClip->setData("delete_clip"); deleteClip->setEnabled(false); QAction *reloadClip = addAction(QStringLiteral("reload_clip"), i18n("Reload Clip"), pCore->bin(), SLOT(slotReloadClip()), KoIconUtils::themedIcon(QStringLiteral("view-refresh"))); reloadClip->setData("reload_clip"); reloadClip->setEnabled(false); QAction *disableEffects = addAction(QStringLiteral("disable_timeline_effects"), i18n("Disable Timeline Effects"), pCore->projectManager(), SLOT(slotDisableTimelineEffects(bool)), KoIconUtils::themedIcon(QStringLiteral("favorite"))); disableEffects->setData("disable_timeline_effects"); disableEffects->setCheckable(true); disableEffects->setChecked(false); QAction *locateClip = addAction(QStringLiteral("locate_clip"), i18n("Locate Clip..."), pCore->bin(), SLOT(slotLocateClip()), KoIconUtils::themedIcon(QStringLiteral("edit-file"))); locateClip->setData("locate_clip"); locateClip->setEnabled(false); QAction *duplicateClip = addAction(QStringLiteral("duplicate_clip"), i18n("Duplicate Clip"), pCore->bin(), SLOT(slotDuplicateClip()), KoIconUtils::themedIcon(QStringLiteral("edit-copy"))); duplicateClip->setData("duplicate_clip"); duplicateClip->setEnabled(false); QAction *proxyClip = new QAction(i18n("Proxy Clip"), this); addAction(QStringLiteral("proxy_clip"), proxyClip); proxyClip->setData(QStringList() << QString::number((int)AbstractClipJob::PROXYJOB)); proxyClip->setCheckable(true); proxyClip->setChecked(false); addAction(QStringLiteral("switch_track_lock"), i18n("Toggle Track Lock"), pCore->projectManager(), SLOT(slotSwitchTrackLock()), QIcon(), Qt::SHIFT + Qt::Key_L); addAction(QStringLiteral("switch_all_track_lock"), i18n("Toggle All Track Lock"), pCore->projectManager(), SLOT(slotSwitchAllTrackLock()), QIcon(), Qt::CTRL + Qt::SHIFT + Qt::Key_L); addAction(QStringLiteral("switch_track_target"), i18n("Toggle Track Target"), pCore->projectManager(), SLOT(slotSwitchTrackTarget()), QIcon(), Qt::SHIFT + Qt::Key_T); QHash actions; actions.insert(QStringLiteral("locate"), locateClip); actions.insert(QStringLiteral("reload"), reloadClip); actions.insert(QStringLiteral("duplicate"), duplicateClip); actions.insert(QStringLiteral("proxy"), proxyClip); actions.insert(QStringLiteral("properties"), clipProperties); actions.insert(QStringLiteral("open"), openClip); actions.insert(QStringLiteral("delete"), deleteClip); actions.insert(QStringLiteral("folder"), addFolder); pCore->bin()->setupMenu(addClips, addClip, actions); // Setup effects and transitions actions. KActionCategory *transitionActions = new KActionCategory(i18n("Transitions"), actionCollection()); // m_transitions = new QAction*[transitions.count()]; for (int i = 0; i < transitions.count(); ++i) { QStringList effectInfo = transitions.effectIdInfo(i); if (effectInfo.isEmpty()) { continue; } auto *transAction = new QAction(effectInfo.at(0), this); transAction->setData(effectInfo); transAction->setIconVisibleInMenu(false); m_transitions << transAction; QString id = effectInfo.at(2); if (id.isEmpty()) { id = effectInfo.at(1); } transitionActions->addAction("transition_" + id, transAction); } // monitor actions addAction(QStringLiteral("extract_frame"), i18n("Extract frame..."), pCore->monitorManager(), SLOT(slotExtractCurrentFrame()), KoIconUtils::themedIcon(QStringLiteral("insert-image"))); addAction(QStringLiteral("extract_frame_to_project"), i18n("Extract frame to project..."), pCore->monitorManager(), SLOT(slotExtractCurrentFrameToProject()), KoIconUtils::themedIcon(QStringLiteral("insert-image"))); } void MainWindow::saveOptions() { KdenliveSettings::self()->save(); } bool MainWindow::readOptions() { KSharedConfigPtr config = KSharedConfig::openConfig(); pCore->projectManager()->recentFilesAction()->loadEntries(KConfigGroup(config, "Recent Files")); if (KdenliveSettings::defaultprojectfolder().isEmpty()) { QDir dir(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); dir.mkpath(QStringLiteral(".")); KdenliveSettings::setDefaultprojectfolder(dir.absolutePath()); } if (KdenliveSettings::trackheight() == 0) { QFontMetrics metrics(font()); int trackHeight = 2 * metrics.height(); QStyle *style = qApp->style(); trackHeight += style->pixelMetric(QStyle::PM_ToolBarIconSize) + 2 * style->pixelMetric(QStyle::PM_ToolBarItemMargin) + style->pixelMetric(QStyle::PM_ToolBarItemSpacing) + 2; KdenliveSettings::setTrackheight(trackHeight); } if (KdenliveSettings::trackheight() == 0) { KdenliveSettings::setTrackheight(50); } bool firstRun = false; KConfigGroup initialGroup(config, "version"); if (!initialGroup.exists() || KdenliveSettings::sdlAudioBackend().isEmpty()) { // First run, check if user is on a KDE Desktop firstRun = true; // this is our first run, show Wizard QPointer w = new Wizard(true); if (w->exec() == QDialog::Accepted && w->isOk()) { w->adjustSettings(); delete w; } else { delete w; ::exit(1); } } else if (!KdenliveSettings::ffmpegpath().isEmpty() && !QFile::exists(KdenliveSettings::ffmpegpath())) { // Invalid entry for FFmpeg, check system QPointer w = new Wizard(true); if (w->exec() == QDialog::Accepted && w->isOk()) { w->adjustSettings(); } delete w; } initialGroup.writeEntry("version", version); return firstRun; } void MainWindow::slotRunWizard() { QPointer w = new Wizard(false, this); if (w->exec() == QDialog::Accepted && w->isOk()) { w->adjustSettings(); } delete w; } void MainWindow::slotRefreshProfiles() { KdenliveSettingsDialog *d = static_cast(KConfigDialog::exists(QStringLiteral("settings"))); if (d) { d->checkProfile(); } } void MainWindow::slotEditProjectSettings() { KdenliveDoc *project = pCore->currentDoc(); QPoint p = getMainTimeline()->getTracksCount(); ProjectSettings *w = new ProjectSettings(project, project->metadata(), getMainTimeline()->controller()->extractCompositionLumas(), p.x(), p.y(), project->projectTempFolder(), true, !project->isModified(), this); connect(w, &ProjectSettings::disableProxies, this, &MainWindow::slotDisableProxies); // connect(w, SIGNAL(disablePreview()), pCore->projectManager()->currentTimeline(), SLOT(invalidateRange())); connect(w, &ProjectSettings::refreshProfiles, this, &MainWindow::slotRefreshProfiles); if (w->exec() == QDialog::Accepted) { QString profile = w->selectedProfile(); // project->setProjectFolder(w->selectedFolder()); bool modified = false; if (m_renderWidget) { m_renderWidget->setDocumentPath(project->projectDataFolder()); } if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) { slotSwitchVideoThumbs(); } if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) { slotSwitchAudioThumbs(); } if (pCore->getCurrentProfile()->path() != profile || project->profileChanged(profile)) { pCore->setCurrentProfile(profile); pCore->projectManager()->slotResetProfiles(); slotUpdateDocumentState(true); } if (project->getDocumentProperty(QStringLiteral("proxyparams")) != w->proxyParams() || project->getDocumentProperty(QStringLiteral("proxyextension")) != w->proxyExtension()) { modified = true; project->setDocumentProperty(QStringLiteral("proxyparams"), w->proxyParams()); project->setDocumentProperty(QStringLiteral("proxyextension"), w->proxyExtension()); if (pCore->binController()->clipCount() > 0 && KMessageBox::questionYesNo(this, i18n("You have changed the proxy parameters. Do you want to recreate all proxy clips for this project?")) == KMessageBox::Yes) { // TODO: rebuild all proxies pCore->bin()->rebuildProxies(); } } if (project->getDocumentProperty(QStringLiteral("generateproxy")) != QString::number((int)w->generateProxy())) { modified = true; project->setDocumentProperty(QStringLiteral("generateproxy"), QString::number((int)w->generateProxy())); } if (project->getDocumentProperty(QStringLiteral("proxyminsize")) != QString::number(w->proxyMinSize())) { modified = true; project->setDocumentProperty(QStringLiteral("proxyminsize"), QString::number(w->proxyMinSize())); } if (project->getDocumentProperty(QStringLiteral("generateimageproxy")) != QString::number((int)w->generateImageProxy())) { modified = true; project->setDocumentProperty(QStringLiteral("generateimageproxy"), QString::number((int)w->generateImageProxy())); } if (project->getDocumentProperty(QStringLiteral("proxyimageminsize")) != QString::number(w->proxyImageMinSize())) { modified = true; project->setDocumentProperty(QStringLiteral("proxyimageminsize"), QString::number(w->proxyImageMinSize())); } if (project->getDocumentProperty(QStringLiteral("resizepreview")) != QString::number(w->resizePreview())) { modified = true; project->setDocumentProperty(QStringLiteral("resizepreview"), QString::number(w->resizePreview())); } if (project->getDocumentProperty(QStringLiteral("previewheight")) != QString::number(w->previewHeight())) { modified = true; project->setDocumentProperty(QStringLiteral("previewheight"), QString::number(w->previewHeight())); } if (project->getDocumentProperty(QStringLiteral("proxyimagesize")) != QString::number(w->proxyImageSize())) { modified = true; project->setDocumentProperty(QStringLiteral("proxyimagesize"), QString::number(w->proxyImageSize())); } if (w->resizePreviewChanged() || project->updatePreviewSettings(w->selectedPreview())) { // preview setting changed, reset cache and update getMainTimeline()->controller()->resetPreview(); } if (QString::number((int)w->useProxy()) != project->getDocumentProperty(QStringLiteral("enableproxy"))) { project->setDocumentProperty(QStringLiteral("enableproxy"), QString::number((int)w->useProxy())); modified = true; slotUpdateProxySettings(); } if (w->metadata() != project->metadata()) { project->setMetadata(w->metadata()); } QString newProjectFolder = w->storageFolder(); if (newProjectFolder.isEmpty()) { newProjectFolder = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); } if (newProjectFolder != project->projectTempFolder()) { KMessageBox::ButtonCode answer; // Project folder changed: if (project->isModified()) { answer = KMessageBox::warningContinueCancel(this, i18n("The current project has not been saved. This will first save the project, then move " "all temporary files from %1 to %2, and the project file will be reloaded", project->projectTempFolder(), newProjectFolder)); if (answer == KMessageBox::Continue) { pCore->projectManager()->saveFile(); } } else { answer = KMessageBox::warningContinueCancel( this, i18n("This will move all temporary files from %1 to %2, the project file will then be reloaded", project->projectTempFolder(), newProjectFolder)); } if (answer == KMessageBox::Continue) { // Proceeed with move QString documentId = QDir::cleanPath(project->getDocumentProperty(QStringLiteral("documentid"))); bool ok; documentId.toLongLong(&ok, 10); if (!ok || documentId.isEmpty()) { KMessageBox::sorry(this, i18n("Cannot perform operation, invalid document id: %1", documentId)); } else { QDir newDir(newProjectFolder); QDir oldDir(project->projectTempFolder()); if (newDir.exists(documentId)) { KMessageBox::sorry(this, i18n("Cannot perform operation, target directory already exists: %1", newDir.absoluteFilePath(documentId))); } else { // Proceed with the move pCore->projectManager()->moveProjectData(oldDir.absoluteFilePath(documentId), newDir.absolutePath()); } } } } if (modified) { project->setModified(); } } delete w; } void MainWindow::slotDisableProxies() { pCore->currentDoc()->setDocumentProperty(QStringLiteral("enableproxy"), QString::number((int)false)); pCore->currentDoc()->setModified(); slotUpdateProxySettings(); } void MainWindow::slotStopRenderProject() { if (m_renderWidget) { m_renderWidget->slotAbortCurrentJob(); } } void MainWindow::slotRenderProject() { KdenliveDoc *project = pCore->currentDoc(); if (!m_renderWidget) { QString projectfolder = project ? project->projectDataFolder() + QDir::separator() : KdenliveSettings::defaultprojectfolder(); if (project) { m_renderWidget = new RenderWidget(projectfolder, project->useProxy(), this); connect(m_renderWidget, &RenderWidget::shutdown, this, &MainWindow::slotShutdown); connect(m_renderWidget, &RenderWidget::selectedRenderProfile, this, &MainWindow::slotSetDocumentRenderProfile); connect(m_renderWidget, &RenderWidget::prepareRenderingData, this, &MainWindow::slotPrepareRendering); connect(m_renderWidget, &RenderWidget::abortProcess, this, &MainWindow::abortRenderJob); connect(m_renderWidget, &RenderWidget::openDvdWizard, this, &MainWindow::slotDvdWizard); connect(this, &MainWindow::updateRenderWidgetProfile, m_renderWidget, &RenderWidget::adjustViewToProfile); double projectDuration = GenTime(getMainTimeline()->controller()->duration(), pCore->getCurrentFps()).ms() / 1000; m_renderWidget->setGuides(project->getGuideModel()->getAllMarkers(), projectDuration); m_renderWidget->setDocumentPath(project->projectDataFolder()); m_renderWidget->setRenderProfile(project->getRenderProperties()); } if (m_compositeAction->currentAction()) { m_renderWidget->errorMessage(RenderWidget::CompositeError, m_compositeAction->currentAction()->data().toInt() == 1 ? i18n("Rendering using low quality track compositing") : QString()); } } slotCheckRenderStatus(); m_renderWidget->show(); // m_renderWidget->showNormal(); // What are the following lines supposed to do? // m_renderWidget->enableAudio(false); // m_renderWidget->export_audio; } void MainWindow::slotCheckRenderStatus() { // Make sure there are no missing clips // TODO /*if (m_renderWidget) m_renderWidget->missingClips(pCore->bin()->hasMissingClips());*/ } void MainWindow::setRenderingProgress(const QString &url, int progress) { emit setRenderProgress(progress); if (m_renderWidget) { m_renderWidget->setRenderJob(url, progress); } } void MainWindow::setRenderingFinished(const QString &url, int status, const QString &error) { emit setRenderProgress(100); if (m_renderWidget) { m_renderWidget->setRenderStatus(url, status, error); } } void MainWindow::addProjectClip(const QString &url) { if (pCore->currentDoc()) { QStringList ids = pCore->binController()->getBinIdsByResource(QFileInfo(url)); if (!ids.isEmpty()) { // Clip is already in project bin, abort return; } ClipCreator::createClipFromFile(url, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel()); } } void MainWindow::addTimelineClip(const QString &url) { if (pCore->currentDoc()) { QStringList ids = pCore->binController()->getBinIdsByResource(QFileInfo(url)); if (!ids.isEmpty()) { pCore->selectBinClip(ids.constFirst()); slotInsertClipInsert(); } } } void MainWindow::addEffect(const QString &effectName) { QStringList effectInfo; effectInfo << effectName << effectName; const QDomElement effect = EffectsListWidget::itemEffect(5, effectInfo); if (!effect.isNull()) { slotAddEffect(effect); } else { qCDebug(KDENLIVE_LOG) << " * * *EFFECT: " << effectName << " NOT AVAILABLE"; exitApp(); } } void MainWindow::scriptRender(const QString &url) { slotRenderProject(); m_renderWidget->slotPrepareExport(true, url); } void MainWindow::exitApp() { QApplication::exit(0); } void MainWindow::slotCleanProject() { if (KMessageBox::warningContinueCancel(this, i18n("This will remove all unused clips from your project."), i18n("Clean up project")) == KMessageBox::Cancel) { return; } pCore->bin()->cleanup(); } void MainWindow::slotUpdateMousePosition(int pos) { if (pCore->currentDoc()) { switch (m_timeFormatButton->currentItem()) { case 0: m_timeFormatButton->setText(pCore->currentDoc()->timecode().getTimecodeFromFrames(pos) + QStringLiteral(" / ") + pCore->currentDoc()->timecode().getTimecodeFromFrames(getMainTimeline()->controller()->duration())); break; default: m_timeFormatButton->setText( QStringLiteral("%1 / %2").arg(pos, 6, 10, QLatin1Char('0')).arg(getMainTimeline()->controller()->duration(), 6, 10, QLatin1Char('0'))); } } } void MainWindow::slotUpdateProjectDuration(int pos) { Q_UNUSED(pos) if (pCore->currentDoc()) { slotUpdateMousePosition(getMainTimeline()->controller()->getMousePos()); } } void MainWindow::slotUpdateDocumentState(bool modified) { setWindowTitle(pCore->currentDoc()->description()); setWindowModified(modified); m_saveAction->setEnabled(modified); } void MainWindow::connectDocument() { KdenliveDoc *project = pCore->currentDoc(); connect(project, &KdenliveDoc::startAutoSave, pCore->projectManager(), &ProjectManager::slotStartAutoSave); connect(project, &KdenliveDoc::reloadEffects, this, &MainWindow::slotReloadEffects); KdenliveSettings::setProject_fps(pCore->getCurrentFps()); // TODO REFAC: reconnect to new timeline /* Timeline *trackView = pCore->projectManager()->currentTimeline(); connect(trackView, &Timeline::configTrack, this, &MainWindow::slotConfigTrack); connect(trackView, &Timeline::updateTracksInfo, this, &MainWindow::slotUpdateTrackInfo); connect(trackView, &Timeline::mousePosition, this, &MainWindow::slotUpdateMousePosition); connect(pCore->producerQueue(), &ProducerQueue::infoProcessingFinished, trackView->projectView(), &CustomTrackView::slotInfoProcessingFinished, Qt::DirectConnection); connect(trackView->projectView(), &CustomTrackView::importKeyframes, this, &MainWindow::slotProcessImportKeyframes); connect(trackView->projectView(), &CustomTrackView::updateTrimMode, this, &MainWindow::setTrimMode); connect(m_projectMonitor, &Monitor::multitrackView, trackView, &Timeline::slotMultitrackView); connect(m_projectMonitor, SIGNAL(renderPosition(int)), trackView, SLOT(moveCursorPos(int))); connect(m_projectMonitor, SIGNAL(zoneUpdated(QPoint)), trackView, SLOT(slotSetZone(QPoint))); connect(trackView->projectView(), &CustomTrackView::guidesUpdated, this, &MainWindow::slotGuidesUpdated); connect(trackView->projectView(), &CustomTrackView::loadMonitorScene, m_projectMonitor, &Monitor::slotShowEffectScene); connect(trackView->projectView(), &CustomTrackView::setQmlProperty, m_projectMonitor, &Monitor::setQmlProperty); connect(m_projectMonitor, SIGNAL(acceptRipple(bool)), trackView->projectView(), SLOT(slotAcceptRipple(bool))); connect(m_projectMonitor, SIGNAL(switchTrimMode(int)), trackView->projectView(), SLOT(switchTrimMode(int))); connect(project, &KdenliveDoc::saveTimelinePreview, trackView, &Timeline::slotSaveTimelinePreview); connect(trackView, SIGNAL(showTrackEffects(int, TrackInfo)), this, SLOT(slotTrackSelected(int, TrackInfo))); connect(trackView->projectView(), &CustomTrackView::clipItemSelected, this, &MainWindow::slotTimelineClipSelected, Qt::DirectConnection); connect(trackView->projectView(), &CustomTrackView::setActiveKeyframe, m_effectStack, &EffectStackView2::setActiveKeyframe); connect(trackView->projectView(), SIGNAL(transitionItemSelected(Transition *, int, QPoint, bool)), m_effectStack, SLOT(slotTransitionItemSelected(Transition *, int, QPoint, bool)), Qt::DirectConnection); connect(trackView->projectView(), SIGNAL(transitionItemSelected(Transition *, int, QPoint, bool)), this, SLOT(slotActivateTransitionView(Transition *))); connect(trackView->projectView(), &CustomTrackView::zoomIn, this, &MainWindow::slotZoomIn); connect(trackView->projectView(), &CustomTrackView::zoomOut, this, &MainWindow::slotZoomOut); connect(trackView, SIGNAL(setZoom(int)), this, SLOT(slotSetZoom(int))); connect(trackView, SIGNAL(displayMessage(QString, MessageType)), m_messageLabel, SLOT(setMessage(QString, MessageType))); connect(trackView->projectView(), SIGNAL(displayMessage(QString, MessageType)), m_messageLabel, SLOT(setMessage(QString, MessageType))); connect(pCore->bin(), &Bin::clipNameChanged, trackView->projectView(), &CustomTrackView::clipNameChanged); connect(trackView->projectView(), SIGNAL(showClipFrame(QString, int)), pCore->bin(), SLOT(selectClipById(QString, int))); connect(trackView->projectView(), SIGNAL(playMonitor()), m_projectMonitor, SLOT(slotPlay())); connect(trackView->projectView(), &CustomTrackView::pauseMonitor, m_projectMonitor, &Monitor::pause, Qt::DirectConnection); connect(m_projectMonitor, &Monitor::addEffect, trackView->projectView(), &CustomTrackView::slotAddEffectToCurrentItem); connect(trackView->projectView(), SIGNAL(transitionItemSelected(Transition *, int, QPoint, bool)), m_projectMonitor, SLOT(slotSetSelectedClip(Transition *))); connect(pCore->bin(), SIGNAL(gotFilterJobResults(QString, int, int, stringMap, stringMap)), trackView->projectView(), SLOT(slotGotFilterJobResults(QString, int, int, stringMap, stringMap))); //TODO //connect(m_projectList, SIGNAL(addMarkers(QString,QList)), trackView->projectView(), SLOT(slotAddClipMarker(QString,QList))); // Effect stack signals connect(m_effectStack, &EffectStackView2::updateEffect, trackView->projectView(), &CustomTrackView::slotUpdateClipEffect); connect(m_effectStack, &EffectStackView2::updateClipRegion, trackView->projectView(), &CustomTrackView::slotUpdateClipRegion); connect(m_effectStack, SIGNAL(removeEffect(ClipItem *, int, QDomElement)), trackView->projectView(), SLOT(slotDeleteEffect(ClipItem *, int, QDomElement))); connect(m_effectStack, SIGNAL(removeEffectGroup(ClipItem *, int, QDomDocument)), trackView->projectView(), SLOT(slotDeleteEffectGroup(ClipItem *, int, QDomDocument))); connect(m_effectStack, SIGNAL(addEffect(ClipItem *, QDomElement, int)), trackView->projectView(), SLOT(slotAddEffect(ClipItem *, QDomElement, int))); connect(m_effectStack, SIGNAL(changeEffectState(ClipItem *, int, QList, bool)), trackView->projectView(), SLOT(slotChangeEffectState(ClipItem *, int, QList, bool))); connect(m_effectStack, SIGNAL(changeEffectPosition(ClipItem *, int, QList, int)), trackView->projectView(), SLOT(slotChangeEffectPosition(ClipItem *, int, QList, int))); connect(m_effectStack, &EffectStackView2::refreshEffectStack, trackView->projectView(), &CustomTrackView::slotRefreshEffects); connect(m_effectStack, &EffectStackView2::seekTimeline, trackView->projectView(), &CustomTrackView::seekCursorPos); connect(m_effectStack, SIGNAL(importClipKeyframes(GraphicsRectItem, ItemInfo, QDomElement, QMap)), trackView->projectView(), SLOT(slotImportClipKeyframes(GraphicsRectItem, ItemInfo, QDomElement, QMap))); // Transition config signals connect(m_effectStack->transitionConfig(), SIGNAL(transitionUpdated(Transition *, QDomElement)), trackView->projectView(), SLOT(slotTransitionUpdated(Transition *, QDomElement))); connect(m_effectStack->transitionConfig(), &TransitionSettings::seekTimeline, trackView->projectView(), &CustomTrackView::seekCursorPos); connect(trackView->projectView(), SIGNAL(activateDocumentMonitor()), m_projectMonitor, SLOT(slotActivateMonitor()), Qt::DirectConnection); connect(project, &KdenliveDoc::updateFps, this, &MainWindow::slotUpdateProfile, Qt::DirectConnection); connect(trackView, &Timeline::zoneMoved, this, &MainWindow::slotZoneMoved); trackView->projectView()->setContextMenu(m_timelineContextMenu, m_timelineContextClipMenu, m_timelineContextTransitionMenu, m_clipTypeGroup, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); */ connect(m_projectMonitor, SIGNAL(zoneUpdated(QPoint)), project, SLOT(setModified())); connect(m_clipMonitor, SIGNAL(zoneUpdated(QPoint)), project, SLOT(setModified())); connect(project, &KdenliveDoc::docModified, this, &MainWindow::slotUpdateDocumentState); connect(pCore->bin(), SIGNAL(displayMessage(QString, int, MessageType)), m_messageLabel, SLOT(setProgressMessage(QString, int, MessageType))); if (m_renderWidget) { slotCheckRenderStatus(); // m_renderWidget->setGuides(pCore->projectManager()->currentTimeline()->projectView()->guidesData(), project->projectDuration()); m_renderWidget->setDocumentPath(project->projectDataFolder()); m_renderWidget->setRenderProfile(project->getRenderProperties()); } m_zoomSlider->setValue(project->zoom().x()); m_commandStack->setActiveStack(project->commandStack().get()); setWindowTitle(project->description()); setWindowModified(project->isModified()); m_saveAction->setEnabled(project->isModified()); m_normalEditTool->setChecked(true); connect(m_projectMonitor, &Monitor::durationChanged, this, &MainWindow::slotUpdateProjectDuration); pCore->monitorManager()->setDocument(project); // TODO REFAC: fix // trackView->updateProfile(1.0); // Init document zone // m_projectMonitor->slotZoneMoved(trackView->inPoint(), trackView->outPoint()); // Update the mouse position display so it will display in DF/NDF format by default based on the project setting. // slotUpdateMousePosition(0); // Update guides info in render widget // slotGuidesUpdated(); // set tool to select tool setTrimMode(QString()); m_buttonSelectTool->setChecked(true); connect(m_projectMonitorDock, &QDockWidget::visibilityChanged, m_projectMonitor, &Monitor::slotRefreshMonitor, Qt::UniqueConnection); connect(m_clipMonitorDock, &QDockWidget::visibilityChanged, m_clipMonitor, &Monitor::slotRefreshMonitor, Qt::UniqueConnection); } void MainWindow::slotZoneMoved(int start, int end) { pCore->currentDoc()->setZone(start, end); QPoint zone(start, end); m_projectMonitor->slotLoadClipZone(zone); } void MainWindow::slotGuidesUpdated() { if (m_renderWidget) { double projectDuration = GenTime(getMainTimeline()->controller()->duration() - TimelineModel::seekDuration - 2, pCore->getCurrentFps()).ms() / 1000; m_renderWidget->setGuides(pCore->currentDoc()->getGuideModel()->getAllMarkers(), projectDuration); } } void MainWindow::slotEditKeys() { KShortcutsDialog dialog(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); dialog.addCollection(actionCollection(), i18nc("general keyboard shortcuts", "General")); dialog.configure(); } void MainWindow::slotPreferences(int page, int option) { /* * An instance of your dialog could be already created and could be * cached, in which case you want to display the cached dialog * instead of creating another one */ if (KConfigDialog::showDialog(QStringLiteral("settings"))) { KdenliveSettingsDialog *d = static_cast(KConfigDialog::exists(QStringLiteral("settings"))); if (page != -1) { d->showPage(page, option); } return; } // KConfigDialog didn't find an instance of this dialog, so lets // create it : // Get the mappable actions in localized form QMap actions; KActionCollection *collection = actionCollection(); QRegExp ampEx("&{1,1}"); for (const QString &action_name : m_actionNames) { QString action_text = collection->action(action_name)->text(); action_text.remove(ampEx); actions[action_text] = action_name; } auto *dialog = new KdenliveSettingsDialog(actions, m_gpuAllowed, this); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::updateConfiguration); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::configurationChanged); connect(dialog, &KdenliveSettingsDialog::doResetProfile, pCore->projectManager(), &ProjectManager::slotResetProfiles); connect(dialog, &KdenliveSettingsDialog::checkTabPosition, this, &MainWindow::slotCheckTabPosition); connect(dialog, &KdenliveSettingsDialog::restartKdenlive, this, &MainWindow::slotRestart); connect(dialog, &KdenliveSettingsDialog::updateLibraryFolder, pCore.get(), &Core::updateLibraryPath); connect(dialog, &KdenliveSettingsDialog::audioThumbFormatChanged, m_timelineTabs, &TimelineTabs::audioThumbFormatChanged); connect(dialog, &KdenliveSettingsDialog::resetView, this, &MainWindow::resetTimelineTracks); dialog->show(); if (page != -1) { dialog->showPage(page, option); } } void MainWindow::slotCheckTabPosition() { int pos = tabPosition(Qt::LeftDockWidgetArea); if (KdenliveSettings::tabposition() != pos) { setTabPosition(Qt::AllDockWidgetAreas, (QTabWidget::TabPosition)KdenliveSettings::tabposition()); } } void MainWindow::slotRestart() { m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } void MainWindow::closeEvent(QCloseEvent *event) { KXmlGuiWindow::closeEvent(event); if (event->isAccepted()) { #ifdef Q_OS_WIN QProcess::startDetached(QStandardPaths::findExecutable(QStringLiteral("kdeinit5")) + " --terminate"); #endif QApplication::exit(m_exitCode); return; } } void MainWindow::updateConfiguration() { // TODO: we should apply settings to all projects, not only the current one m_buttonAudioThumbs->setChecked(KdenliveSettings::audiothumbnails()); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); m_buttonShowMarkers->setChecked(KdenliveSettings::showmarkers()); slotSwitchAutomaticTransition(); // Update list of transcoding profiles buildDynamicActions(); loadClipActions(); } void MainWindow::slotSwitchVideoThumbs() { KdenliveSettings::setVideothumbnails(!KdenliveSettings::videothumbnails()); m_timelineTabs->showThumbnailsChanged(); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); } void MainWindow::slotSwitchAudioThumbs() { KdenliveSettings::setAudiothumbnails(!KdenliveSettings::audiothumbnails()); m_timelineTabs->showAudioThumbnailsChanged(); m_buttonAudioThumbs->setChecked(KdenliveSettings::audiothumbnails()); } void MainWindow::slotSwitchMarkersComments() { KdenliveSettings::setShowmarkers(!KdenliveSettings::showmarkers()); getMainTimeline()->controller()->showMarkersChanged(); m_buttonShowMarkers->setChecked(KdenliveSettings::showmarkers()); } void MainWindow::slotSwitchSnap() { KdenliveSettings::setSnaptopoints(!KdenliveSettings::snaptopoints()); m_buttonSnap->setChecked(KdenliveSettings::snaptopoints()); getMainTimeline()->controller()->snapChanged(KdenliveSettings::snaptopoints()); } void MainWindow::slotSwitchAutomaticTransition() { KdenliveSettings::setAutomatictransitions(!KdenliveSettings::automatictransitions()); m_buttonAutomaticTransition->setChecked(KdenliveSettings::automatictransitions()); } void MainWindow::slotDeleteItem() { if ((QApplication::focusWidget() != nullptr) && (QApplication::focusWidget()->parentWidget() != nullptr) && QApplication::focusWidget()->parentWidget() == pCore->bin()) { pCore->bin()->slotDeleteClip(); } else { QWidget *widget = QApplication::focusWidget(); while ((widget != nullptr) && widget != this) { if (widget == m_effectStackDock) { // TODO refac: reimplement // m_effectStack->deleteCurrentEffect(); return; } widget = widget->parentWidget(); } // effect stack has no focus getMainTimeline()->controller()->deleteSelectedClips(); } } void MainWindow::slotAddClipMarker() { std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { return; } else { clip = m_clipMonitor->currentController(); pos = GenTime(m_clipMonitor->position(), pCore->getCurrentFps()); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to add marker"), ErrorMessage); return; } QString id = clip->AbstractProjectItem::clipId(); clip->getMarkerModel()->editMarkerGui(pos, this, true, clip.get()); } void MainWindow::slotDeleteClipMarker(bool allowGuideDeletion) { std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { // TODO refac retrieve active clip /* if (pCore->projectManager()->currentTimeline()) { ClipItem *item = pCore->projectManager()->currentTimeline()->projectView()->getActiveClipUnderCursor(); if (item) { pos = (GenTime(m_projectMonitor->position(), pCore->getCurrentFps()) - item->startPos() + item->cropStart()) / item->speed(); clip = pCore->bin()->getBinClip(item->getBinId()); } } */ } else { clip = m_clipMonitor->currentController(); pos = GenTime(m_clipMonitor->position(), pCore->getCurrentFps()); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to remove marker"), ErrorMessage); return; } QString id = clip->AbstractProjectItem::clipId(); bool markerFound = false; CommentedTime marker = clip->getMarkerModel()->getMarker(pos, &markerFound); if (!markerFound) { if (allowGuideDeletion && m_projectMonitor->isActive()) { slotDeleteGuide(); } else { m_messageLabel->setMessage(i18n("No marker found at cursor time"), ErrorMessage); } return; } clip->getMarkerModel()->removeMarker(pos); } void MainWindow::slotDeleteAllClipMarkers() { std::shared_ptr clip(nullptr); if (m_projectMonitor->isActive()) { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { ClipItem *item = pCore->projectManager()->currentTimeline()->projectView()->getActiveClipUnderCursor(); if (item) { clip = pCore->bin()->getBinClip(item->getBinId()); } } */ } else { clip = m_clipMonitor->currentController(); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to remove marker"), ErrorMessage); return; } bool ok = clip->getMarkerModel()->removeAllMarkers(); if (!ok) { m_messageLabel->setMessage(i18n("An error occured while deleting markers"), ErrorMessage); return; } } void MainWindow::slotEditClipMarker() { std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { ClipItem *item = pCore->projectManager()->currentTimeline()->projectView()->getActiveClipUnderCursor(); if (item) { pos = (GenTime(m_projectMonitor->position(), pCore->getCurrentFps()) - item->startPos() + item->cropStart()) / item->speed(); clip = pCore->bin()->getBinClip(item->getBinId()); } } */ } else { clip = m_clipMonitor->currentController(); pos = GenTime(m_clipMonitor->position(), pCore->getCurrentFps()); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to edit marker"), ErrorMessage); return; } QString id = clip->AbstractProjectItem::clipId(); bool markerFound = false; CommentedTime oldMarker = clip->getMarkerModel()->getMarker(pos, &markerFound); if (!markerFound) { m_messageLabel->setMessage(i18n("No marker found at cursor time"), ErrorMessage); return; } clip->getMarkerModel()->editMarkerGui(pos, this, false, clip.get()); } void MainWindow::slotAddMarkerGuideQuickly() { if (!getMainTimeline() || !pCore->currentDoc()) { return; } if (m_clipMonitor->isActive()) { std::shared_ptr clip(m_clipMonitor->currentController()); GenTime pos(m_clipMonitor->position(), pCore->getCurrentFps()); if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to add marker"), ErrorMessage); return; } CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type()); clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType()); } else { getMainTimeline()->controller()->switchGuide(); } } void MainWindow::slotAddGuide() { getMainTimeline()->controller()->switchGuide(); } void MainWindow::slotInsertSpace() { getMainTimeline()->controller()->insertSpace(); } void MainWindow::slotRemoveSpace() { getMainTimeline()->controller()->removeSpace(-1, -1, false); } void MainWindow::slotRemoveAllSpace() { getMainTimeline()->controller()->removeSpace(-1, -1, true); } void MainWindow::slotInsertTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); getMainTimeline()->controller()->addTrack(-1); } void MainWindow::slotDeleteTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); getMainTimeline()->controller()->addTrack(-1); } void MainWindow::slotConfigTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); getMainTimeline()->controller()->deleteTrack(-1); } void MainWindow::slotSelectTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->slotSelectClipsInTrack(); } */ } void MainWindow::slotSelectAllTracks() { // TODO refac /* pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->slotSelectAllClips(); } */ } void MainWindow::slotUnselectAllTracks() { // TODO refac /* pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->clearSelection(); } */ } void MainWindow::slotEditGuide() { getMainTimeline()->controller()->editGuide(); } void MainWindow::slotDeleteGuide() { getMainTimeline()->controller()->switchGuide(-1, true); } void MainWindow::slotDeleteAllGuides() { pCore->currentDoc()->getGuideModel()->removeAllMarkers(); } void MainWindow::slotCutTimelineClip() { getMainTimeline()->controller()->cutClipUnderCursor(); } void MainWindow::slotInsertClipOverwrite() { const QString &binId = m_clipMonitor->activeClipId(); if (binId.isEmpty()) { // No clip in monitor return; } getMainTimeline()->controller()->insertZone(binId, m_clipMonitor->getZoneInfo(), true); } void MainWindow::slotInsertClipInsert() { const QString &binId = m_clipMonitor->activeClipId(); if (binId.isEmpty()) { // No clip in monitor return; } getMainTimeline()->controller()->insertZone(binId, m_clipMonitor->getZoneInfo(), false); } void MainWindow::slotExtractZone() { getMainTimeline()->controller()->extractZone(m_clipMonitor->getZoneInfo()); } void MainWindow::slotLiftZone() { getMainTimeline()->controller()->liftZone(m_clipMonitor->getZoneInfo()); } void MainWindow::slotPreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->startPreviewRender(); } } void MainWindow::slotStopPreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->stopPreviewRender(); } } void MainWindow::slotDefinePreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->addPreviewRange(true); } } void MainWindow::slotRemovePreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->addPreviewRange(false); } } void MainWindow::slotClearPreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->clearPreviewRange(); } } void MainWindow::slotSelectTimelineClip() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineClip, true); } void MainWindow::slotSelectTimelineTransition() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineComposition, true); } void MainWindow::slotDeselectTimelineClip() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineClip, false); } void MainWindow::slotDeselectTimelineTransition() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineComposition, false); } void MainWindow::slotSelectAddTimelineClip() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineClip, true, true); } void MainWindow::slotSelectAddTimelineTransition() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineComposition, true, true); } void MainWindow::slotGroupClips() { getCurrentTimeline()->controller()->groupSelection(); } void MainWindow::slotUnGroupClips() { getCurrentTimeline()->controller()->unGroupSelection(); } void MainWindow::slotEditItemDuration() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->editItemDuration(); } */ } void MainWindow::slotAddProjectClip(const QUrl &url, const QStringList &folderInfo) { pCore->bin()->droppedUrls(QList() << url, folderInfo); } void MainWindow::slotAddProjectClipList(const QList &urls) { pCore->bin()->droppedUrls(urls); } void MainWindow::slotAddTransition(QAction *result) { if (!result) { return; } // TODO refac /* QStringList info = result->data().toStringList(); if (info.isEmpty() || info.count() < 2) { return; } QDomElement transition = transitions.getEffectByTag(info.at(0), info.at(1)); if (pCore->projectManager()->currentTimeline() && !transition.isNull()) { pCore->projectManager()->currentTimeline()->projectView()->slotAddTransitionToSelectedClips(transition.cloneNode().toElement()); } */ } void MainWindow::slotAddVideoEffect(QAction *result) { if (!result) { return; } QStringList info = result->data().toStringList(); if (info.isEmpty() || info.size() < 3) { return; } QDomElement effect; int effectType = info.last().toInt(); switch (effectType) { case EffectsList::EFFECT_VIDEO: case EffectsList::EFFECT_GPU: effect = videoEffects.getEffectByTag(info.at(0), info.at(1)); break; case EffectsList::EFFECT_AUDIO: effect = audioEffects.getEffectByTag(info.at(0), info.at(1)); break; case EffectsList::EFFECT_CUSTOM: effect = customEffects.getEffectByTag(info.at(0), info.at(1)); break; default: effect = videoEffects.getEffectByTag(info.at(0), info.at(1)); if (!effect.isNull()) { break; } effect = audioEffects.getEffectByTag(info.at(0), info.at(1)); if (!effect.isNull()) { break; } effect = customEffects.getEffectByTag(info.at(0), info.at(1)); break; } if (!effect.isNull()) { slotAddEffect(effect); } else { m_messageLabel->setMessage(i18n("Cannot find effect %1 / %2", info.at(0), info.at(1)), ErrorMessage); } } void MainWindow::slotZoomIn(bool zoomOnMouse) { slotSetZoom(m_zoomSlider->value() - 1, zoomOnMouse); slotShowZoomSliderToolTip(); } void MainWindow::slotZoomOut(bool zoomOnMouse) { slotSetZoom(m_zoomSlider->value() + 1, zoomOnMouse); slotShowZoomSliderToolTip(); } void MainWindow::slotFitZoom() { /* if (pCore->projectManager()->currentTimeline()) { m_zoomSlider->setValue(pCore->projectManager()->currentTimeline()->fitZoom()); // Make sure to reset scroll bar to start pCore->projectManager()->currentTimeline()->projectView()->scrollToStart(); } */ } void MainWindow::slotSetZoom(int value, bool zoomOnMouse) { value = qBound(m_zoomSlider->minimum(), value, m_zoomSlider->maximum()); m_timelineTabs->changeZoom(value, zoomOnMouse); m_zoomOut->setEnabled(value < m_zoomSlider->maximum()); m_zoomIn->setEnabled(value > m_zoomSlider->minimum()); slotUpdateZoomSliderToolTip(value); m_zoomSlider->blockSignals(true); m_zoomSlider->setValue(value); m_zoomSlider->blockSignals(false); } void MainWindow::slotShowZoomSliderToolTip(int zoomlevel) { if (zoomlevel != -1) { slotUpdateZoomSliderToolTip(zoomlevel); } QPoint global = m_zoomSlider->rect().topLeft(); global.ry() += m_zoomSlider->height() / 2; QHelpEvent toolTipEvent(QEvent::ToolTip, QPoint(0, 0), m_zoomSlider->mapToGlobal(global)); QApplication::sendEvent(m_zoomSlider, &toolTipEvent); } void MainWindow::slotUpdateZoomSliderToolTip(int zoomlevel) { m_zoomSlider->setToolTip(i18n("Zoom Level: %1/13", (13 - zoomlevel))); } void MainWindow::slotGotProgressInfo(const QString &message, int progress, MessageType type) { m_messageLabel->setProgressMessage(message, progress, type); } void MainWindow::customEvent(QEvent *e) { if (e->type() == QEvent::User) { m_messageLabel->setMessage(static_cast(e)->message(), MltError); } } void MainWindow::slotSnapRewind() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->gotoPreviousSnap(); } else { m_clipMonitor->slotSeekToPreviousSnap(); } } void MainWindow::slotSnapForward() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->gotoNextSnap(); } else { m_clipMonitor->slotSeekToNextSnap(); } } void MainWindow::slotClipStart() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->seekCurrentClip(false); } } void MainWindow::slotClipEnd() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->seekCurrentClip(true); } } void MainWindow::slotChangeTool(QAction *action) { if (action == m_buttonSelectTool) { slotSetTool(SelectTool); } else if (action == m_buttonRazorTool) { slotSetTool(RazorTool); } else if (action == m_buttonSpacerTool) { slotSetTool(SpacerTool); } } void MainWindow::slotChangeEdit(QAction *action) { Q_UNUSED(action) // TODO refac /* if (!pCore->projectManager()->currentTimeline()) { return; } if (action == m_overwriteEditTool) { pCore->projectManager()->currentTimeline()->projectView()->setEditMode(TimelineMode::OverwriteEdit); } else if (action == m_insertEditTool) { pCore->projectManager()->currentTimeline()->projectView()->setEditMode(TimelineMode::InsertEdit); } else { pCore->projectManager()->currentTimeline()->projectView()->setEditMode(TimelineMode::NormalEdit); } */ } void MainWindow::slotSetTool(ProjectTool tool) { if (pCore->currentDoc()) { // pCore->currentDoc()->setTool(tool); QString message; switch (tool) { case SpacerTool: message = i18n("Ctrl + click to use spacer on current track only"); break; case RazorTool: message = i18n("Click on a clip to cut it, Shift + move to preview cut frame"); break; default: message = i18n("Shift + click to create a selection rectangle, Ctrl + click to add an item to selection"); break; } m_messageLabel->setMessage(message, InformationMessage); getMainTimeline()->setTool(tool); } } void MainWindow::slotCopy() { getMainTimeline()->controller()->copyItem(); } void MainWindow::slotPaste() { getMainTimeline()->controller()->pasteItem(); } void MainWindow::slotPasteEffects() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->pasteClipEffects(); } */ } void MainWindow::slotClipInTimeline(const QString &clipId, QList ids) { Q_UNUSED(clipId) QMenu *inTimelineMenu = static_cast(factory()->container(QStringLiteral("clip_in_timeline"), this)); QList actionList; for (int i = 0; i < ids.count(); ++i) { QString track = getMainTimeline()->controller()->getTrackNameFromIndex(pCore->getItemTrack(ObjectId(ObjectType::TimelineClip, ids.at(i)))); QString start = pCore->currentDoc()->timecode().getTimecodeFromFrames(pCore->getItemPosition(ObjectId(ObjectType::TimelineClip, ids.at(i)))); int j = 0; QAction *a = new QAction(track + QStringLiteral(": ") + start, inTimelineMenu); a->setData(ids.at(i)); connect(a, &QAction::triggered, this, &MainWindow::slotSelectClipInTimeline); while (j < actionList.count()) { if (actionList.at(j)->text() > a->text()) { break; } j++; } actionList.insert(j, a); } QList list = inTimelineMenu->actions(); unplugActionList(QStringLiteral("timeline_occurences")); qDeleteAll(list); plugActionList(QStringLiteral("timeline_occurences"), actionList); if (actionList.isEmpty()) { inTimelineMenu->setEnabled(false); } else { inTimelineMenu->setEnabled(true); } } void MainWindow::slotClipInProjectTree() { QList ids = getMainTimeline()->controller()->selection(); if (!ids.isEmpty()) { m_projectBinDock->raise(); ObjectId id(ObjectType::TimelineClip, ids.constFirst()); int start = pCore->getItemIn(id); int duration = pCore->getItemDuration(id); QPoint zone(start, start + duration); qDebug()<<" - - selecting clip on monitor, zone: "<isActive()) { slotSwitchMonitors(); } int pos = m_projectMonitor->position(); int itemPos = pCore->getItemPosition(id); if (pos >= itemPos && pos < itemPos + duration) { pos -= itemPos; } else { pos = -1; } pCore->selectBinClip(getMainTimeline()->controller()->getClipBinId(ids.constFirst()), pos, zone); } } void MainWindow::slotSelectClipInTimeline() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); QAction *action = qobject_cast(sender()); int clipId = action->data().toInt(); getMainTimeline()->controller()->focusItem(clipId); } /** Gets called when the window gets hidden */ void MainWindow::hideEvent(QHideEvent * /*event*/) { if (isMinimized() && pCore->monitorManager()) { pCore->monitorManager()->pauseActiveMonitor(); } } /*void MainWindow::slotSaveZone(Render *render, const QPoint &zone, DocClipBase *baseClip, QUrl path) { QPointer dialog = new QDialog(this); dialog->setWindowTitle("Save clip zone"); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QVBoxLayout *mainLayout = new QVBoxLayout; dialog->setLayout(mainLayout); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dialog->connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept())); dialog->connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject())); QLabel *label1 = new QLabel(i18n("Save clip zone as:"), this); if (path.isEmpty()) { QString tmppath = pCore->currentDoc()->projectFolder().path() + QDir::separator(); if (baseClip == nullptr) { tmppath.append("untitled.mlt"); } else { tmppath.append((baseClip->name().isEmpty() ? baseClip->fileURL().fileName() : baseClip->name()) + '-' + QString::number(zone.x()).rightJustified(4, '0') + QStringLiteral(".mlt")); } path = QUrl(tmppath); } KUrlRequester *url = new KUrlRequester(path, this); url->setFilter("video/mlt-playlist"); QLabel *label2 = new QLabel(i18n("Description:"), this); QLineEdit *edit = new QLineEdit(this); mainLayout->addWidget(label1); mainLayout->addWidget(url); mainLayout->addWidget(label2); mainLayout->addWidget(edit); mainLayout->addWidget(buttonBox); if (dialog->exec() == QDialog::Accepted) { if (QFile::exists(url->url().path())) { if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", url->url().path())) == KMessageBox::No) { slotSaveZone(render, zone, baseClip, url->url()); delete dialog; return; } } if (baseClip && !baseClip->fileURL().isEmpty()) { // create zone from clip url, so that we don't have problems with proxy clips QProcess p; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("MLT_PROFILE"); p.setProcessEnvironment(env); p.start(KdenliveSettings::rendererpath(), QStringList() << baseClip->fileURL().toLocalFile() << "in=" + QString::number(zone.x()) << "out=" + QString::number(zone.y()) << "-consumer" << "xml:" + url->url().path()); if (!p.waitForStarted(3000)) { KMessageBox::sorry(this, i18n("Cannot start MLT's renderer:\n%1", KdenliveSettings::rendererpath())); } else if (!p.waitForFinished(5000)) { KMessageBox::sorry(this, i18n("Timeout while creating xml output")); } } else render->saveZone(url->url(), edit->text(), zone); } delete dialog; }*/ void MainWindow::slotResizeItemStart() { getMainTimeline()->controller()->setInPoint(); } void MainWindow::slotResizeItemEnd() { getMainTimeline()->controller()->setOutPoint(); } int MainWindow::getNewStuff(const QString &configFile) { KNS3::Entry::List entries; QPointer dialog = new KNS3::DownloadDialog(configFile); if (dialog->exec() != 0) { entries = dialog->changedEntries(); } for (const KNS3::Entry &entry : entries) { if (entry.status() == KNS3::Entry::Installed) { qCDebug(KDENLIVE_LOG) << "// Installed files: " << entry.installedFiles(); } } delete dialog; return entries.size(); } void MainWindow::slotGetNewTitleStuff() { if (getNewStuff(QStringLiteral(":data/kdenlive_titles.knsrc")) > 0) { // get project title path QString titlePath = pCore->currentDoc()->projectDataFolder() + QStringLiteral("/titles/"); TitleWidget::refreshTitleTemplates(titlePath); } } void MainWindow::slotGetNewLumaStuff() { if (getNewStuff(QStringLiteral(":data/kdenlive_wipes.knsrc")) > 0) { initEffects::refreshLumas(); // TODO: refresh currently displayd trans ? } } void MainWindow::slotGetNewKeyboardStuff() { if (getNewStuff(QStringLiteral(":data/kdenlive_keyboardschemes.knsrc")) > 0) { //Is there something to do ? } } void MainWindow::slotGetNewRenderStuff() { if (getNewStuff(QStringLiteral(":data/kdenlive_renderprofiles.knsrc")) > 0) if (m_renderWidget) { m_renderWidget->reloadProfiles(); } } void MainWindow::slotAutoTransition() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->autoTransition(); } */ } void MainWindow::slotSplitAudio() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->splitAudio(); } */ } void MainWindow::slotSetAudioAlignReference() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->setAudioAlignReference(); } */ } void MainWindow::slotAlignAudio() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->alignAudio(); } */ } void MainWindow::slotUpdateClipType(QAction *action) { Q_UNUSED(action) // TODO refac /* if (pCore->projectManager()->currentTimeline()) { PlaylistState::ClipState state = (PlaylistState::ClipState)action->data().toInt(); pCore->projectManager()->currentTimeline()->projectView()->setClipType(state); } */ } void MainWindow::slotDvdWizard(const QString &url) { // We must stop the monitors since we create a new on in the dvd wizard QPointer w = new DvdWizard(pCore->monitorManager(), url, this); w->exec(); delete w; pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor); } void MainWindow::slotShowTimeline(bool show) { if (!show) { m_timelineState = saveState(); centralWidget()->setHidden(true); } else { centralWidget()->setHidden(false); restoreState(m_timelineState); } } void MainWindow::loadClipActions() { unplugActionList(QStringLiteral("add_effect")); plugActionList(QStringLiteral("add_effect"), m_effectsMenu->actions()); QList clipJobActions = getExtraActions(QStringLiteral("clipjobs")); unplugActionList(QStringLiteral("clip_jobs")); plugActionList(QStringLiteral("clip_jobs"), clipJobActions); QList atcActions = getExtraActions(QStringLiteral("audiotranscoderslist")); unplugActionList(QStringLiteral("audio_transcoders_list")); plugActionList(QStringLiteral("audio_transcoders_list"), atcActions); QList tcActions = getExtraActions(QStringLiteral("transcoderslist")); unplugActionList(QStringLiteral("transcoders_list")); plugActionList(QStringLiteral("transcoders_list"), tcActions); } void MainWindow::loadDockActions() { QList list = kdenliveCategoryMap.value(QStringLiteral("interface"))->actions(); // Sort actions QMap sorted; QStringList sortedList; for (QAction *a : list) { sorted.insert(a->text(), a); sortedList << a->text(); } QList orderedList; sortedList.sort(Qt::CaseInsensitive); for (const QString &text : sortedList) { orderedList << sorted.value(text); } unplugActionList(QStringLiteral("dock_actions")); plugActionList(QStringLiteral("dock_actions"), orderedList); } void MainWindow::buildDynamicActions() { KActionCategory *ts = nullptr; if (kdenliveCategoryMap.contains(QStringLiteral("clipjobs"))) { ts = kdenliveCategoryMap.take(QStringLiteral("clipjobs")); delete ts; } ts = new KActionCategory(i18n("Clip Jobs"), m_extraFactory->actionCollection()); Mlt::Profile profile; std::unique_ptr filter; for (const QString &stab : {QStringLiteral("vidstab"), QStringLiteral("videostab2"), QStringLiteral("videostab")}) { filter.reset(Mlt::Factory::filter(profile, stab.toUtf8().data())); if ((filter != nullptr) && filter->is_valid()) { QAction *action = new QAction(i18n("Stabilize") + QStringLiteral(" (") + stab + QLatin1Char(')'), m_extraFactory->actionCollection()); ts->addAction(action->text(), action); connect(action, &QAction::triggered, [&]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18n("Stabilize clips"), stab); }); break; } } filter.reset(Mlt::Factory::filter(profile, "motion_est")); if (filter) { if (filter->is_valid()) { QAction *action = new QAction(i18n("Automatic scene split"), m_extraFactory->actionCollection()); ts->addAction(action->text(), action); connect(action, &QAction::triggered, [&]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18np("Stabilize clip", "Stabilize clips", pCore->bin()->selectedClipsIds().size())); }); } } if (true /* TODO: check if timewarp producer is available */) { QAction *action = new QAction(i18n("Duplicate clip with speed change"), m_extraFactory->actionCollection()); ts->addAction(action->text(), action); connect(action, &QAction::triggered, [&]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18n("Change clip speed")); }); } // TODO refac reimplement analyseclipjob /* QAction *action = new QAction(i18n("Analyse keyframes"), m_extraFactory->actionCollection()); QStringList stabJob(QString::number((int)AbstractClipJob::ANALYSECLIPJOB)); action->setData(stabJob); ts->addAction(action->text(), action); connect(action, &QAction::triggered, pCore->bin(), &Bin::slotStartClipJob); */ kdenliveCategoryMap.insert(QStringLiteral("clipjobs"), ts); if (kdenliveCategoryMap.contains(QStringLiteral("transcoderslist"))) { ts = kdenliveCategoryMap.take(QStringLiteral("transcoderslist")); delete ts; } if (kdenliveCategoryMap.contains(QStringLiteral("audiotranscoderslist"))) { ts = kdenliveCategoryMap.take(QStringLiteral("audiotranscoderslist")); delete ts; } // TODO refac : reimplement transcode /* ts = new KActionCategory(i18n("Transcoders"), m_extraFactory->actionCollection()); KActionCategory *ats = new KActionCategory(i18n("Extract Audio"), m_extraFactory->actionCollection()); KSharedConfigPtr config = KSharedConfig::openConfig(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdenlivetranscodingrc")), KConfig::CascadeConfig); KConfigGroup transConfig(config, "Transcoding"); // read the entries QMap profiles = transConfig.entryMap(); QMapIterator i(profiles); while (i.hasNext()) { i.next(); QStringList transList; transList << QString::number((int)AbstractClipJob::TRANSCODEJOB); transList << i.value().split(QLatin1Char(';')); auto *a = new QAction(i.key(), m_extraFactory->actionCollection()); a->setData(transList); if (transList.count() > 1) { a->setToolTip(transList.at(1)); } // slottranscode connect(a, &QAction::triggered, pCore->bin(), &Bin::slotStartClipJob); if (transList.count() > 3 && transList.at(3) == QLatin1String("audio")) { // This is an audio transcoding action ats->addAction(i.key(), a); } else { ts->addAction(i.key(), a); } } kdenliveCategoryMap.insert(QStringLiteral("transcoderslist"), ts); kdenliveCategoryMap.insert(QStringLiteral("audiotranscoderslist"), ats); */ // Populate View menu with show / hide actions for dock widgets KActionCategory *guiActions = nullptr; if (kdenliveCategoryMap.contains(QStringLiteral("interface"))) { guiActions = kdenliveCategoryMap.take(QStringLiteral("interface")); delete guiActions; } guiActions = new KActionCategory(i18n("Interface"), actionCollection()); QAction *showTimeline = new QAction(i18n("Timeline"), this); showTimeline->setCheckable(true); showTimeline->setChecked(true); connect(showTimeline, &QAction::triggered, this, &MainWindow::slotShowTimeline); guiActions->addAction(showTimeline->text(), showTimeline); actionCollection()->addAction(showTimeline->text(), showTimeline); QList docks = findChildren(); for (int j = 0; j < docks.count(); ++j) { QDockWidget *dock = docks.at(j); QAction *dockInformations = dock->toggleViewAction(); if (!dockInformations) { continue; } dockInformations->setChecked(!dock->isHidden()); guiActions->addAction(dockInformations->text(), dockInformations); } kdenliveCategoryMap.insert(QStringLiteral("interface"), guiActions); } QList MainWindow::getExtraActions(const QString &name) { if (!kdenliveCategoryMap.contains(name)) { return QList(); } return kdenliveCategoryMap.value(name)->actions(); } void MainWindow::slotTranscode(const QStringList &urls) { Q_UNUSED(urls) // TODO refac : remove or reimplement transcoding /* QString params; QString desc; if (urls.isEmpty()) { QAction *action = qobject_cast(sender()); QStringList transList = action->data().toStringList(); pCore->bin()->startClipJob(transList); return; } if (urls.isEmpty()) { m_messageLabel->setMessage(i18n("No clip to transcode"), ErrorMessage); return; } qCDebug(KDENLIVE_LOG) << "// TRANSODING FOLDER: " << pCore->bin()->getFolderInfo(); ClipTranscode *d = new ClipTranscode(urls, params, QStringList(), desc, pCore->bin()->getFolderInfo()); connect(d, &ClipTranscode::addClip, this, &MainWindow::slotAddProjectClip); d->show(); */ } void MainWindow::slotTranscodeClip() { // TODO refac : remove or reimplement transcoding /* QString allExtensions = ClipCreationDialog::getExtensions().join(QLatin1Char(' ')); const QString dialogFilter = i18n("All Supported Files") + QLatin1Char('(') + allExtensions + QStringLiteral(");;") + i18n("All Files") + QStringLiteral("(*)"); QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); QStringList urls = QFileDialog::getOpenFileNames(this, i18n("Files to transcode"), clipFolder, dialogFilter); if (urls.isEmpty()) { return; } slotTranscode(urls); */ } void MainWindow::slotSetDocumentRenderProfile(const QMap &props) { KdenliveDoc *project = pCore->currentDoc(); bool modified = false; QMapIterator i(props); while (i.hasNext()) { i.next(); if (project->getDocumentProperty(i.key()) == i.value()) { continue; } project->setDocumentProperty(i.key(), i.value()); modified = true; } if (modified) { project->setModified(); } } void MainWindow::slotPrepareRendering(bool scriptExport, bool zoneOnly, const QString &chapterFile, QString scriptPath) { KdenliveDoc *project = pCore->currentDoc(); if (m_renderWidget == nullptr) { return; } QString playlistPath; QString mltSuffix(QStringLiteral(".mlt")); QList playlistPaths; QList trackNames; int tracksCount = 1; bool stemExport = m_renderWidget->isStemAudioExportEnabled(); if (scriptExport) { // QString scriptsFolder = project->projectFolder().path(QUrl::AddTrailingSlash) + "scripts/"; if (scriptPath.isEmpty()) { QString path = m_renderWidget->getFreeScriptName(project->url()); QPointer getUrl = new KUrlRequesterDialog(QUrl::fromLocalFile(path), i18n("Create Render Script"), this); getUrl->urlRequester()->setMode(KFile::File); if (getUrl->exec() == QDialog::Rejected) { delete getUrl; return; } scriptPath = getUrl->selectedUrl().toLocalFile(); delete getUrl; } QFile f(scriptPath); if (f.exists()) { if (KMessageBox::warningYesNo(this, i18n("Script file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { return; } } playlistPath = scriptPath; } else { QTemporaryFile temp(QDir::tempPath() + QStringLiteral("/kdenlive_rendering_XXXXXX.mlt")); temp.setAutoRemove(false); temp.open(); playlistPath = temp.fileName(); } int in = 0; int out; if (zoneOnly) { in = getMainTimeline()->controller()->zoneIn(); out = getMainTimeline()->controller()->zoneOut(); } else { out = getMainTimeline()->controller()->duration() - TimelineModel::seekDuration - 2; } QString playlistContent = pCore->projectManager()->projectSceneList(project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile()); if (!chapterFile.isEmpty()) { QDomDocument doc; QDomElement chapters = doc.createElement(QStringLiteral("chapters")); chapters.setAttribute(QStringLiteral("fps"), pCore->getCurrentFps()); doc.appendChild(chapters); const QList guidesList = project->getGuideModel()->getAllMarkers(); for (int i = 0; i < guidesList.count(); i++) { CommentedTime c = guidesList.at(i); int time = c.time().frames(pCore->getCurrentFps()); if (time >= in && time < out) { if (zoneOnly) { time = time - in; } } QDomElement chapter = doc.createElement(QStringLiteral("chapter")); chapters.appendChild(chapter); chapter.setAttribute(QStringLiteral("title"), c.comment()); chapter.setAttribute(QStringLiteral("time"), time); } if (!chapters.childNodes().isEmpty()) { if (!project->getGuideModel()->hasMarker(out)) { // Always insert a guide in pos 0 QDomElement chapter = doc.createElement(QStringLiteral("chapter")); chapters.insertBefore(chapter, QDomNode()); chapter.setAttribute(QStringLiteral("title"), i18nc("the first in a list of chapters", "Start")); chapter.setAttribute(QStringLiteral("time"), QStringLiteral("0")); } // save chapters file QFile file(chapterFile); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing DVD CHAPTER file: " << chapterFile; } else { file.write(doc.toString().toUtf8()); if (file.error() != QFile::NoError) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing DVD CHAPTER file: " << chapterFile; } file.close(); } } } // check if audio export is selected bool exportAudio; if (m_renderWidget->automaticAudioExport()) { // TODO check if projact contains audio // exportAudio = pCore->projectManager()->currentTimeline()->checkProjectAudio(); exportAudio = true; } else { exportAudio = m_renderWidget->selectedAudioExport(); } // Set playlist audio volume to 100% QDomDocument doc; doc.setContent(playlistContent); QDomElement tractor = doc.documentElement().firstChildElement(QStringLiteral("tractor")); if (!tractor.isNull()) { QDomNodeList props = tractor.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == QLatin1String("meta.volume")) { props.at(i).firstChild().setNodeValue(QStringLiteral("1")); break; } } } // Add autoclose to playlists. QDomNodeList playlists = doc.elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < playlists.length(); ++i) { playlists.item(i).toElement().setAttribute(QStringLiteral("autoclose"), 1); } // Do we want proxy rendering if (project->useProxy() && !m_renderWidget->proxyRendering()) { QString root = doc.documentElement().attribute(QStringLiteral("root")); if (!root.isEmpty() && !root.endsWith(QLatin1Char('/'))) { root.append(QLatin1Char('/')); } // replace proxy clips with originals QMap proxies = pCore->projectItemModel()->getProxies(pCore->currentDoc()->documentRoot()); QDomNodeList producers = doc.elementsByTagName(QStringLiteral("producer")); QString producerResource; QString producerService; QString suffix; QString prefix; for (int n = 0; n < producers.length(); ++n) { QDomElement e = producers.item(n).toElement(); producerResource = EffectsList::property(e, QStringLiteral("resource")); producerService = EffectsList::property(e, QStringLiteral("mlt_service")); if (producerResource.isEmpty() || producerService == QLatin1String("color")) { continue; } if (producerService == QLatin1String("timewarp")) { // slowmotion producer prefix = producerResource.section(QLatin1Char(':'), 0, 0) + QLatin1Char(':'); producerResource = producerResource.section(QLatin1Char(':'), 1); } else { prefix.clear(); } if (producerService == QLatin1String("framebuffer")) { // slowmotion producer suffix = QLatin1Char('?') + producerResource.section(QLatin1Char('?'), 1); producerResource = producerResource.section(QLatin1Char('?'), 0, 0); } else { suffix.clear(); } if (!producerResource.isEmpty()) { if (QFileInfo(producerResource).isRelative()) { producerResource.prepend(root); } if (proxies.contains(producerResource)) { QString replacementResource = proxies.value(producerResource); EffectsList::setProperty(e, QStringLiteral("resource"), prefix + replacementResource + suffix); if (producerService == QLatin1String("timewarp")) { EffectsList::setProperty(e, QStringLiteral("warp_resource"), replacementResource); } // We need to delete the "aspect_ratio" property because proxy clips // sometimes have different ratio than original clips EffectsList::removeProperty(e, QStringLiteral("aspect_ratio")); EffectsList::removeMetaProperties(e); } } } } QList docList; // check which audio tracks have to be exported if (stemExport) { // TODO refac /* //TODO port to new timeline model Timeline *ct = pCore->projectManager()->currentTimeline(); int allTracksCount = ct->tracksCount(); // reset tracks count (tracks to be rendered) tracksCount = 0; // begin with track 1 (track zero is a hidden black track) for (int i = 1; i < allTracksCount; i++) { Track *track = ct->track(i); // add only tracks to render list that are not muted and have audio if ((track != nullptr) && !track->info().isMute && track->hasAudio()) { QDomDocument docCopy = doc.cloneNode(true).toDocument(); QString trackName = track->info().trackName; // save track name trackNames << trackName; qCDebug(KDENLIVE_LOG) << "Track-Name: " << trackName; // create stem export doc content QDomNodeList tracks = docCopy.elementsByTagName(QStringLiteral("track")); for (int j = 0; j < allTracksCount; j++) { if (j != i) { // mute other tracks tracks.at(j).toElement().setAttribute(QStringLiteral("hide"), QStringLiteral("both")); } } docList << docCopy; tracksCount++; } } */ } else { docList << doc; } // create full playlistPaths for (int i = 0; i < tracksCount; i++) { QString plPath(playlistPath); // add track number to path name if (stemExport) { plPath = plPath + QLatin1Char('_') + QString(trackNames.at(i)).replace(QLatin1Char(' '), QLatin1Char('_')); } // add mlt suffix if (!plPath.endsWith(mltSuffix)) { plPath += mltSuffix; } playlistPaths << plPath; qCDebug(KDENLIVE_LOG) << "playlistPath: " << plPath << endl; // Do save scenelist QFile file(plPath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { m_messageLabel->setMessage(i18n("Cannot write to file %1", plPath), ErrorMessage); return; } file.write(docList.at(i).toString().toUtf8()); if (file.error() != QFile::NoError) { m_messageLabel->setMessage(i18n("Cannot write to file %1", plPath), ErrorMessage); file.close(); return; } file.close(); } m_renderWidget->slotExport(scriptExport, in, out, project->metadata(), playlistPaths, trackNames, scriptPath, exportAudio); } void MainWindow::slotUpdateTimecodeFormat(int ix) { KdenliveSettings::setFrametimecode(ix == 1); m_clipMonitor->updateTimecodeFormat(); m_projectMonitor->updateTimecodeFormat(); // TODO refac: reimplement ? // m_effectStack->transitionConfig()->updateTimecodeFormat(); // m_effectStack->updateTimecodeFormat(); pCore->bin()->updateTimecodeFormat(); getMainTimeline()->controller()->frameFormatChanged(); m_timeFormatButton->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); } void MainWindow::slotRemoveFocus() { getMainTimeline()->setFocus(); } void MainWindow::slotShutdown() { pCore->currentDoc()->setModified(false); // Call shutdown QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); if ((interface != nullptr) && interface->isServiceRegistered(QStringLiteral("org.kde.ksmserver"))) { QDBusInterface smserver(QStringLiteral("org.kde.ksmserver"), QStringLiteral("/KSMServer"), QStringLiteral("org.kde.KSMServerInterface")); smserver.call(QStringLiteral("logout"), 1, 2, 2); } else if ((interface != nullptr) && interface->isServiceRegistered(QStringLiteral("org.gnome.SessionManager"))) { QDBusInterface smserver(QStringLiteral("org.gnome.SessionManager"), QStringLiteral("/org/gnome/SessionManager"), QStringLiteral("org.gnome.SessionManager")); smserver.call(QStringLiteral("Shutdown")); } } void MainWindow::slotSwitchMonitors() { pCore->monitorManager()->slotSwitchMonitors(!m_clipMonitor->isActive()); if (m_projectMonitor->isActive()) { getMainTimeline()->setFocus(); } else { pCore->bin()->focusBinView(); } } void MainWindow::slotSwitchMonitorOverlay(QAction *action) { if (pCore->monitorManager()->isActive(Kdenlive::ClipMonitor)) { m_clipMonitor->switchMonitorInfo(action->data().toInt()); } else { m_projectMonitor->switchMonitorInfo(action->data().toInt()); } } void MainWindow::slotSwitchDropFrames(bool drop) { m_clipMonitor->switchDropFrames(drop); m_projectMonitor->switchDropFrames(drop); } void MainWindow::slotSetMonitorGamma(int gamma) { KdenliveSettings::setMonitor_gamma(gamma); m_clipMonitor->updateMonitorGamma(); m_projectMonitor->updateMonitorGamma(); } void MainWindow::slotInsertZoneToTree() { if (!m_clipMonitor->isActive() || m_clipMonitor->currentController() == nullptr) { return; } QPoint info = m_clipMonitor->getZoneInfo(); QString id; pCore->projectItemModel()->requestAddBinSubClip(id, info.x(), info.y(), m_clipMonitor->activeClipId()); } void MainWindow::slotInsertZoneToTimeline() { QPoint info = m_clipMonitor->getZoneInfo(); QString clipData = QString("%1#%2#%3").arg(m_clipMonitor->activeClipId()).arg(info.x()).arg(info.y()); int cid = getMainTimeline()->controller()->insertClip(-1, -1, clipData, true, true); if (cid == -1) { pCore->displayMessage(i18n("Cannot insert clip at requested position"), InformationMessage); } else { getMainTimeline()->controller()->seekToClip(cid, true); } } void MainWindow::slotMonitorRequestRenderFrame(bool request) { if (request) { m_projectMonitor->sendFrameForAnalysis(true); return; } for (int i = 0; i < m_gfxScopesList.count(); ++i) { if (m_gfxScopesList.at(i)->isVisible() && tabifiedDockWidgets(m_gfxScopesList.at(i)).isEmpty() && static_cast(m_gfxScopesList.at(i)->widget())->autoRefreshEnabled()) { request = true; break; } } #ifdef DEBUG_MAINW qCDebug(KDENLIVE_LOG) << "Any scope accepting new frames? " << request; #endif if (!request) { m_projectMonitor->sendFrameForAnalysis(false); } } void MainWindow::slotUpdateProxySettings() { KdenliveDoc *project = pCore->currentDoc(); if (m_renderWidget) { m_renderWidget->updateProxyConfig(project->useProxy()); } pCore->bin()->refreshProxySettings(); } void MainWindow::slotArchiveProject() { // TODO refac /* QList> list = pCore->binController()->getControllerList(); KdenliveDoc *doc = pCore->currentDoc(); pCore->binController()->saveDocumentProperties(pCore->projectManager()->currentTimeline()->documentProperties(), doc->metadata(), doc->getGuideModel()); QDomDocument xmlDoc = doc->xmlSceneList(m_projectMonitor->sceneList(doc->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile())); QScopedPointer d( new ArchiveWidget(doc->url().fileName(), xmlDoc, list, pCore->projectManager()->currentTimeline()->projectView()->extractTransitionsLumas(), this)); if (d->exec() != 0) { m_messageLabel->setMessage(i18n("Archiving project"), OperationCompletedMessage); } */ } void MainWindow::slotDownloadResources() { QString currentFolder; if (pCore->currentDoc()) { currentFolder = pCore->currentDoc()->projectDataFolder(); } else { currentFolder = KdenliveSettings::defaultprojectfolder(); } auto *d = new ResourceWidget(currentFolder); connect(d, &ResourceWidget::addClip, this, &MainWindow::slotAddProjectClip); d->show(); } void MainWindow::slotProcessImportKeyframes(GraphicsRectItem type, const QString &tag, const QString &keyframes) { Q_UNUSED(keyframes) Q_UNUSED(tag) if (type == AVWidget) { // This data should be sent to the effect stack // TODO REFAC reimplement // m_effectStack->setKeyframes(tag, data); } else if (type == TransitionWidget) { // This data should be sent to the transition stack // TODO REFAC reimplement // m_effectStack->transitionConfig()->setKeyframes(tag, data); } else { // Error } } void MainWindow::slotAlignPlayheadToMousePos() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); getMainTimeline()->controller()->seekToMouse(); } void MainWindow::triggerKey(QKeyEvent *ev) { // Hack: The QQuickWindow that displays fullscreen monitor does not integrate quith QActions. // so on keypress events we parse keys and check for shortcuts in all existing actions QKeySequence seq; // Remove the Num modifier or some shortcuts like "*" will not work if (ev->modifiers() != Qt::KeypadModifier) { seq = QKeySequence(ev->key() + static_cast(ev->modifiers())); } else { seq = QKeySequence(ev->key()); } QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); for (QAction *tempAction : coll->actions()) { if (tempAction->shortcuts().contains(seq)) { // Trigger action tempAction->trigger(); ev->accept(); return; } } } } QDockWidget *MainWindow::addDock(const QString &title, const QString &objectName, QWidget *widget, Qt::DockWidgetArea area) { QDockWidget *dockWidget = new QDockWidget(title, this); dockWidget->setObjectName(objectName); dockWidget->setWidget(widget); addDockWidget(area, dockWidget); connect(dockWidget, &QDockWidget::dockLocationChanged, this, &MainWindow::updateDockTitleBars); connect(dockWidget, &QDockWidget::topLevelChanged, this, &MainWindow::updateDockTitleBars); return dockWidget; } void MainWindow::slotUpdateMonitorOverlays(int id, int code) { QMenu *monitorOverlay = static_cast(factory()->container(QStringLiteral("monitor_config_overlay"), this)); if (!monitorOverlay) { return; } QList actions = monitorOverlay->actions(); for (QAction *ac : actions) { int mid = ac->data().toInt(); if (mid == 0x010) { ac->setEnabled(id == Kdenlive::ClipMonitor); } ac->setChecked(code & mid); } } void MainWindow::slotChangeStyle(QAction *a) { QString style = a->data().toString(); KdenliveSettings::setWidgetstyle(style); doChangeStyle(); } void MainWindow::doChangeStyle() { QString newStyle = KdenliveSettings::widgetstyle(); if (newStyle.isEmpty() || newStyle == QStringLiteral("Default")) { newStyle = defaultStyle("Breeze"); } QApplication::setStyle(QStyleFactory::create(newStyle)); // Changing widget style resets color theme, so update color theme again ThemeManager::instance()->slotChangePalette(); } bool MainWindow::isTabbedWith(QDockWidget *widget, const QString &otherWidget) { QList tabbed = tabifiedDockWidgets(widget); for (int i = 0; i < tabbed.count(); i++) { if (tabbed.at(i)->objectName() == otherWidget) { return true; } } return false; } void MainWindow::updateDockTitleBars(bool isTopLevel) { if (!KdenliveSettings::showtitlebars() || !isTopLevel) { return; } QList docks = pCore->window()->findChildren(); for (int i = 0; i < docks.count(); ++i) { QDockWidget *dock = docks.at(i); QWidget *bar = dock->titleBarWidget(); if (dock->isFloating()) { if (bar) { dock->setTitleBarWidget(nullptr); delete bar; } continue; } QList docked = pCore->window()->tabifiedDockWidgets(dock); if (docked.isEmpty()) { if (bar) { dock->setTitleBarWidget(nullptr); delete bar; } continue; } bool hasVisibleDockSibling = false; for (QDockWidget *sub : docked) { if (sub->toggleViewAction()->isChecked()) { // we have another docked widget, so tabs are visible and can be used instead of title bars hasVisibleDockSibling = true; break; } } if (!hasVisibleDockSibling) { if (bar) { dock->setTitleBarWidget(nullptr); delete bar; } continue; } if (!bar) { dock->setTitleBarWidget(new QWidget); } } } void MainWindow::slotToggleAutoPreview(bool enable) { Q_UNUSED(enable) // TODO refac /* KdenliveSettings::setAutopreview(enable); if (enable && pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->startPreviewRender(); } */ } void MainWindow::configureToolbars() { // Since our timeline toolbar is a non-standard toolbar (as it is docked in a custom widget, not // in a QToolBarDockArea, we have to hack KXmlGuiWindow to avoid a crash when saving toolbar config. // This is why we hijack the configureToolbars() and temporarily move the toolbar to a standard location QVBoxLayout *ctnLay = (QVBoxLayout *)m_timelineToolBarContainer->layout(); ctnLay->removeWidget(m_timelineToolBar); addToolBar(Qt::BottomToolBarArea, m_timelineToolBar); auto *toolBarEditor = new KEditToolBar(guiFactory(), this); toolBarEditor->setAttribute(Qt::WA_DeleteOnClose); connect(toolBarEditor, SIGNAL(newToolBarConfig()), SLOT(saveNewToolbarConfig())); connect(toolBarEditor, &QDialog::finished, this, &MainWindow::rebuildTimlineToolBar); toolBarEditor->show(); } void MainWindow::rebuildTimlineToolBar() { // Timeline toolbar settings changed, we can now re-add our toolbar to custom location m_timelineToolBar = toolBar(QStringLiteral("timelineToolBar")); removeToolBar(m_timelineToolBar); m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); QVBoxLayout *ctnLay = (QVBoxLayout *)m_timelineToolBarContainer->layout(); if (ctnLay) { ctnLay->insertWidget(0, m_timelineToolBar); } m_timelineToolBar->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_timelineToolBar, &QWidget::customContextMenuRequested, this, &MainWindow::showTimelineToolbarMenu); m_timelineToolBar->setVisible(true); } void MainWindow::showTimelineToolbarMenu(const QPoint &pos) { QMenu menu; menu.addAction(actionCollection()->action(KStandardAction::name(KStandardAction::ConfigureToolbars))); QMenu *contextSize = new QMenu(i18n("Icon Size")); menu.addMenu(contextSize); auto *sizeGroup = new QActionGroup(contextSize); int currentSize = m_timelineToolBar->iconSize().width(); QAction *a = new QAction(i18nc("@item:inmenu Icon size", "Default"), contextSize); a->setData(m_timelineToolBar->iconSizeDefault()); a->setCheckable(true); if (m_timelineToolBar->iconSizeDefault() == currentSize) { a->setChecked(true); } a->setActionGroup(sizeGroup); contextSize->addAction(a); KIconTheme *theme = KIconLoader::global()->theme(); QList avSizes; if (theme) { avSizes = theme->querySizes(KIconLoader::Toolbar); } qSort(avSizes); if (avSizes.count() < 10) { // Fixed or threshold type icons Q_FOREACH (int it, avSizes) { QString text; if (it < 19) { text = i18n("Small (%1x%2)", it, it); } else if (it < 25) { text = i18n("Medium (%1x%2)", it, it); } else if (it < 35) { text = i18n("Large (%1x%2)", it, it); } else { text = i18n("Huge (%1x%2)", it, it); } // save the size in the contextIconSizes map auto *sizeAction = new QAction(text, contextSize); sizeAction->setData(it); sizeAction->setCheckable(true); sizeAction->setActionGroup(sizeGroup); if (it == currentSize) { sizeAction->setChecked(true); } contextSize->addAction(sizeAction); } } else { // Scalable icons. const int progression[] = {16, 22, 32, 48, 64, 96, 128, 192, 256}; for (uint i = 0; i < 9; i++) { Q_FOREACH (int it, avSizes) { if (it >= progression[i]) { QString text; if (it < 19) { text = i18n("Small (%1x%2)", it, it); } else if (it < 25) { text = i18n("Medium (%1x%2)", it, it); } else if (it < 35) { text = i18n("Large (%1x%2)", it, it); } else { text = i18n("Huge (%1x%2)", it, it); } // save the size in the contextIconSizes map auto *sizeAction = new QAction(text, contextSize); sizeAction->setData(it); sizeAction->setCheckable(true); sizeAction->setActionGroup(sizeGroup); if (it == currentSize) { sizeAction->setChecked(true); } contextSize->addAction(sizeAction); break; } } } } connect(contextSize, &QMenu::triggered, this, &MainWindow::setTimelineToolbarIconSize); menu.exec(m_timelineToolBar->mapToGlobal(pos)); contextSize->deleteLater(); } void MainWindow::setTimelineToolbarIconSize(QAction *a) { if (!a) { return; } int size = a->data().toInt(); m_timelineToolBar->setIconDimensions(size); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup mainConfig(config, QStringLiteral("MainWindow")); KConfigGroup tbGroup(&mainConfig, QStringLiteral("Toolbar timelineToolBar")); m_timelineToolBar->saveSettings(tbGroup); } void MainWindow::slotManageCache() { QDialog d(this); d.setWindowTitle(i18n("Manage Cache Data")); auto *lay = new QVBoxLayout; TemporaryData tmp(pCore->currentDoc(), false, this); connect(&tmp, &TemporaryData::disableProxies, this, &MainWindow::slotDisableProxies); // TODO refac /* connect(&tmp, SIGNAL(disablePreview()), pCore->projectManager()->currentTimeline(), SLOT(invalidateRange())); */ QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::reject); lay->addWidget(&tmp); lay->addWidget(buttonBox); d.setLayout(lay); d.exec(); } void MainWindow::slotUpdateCompositing(QAction *compose) { int mode = compose->data().toInt(); getMainTimeline()->controller()->switchCompositing(mode); if (m_renderWidget) { m_renderWidget->errorMessage(RenderWidget::CompositeError, mode == 1 ? i18n("Rendering using low quality track compositing") : QString()); } } void MainWindow::slotUpdateCompositeAction(int mode) { QList actions = m_compositeAction->actions(); for (int i = 0; i < actions.count(); i++) { if (actions.at(i)->data().toInt() == mode) { m_compositeAction->setCurrentAction(actions.at(i)); break; } } if (m_renderWidget) { m_renderWidget->errorMessage(RenderWidget::CompositeError, mode == 1 ? i18n("Rendering using low quality track compositing") : QString()); } } void MainWindow::showMenuBar(bool show) { if (!show) { KMessageBox::information(this, i18n("This will hide the menu bar completely. You can show it again by typing Ctrl+M."), i18n("Hide menu bar"), QStringLiteral("show-menubar-warning")); } menuBar()->setVisible(show); } void MainWindow::forceIconSet(bool force) { KdenliveSettings::setForce_breeze(force); if (force) { // Check current color theme QColor background = qApp->palette().window().color(); bool useDarkIcons = background.value() < 100; KdenliveSettings::setUse_dark_breeze(useDarkIcons); } if (KMessageBox::warningContinueCancel(this, i18n("Kdenlive needs to be restarted to apply icon theme change. Restart now ?")) == KMessageBox::Continue) { slotRestart(); } } void MainWindow::slotSwitchTrimMode() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->switchTrimMode(); } */ } void MainWindow::setTrimMode(const QString &mode) { Q_UNUSED(mode) // TODO refac /* if (pCore->projectManager()->currentTimeline()) { m_trimLabel->setText(mode); m_trimLabel->setVisible(!mode.isEmpty()); } */ } TimelineWidget *MainWindow::getMainTimeline() const { return m_timelineTabs->getMainTimeline(); } TimelineWidget *MainWindow::getCurrentTimeline() const { return m_timelineTabs->getCurrentTimeline(); } void MainWindow::resetTimelineTracks() { TimelineWidget *current = getCurrentTimeline(); if (current) { current->controller()->resetTrackHeight(); } } void MainWindow::slotChangeSpeed(int speed) { ObjectId owner = m_assetPanel->effectStackOwner(); // TODO: manage bin clips / tracks if (owner.first == ObjectType::TimelineClip) { getCurrentTimeline()->controller()->changeItemSpeed(owner.second, speed); } } void MainWindow::slotSwitchTimelineZone(bool toggled) { KdenliveSettings::setUseTimelineZoneToEdit(toggled); getCurrentTimeline()->controller()->useRulerChanged(); } #ifdef DEBUG_MAINW #undef DEBUG_MAINW #endif diff --git a/src/mltcontroller/clipcontroller.cpp b/src/mltcontroller/clipcontroller.cpp index a08a08cd2..7f7a83992 100644 --- a/src/mltcontroller/clipcontroller.cpp +++ b/src/mltcontroller/clipcontroller.cpp @@ -1,852 +1,833 @@ /* 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 "clipcontroller.h" #include "bin/model/markerlistmodel.hpp" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/effectscontroller.h" #include "profiles/profilemodel.hpp" #include "core.h" #include "kdenlive_debug.h" #include #include #include #include std::shared_ptr ClipController::mediaUnavailable; ClipController::ClipController(const QString clipId, std::shared_ptr producer) : selectedEffectIndex(1) , m_audioThumbCreated(false) , m_masterProducer(producer) , m_properties(producer ? new Mlt::Properties(producer->get_properties()) : nullptr) , m_usesProxy(false) , m_audioInfo(nullptr) , m_audioIndex(0) , m_videoIndex(0) , m_clipType(ClipType::Unknown) , m_hasLimitedDuration(true) , m_effectStack(producer ? EffectStackModel::construct(producer, {ObjectType::BinClip, clipId.toInt()}, pCore->undoStack()) : nullptr) , m_controllerBinId(clipId) { if (m_masterProducer && !m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; return; } if (m_masterProducer) { - checkAudio(); + checkAudioVideo(); } if (m_properties) { setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); m_service = m_properties->get("mlt_service"); QString proxy = m_properties->get("kdenlive:proxy"); QString path = m_properties->get("resource"); if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(pCore->currentDoc()->documentRoot()); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative() && path != QLatin1String("")) { path.prepend(pCore->currentDoc()->documentRoot()); } m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath(); getInfoForProducer(); } else { m_producerLock.lock(); } } ClipController::~ClipController() { delete m_properties; m_masterProducer.reset(); } const QString ClipController::binId() const { return m_controllerBinId; } const std::unique_ptr &ClipController::audioInfo() const { return m_audioInfo; } void ClipController::addMasterProducer(const std::shared_ptr &producer) { qDebug() << "################### ClipController::addmasterproducer"; QString documentRoot = pCore->currentDoc()->documentRoot(); m_masterProducer = producer; m_properties = new Mlt::Properties(m_masterProducer->get_properties()); int id = m_controllerBinId.toInt(); m_effectStack = EffectStackModel::construct(producer, {ObjectType::BinClip, id}, pCore->undoStack()); if (!m_masterProducer->is_valid()) { m_masterProducer = ClipController::mediaUnavailable; m_producerLock.unlock(); qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { - checkAudio(); + checkAudioVideo(); m_producerLock.unlock(); QString proxy = m_properties->get("kdenlive:proxy"); m_service = m_properties->get("mlt_service"); QString path = m_properties->get("resource"); m_usesProxy = false; if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath(); getInfoForProducer(); emitProducerChanged(m_controllerBinId, producer); setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); } connectEffectStack(); } namespace { QString producerXml(const std::shared_ptr &producer, bool includeMeta) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); if (!s.is_valid()) { return QString(); } int ignore = s.get_int("ignore_points"); if (ignore != 0) { s.set("ignore_points", 0); } c.set("time_format", "frames"); if (!includeMeta) { c.set("no_meta", 1); } c.set("store", "kdenlive"); c.set("no_root", 1); c.set("root", "/"); c.connect(s); c.start(); if (ignore != 0) { s.set("ignore_points", ignore); } return QString::fromUtf8(c.get("string")); } } // namespace void ClipController::getProducerXML(QDomDocument &document, bool includeMeta) { // TODO refac this is a probable duplicate with Clip::xml if (m_masterProducer) { QString xml = producerXml(m_masterProducer, includeMeta); document.setContent(xml); } else { qCDebug(KDENLIVE_LOG) << " + + ++ NO MASTER PROD"; } } void ClipController::getInfoForProducer() { date = QFileInfo(m_path).lastModified(); m_audioIndex = -1; m_videoIndex = -1; // special case: playlist with a proxy clip have to be detected separately if (m_usesProxy && m_path.endsWith(QStringLiteral(".mlt"))) { m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("avformat") || m_service == QLatin1String("avformat-novalidate")) { m_audioIndex = getProducerIntProperty(QStringLiteral("audio_index")); m_videoIndex = getProducerIntProperty(QStringLiteral("video_index")); if (m_audioIndex == -1) { m_clipType = ClipType::Video; } else if (m_videoIndex == -1) { m_clipType = ClipType::Audio; } else { m_clipType = ClipType::AV; } } else if (m_service == QLatin1String("qimage") || m_service == QLatin1String("pixbuf")) { if (m_path.contains(QLatin1Char('%')) || m_path.contains(QStringLiteral("/.all."))) { m_clipType = ClipType::SlideShow; } else { m_clipType = ClipType::Image; } m_hasLimitedDuration = false; } else if (m_service == QLatin1String("colour") || m_service == QLatin1String("color")) { m_clipType = ClipType::Color; m_hasLimitedDuration = false; } else if (m_service == QLatin1String("kdenlivetitle")) { if (!m_path.isEmpty()) { m_clipType = ClipType::TextTemplate; } else { m_clipType = ClipType::Text; } m_hasLimitedDuration = false; } else if (m_service == QLatin1String("xml") || m_service == QLatin1String("consumer")) { m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("webvfx")) { m_clipType = ClipType::WebVfx; } else if (m_service == QLatin1String("qtext")) { m_clipType = ClipType::QText; } else { m_clipType = ClipType::Unknown; } if (m_audioIndex > -1 || m_clipType == ClipType::Playlist) { m_audioInfo.reset(new AudioStreamInfo(m_masterProducer.get(), m_audioIndex)); } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->get_int("kdenlive:duration"); if (playtime <= 0) { // Fix clips having missing kdenlive:duration m_masterProducer->set("kdenlive:duration", m_masterProducer->get_playtime()); m_masterProducer->set("out", m_masterProducer->get_length() - 1); } } } bool ClipController::hasLimitedDuration() const { return m_hasLimitedDuration; } void ClipController::forceLimitedDuration() { m_hasLimitedDuration = true; } std::shared_ptr ClipController::originalProducer() { QMutexLocker lock(&m_producerLock); return m_masterProducer; } Mlt::Producer *ClipController::masterProducer() { return new Mlt::Producer(*m_masterProducer); } bool ClipController::isValid() { if (m_masterProducer == nullptr) { return false; } return m_masterProducer->is_valid(); } // static const char *ClipController::getPassPropertiesList(bool passLength) { if (!passLength) { return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,file_hash,autorotate"; } return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,templatetext,file_hash,autorotate,xmldata,length"; } QMap ClipController::getPropertiesFromPrefix(const QString &prefix, bool withPrefix) { Mlt::Properties subProperties; subProperties.pass_values(*m_properties, prefix.toUtf8().constData()); QMap subclipsData; for (int i = 0; i < subProperties.count(); i++) { subclipsData.insert(withPrefix ? QString(prefix + subProperties.get_name(i)) : subProperties.get_name(i), subProperties.get(i)); } return subclipsData; } void ClipController::updateProducer(const std::shared_ptr &producer) { qDebug() << "################### ClipController::updateProducer"; // TODO replace all track producers if (!m_properties) { // producer has not been initialized return addMasterProducer(producer); } Mlt::Properties passProperties; // Keep track of necessary properties QString proxy = producer->get("kdenlive:proxy"); if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property m_usesProxy = true; } else { m_usesProxy = false; } passProperties.pass_list(*m_properties, getPassPropertiesList(m_usesProxy)); delete m_properties; *m_masterProducer = producer.get(); - checkAudio(); + checkAudioVideo(); m_properties = new Mlt::Properties(m_masterProducer->get_properties()); // Pass properties from previous producer m_properties->pass_list(passProperties, getPassPropertiesList(m_usesProxy)); m_producerLock.unlock(); if (!m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { m_effectStack->resetService(m_masterProducer); // URL and name shoule not be updated otherwise when proxying a clip we cannot find back the original url /*m_url = QUrl::fromLocalFile(m_masterProducer->get("resource")); if (m_url.isValid()) { m_name = m_url.fileName(); } */ } qDebug() << "// replace finished: " << binId() << " : " << m_masterProducer->get("resource"); } -Mlt::Producer *ClipController::getTrackProducer(const QString &trackName, PlaylistState::ClipState clipState, double speed) -{ - // TODO Delete this - Q_UNUSED(speed); - Q_UNUSED(trackName); - Q_UNUSED(clipState); - return m_masterProducer.get(); - /* - //TODO - Q_UNUSED(speed) - - if (trackName.isEmpty()) { - return m_masterProducer; - } - if (m_clipType != AV && m_clipType != Audio && m_clipType != Playlist) { - // Only producers with audio need a different producer for each track (or we have an audio crackle bug) - return new Mlt::Producer(m_masterProducer->parent()); - } - QString clipWithTrackId = clipId(); - clipWithTrackId.append(QLatin1Char('_') + trackName); - - //TODO handle audio / video only producers and framebuffer - if (clipState == PlaylistState::AudioOnly) { - clipWithTrackId.append(QStringLiteral("_audio")); - } else if (clipState == PlaylistState::VideoOnly) { - clipWithTrackId.append(QStringLiteral("_video")); - } - - Mlt::Producer *clone = m_binController->cloneProducer(*m_masterProducer); - clone->set("id", clipWithTrackId.toUtf8().constData()); - //m_binController->replaceBinPlaylistClip(clipWithTrackId, clone->parent()); - return clone; - */ -} const QString ClipController::getStringDuration() { if (m_masterProducer) { int playtime = m_masterProducer->get_int("kdenlive:duration"); if (playtime > 0) { return QString(m_properties->frames_to_time(playtime, mlt_time_smpte_df)); } return m_masterProducer->get_length_time(mlt_time_smpte_df); } return i18n("Unknown"); } GenTime ClipController::getPlaytime() const { if (!m_masterProducer || !m_masterProducer->is_valid()) { return GenTime(); } double fps = pCore->getCurrentFps(); if (!m_hasLimitedDuration) { int playtime = m_masterProducer->get_int("kdenlive:duration"); return GenTime(playtime == 0 ? m_masterProducer->get_playtime() : playtime, fps); } return GenTime(m_masterProducer->get_playtime(), fps); } int ClipController::getFramePlaytime() const { if (!m_masterProducer || !m_masterProducer->is_valid()) { return 0; } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->get_int("kdenlive:duration"); return playtime == 0 ? m_masterProducer->get_playtime() : playtime; } return m_masterProducer->get_playtime(); } QString ClipController::getProducerProperty(const QString &name) const { if (!m_properties) { return QString(); } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get(correctedName.toUtf8().constData()); } return QString(m_properties->get(name.toUtf8().constData())); } int ClipController::getProducerIntProperty(const QString &name) const { if (!m_properties) { return 0; } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get_int(correctedName.toUtf8().constData()); } return m_properties->get_int(name.toUtf8().constData()); } qint64 ClipController::getProducerInt64Property(const QString &name) const { if (!m_properties) { return 0; } return m_properties->get_int64(name.toUtf8().constData()); } double ClipController::getProducerDoubleProperty(const QString &name) const { if (!m_properties) { return 0; } return m_properties->get_double(name.toUtf8().constData()); } QColor ClipController::getProducerColorProperty(const QString &name) const { if (!m_properties) { return QColor(); } mlt_color color = m_properties->get_color(name.toUtf8().constData()); return QColor::fromRgb(color.r, color.g, color.b); } QMap ClipController::currentProperties(const QMap &props) { QMap currentProps; QMap::const_iterator i = props.constBegin(); while (i != props.constEnd()) { currentProps.insert(i.key(), getProducerProperty(i.key())); ++i; } return currentProps; } double ClipController::originalFps() const { if (!m_properties) { return 0; } QString propertyName = QStringLiteral("meta.media.%1.stream.frame_rate").arg(m_videoIndex); return m_properties->get_double(propertyName.toUtf8().constData()); } QString ClipController::videoCodecProperty(const QString &property) const { if (!m_properties) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.%2").arg(m_videoIndex).arg(property); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::codec(bool audioCodec) const { if ((m_properties == nullptr) || (m_clipType != ClipType::AV && m_clipType != ClipType::Video && m_clipType != ClipType::Audio)) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.name").arg(audioCodec ? m_audioIndex : m_videoIndex); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::clipUrl() const { return m_path; } QString ClipController::clipName() const { QString name = getProducerProperty(QStringLiteral("kdenlive:clipname")); if (!name.isEmpty()) { return name; } return QFileInfo(m_path).fileName(); } QString ClipController::description() const { if (m_clipType == ClipType::TextTemplate) { QString name = getProducerProperty(QStringLiteral("templatetext")); return name; } QString name = getProducerProperty(QStringLiteral("kdenlive:description")); if (!name.isEmpty()) { return name; } return getProducerProperty(QStringLiteral("meta.attr.comment.markup")); } QString ClipController::serviceName() const { return m_service; } void ClipController::setProducerProperty(const QString &name, int value) { if (!m_masterProducer) return; // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, double value) { if (!m_masterProducer) return; // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, const QString &value) { if (!m_masterProducer) return; // TODO: also set property on all track producers if (value.isEmpty()) { m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } else { m_masterProducer->parent().set(name.toUtf8().constData(), value.toUtf8().constData()); } } void ClipController::resetProducerProperty(const QString &name) { // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } ClipType ClipController::clipType() const { return m_clipType; } const QSize ClipController::getFrameSize() const { if (m_masterProducer == nullptr) { return QSize(); } int width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } int height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } return QSize(width, height); } bool ClipController::hasAudio() const { return m_hasAudio; } -void ClipController::checkAudio() +void ClipController::checkAudioVideo() { m_masterProducer->seek(0); Mlt::Frame *frame = m_masterProducer->get_frame(); - // test_audio returns 1 if there is NO audio (strange but at the time this code is written) + // test_audio returns 1 if there is NO audio (strange but true at the time this code is written) m_hasAudio = frame->get_int("test_audio") == 0; + m_hasVideo = frame->get_int("test_image") == 0; +} +bool ClipController::hasVideo() const +{ + return m_hasVideo; +} +PlaylistState ClipController::defaultState() const +{ + if (hasVideo()) { + return PlaylistState::VideoOnly; + } + if (hasAudio()) { + return PlaylistState::AudioOnly; + } + return PlaylistState::Disabled; } QPixmap ClipController::pixmap(int framePosition, int width, int height) { // TODO refac this should use the new thumb infrastructure m_masterProducer->seek(framePosition); Mlt::Frame *frame = m_masterProducer->get_frame(); if (frame == nullptr || !frame->is_valid()) { QPixmap p(width, height); p.fill(QColor(Qt::red).rgb()); return p; } frame->set("rescale.interp", "bilinear"); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (width == 0) { width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } } if (height == 0) { height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } } // int ow = frameWidth; // int oh = height; mlt_image_format format = mlt_image_rgb24a; width += width % 2; height += height % 2; const uchar *imagedata = frame->get_image(format, width, height); QImage image(imagedata, width, height, QImage::Format_RGBA8888); QPixmap pixmap; pixmap.convertFromImage(image); delete frame; return pixmap; } void ClipController::setZone(const QPoint &zone) { setProducerProperty(QStringLiteral("kdenlive:zone_in"), zone.x()); setProducerProperty(QStringLiteral("kdenlive:zone_out"), zone.y()); } QPoint ClipController::zone() const { int in = getProducerIntProperty(QStringLiteral("kdenlive:zone_in")); int max = getFramePlaytime() - 1; int out = qMin(getProducerIntProperty(QStringLiteral("kdenlive:zone_out")), max); if (out <= in) { out = max; } QPoint zone(in, out); return zone; } const QString ClipController::getClipHash() const { return getProducerProperty(QStringLiteral("kdenlive:file_hash")); } Mlt::Properties &ClipController::properties() { return *m_properties; } void ClipController::addEffect(QDomElement &xml) { Q_UNUSED(xml) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service = m_masterProducer->parent(); ItemInfo info; info.cropStart = GenTime(); info.cropDuration = getPlaytime(); EffectsList eff = effectList(); EffectsController::initEffect(info, eff, getProducerProperty(QStringLiteral("kdenlive:proxy")), xml); // Add effect to list and setup a kdenlive_ix value int kdenlive_ix = 0; for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); int ix = effect->get_int("kdenlive_ix"); if (ix > kdenlive_ix) { kdenlive_ix = ix; } } kdenlive_ix++; xml.setAttribute(QStringLiteral("kdenlive_ix"), kdenlive_ix); EffectsParameterList params = EffectsController::getEffectArgs(xml); EffectManager effect(service); effect.addEffect(params, getPlaytime().frames(pCore->getCurrentFps())); if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::removeEffect(int effectIndex, bool delayRefresh){ Q_UNUSED(effectIndex) Q_UNUSED(delayRefresh) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.removeEffect(effectIndex, true); if (!delayRefresh) { if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); } */ } EffectsList ClipController::effectList() { return xmlEffectList(m_masterProducer->profile(), m_masterProducer->parent()); } void ClipController::moveEffect(int oldPos, int newPos) { Q_UNUSED(oldPos) Q_UNUSED(newPos) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.moveEffect(oldPos, newPos); */ } int ClipController::effectsCount() { int count = 0; Mlt::Service service(m_masterProducer->parent()); for (int ix = 0; ix < service.filter_count(); ++ix) { QScopedPointer effect(service.filter(ix)); QString id = effect->get("kdenlive_id"); if (!id.isEmpty()) { count++; } } return count; } // static EffectsList ClipController::xmlEffectList(Mlt::Profile *profile, Mlt::Service &service) { Q_UNUSED(profile) Q_UNUSED(service) EffectsList effList(true); // TODO refac : rewrite this /* for (int ix = 0; ix < service.filter_count(); ++ix) { QScopedPointer effect(service.filter(ix)); QDomElement clipeffect = Timeline::getEffectByTag(effect->get("tag"), effect->get("kdenlive_id")); QDomElement currenteffect = clipeffect.cloneNode().toElement(); // recover effect parameters QDomNodeList params = currenteffect.elementsByTagName(QStringLiteral("parameter")); if (effect->get_int("disable") == 1) { currenteffect.setAttribute(QStringLiteral("disable"), 1); } for (int i = 0; i < params.count(); ++i) { QDomElement param = params.item(i).toElement(); Timeline::setParam(param, effect->get(param.attribute(QStringLiteral("name")).toUtf8().constData())); } effList.append(currenteffect); } */ return effList; } void ClipController::changeEffectState(const QList &indexes, bool disable) { Q_UNUSED(indexes) Q_UNUSED(disable) // TODO refac : this must be rewritten /* Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if ((effect != nullptr) && effect->is_valid() && indexes.contains(effect->get_int("kdenlive_ix"))) { effect->set("disable", (int)disable); } } if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::updateEffect(const QDomElement &e, int ix) { Q_UNUSED(e) Q_UNUSED(ix) // TODO refac : this must be rewritten /* QString tag = e.attribute(QStringLiteral("id")); if (tag == QLatin1String("autotrack_rectangle") || tag.startsWith(QLatin1String("ladspa")) || tag == QLatin1String("sox")) { // this filters cannot be edited, remove and re-add it removeEffect(ix, true); QDomElement clone = e.cloneNode().toElement(); addEffect(clone); return; } EffectsParameterList params = EffectsController::getEffectArgs(e); Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if (!effect || !effect->is_valid() || effect->get_int("kdenlive_ix") != ix) { continue; } service.lock(); QString prefix; QString ser = effect->get("mlt_service"); if (ser == QLatin1String("region")) { prefix = QStringLiteral("filter0."); } for (int j = 0; j < params.count(); ++j) { effect->set((prefix + params.at(j).name()).toUtf8().constData(), params.at(j).value().toUtf8().constData()); // qCDebug(KDENLIVE_LOG)<updateTrackProducer(m_controllerBinId); // slotRefreshTracks(); */ } bool ClipController::hasEffects() const { return m_effectStack->rowCount() > 0; } void ClipController::setBinEffectsEnabled(bool enabled) { m_effectStack->setEffectStackEnabled(enabled); } void ClipController::saveZone(QPoint zone, const QDir &dir) { QString path = QString(clipName() + QLatin1Char('_') + QString::number(zone.x()) + QStringLiteral(".mlt")); if (dir.exists(path)) { // TODO ask for overwrite } Mlt::Consumer xmlConsumer(pCore->getCurrentProfile()->profile(), ("xml:" + dir.absoluteFilePath(path)).toUtf8().constData()); xmlConsumer.set("terminate_on_pause", 1); Mlt::Producer prod(m_masterProducer->get_producer()); Mlt::Producer *prod2 = prod.cut(zone.x(), zone.y()); Mlt::Playlist list(pCore->getCurrentProfile()->profile()); list.insert_at(0, *prod2, 0); // list.set("title", desc.toUtf8().constData()); xmlConsumer.connect(list); xmlConsumer.run(); delete prod2; } std::shared_ptr ClipController::getEffectStack() const { return m_effectStack; } void ClipController::addEffect(const QString &effectId) { m_effectStack->appendEffect(effectId, true); } bool ClipController::copyEffect(std::shared_ptr stackModel, int rowId) { m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId)); return true; } std::shared_ptr ClipController::getMarkerModel() const { return m_markerModel; } diff --git a/src/mltcontroller/clipcontroller.h b/src/mltcontroller/clipcontroller.h index cbdf05dc1..1fde5ade5 100644 --- a/src/mltcontroller/clipcontroller.h +++ b/src/mltcontroller/clipcontroller.h @@ -1,232 +1,232 @@ /* 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 . */ #ifndef CLIPCONTROLLER_H #define CLIPCONTROLLER_H #include "definitions.h" #include #include #include #include #include #include #include class QPixmap; class Bin; class AudioStreamInfo; class EffectStackModel; class MarkerListModel; /** * @class ClipController * @brief Provides a convenience wrapper around the project Bin clip producers. * It also holds a QList of track producers for the 'master' producer in case we * need to update or replace them */ class ClipController { public: friend class Bin; /** * @brief Constructor. The constructor is protected because you should call the static Construct instead * @param bincontroller reference to the bincontroller * @param producer producer to create reference to */ explicit ClipController(const QString id, std::shared_ptr producer = nullptr); public: virtual ~ClipController(); /** @brief Returns true if the master producer is valid */ bool isValid(); /** @brief Stores the file's creation time */ QDateTime date; /** @brief Replaces the master producer and (TODO) the track producers with an updated producer, for example a proxy */ void updateProducer(const std::shared_ptr &producer); void getProducerXML(QDomDocument &document, bool includeMeta = false); /** @brief Returns a clone of our master producer. Delete after use! */ Mlt::Producer *masterProducer(); /** @brief Returns the clip name (usually file name) */ QString clipName() const; /** @brief Returns the clip's description or metadata comment */ QString description() const; /** @brief Returns the clip's MLT resource */ const QString clipUrl() const; /** @brief Returns the clip's type as defined in definitions.h */ ClipType clipType() const; /** @brief Returns the MLT's producer id */ const QString binId() const; /** @brief Returns the clip's duration */ GenTime getPlaytime() const; int getFramePlaytime() const; /** * @brief Sets a property. * @param name name of the property * @param value the new value */ void setProducerProperty(const QString &name, const QString &value); void setProducerProperty(const QString &name, int value); void setProducerProperty(const QString &name, double value); /** @brief Reset a property on the MLT producer (=delete the property). */ void resetProducerProperty(const QString &name); /** * @brief Returns the list of all properties starting with prefix. For subclips, the list is of this type: * { subclip name , subclip in/out } where the subclip in/ou value is a semi-colon separated in/out value, like "25;220" */ QMap getPropertiesFromPrefix(const QString &prefix, bool withPrefix = false); /** * @brief Returns the value of a property. * @param name name o the property */ QMap currentProperties(const QMap &props); QString getProducerProperty(const QString &key) const; int getProducerIntProperty(const QString &key) const; qint64 getProducerInt64Property(const QString &key) const; QColor getProducerColorProperty(const QString &key) const; double getProducerDoubleProperty(const QString &key) const; double originalFps() const; QString videoCodecProperty(const QString &property) const; const QString codec(bool audioCodec) const; const QString getClipHash() const; const QSize getFrameSize() const; /** @brief Returns the clip duration as a string like 00:00:02:01. */ const QString getStringDuration(); /** * @brief Returns a pixmap created from a frame of the producer. * @param position frame position * @param width width of the pixmap (only a guidance) * @param height height of the pixmap (only a guidance) */ QPixmap pixmap(int position = 0, int width = 0, int height = 0); /** @brief Returns the MLT producer's service. */ QString serviceName() const; /** @brief Returns the original master producer. */ std::shared_ptr originalProducer(); /** @brief Holds index of currently selected master clip effect. */ int selectedEffectIndex; - /** @brief Get a clone of master producer for a specific track. Retrieve it if it already exists - * in our list, otherwise we create it. - * Deprecated, track logic should be handled in timeline/track.cpp */ - Q_DECL_DEPRECATED Mlt::Producer *getTrackProducer(const QString &trackName, PlaylistState::ClipState clipState = PlaylistState::Original, - double speed = 1.0); /** @brief Sets the master producer for this clip when we build the controller without master clip. */ void addMasterProducer(const std::shared_ptr &producer); /* @brief Returns the marker model associated with this clip */ std::shared_ptr getMarkerModel() const; void setZone(const QPoint &zone); QPoint zone() const; bool hasLimitedDuration() const; void forceLimitedDuration(); Mlt::Properties &properties(); void addEffect(QDomElement &xml); bool copyEffect(std::shared_ptr stackModel, int rowId); void removeEffect(int effectIndex, bool delayRefresh = false); EffectsList effectList(); /** @brief Enable/disable an effect. */ void changeEffectState(const QList &indexes, bool disable); void updateEffect(const QDomElement &e, int ix); /** @brief Returns true if the bin clip has effects */ bool hasEffects() const; /** @brief Returns true if the clip contains at least one audio stream */ bool hasAudio() const; + /** @brief Returns true if the clip contains at least one video stream */ + bool hasVideo() const; + /** @brief Returns the default state a clip should be in. If the clips contains both video and audio, this defaults to video */ + PlaylistState defaultState() const; /** @brief Returns info about clip audio */ const std::unique_ptr &audioInfo() const; /** @brief Returns true if audio thumbnails for this clip are cached */ bool m_audioThumbCreated; /** @brief When replacing a producer, it is important that we keep some properties, for exemple force_ stuff and url for proxies * this method returns a list of properties that we want to keep when replacing a producer . */ static const char *getPassPropertiesList(bool passLength = true); /** @brief Disable all Kdenlive effects on this clip */ void setBinEffectsEnabled(bool enabled); /** @brief Create a Kdenlive EffectList xml data from an MLT service */ static EffectsList xmlEffectList(Mlt::Profile *profile, Mlt::Service &service); /** @brief Returns the number of Kdenlive added effects for this bin clip */ int effectsCount(); /** @brief Move an effect in stack for this bin clip */ void moveEffect(int oldPos, int newPos); /** @brief Save an xml playlist of current clip with in/out points as zone.x()/y() */ void saveZone(QPoint zone, const QDir &dir); /* @brief This is the producer that serves as a placeholder while a clip is being loaded. It is created in Core at startup */ static std::shared_ptr mediaUnavailable; /** @brief Returns a ptr to the effetstack associated with this element */ std::shared_ptr getEffectStack() const; /** @brief Append an effect to this producer's effect list */ void addEffect(const QString &effectId); protected: virtual void emitProducerChanged(const QString &, const std::shared_ptr &){}; virtual void connectEffectStack(){}; - // This is the helper function that checks the clip for audio and stores the result - void checkAudio(); + // This is the helper function that checks if the clip has audio and video and stores the result + void checkAudioVideo(); std::shared_ptr m_masterProducer; Mlt::Properties *m_properties; bool m_usesProxy; std::unique_ptr m_audioInfo; QString m_service; QString m_path; int m_audioIndex; int m_videoIndex; ClipType m_clipType; bool m_hasLimitedDuration; QMutex m_effectMutex; void getInfoForProducer(); // void rebuildEffectList(ProfileInfo info); std::shared_ptr m_effectStack; std::shared_ptr m_markerModel; bool m_hasAudio; + bool m_hasVideo; private: QMutex m_producerLock; QString m_controllerBinId; }; #endif diff --git a/src/timeline2/model/builders/meltBuilder.cpp b/src/timeline2/model/builders/meltBuilder.cpp index b1f344bfc..17963f5bb 100644 --- a/src/timeline2/model/builders/meltBuilder.cpp +++ b/src/timeline2/model/builders/meltBuilder.cpp @@ -1,244 +1,279 @@ /*************************************************************************** * 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 "meltBuilder.hpp" #include "../clipmodel.hpp" #include "../timelineitemmodel.hpp" #include "../timelinemodel.hpp" #include "../trackmodel.hpp" #include "../undohelper.hpp" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "core.h" #include "kdenlivesettings.h" #include #include #include #include #include -bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track, const std::unordered_map &binIdCorresp,Fun &undo, Fun &redo); +bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track, + const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo); bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track, const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo); bool constructTimelineFromMelt(const std::shared_ptr &timeline, Mlt::Tractor tractor) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; // First, we destruct the previous tracks timeline->requestReset(undo, redo); std::unordered_map binIdCorresp; pCore->projectItemModel()->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp); QSet reserved_names{QLatin1String("playlistmain"), QLatin1String("timeline_preview"), QLatin1String("timeline_overlay"), QLatin1String("black_track")}; bool ok = true; qDebug() << "//////////////////////\nTrying to construct" << tractor.count() << "tracks.\n////////////////////////////////"; for (int i = 0; i < tractor.count() && ok; i++) { std::unique_ptr track(tractor.track(i)); QString playlist_name = track->get("id"); if (reserved_names.contains(playlist_name)) { continue; } switch (track->type()) { case producer_type: // TODO check that it is the black track, and otherwise log an error qDebug() << "SUSPICIOUS: we weren't expecting a producer when parsing the timeline"; break; case tractor_type: { // that is a double track int tid; bool audioTrack = track->get_int("kdenlive:audio_track") == 1; ok = timeline->requestTrackInsertion(-1, tid, QString(), audioTrack, undo, redo, false); int lockState = track->get_int("kdenlive:locked_track"); if (lockState > 0) { timeline->setTrackProperty(tid, QStringLiteral("kdenlive:locked_track"), QString::number(lockState)); } Mlt::Tractor local_tractor(*track); ok = ok && constructTrackFromMelt(timeline, tid, local_tractor, binIdCorresp, undo, redo); break; } case playlist_type: { // that is a single track qDebug() << "Adding track: " << track->get("id"); int tid; Mlt::Playlist local_playlist(*track); const QString trackName = local_playlist.get("kdenlive:track_name"); bool audioTrack = local_playlist.get_int("kdenlive:audio_track") == 1; ok = timeline->requestTrackInsertion(-1, tid, trackName, audioTrack, undo, redo, false); int muteState = track->get_int("hide"); if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) { timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState)); } int lockState = local_playlist.get_int("kdenlive:locked_track"); if (lockState > 0) { timeline->setTrackProperty(tid, QStringLiteral("kdenlive:locked_track"), QString::number(lockState)); } ok = ok && constructTrackFromMelt(timeline, tid, local_playlist, binIdCorresp, undo, redo); break; } default: qDebug() << "ERROR: Unexpected item in the timeline"; } } // Loading compositions QScopedPointer service(tractor.producer()); - QList compositions; + QList compositions; while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString id(t.get("kdenlive_id")); QString internal(t.get("internal_added")); if (internal.isEmpty()) { compositions << new Mlt::Transition(t); if (id.isEmpty()) { qDebug() << "// Warning, this should not happen, transition without id: " << t.get("id") << " = " << t.get("mlt_service"); t.set("kdenlive_id", t.get("mlt_service")); } } } service.reset(service->producer()); } // Sort compositions and insert if (!compositions.isEmpty()) { std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); while (!compositions.isEmpty()) { - QScopedPointert(compositions.takeFirst()); + QScopedPointer t(compositions.takeFirst()); Mlt::Properties transProps(t->get_properties()); QString id(t->get("kdenlive_id")); int compoId; - ok = timeline->requestCompositionInsertion(id, timeline->getTrackIndexFromPosition(t->get_b_track() - 1), t->get_a_track(), t->get_in(), t->get_length(), &transProps, compoId, undo, redo); + ok = timeline->requestCompositionInsertion(id, timeline->getTrackIndexFromPosition(t->get_b_track() - 1), t->get_a_track(), t->get_in(), + t->get_length(), &transProps, compoId, undo, redo); if (!ok) { qDebug() << "ERROR : failed to insert composition in track " << t->get_b_track() << ", position" << t->get_in(); break; } - qDebug() << "Inserted composition in track " << t->get_b_track() << ", position" << t->get_in()<<"/"<< t->get_out(); + qDebug() << "Inserted composition in track " << t->get_b_track() << ", position" << t->get_in() << "/" << t->get_out(); } } // build internal track compositing timeline->buildTrackCompositing(); timeline->updateDuration(); if (!ok) { // TODO log error undo(); return false; } return true; } -bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track, const std::unordered_map &binIdCorresp,Fun &undo, Fun &redo) +bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track, + const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo) { if (track.count() != 2) { // we expect a tractor with two tracks (a "fake" track) qDebug() << "ERROR : wrong number of subtracks"; return false; } for (int i = 0; i < track.count(); i++) { std::unique_ptr sub_track(track.track(i)); if (sub_track->type() != playlist_type) { qDebug() << "ERROR : SubTracks must be MLT::Playlist"; return false; } Mlt::Playlist playlist(*sub_track); constructTrackFromMelt(timeline, tid, playlist, binIdCorresp, undo, redo); if (i == 0) { // Pass track properties int height = track.get_int("kdenlive:trackheight"); timeline->setTrackProperty(tid, "kdenlive:trackheight", height == 0 ? "100" : QString::number(height)); QString trackName = track.get("kdenlive:track_name"); if (!trackName.isEmpty()) { timeline->setTrackProperty(tid, QStringLiteral("kdenlive:track_name"), trackName.toUtf8().constData()); } bool audioTrack = track.get_int("kdenlive:audio_track") == 1; if (audioTrack) { // This is an audio track timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_track"), QStringLiteral("1")); } int muteState = playlist.get_int("hide"); if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) { timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState)); } } } return true; } +namespace { + +// This function tries to recover the state of the producer (audio or video or both) +PlaylistState inferState(std::shared_ptr prod) +{ + auto getProperty = [prod](const QString &name) { + if (prod->parent().is_valid()) { + return QString::fromUtf8(prod->parent().get(name.toUtf8().constData())); + } + return QString::fromUtf8(prod->get(name.toUtf8().constData())); + }; + auto getIntProperty = [prod](const QString &name) { + if (prod->parent().is_valid()) { + return prod->parent().get_int(name.toUtf8().constData()); + } + return prod->get_int(name.toUtf8().constData()); + }; + QString service = getProperty("mlt_service"); + std::pair VidAud{true, true}; + VidAud.first = getIntProperty("set.test_image") == 0; + VidAud.second = getIntProperty("set.test_audio") == 0; + if (service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("video_index")) == -1) { + VidAud.first = false; + } + if (service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("audio_index")) == -1) { + VidAud.second = false; + } + return stateFromBool(VidAud); +} +} // namespace + bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track, const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo) { for (int i = 0; i < track.count(); i++) { if (track.is_blank(i)) { continue; } std::shared_ptr clip(track.get_clip(i)); int position = track.clip_start(i); switch (clip->type()) { case unknown_type: case producer_type: { - //qDebug() << "Looking for clip clip "<< clip->parent().get("kdenlive:id")<<" = "<parent().get("kdenlive:clipname"); + // qDebug() << "Looking for clip clip "<< clip->parent().get("kdenlive:id")<<" = "<parent().get("kdenlive:clipname"); QString binId; if (clip->parent().get_int("_kdenlive_processed") == 1) { // This is a bin clip, already processed no need to change id binId = QString(clip->parent().get("kdenlive:id")); } else { QString clipId = clip->parent().get("kdenlive:id"); if (clipId.startsWith(QStringLiteral("slowmotion"))) { clipId = clipId.section(QLatin1Char(':'), 1, 1); } if (clipId.isEmpty()) { clipId = clip->get("kdenlive:id"); } Q_ASSERT(binIdCorresp.count(clipId) > 0); binId = binIdCorresp.at(clipId); } bool ok = false; if (pCore->bin()->getBinClip(binId)) { - int cid = ClipModel::construct(timeline, binId, clip); + PlaylistState st = inferState(clip); + int cid = ClipModel::construct(timeline, binId, clip, st); ok = timeline->requestClipMove(cid, tid, position, true, false, undo, redo); } else { qDebug() << "// Cannot find bin clip: " << binId << " - " << clip->get("id"); } if (!ok) { qDebug() << "ERROR : failed to insert clip in track" << tid << "position" << position; return false; } qDebug() << "Inserted clip in track" << tid << "at " << position; break; } case tractor_type: { // TODO This is a nested timeline qDebug() << "NOT_IMPLEMENTED: code for parsing nested timeline is not there yet."; break; } default: qDebug() << "ERROR : unexpected object found on playlist"; return false; break; } } return true; } diff --git a/src/timeline2/model/clipmodel.cpp b/src/timeline2/model/clipmodel.cpp index 473b20b9a..24fa750b1 100644 --- a/src/timeline2/model/clipmodel.cpp +++ b/src/timeline2/model/clipmodel.cpp @@ -1,486 +1,503 @@ /*************************************************************************** * 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 "clipmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "macros.hpp" #include "timelinemodel.hpp" #include "trackmodel.hpp" #include #include #include // this can be deleted #include "bin/model/markerlistmodel.hpp" #include "gentime.h" #include ClipModel::ClipModel(std::shared_ptr parent, std::shared_ptr prod, const QString &binClipId, int id) : MoveableItem(parent, id) , m_producer(std::move(prod)) , m_effectStack(EffectStackModel::construct(m_producer, {ObjectType::TimelineClip, m_id}, parent->m_undoStack)) , m_binClipId(binClipId) , forceThumbReload(false) { m_producer->set("kdenlive:id", binClipId.toUtf8().constData()); m_producer->set("_kdenlive_cid", m_id); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); if (binClip) { m_endlessResize = !binClip->hasLimitedDuration(); } else { m_endlessResize = false; } } -int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState::ClipState state) +int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState state) { + id = (id == -1 ? TimelineModel::getNextId() : id); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId); - std::shared_ptr cutProducer = binClip->timelineProducer(state); - return construct(parent, binClipId, cutProducer, id); + + // We refine the state according to what the clip can actually produce + std::pair videoAudio = stateToBool(state); + videoAudio.first = videoAudio.first && binClip->hasVideo(); + videoAudio.second = videoAudio.second && binClip->hasAudio(); + state = stateFromBool(videoAudio); + + std::shared_ptr cutProducer = binClip->getTimelineProducer(id, state, 1.); + + std::shared_ptr clip(new ClipModel(parent, cutProducer, binClipId, id)); + clip->setClipState(state); + parent->registerClip(clip); + return id; } -int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, std::shared_ptr producer, int id) +int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, std::shared_ptr producer, PlaylistState state) { - std::shared_ptr clip(new ClipModel(parent, producer, binClipId, id)); - id = clip->m_id; + + // we hand the producer to the bin clip, and in return we get a cut to a good master producer + // We might not be able to use directly the producer that we receive as an argument, because it cannot share the same master producer with any other + // clipModel (due to a mlt limitation, see ProjectClip doc) + + int id = (id == -1 ? TimelineModel::getNextId() : id); + std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId); + + // We refine the state according to what the clip can actually produce + std::pair videoAudio = stateToBool(state); + videoAudio.first = videoAudio.first && binClip->hasVideo(); + videoAudio.second = videoAudio.second && binClip->hasAudio(); + state = stateFromBool(videoAudio); + + auto result = binClip->giveMasterAndGetTimelineProducer(id, producer, state); + std::shared_ptr clip(new ClipModel(parent, result.first, binClipId, id)); + clip->m_effectStack->importEffects(producer, result.second); + clip->setClipState(state); parent->registerClip(clip); - clip->m_effectStack->loadEffects(); + return id; } void ClipModel::registerClipToBin() { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); if (!binClip) { qDebug() << "Error : Bin clip for id: " << m_binClipId << " NOT AVAILABLE!!!"; } qDebug() << "REGISTRATION " << m_id << "ptr count" << m_parent.use_count(); binClip->registerTimelineClip(m_parent, m_id); } void ClipModel::deregisterClipToBin() { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); binClip->deregisterTimelineClip(m_id); } -ClipModel::~ClipModel() -{ -} +ClipModel::~ClipModel() {} bool ClipModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo) { - qDebug()<<"++++++++++ PERFORMAING CLIP RESIZE===="; + qDebug() << "++++++++++ PERFORMAING CLIP RESIZE===="; QWriteLocker locker(&m_lock); // qDebug() << "RESIZE CLIP" << m_id << "target size=" << size << "right=" << right << "endless=" << m_endlessResize << "total length" << // m_producer->get_length() << "current length" << getPlaytime(); if (!m_endlessResize && (size <= 0 || size > m_producer->get_length())) { return false; } int oldDuration = getPlaytime(); int delta = oldDuration - size; if (delta == 0) { return true; } int in = m_producer->get_in(); int out = m_producer->get_out(); int old_in = in, old_out = out; // check if there is enough space on the chosen side if (!right && in + delta < 0 && !m_endlessResize) { return false; } if (!m_endlessResize && right && out - delta >= m_producer->get_length()) { return false; } if (right) { out -= delta; } else { in += delta; } // qDebug() << "Resize facts delta =" << delta << "old in" << old_in << "old_out" << old_out << "in" << in << "out" << out; std::function track_operation = []() { return true; }; std::function track_reverse = []() { return true; }; int outPoint = out; int inPoint = in; if (m_endlessResize) { outPoint = out - in; inPoint = 0; } if (m_currentTrackId != -1) { if (auto ptr = m_parent.lock()) { track_operation = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, inPoint, outPoint, right); } else { qDebug() << "Error : Moving clip failed because parent timeline is not available anymore"; Q_ASSERT(false); } } Fun operation = [this, inPoint, outPoint, track_operation]() { if (track_operation()) { m_producer->set_in_and_out(inPoint, outPoint); return true; } return false; }; if (operation()) { // Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here auto ptr = m_parent.lock(); if (m_currentTrackId != -1 && ptr) { track_reverse = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, old_in, old_out, right); } Fun reverse = [this, old_in, old_out, track_reverse]() { if (track_reverse()) { m_producer->set_in_and_out(old_in, old_out); return true; } return false; }; - qDebug()<<"// ADJUSTING EFFECT LENGTH, LOGUNDO "<get_playtime(); + qDebug() << "// ADJUSTING EFFECT LENGTH, LOGUNDO " << logUndo << ", " << old_in << "/" << inPoint << ", " << m_producer->get_playtime(); if (logUndo) adjustEffectLength(right, old_in, inPoint, oldDuration, m_producer->get_playtime(), reverse, operation, logUndo); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } const QString ClipModel::getProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return QString::fromUtf8(service()->parent().get(name.toUtf8().constData())); } return QString::fromUtf8(service()->get(name.toUtf8().constData())); } int ClipModel::getIntProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return service()->parent().get_int(name.toUtf8().constData()); } return service()->get_int(name.toUtf8().constData()); } QSize ClipModel::getFrameSize() const { READ_LOCK(); if (service()->parent().is_valid()) { return QSize(service()->parent().get_int("meta.media.width"), service()->parent().get_int("meta.media.height")); } return QSize(service()->get_int("meta.media.width"), service()->get_int("meta.media.height")); } double ClipModel::getDoubleProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return service()->parent().get_double(name.toUtf8().constData()); } return service()->get_double(name.toUtf8().constData()); } Mlt::Producer *ClipModel::service() const { READ_LOCK(); return m_producer.get(); } int ClipModel::getPlaytime() const { READ_LOCK(); return m_producer->get_playtime(); } void ClipModel::setTimelineEffectsEnabled(bool enabled) { QWriteLocker locker(&m_lock); m_effectStack->setEffectStackEnabled(enabled); } bool ClipModel::addEffect(const QString &effectId) { QWriteLocker locker(&m_lock); m_effectStack->appendEffect(effectId); return true; } bool ClipModel::copyEffect(std::shared_ptr stackModel, int rowId) { QWriteLocker locker(&m_lock); m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId)); return true; } bool ClipModel::importEffects(std::shared_ptr stackModel) { QWriteLocker locker(&m_lock); m_effectStack->importEffects(stackModel); return true; } +bool ClipModel::importEffects(std::weak_ptr service) +{ + QWriteLocker locker(&m_lock); + m_effectStack->importEffects(service); + return true; +} + bool ClipModel::removeFade(bool fromStart) { QWriteLocker locker(&m_lock); m_effectStack->removeFade(fromStart); return true; } bool ClipModel::adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, Fun &undo, Fun &redo, bool logUndo) { QWriteLocker locker(&m_lock); return m_effectStack->adjustStackLength(adjustFromEnd, oldIn, oldDuration, newIn, duration, undo, redo, logUndo); } bool ClipModel::adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); - qDebug()<<".... ADJUSTING FADE LENGTH: "<adjustFadeLength(duration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), hasAudio(), !isAudioOnly()); + return m_effectStack->adjustFadeLength(duration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(), + !isAudioOnly()); }; if (operation() && originalDuration > 0) { Fun reverse = [this, originalDuration, effectName]() { - return m_effectStack->adjustFadeLength(originalDuration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), hasAudio(), !isAudioOnly()); + return m_effectStack->adjustFadeLength(originalDuration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), + audioEnabled(), !isAudioOnly()); }; UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return true; } -bool ClipModel::hasAudio() const +bool ClipModel::audioEnabled() const { READ_LOCK(); - QString service = getProperty("mlt_service"); - if (service == QLatin1String("xml")) { - // Playlist clip, assume audio - return true; - } - qDebug() << "checking audio:" << service - << ((service.contains(QStringLiteral("avformat")) || service == QLatin1String("timewarp")) && - (getIntProperty(QStringLiteral("audio_index")) > -1)); - if ((service.contains(QStringLiteral("avformat")) || service == QLatin1String("timewarp")) && (getIntProperty(QStringLiteral("audio_index")) > -1)) { - return true; - } - std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); - return binClip->hasAudio(); + return stateToBool(m_currentState).second; } bool ClipModel::isAudioOnly() const { READ_LOCK(); - QString service = getProperty("mlt_service"); - return service.contains(QStringLiteral("avformat")) && (getIntProperty(QStringLiteral("video_index")) == -1); + return m_currentState == PlaylistState::AudioOnly; } -void ClipModel::refreshProducerFromBin(PlaylistState::ClipState state) +void ClipModel::refreshProducerFromBin(PlaylistState state) { QWriteLocker locker(&m_lock); if (getProperty("mlt_service") == QLatin1String("timewarp")) { // slowmotion producer, keep it int space = -1; if (m_currentTrackId != -1) { if (auto ptr = m_parent.lock()) { space = ptr->getTrackById(m_currentTrackId)->getBlankSizeNearClip(m_id, true); } else { qDebug() << "Error : Moving clip failed because parent timeline is not available anymore"; Q_ASSERT(false); } } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; useTimewarpProducer(m_producer->get_double("warp_speed"), space, local_undo, local_redo); return; } int in = getIn(); int out = getOut(); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); - std::shared_ptr binProducer = binClip->timelineProducer(state, m_currentTrackId); + std::shared_ptr binProducer = binClip->getTimelineProducer(m_id, state); m_producer = std::move(binProducer); m_producer->set_in_and_out(in, out); - // m_producer.reset(binProducer->cut(in, out)); // replant effect stack in updated service m_effectStack->resetService(m_producer); m_producer->set("kdenlive:id", binClip->AbstractProjectItem::clipId().toUtf8().constData()); m_producer->set("_kdenlive_cid", m_id); m_endlessResize = !binClip->hasLimitedDuration(); } +void ClipModel::refreshProducerFromBin() +{ + refreshProducerFromBin(m_currentState); +} bool ClipModel::useTimewarpProducer(double speed, int extraSpace, Fun &undo, Fun &redo) { double previousSpeed = getSpeed(); auto operation = useTimewarpProducer_lambda(speed, extraSpace); if (operation()) { auto reverse = useTimewarpProducer_lambda(previousSpeed, extraSpace); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } Fun ClipModel::useTimewarpProducer_lambda(double speed, int extraSpace) { Q_UNUSED(extraSpace) QWriteLocker locker(&m_lock); // TODO: disable timewarp on color clips int in = getIn(); int out = getOut(); int warp_in; int warp_out; if (getProperty("mlt_service") == QLatin1String("timewarp")) { // slowmotion producer, get current speed warp_in = m_producer->get_int("warp_in"); warp_out = m_producer->get_int("warp_out"); } else { // store original in/out warp_in = in; warp_out = out; } in = warp_in / speed; - out = qMin((int) (warp_out / speed), extraSpace); + out = qMin((int)(warp_out / speed), extraSpace); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); std::shared_ptr originalProducer = binClip->originalProducer(); bool limitedDuration = binClip->hasLimitedDuration(); return [originalProducer, speed, in, out, warp_in, warp_out, limitedDuration, this]() { if (qFuzzyCompare(speed, 1.0)) { m_producer.reset(originalProducer->cut(in, out)); } else { QLocale locale; QString resource = QString("timewarp:%1:%2").arg(locale.toString(speed)).arg(originalProducer->get("resource")); std::shared_ptr warpProducer(new Mlt::Producer(*m_producer->profile(), resource.toUtf8().constData())); // Make sure we use a cut so that the source producer in/out are not modified m_producer.reset(warpProducer->cut(0, warpProducer->get_length())); setInOut(in, out); } // replant effect stack in updated service m_effectStack->resetService(m_producer); m_producer->set("kdenlive:id", m_binClipId.toUtf8().constData()); m_producer->set("_kdenlive_cid", m_id); m_producer->set("warp_in", warp_in); m_producer->set("warp_out", warp_out); m_endlessResize = !limitedDuration; return true; }; return []() { return false; }; } QVariant ClipModel::getAudioWaveform() { READ_LOCK(); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); if (binClip) { return QVariant::fromValue(binClip->audioFrameCache); } return QVariant(); } const QString &ClipModel::binId() const { return m_binClipId; } std::shared_ptr ClipModel::getMarkerModel() const { READ_LOCK(); return pCore->projectItemModel()->getClipByBinID(m_binClipId)->getMarkerModel(); } int ClipModel::fadeIn() const { return m_effectStack->getFadePosition(true); } int ClipModel::fadeOut() const { return m_effectStack->getFadePosition(false); } double ClipModel::getSpeed() const { if (getProperty("mlt_service") == QLatin1String("timewarp")) { // slowmotion producer, get current speed return m_producer->parent().get_double("warp_speed"); } return 1.0; } KeyframeModel *ClipModel::getKeyframeModel() { return m_effectStack->getEffectKeyframeModel(); } bool ClipModel::showKeyframes() const { READ_LOCK(); return !service()->get_int("kdenlive:hide_keyframes"); } void ClipModel::setShowKeyframes(bool show) { QWriteLocker locker(&m_lock); service()->set("kdenlive:hide_keyframes", (int)!show); } -bool ClipModel::setClipState(PlaylistState::ClipState state) +bool ClipModel::setClipState(PlaylistState state) { QWriteLocker locker(&m_lock); - if (!getProperty("mlt_service").startsWith(QStringLiteral("avformat"))) { - return false; - } - std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); - std::shared_ptr binProducer = binClip->timelineProducer(state, m_currentTrackId); - int in = getIn(); - int out = getOut(); - m_producer = std::move(binProducer); - m_producer->set_in_and_out(in, out); - m_producer->set("kdenlive:id", m_binClipId.toUtf8().constData()); - m_producer->set("_kdenlive_cid", m_id); - - // replant effect stack in updated service - m_effectStack->resetService(m_producer); + std::pair VidAud = stateToBool(state); + m_producer->parent().set("set.test_image", int(VidAud.first ? 0 : 1)); + m_producer->parent().set("set.test_audio", int(VidAud.second ? 0 : 1)); return true; } -PlaylistState::ClipState ClipModel::clipState() const +PlaylistState ClipModel::clipState() const { READ_LOCK(); + return m_currentState; + /* if (service()->parent().get_int("audio_index") == -1) { if (service()->parent().get_int("video_index") == -1) { return PlaylistState::Disabled; } else { return PlaylistState::VideoOnly; } } else if (service()->parent().get_int("video_index") == -1) { return PlaylistState::AudioOnly; } return PlaylistState::Original; + */ } -void ClipModel::passTimelineProperties(std::shared_ptr other) +void ClipModel::passTimelineProperties(std::shared_ptr other) { READ_LOCK(); Mlt::Properties source(m_producer->get_properties()); Mlt::Properties dest(other->service()->get_properties()); dest.pass_list(source, "kdenlive:hide_keyframes,kdenlive:activeeffect"); } diff --git a/src/timeline2/model/clipmodel.hpp b/src/timeline2/model/clipmodel.hpp index 7026e92fc..9c6b32ad8 100644 --- a/src/timeline2/model/clipmodel.hpp +++ b/src/timeline2/model/clipmodel.hpp @@ -1,159 +1,165 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef CLIPMODEL_H #define CLIPMODEL_H #include "moveableItem.hpp" #include "undohelper.hpp" #include #include namespace Mlt { class Producer; } class EffectStackModel; class MarkerListModel; class ProjectClip; class TimelineModel; class TrackModel; class KeyframeModel; /* @brief This class represents a Clip object, as viewed by the backend. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications */ class ClipModel : public MoveableItem { ClipModel() = delete; protected: /* This constructor is not meant to be called, call the static construct instead */ ClipModel(std::shared_ptr parent, std::shared_ptr prod, const QString &binClipId, int id = -1); public: ~ClipModel(); /* @brief Creates a clip, which references itself to the parent timeline Returns the (unique) id of the created clip @param parent is a pointer to the timeline @param binClip is the id of the bin clip associated @param id Requested id of the clip. Automatic if -1 */ - static int construct(const std::shared_ptr &parent, const QString &binClipId, int id = -1, - PlaylistState::ClipState state = PlaylistState::Original); - /* @brief Creates a clip from an instance in MLT's playlist, + static int construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState state); + + /* @brief Creates a clip, which references itself to the parent timeline Returns the (unique) id of the created clip - @param parent is a pointer to the timeline - @param binClip is the id of the bin clip associated - @param producer is the producer to be inserted - @param id Requested id of the clip. Automatic if -1 + This variants assumes a producer is already known, which should typically happen only at loading time. + Note that there is no guarantee that this producer is actually going to be used. It might be discarded. */ - static int construct(const std::shared_ptr &parent, const QString &binClipId, std::shared_ptr producer, int id = -1); + static int construct(const std::shared_ptr &parent, const QString &binClipId, std::shared_ptr producer, PlaylistState state); /* @brief returns a property of the clip, or from it's parent if it's a cut */ const QString getProperty(const QString &name) const override; int getIntProperty(const QString &name) const; double getDoubleProperty(const QString &name) const; QSize getFrameSize() const; Q_INVOKABLE bool showKeyframes() const; Q_INVOKABLE void setShowKeyframes(bool show); + /** @brief Returns the timeline clip status (video / audio only) */ - PlaylistState::ClipState clipState() const; + PlaylistState clipState() const; /** @brief Sets the timeline clip status (video / audio only) */ - bool setClipState(PlaylistState::ClipState state); + bool setClipState(PlaylistState state); /* @brief returns the length of the item on the timeline */ int getPlaytime() const override; /** @brief Returns audio cache data from bin clip to display audio thumbs */ QVariant getAudioWaveform(); /** @brief Returns the bin clip's id */ const QString &binId() const; void registerClipToBin(); void deregisterClipToBin(); bool addEffect(const QString &effectId); bool copyEffect(std::shared_ptr stackModel, int rowId); + /* @brief Import effects from a different stackModel */ bool importEffects(std::shared_ptr stackModel); + /* @brief Import effects from a service that contains some (another clip?) */ + bool importEffects(std::weak_ptr service); + bool removeFade(bool fromStart); /** @brief Adjust effects duration. Should be called after each resize / cut operation */ bool adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, Fun &undo, Fun &redo, bool logUndo); bool adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo); void passTimelineProperties(std::shared_ptr other); KeyframeModel *getKeyframeModel(); int fadeIn() const; int fadeOut() const; friend class TrackModel; friend class TimelineModel; friend class TimelineItemModel; friend class TimelineController; friend struct TimelineFunctions; protected: Mlt::Producer *service() const override; /* @brief Performs a resize of the given clip. Returns true if the operation succeeded, and otherwise nothing is modified This method is protected because it shouldn't be called directly. Call the function in the timeline instead. If a snap point is within reach, the operation will be coerced to use it. @param size is the new size of the clip @param right is true if we change the right side of the clip, false otherwise @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo = true) override; /* @brief This function change the global (timeline-wise) enabled state of the effects */ void setTimelineEffectsEnabled(bool enabled); /* @brief This functions should be called when the producer of the binClip changes, to allow refresh */ - void refreshProducerFromBin(PlaylistState::ClipState state = PlaylistState::Original); + void refreshProducerFromBin(PlaylistState state); + void refreshProducerFromBin(); /* @brief This functions replaces the current producer with a slowmotion one */ bool useTimewarpProducer(double speed, int extraSpace, Fun &undo, Fun &redo); Fun useTimewarpProducer_lambda(double speed, int extraSpace); /** @brief Returns the marker model associated with this clip */ std::shared_ptr getMarkerModel() const; - bool hasAudio() const; + bool audioEnabled() const; bool isAudioOnly() const; double getSpeed() const; protected: std::shared_ptr m_producer; std::shared_ptr m_effectStack; QString m_binClipId; // This is the Id of the bin clip this clip corresponds to. bool m_endlessResize; // Whether this clip can be freely resized bool forceThumbReload; // Used to trigger a forced thumb reload, when producer changes + + PlaylistState m_currentState; }; #endif diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp index 2b81beee9..2ba24fc94 100644 --- a/src/timeline2/model/timelinefunctions.cpp +++ b/src/timeline2/model/timelinefunctions.cpp @@ -1,551 +1,551 @@ /* Copyright (C) 2017 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 "timelinefunctions.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "timelineitemmodel.hpp" #include "trackmodel.hpp" #include #include -bool TimelineFunctions::copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo) +bool TimelineFunctions::copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState state, Fun &undo, Fun &redo) { // Special case: slowmotion clips double clipSpeed = timeline->m_allClips[clipId]->getSpeed(); bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, undo, redo); timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize; // Apply speed effect if necessary if (!qFuzzyCompare(clipSpeed, 1.0)) { timeline->m_allClips[newId]->useTimewarpProducer(clipSpeed, -1, undo, redo); } // copy useful timeline properties timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]); int duration = timeline->getClipPlaytime(clipId); int init_duration = timeline->getClipPlaytime(newId); if (duration != init_duration) { int in = timeline->m_allClips[clipId]->getIn(); res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo); res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo); } if (!res) { return false; } std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->importEffects(sourceStack); return res; } bool TimelineFunctions::requestMultipleClipsInsertion(std::shared_ptr timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; for (const QString &binId : binIds) { int clipId; if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, undo, redo)) { clipIds.append(clipId); position += timeline->getItemPlaytime(clipId); } else { undo(); clipIds.clear(); return false; } } if (logUndo) { pCore->pushUndo(undo, redo, i18n("Insert Clips")); } return true; } bool TimelineFunctions::processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo) { int trackId = timeline->getClipTrackId(clipId); int trackDuration = timeline->getTrackById_const(trackId)->trackDuration(); int start = timeline->getClipPosition(clipId); int duration = timeline->getClipPlaytime(clipId); if (start > position || (start + duration) < position) { return false; } - PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState(); + PlaylistState state = timeline->m_allClips[clipId]->clipState(); bool res = copyClip(timeline, clipId, newId, state, undo, redo); res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo); int newDuration = timeline->getClipPlaytime(clipId); res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo); // parse effects std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); sourceStack->cleanFadeEffects(true, undo, redo); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->cleanFadeEffects(false, undo, redo); // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration(); res = res && timeline->requestClipMove(newId, trackId, position, true, false, undo, redo); if (durationChanged) { // Track length changed, check project duration Fun updateDuration = [timeline]() { timeline->updateDuration(); return true; }; updateDuration(); PUSH_LAMBDA(updateDuration, redo); } return res; } bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Cut clip")); } return result; } bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo) { const std::unordered_set clips = timeline->getGroupElements(clipId); int count = 0; for (int cid : clips) { int start = timeline->getClipPosition(cid); int duration = timeline->getClipPlaytime(cid); if (start < position && (start + duration) > position) { count++; int newId; bool res = processClipCut(timeline, cid, position, newId, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } // splitted elements go temporarily in the same group as original ones. timeline->m_groups->setInGroupOf(newId, cid, undo, redo); } } if (count > 0 && timeline->m_groups->isInGroup(clipId)) { // we now split the group hiearchy. // As a splitting criterion, we compare start point with split position auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; }; int root = timeline->m_groups->getRootId(clipId); bool res = timeline->m_groups->split(root, criterion, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } return count > 0; } int TimelineFunctions::requestSpacerStartOperation(std::shared_ptr timeline, int trackId, int position) { std::unordered_set clips = timeline->getItemsAfterPosition(trackId, position, -1); if (clips.size() > 0) { timeline->requestClipsGroup(clips, false); return (*clips.cbegin()); } return -1; } bool TimelineFunctions::requestSpacerEndOperation(std::shared_ptr timeline, int clipId, int startPosition, int endPosition) { // Move group back to original position int track = timeline->getItemTrackId(clipId); timeline->requestClipMove(clipId, track, startPosition, false, false); std::unordered_set clips = timeline->getGroupElements(clipId); // break group timeline->requestClipUngroup(clipId, false); // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; int res = timeline->requestClipsGroup(clips, undo, redo); bool final = false; if (res > -1) { if (clips.size() > 1) { final = timeline->requestGroupMove(clipId, res, 0, endPosition - startPosition, true, true, undo, redo); } else { // only 1 clip to be moved final = timeline->requestClipMove(clipId, track, endPosition, true, true, undo, redo); } } if (final && clips.size() > 1) { final = timeline->requestClipUngroup(clipId, undo, redo); } if (final) { pCore->pushUndo(undo, redo, i18n("Insert space")); return true; } return false; } bool TimelineFunctions::extractZone(std::shared_ptr timeline, QVector tracks, QPoint zone, bool liftOnly) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = false; for (int trackId : tracks) { result = TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo); if (result && !liftOnly) { result = TimelineFunctions::removeSpace(timeline, trackId, zone, undo, redo); } } pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone")); return result; } bool TimelineFunctions::insertZone(std::shared_ptr timeline, int trackId, const QString &binId, int insertFrame, QPoint zone, bool overwrite) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = false; if (overwrite) { result = TimelineFunctions::liftZone(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); } else { // Cut all tracks auto it = timeline->m_allTracks.cbegin(); while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (timeline->getTrackById_const(target_track)->isLocked()) { ++it; continue; } int startClipId = timeline->getClipByPosition(target_track, insertFrame); if (startClipId > -1) { // There is a clip, cut it TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo); } ++it; } result = TimelineFunctions::insertSpace(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); } int newId = -1; QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1); timeline->requestClipInsertion(binClipId, trackId, insertFrame, newId, true, true, undo, redo); pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); return result; } bool TimelineFunctions::liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { // Check if there is a clip at start point int startClipId = timeline->getClipByPosition(trackId, zone.x()); if (startClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(startClipId) < zone.x()) { TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo); } } int endClipId = timeline->getClipByPosition(trackId, zone.y()); if (endClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) { TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo); } } std::unordered_set clips = timeline->getItemsAfterPosition(trackId, zone.x(), zone.y() - 1); for (const auto &clipId : clips) { timeline->requestItemDeletion(clipId, undo, redo); } return true; } bool TimelineFunctions::removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsAfterPosition(-1, zone.y() - 1, -1, true); bool result = false; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { int res = timeline->requestClipsGroup(clips, undo, redo); if (res > -1) { result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo); if (result) { result = timeline->requestClipUngroup(clipId, undo, redo); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, undo, redo); } } return result; } bool TimelineFunctions::insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsAfterPosition(-1, zone.x(), -1, true); bool result = false; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { int res = timeline->requestClipsGroup(clips, undo, redo); if (res > -1) { result = timeline->requestGroupMove(clipId, res, 0, zone.y() - zone.x(), true, true, undo, redo); if (result) { result = timeline->requestClipUngroup(clipId, undo, redo); } else { pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart + (zone.y() - zone.x()), true, true, undo, redo); } } return result; } bool TimelineFunctions::requestItemCopy(std::shared_ptr timeline, int clipId, int trackId, int position) { Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId)); int deltaPos = position - timeline->getItemPosition(clipId); std::unordered_set allIds = timeline->getGroupElements(clipId); std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips bool res = true; for (int id : allIds) { int newId = -1; if (timeline->isClip(id)) { - PlaylistState::ClipState state = timeline->m_allClips[id]->clipState(); + PlaylistState state = timeline->m_allClips[id]->clipState(); res = copyClip(timeline, id, newId, state, undo, redo); res = res && (newId != -1); } int target_position = timeline->getItemPosition(id) + deltaPos; int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack; if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) { auto it = timeline->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); if (timeline->isClip(id)) { res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo); } else { const QString &transitionId = timeline->m_allCompositions[id]->getAssetId(); QScopedPointer transProps(timeline->m_allCompositions[id]->properties()); res = res & timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position, timeline->m_allCompositions[id]->getPlaytime(), transProps.data(), newId, undo, redo); } } else { res = false; } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } mapping[id] = newId; } qDebug() << "Sucessful copy, coping groups..."; res = timeline->m_groups->copyGroups(mapping, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } return true; } void TimelineFunctions::showClipKeyframes(std::shared_ptr timeline, int clipId, bool value) { timeline->m_allClips[clipId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::KeyframesRole}); } void TimelineFunctions::showCompositionKeyframes(std::shared_ptr timeline, int compoId, bool value) { timeline->m_allCompositions[compoId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::KeyframesRole}); } -bool TimelineFunctions::changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status) +bool TimelineFunctions::changeClipState(std::shared_ptr timeline, int clipId, PlaylistState status) { - PlaylistState::ClipState oldState = timeline->m_allClips[clipId]->clipState(); + PlaylistState oldState = timeline->m_allClips[clipId]->clipState(); if (oldState == status) { return true; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = changeClipState(timeline, clipId, status, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Change clip state")); } return result; } -bool TimelineFunctions::changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo) +bool TimelineFunctions::changeClipState(std::shared_ptr timeline, int clipId, PlaylistState status, Fun &undo, Fun &redo) { - PlaylistState::ClipState oldState = timeline->m_allClips[clipId]->clipState(); + PlaylistState oldState = timeline->m_allClips[clipId]->clipState(); if (oldState == status) { return true; } Fun operation = [timeline, clipId, status]() { int trackId = timeline->getClipTrackId(clipId); bool res = timeline->m_allClips[clipId]->setClipState(status); // in order to make the producer change effective, we need to unplant / replant the clip in int track if (res && trackId != -1) { timeline->getTrackById(trackId)->replugClip(clipId); QModelIndex ix = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(ix, ix, {TimelineModel::StatusRole}); int start = timeline->getItemPosition(clipId); int end = start + timeline->getItemPlaytime(clipId); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); } return res; }; Fun reverse = [timeline, clipId, oldState]() { bool res = timeline->m_allClips[clipId]->setClipState(oldState); // in order to make the producer change effective, we need to unplant / replant the clip in int track int trackId = timeline->getClipTrackId(clipId); if (res && trackId != -1) { int start = timeline->getItemPosition(clipId); int end = start + timeline->getItemPlaytime(clipId); timeline->getTrackById(trackId)->replugClip(clipId); QModelIndex ix = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(ix, ix, {TimelineModel::StatusRole}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); } return res; }; bool result = reverse(); if (result) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return result; } bool TimelineFunctions::requestSplitAudio(std::shared_ptr timeline, int clipId, int audioTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups pCore->clearSelection(); for (int cid : clips) { - if (!timeline->getClipPtr(cid)->hasAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) { + if (!timeline->getClipPtr(cid)->audioEnabled() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) { // clip without audio or audio only, skip continue; } int position = timeline->getClipPosition(cid); int track = timeline->getClipTrackId(cid); QList possibleTracks = audioTarget >= 0 ? QList() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack); if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage); return false; } int newId; bool res = copyClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo); } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo); success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } done = true; } if (done) { pCore->pushUndo(undo, redo, i18n("Split Audio")); } return done; } void TimelineFunctions::setCompositionATrack(std::shared_ptr timeline, int cid, int aTrack) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; std::shared_ptr compo = timeline->getCompositionPtr(cid); int previousATrack = compo->getATrack(); int previousAutoTrack = compo->getForcedTrack() == -1; bool autoTrack = aTrack < 0; if (autoTrack) { // Automatic track compositing, find lower video track aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId()); } int start = timeline->getItemPosition(cid); int end = start + timeline->getItemPlaytime(cid); Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack); timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!previousAutoTrack); timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; if (local_redo()) { PUSH_LAMBDA(local_undo, undo); PUSH_LAMBDA(local_redo, redo); } pCore->pushUndo(undo, redo, i18n("Change Composition Track")); } diff --git a/src/timeline2/model/timelinefunctions.hpp b/src/timeline2/model/timelinefunctions.hpp index 21fe11bd5..5efd9723b 100644 --- a/src/timeline2/model/timelinefunctions.hpp +++ b/src/timeline2/model/timelinefunctions.hpp @@ -1,92 +1,92 @@ /* Copyright (C) 2017 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 . */ #ifndef TIMELINEFUNCTIONS_H #define TIMELINEFUNCTIONS_H #include "definitions.h" #include "undohelper.hpp" #include #include /** * @namespace TimelineFunction * @brief This namespace contains a list of static methods for advanced timeline editing features * based on timelinemodel methods */ class TimelineItemModel; struct TimelineFunctions { /* @brief Cuts a clip at given position If the clip is part of the group, all clips of the groups are cut at the same position. The group structure is then preserved for clips on both sides Returns true on success @param timeline : ptr to the timeline model @param clipId: Id of the clip to split @param position: position (in frames) where to cut */ static bool requestClipCut(std::shared_ptr timeline, int clipId, int position); /* This is the same function, except that it accumulates undo/redo */ static bool requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo); /* This is the same function, except that it accumulates undo/redo and do not deal with groups. Do not call directly */ static bool processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo); /* @brief Makes a perfect copy of a given clip, but do not insert it */ - static bool copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo); + static bool copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState state, Fun &undo, Fun &redo); /* @brief Request the addition of multiple clips to the timeline * If the addition of any of the clips fails, the entire operation is undone. * @returns true on success, false otherwise. * @param binIds the list of bin ids to be inserted * @param trackId the track where the insertion should happen * @param position the position at which the clips should be inserted * @param clipIds a return parameter with the ids assigned to the clips if success, empty otherwise */ static bool requestMultipleClipsInsertion(std::shared_ptr timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView); static int requestSpacerStartOperation(std::shared_ptr timeline, int trackId, int position); static bool requestSpacerEndOperation(std::shared_ptr timeline, int clipId, int startPosition, int endPosition); static bool extractZone(std::shared_ptr timeline, QVector tracks, QPoint zone, bool liftOnly); static bool liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); static bool removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); static bool insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); static bool insertZone(std::shared_ptr timeline, int trackId, const QString &binId, int insertFrame, QPoint zone, bool overwrite); static bool requestItemCopy(std::shared_ptr timeline, int clipId, int trackId, int position); static void showClipKeyframes(std::shared_ptr timeline, int clipId, bool value); static void showCompositionKeyframes(std::shared_ptr timeline, int compoId, bool value); /* @brief Change the status of the clip to Video+audio, video only, or audio only * @param timeline: pointer to the timeline that we modify * @param clipId: Id of the clip to modify * @param status: target status of the clip This function creates an undo object and returns true on success */ - static bool changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status); + static bool changeClipState(std::shared_ptr timeline, int clipId, PlaylistState status); /* @brief Same function as above, but accumulates for undo/redo */ - static bool changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo); + static bool changeClipState(std::shared_ptr timeline, int clipId, PlaylistState status, Fun &undo, Fun &redo); static bool requestSplitAudio(std::shared_ptr timeline, int clipId, int audioTarget); static void setCompositionATrack(std::shared_ptr timeline, int cid, int aTrack); }; #endif diff --git a/src/timeline2/model/timelineitemmodel.cpp b/src/timeline2/model/timelineitemmodel.cpp index d395ac25e..6c8ccf0f3 100644 --- a/src/timeline2/model/timelineitemmodel.cpp +++ b/src/timeline2/model/timelineitemmodel.cpp @@ -1,496 +1,496 @@ /*************************************************************************** * 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 "timelineitemmodel.hpp" #include "assets/keyframes/model/keyframemodel.hpp" #include "bin/model/markerlistmodel.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include #include TimelineItemModel::TimelineItemModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : TimelineModel(profile, undo_stack) { } void TimelineItemModel::finishConstruct(std::shared_ptr ptr, std::shared_ptr guideModel) { ptr->weak_this_ = ptr; ptr->m_groups = std::unique_ptr(new GroupsModel(ptr)); guideModel->registerSnapModel(ptr->m_snaps); } std::shared_ptr TimelineItemModel::construct(Mlt::Profile *profile, std::shared_ptr guideModel, std::weak_ptr undo_stack) { std::shared_ptr ptr(new TimelineItemModel(profile, std::move(undo_stack))); finishConstruct(ptr, std::move(guideModel)); return ptr; } TimelineItemModel::~TimelineItemModel() = default; QModelIndex TimelineItemModel::index(int row, int column, const QModelIndex &parent) const { READ_LOCK(); QModelIndex result; if (parent.isValid()) { auto trackId = int(parent.internalId()); Q_ASSERT(isTrack(trackId)); int clipId = getTrackById_const(trackId)->getClipByRow(row); if (clipId != -1) { result = createIndex(row, 0, quintptr(clipId)); } else { int compoId = getTrackById_const(trackId)->getCompositionByRow(row); if (compoId != -1) { result = createIndex(row, 0, quintptr(compoId)); } } } else if (row < getTracksCount() && row >= 0) { auto it = m_allTracks.cbegin(); std::advance(it, row); int trackId = (*it)->getId(); result = createIndex(row, column, quintptr(trackId)); } return result; } /*QModelIndex TimelineItemModel::makeIndex(int trackIndex, int clipIndex) const { return index(clipIndex, 0, index(trackIndex)); }*/ QModelIndex TimelineItemModel::makeClipIndexFromID(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = m_allClips.at(clipId)->getCurrentTrackId(); if (trackId == -1) { // Clip is not inserted in a track return QModelIndex(); } int row = getTrackById_const(trackId)->getRowfromClip(clipId); return index(row, 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeCompositionIndexFromID(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); int trackId = m_allCompositions.at(compoId)->getCurrentTrackId(); return index(getTrackById_const(trackId)->getRowfromComposition(compoId), 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeTrackIndexFromID(int trackId) const { // we retrieve iterator Q_ASSERT(m_iteratorTable.count(trackId) > 0); auto it = m_iteratorTable.at(trackId); int ind = (int)std::distance(m_allTracks.begin(), it); return index(ind); } QModelIndex TimelineItemModel::parent(const QModelIndex &index) const { READ_LOCK(); // qDebug() << "TimelineItemModel::parent"<< index; if (index == QModelIndex()) { return index; } const int id = static_cast(index.internalId()); if (!index.isValid() || isTrack(id)) { return QModelIndex(); } if (isClip(id)) { const int trackId = getClipTrackId(id); return makeTrackIndexFromID(trackId); } if (isComposition(id)) { const int trackId = getCompositionTrackId(id); return makeTrackIndexFromID(trackId); } return QModelIndex(); } int TimelineItemModel::rowCount(const QModelIndex &parent) const { READ_LOCK(); if (parent.isValid()) { const int id = (int)parent.internalId(); if (!isTrack(id)) { // clips don't have children // if it is not a track, it is something invalid return 0; } return getTrackClipsCount(id) + getTrackCompositionsCount(id); } return getTracksCount(); } int TimelineItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 1; } QHash TimelineItemModel::roleNames() const { QHash roles; roles[NameRole] = "name"; roles[ResourceRole] = "resource"; roles[ServiceRole] = "mlt_service"; roles[BinIdRole] = "binId"; roles[IsBlankRole] = "blank"; roles[StartRole] = "start"; roles[DurationRole] = "duration"; roles[MarkersRole] = "markers"; roles[KeyframesRole] = "keyframeModel"; roles[ShowKeyframesRole] = "showKeyframes"; roles[StatusRole] = "clipStatus"; roles[InPointRole] = "in"; roles[OutPointRole] = "out"; roles[FramerateRole] = "fps"; roles[GroupedRole] = "grouped"; roles[GroupDragRole] = "groupDrag"; roles[IsMuteRole] = "mute"; roles[IsHiddenRole] = "hidden"; roles[IsAudioRole] = "audio"; roles[AudioLevelsRole] = "audioLevels"; roles[IsCompositeRole] = "composite"; roles[IsLockedRole] = "locked"; roles[FadeInRole] = "fadeIn"; roles[FadeOutRole] = "fadeOut"; roles[IsCompositionRole] = "isComposition"; roles[FileHashRole] = "hash"; roles[SpeedRole] = "speed"; roles[HeightRole] = "trackHeight"; roles[TrackTagRole] = "trackTag"; roles[ItemIdRole] = "item"; roles[ItemATrack] = "a_track"; roles[HasAudio] = "hasAudio"; roles[ReloadThumbRole] = "reloadThumb"; return roles; } QVariant TimelineItemModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (!m_tractor || !index.isValid()) { // qDebug() << "DATA abort. Index validity="< clip = m_allClips.at(id); // Get data for a clip switch (role) { // TODO case NameRole: case Qt::DisplayRole: { QString result = clip->getProperty("kdenlive:clipname"); if (result.isEmpty()) { result = clip->getProperty("resource"); if (!result.isEmpty()) { result = QFileInfo(result).fileName(); } else { result = clip->getProperty("mlt_service"); } } return result; } case ResourceRole: { QString result = clip->getProperty("resource"); if (result == QLatin1String("")) { result = clip->getProperty("mlt_service"); } return result; } case GroupDragRole: return clip->isInGroupDrag; case BinIdRole: return clip->binId(); case ServiceRole: return clip->getProperty("mlt_service"); break; case AudioLevelsRole: return clip->getAudioWaveform(); case HasAudio: - return clip->hasAudio(); + return clip->audioEnabled(); case IsAudioRole: return clip->isAudioOnly(); case MarkersRole: { return QVariant::fromValue(clip->getMarkerModel().get()); } case KeyframesRole: { return QVariant::fromValue(clip->getKeyframeModel()); } case StatusRole: - return clip->clipState(); + return QVariant::fromValue(clip->clipState()); case StartRole: return clip->getPosition(); case DurationRole: return clip->getPlaytime(); case GroupedRole: return (m_groups->isInGroup(id) && !isInSelection(id)); case InPointRole: return clip->getIn(); case OutPointRole: return clip->getOut(); case IsCompositionRole: return false; case ShowKeyframesRole: return clip->showKeyframes(); case FadeInRole: return clip->fadeIn(); case FadeOutRole: return clip->fadeOut(); case ReloadThumbRole: return clip->forceThumbReload; case SpeedRole: return clip->getSpeed(); default: break; } } else if (isTrack(id)) { // qDebug() << "DATA REQUESTED FOR TRACK "<< id; switch (role) { case NameRole: case Qt::DisplayRole: { QString tName = getTrackById_const(id)->getProperty("kdenlive:track_name").toString(); return tName; } case DurationRole: // qDebug() << "DATA yielding duration" << m_tractor->get_playtime(); return m_tractor->get_playtime(); case IsMuteRole: // qDebug() << "DATA yielding mute" << 0; return getTrackById_const(id)->getProperty("hide").toInt() & 2; case IsHiddenRole: return getTrackById_const(id)->getProperty("hide").toInt() & 1; case IsAudioRole: return getTrackById_const(id)->getProperty("kdenlive:audio_track").toInt() == 1; case TrackTagRole: return getTrackTagById(id); case IsLockedRole: return getTrackById_const(id)->getProperty("kdenlive:locked_track").toInt() == 1; case HeightRole: { int height = getTrackById_const(id)->getProperty("kdenlive:trackheight").toInt(); // qDebug() << "DATA yielding height" << height; return (height > 0 ? height : 60); } case IsCompositeRole: { return Qt::Unchecked; } default: break; } } else if (isComposition(id)) { std::shared_ptr compo = m_allCompositions.at(id); switch (role) { case NameRole: case Qt::DisplayRole: case ResourceRole: case ServiceRole: return compo->displayName(); break; case IsBlankRole: // probably useless return false; case StartRole: return compo->getPosition(); case DurationRole: return compo->getPlaytime(); case GroupedRole: return m_groups->isInGroup(id); case GroupDragRole: return compo->isInGroupDrag; case InPointRole: return 0; case OutPointRole: return 100; case BinIdRole: return 5; case KeyframesRole: { return QVariant::fromValue(compo->getEffectKeyframeModel()); } case ShowKeyframesRole: return compo->showKeyframes(); case ItemATrack: return compo->getForcedTrack(); case MarkersRole: { QVariantList markersList; return markersList; } case IsCompositionRole: return true; default: break; } } else { qDebug() << "UNKNOWN DATA requested " << index << roleNames()[role]; } return QVariant(); } void TimelineItemModel::setTrackProperty(int trackId, const QString &name, const QString &value) { getTrackById(trackId)->setProperty(name, value); QVector roles; if (name == QLatin1String("kdenlive:track_name")) { roles.push_back(NameRole); } else if (name == QLatin1String("kdenlive:locked_track")) { roles.push_back(IsLockedRole); } else if (name == QLatin1String("hide")) { roles.push_back(IsMuteRole); roles.push_back(IsHiddenRole); } if (!roles.isEmpty()) { QModelIndex ix = makeTrackIndexFromID(trackId); emit dataChanged(ix, ix, roles); } } QVariant TimelineItemModel::getTrackProperty(int tid, const QString &name) { return getTrackById(tid)->getProperty(name); } void TimelineItemModel::buildTrackCompositing() { auto it = m_allTracks.cbegin(); QScopedPointer field(m_tractor->field()); field->lock(); QString composite = TransitionsRepository::get()->getCompositingTransition(); while (it != m_allTracks.cend()) { int trackId = getTrackMltIndex((*it)->getId()); if (!composite.isEmpty() && (*it)->getProperty("kdenlive:audio_track").toInt() != 1) { // video track, add composition Mlt::Transition *transition = TransitionsRepository::get()->getTransition(composite); transition->set("internal_added", 237); transition->set("always_active", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); } // audio mix Mlt::Transition *transition = TransitionsRepository::get()->getTransition(QStringLiteral("mix")); transition->set("internal_added", 237); transition->set("always_active", 1); transition->set("sum", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); ++it; } field->unlock(); if (composite.isEmpty()) { pCore->displayMessage(i18n("Could not setup track compositing, check your install"), MessageType::ErrorMessage); } } const QString TimelineItemModel::groupsData() { return m_groups->toJson(); } bool TimelineItemModel::loadGroups(const QString &groupsData) { return m_groups->fromJson(groupsData); } bool TimelineItemModel::isInSelection(int cid) const { if (m_temporarySelectionGroup == -1 || !m_groups->isInGroup(cid)) { return false; } bool res = (m_groups->getRootId(cid) == m_temporarySelectionGroup); return res; } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) { QVector roles; if (start) { roles.push_back(TimelineModel::StartRole); if (updateThumb) { roles.push_back(TimelineModel::InPointRole); } } if (duration) { roles.push_back(TimelineModel::DurationRole); if (updateThumb) { roles.push_back(TimelineModel::OutPointRole); } } emit dataChanged(topleft, bottomright, roles); } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) { emit dataChanged(topleft, bottomright, roles); } void TimelineItemModel::_beginRemoveRows(const QModelIndex &i, int j, int k) { // qDebug()<<"FORWARDING beginRemoveRows"<. * ***************************************************************************/ #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 "groupsmodel.hpp" #include "kdenlivesettings.h" #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" 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) { // 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); #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::getClipIn(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getIn(); return pos; } 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; } 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; } int audioTrack = (*it)->getProperty("kdenlive:audio_track").toInt(); if (type == TrackType::AudioTrack && audioTrack == 1) { results << (*it)->getId(); } else if (type == TrackType::VideoTrack && audioTrack == 0) { results << (*it)->getId(); } } return results; } 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)->getProperty("kdenlive:audio_track").toInt() == 0) { break; } } return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId()); } 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 (getTrackById_const(trackId)->isLocked()) { return false; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool ok = true; int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { if (getTrackById_const(old_trackId)->isLocked()) { return false; } ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, invalidateTimeline, local_undo, local_redo); if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } ok = getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, local_undo, local_redo); if (!ok) { // qDebug()<<"-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------"; bool undone = local_undo(); Q_ASSERT(undone); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } 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); 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 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")); } return res; } int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int snapDistance) { #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); if (currentPos == position) { 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_clips = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_clips = m_groups->getLeaves(groupId); } for (int current_clipId : all_clips) { 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(), ignored_pts, snapDistance); // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = requestClipMove(clipId, trackId, position, false, false, false); // bool possible = requestClipMove(clipId, trackId, position, false, false, undo, redo); if (possible) { return position; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Easy 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 false; } possible = requestClipMove(clipId, trackId, position, false, false, false); return possible ? position : currentPos; } // find best pos for groups int groupId = m_groups->getRootId(clipId); std::unordered_set all_clips = 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_clips) { int clipTrack = getClipTrackId(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); possible = requestClipMove(clipId, trackId, updatedPos, false, false, false); if (possible) { return updatedPos; } } return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, 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_clips = m_groups->getLeaves(groupId); for (int current_compoId : all_clips) { // 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, snapDistance); qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool possible = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, false, undo, redo); qDebug() << "Original move success" << possible; if (possible) { bool undone = undo(); Q_ASSERT(undone); 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; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView) { #ifdef LOGGING m_logFile << "timeline->requestClipInsertion(" << binClipId.toStdString() << "," << trackId << " ," << position << ", dummy_id );" << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Clip")); } return result; } -bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, Fun &undo, Fun &redo) +bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState state, Fun &undo, Fun &redo) { int clipId = TimelineModel::getNextId(); id = clipId; Fun local_undo = deregisterClip_lambda(clipId); QString bid = binClipId; if (binClipId.contains(QLatin1Char('/'))) { bid = binClipId.section(QLatin1Char('/'), 0, 0); } if (!pCore->projectItemModel()->hasClip(bid)) { return false; } ClipModel::construct(shared_from_this(), bid, clipId, state); auto clip = m_allClips[clipId]; Fun local_redo = [clip, this, state]() { // 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); 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 = 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, Fun &undo, Fun &redo) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool res = false; if (getTrackById_const(trackId)->isLocked()) { return false; } ClipType type = ClipType::Unknown; QString bid = binClipId.section(QLatin1Char('/'), 0, 0); if (!pCore->projectItemModel()->hasClip(bid)) { return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); type = master->clipType(); if (type == ClipType::AV) { if (m_audioTarget >= 0 && m_videoTarget == -1) { // 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, audioDrop ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); if (m_videoTarget >= 0 && m_audioTarget == -1) { // No audio target defined, only extract video audioDrop = true; } else if (m_audioTarget >= 0) { if (getTrackById_const(m_audioTarget)->isLocked()) { // Audio target locked, only extract video audioDrop = true; } } if (res && logUndo && !audioDrop) { QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack); if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort pCore->displayMessage(i18n("No available audio 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, PlaylistState::AudioOnly, 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 { - res = requestClipCreation(binClipId, id, PlaylistState::Original, local_undo, local_redo); + std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId); + res = requestClipCreation(binClipId, id, binClip->defaultState(), 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); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (m_groups->isInGroup(itemId)) { return requestGroupDeletion(itemId, logUndo); } 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); } 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); return true; }; if (operation()) { 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, 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()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } std::unordered_set TimelineModel::getItemsAfterPosition(int trackId, int position, int end, bool listCompositions) { Q_UNUSED(listCompositions) std::unordered_set allClips; auto it = m_allTracks.cbegin(); if (trackId == -1) { while (it != m_allTracks.cend()) { std::unordered_set clipTracks = (*it)->getClipsAfterPosition(position, end); allClips.insert(clipTracks.begin(), clipTracks.end()); ++it; } } else { int target_track_position = getTrackPosition(trackId); std::advance(it, target_track_position); std::unordered_set clipTracks = (*it)->getClipsAfterPosition(position, end); allClips.insert(clipTracks.begin(), clipTracks.end()); } return allClips; } bool TimelineModel::requestGroupMove(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 = requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } return res; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo) { Q_UNUSED(clipId) #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_clips = m_groups->getLeaves(groupId); std::vector sorted_clips(all_clips.begin(), all_clips.end()); // we have to sort clip in an order that allows to do the move without self conflicts // If we move up, we move first the clips on the upper tracks (and conversely). // If we move left, we move first the leftmost clips (and conversely). std::sort(sorted_clips.begin(), sorted_clips.end(), [delta_track, delta_pos, this](int clipId1, int clipId2) { int trackId1 = getItemTrackId(clipId1); int trackId2 = getItemTrackId(clipId2); int track_pos1 = getTrackPosition(trackId1); int track_pos2 = getTrackPosition(trackId2); if (trackId1 == trackId2) { 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 !(p1 <= p2) == !(delta_pos <= 0); } return !(track_pos1 <= track_pos2) == !(delta_track <= 0); }); // Parse all tracks then check none is locked. Maybe find a better way/place to do this QList trackList; for (int clip : sorted_clips) { int current_track_id = getItemTrackId(clip); if (!trackList.contains(current_track_id)) { trackList << current_track_id; } } // Check that the group is not on a locked track for (int tid : trackList) { if (getTrackById_const(tid)->isLocked()) { return false; } } for (int clip : sorted_clips) { int current_track_id = getItemTrackId(clip); int current_track_position = getTrackPosition(current_track_id); int target_track_position = current_track_position + delta_track; 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(); if (isClip(clip)) { int target_position = m_allClips[clip]->getPosition() + delta_pos; ok = requestClipMove(clip, target_track, target_position, updateView, finalMove, undo, redo); } else { int target_position = m_allCompositions[clip]->getPosition() + delta_pos; ok = requestCompositionMove(clip, target_track, m_allCompositions[clip]->getForcedTrack(), target_position, updateView, undo, redo); } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestGroupDeletion(" << clipId << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestGroupDeletion(clipId, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Remove group")); } 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_clips; 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_clips.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_clips) { 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); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (size <= 0) 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)) { qDebug()<<"+++MODEL REQUEST RESIZE (LOGUNDO) "<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); 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; } 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) { 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); } 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); 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")); } 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); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipUngroup(id, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } if (id == m_temporarySelectionGroup) { m_temporarySelectionGroup = -1; } 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); 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")); } 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); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackDeletion(trackId, undo, redo); if (result) { PUSH_UNDO(undo, redo, i18n("Delete Track")); } 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) { 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(); 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); return (*m_iteratorTable.at(trackId))->addEffect(effectId); } 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) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->addEffect(effectId); } 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(); } 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 snapDistance) { if (!pts.empty()) { m_snaps->ignore(pts); } int snapped_start = m_snaps->getClosestPoint(pos); int snapped_end = m_snaps->getClosestPoint(pos + length); m_snaps->unIgnore(); 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); 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) { 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, local_undo, local_redo); qDebug() << "trying to move" << trackId << "pos" << position << "succes " << res; if (res) { res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); qDebug() << "trying to resize" << compositionId << "length" << length << "succes " << 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 compoId) const { return getTrackById_const(compoId)->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, 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::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, 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); 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) { UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, 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, 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) { UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } 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); QVector roles; roles.push_back(ItemATrack); notifyChange(modelIndex, modelIndex, roles); } 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() << "Constistency 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; } } // Check snaps auto stored_snaps = m_snaps->_snaps(); if (snaps.size() != stored_snaps.size()) { qDebug() << "Wrong number of snaps"; 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 && getTrackMltIndex(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; } return true; } bool TimelineModel::requestItemResizeToPos(int itemId, int position, bool right) { QWriteLocker locker(&m_lock); Q_ASSERT(isClip(itemId) || isComposition(itemId)); int in, out; if (isClip(itemId)) { in = getClipPosition(itemId); out = in + getClipPlaytime(itemId) - 1; } else { in = getCompositionPosition(itemId); out = in + getCompositionPlaytime(itemId) - 1; } int size = 0; if (!right) { if (position < out) { size = out - position; } } else { if (position > in) { size = position - in; } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestItemResize(itemId, size, right, true, undo, redo); if (result) { if (isClip(itemId)) { PUSH_UNDO(undo, redo, i18n("Resize clip")); } else { PUSH_UNDO(undo, redo, i18n("Resize composition")); } } return result; } 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) { //pCore->adjustAssetRange(clipId, in, out); } void TimelineModel::requestClipReload(int clipId) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; m_allClips[clipId]->refreshProducerFromBin(); // 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); if (old_trackId != -1) { getTrackById(old_trackId)->requestClipDeletion(clipId, false, true, local_undo, local_redo); getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, true, 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, int trackId, int blankSpace, double speed, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); 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; success = getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo); if (success) { success = m_allClips[clipId]->useTimewarpProducer(speed, blankSpace, local_undo, local_redo); } if (success) { success = getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); if (!success) { local_undo(); return false; } } PUSH_LAMBDA(local_undo, undo); PUSH_LAMBDA(local_redo, redo); return success; } bool TimelineModel::changeItemSpeed(int clipId, int speed) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Get main clip info int trackId = getClipTrackId(clipId); int splitId = -1; // Check if clip has a split partner bool result = true; if (trackId != -1) { int blankSpace = getTrackById(trackId)->getBlankSizeNearClip(clipId, true) + getClipPlaytime(clipId) - 1; splitId = m_groups->getSplitPartner(clipId); bool success = true; if (splitId > -1) { int split_trackId = getClipTrackId(splitId); blankSpace = qMin(blankSpace, getTrackById(split_trackId)->getBlankSizeNearClip(splitId, true)); result = requestClipTimeWarp(splitId, split_trackId, blankSpace, speed / 100.0, undo, redo); } if (result) { result = requestClipTimeWarp(clipId, trackId, blankSpace, speed / 100.0, undo, redo); } } else { m_allClips[clipId]->useTimewarpProducer(speed, -1, undo, redo); } if (result) { QVector roles; roles.push_back(SpeedRole); Fun update_model = [clipId, roles, this]() { QModelIndex modelIndex = makeClipIndexFromID(clipId); notifyChange(modelIndex, modelIndex, roles); return true; }; PUSH_LAMBDA(update_model, undo); PUSH_LAMBDA(update_model, redo); if (splitId > -1) { Fun update_model2 = [splitId, roles, this]() { QModelIndex modelIndex = makeClipIndexFromID(splitId); notifyChange(modelIndex, modelIndex, roles); return true; }; PUSH_LAMBDA(update_model2, undo); PUSH_LAMBDA(update_model2, redo); } PUSH_UNDO(undo, redo, i18n("Change clip speed")); return true; } return false; } const QString TimelineModel::getTrackTagById(int trackId) const { 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); } diff --git a/src/timeline2/model/timelinemodel.hpp b/src/timeline2/model/timelinemodel.hpp index 50c8bf5c1..b7c0bb5fb 100644 --- a/src/timeline2/model/timelinemodel.hpp +++ b/src/timeline2/model/timelinemodel.hpp @@ -1,666 +1,666 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef TIMELINEMODEL_H #define TIMELINEMODEL_H #include "definitions.h" #include "undohelper.hpp" #include #include #include #include #include #include #include #include //#define LOGGING 1 // If set to 1, we log the actions requested to the timeline as a reproducer script #ifdef LOGGING #include #endif class AssetParameterModel; class EffectStackModel; class ClipModel; class CompositionModel; class DocUndoStack; class GroupsModel; class SnapModel; class TimelineItemModel; class TrackModel; /* @brief This class represents a Timeline object, as viewed by the backend. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications. This class also serves to keep track of all objects. It holds pointers to all tracks and clips, and gives them unique IDs on creation. These Ids are used in any interactions with the objects and have nothing to do with Melt IDs. This is the entry point for any modifications that has to be made on an element. The dataflow beyond this entry point may vary, for example when the user request a clip resize, the call is deferred to the clip itself, that check if there is enough data to extend by the requested amount, compute the new in and out, and then asks the track if there is enough room for extension. To avoid any confusion on which function to call first, rembember to always call the version in timeline. This is also required to generate the Undo/Redo operators Generally speaking, we don't check ahead of time if an action is going to succeed or not before applying it. We just apply it naively, and if it fails at some point, we use the undo operator that we are constructing on the fly to revert what we have done so far. For example, when we move a group of clips, we apply the move operation to all the clips inside this group (in the right order). If none fails, we are good, otherwise we revert what we've already done. This kind of behaviour frees us from the burden of simulating the actions before actually applying theme. This is a good thing because this simulation step would be very sensitive to corruptions and small discrepancies, which we try to avoid at all cost. It derives from AbstractItemModel (indirectly through TimelineItemModel) to provide the model to the QML interface. An itemModel is organized with row and columns that contain the data. It can be hierarchical, meaning that a given index (row,column) can contain another level of rows and column. Our organization is as follows: at the top level, each row contains a track. These rows are in the same order as in the actual timeline. Then each of this row contains itself sub-rows that correspond to the clips. Here the order of these sub-rows is unrelated to the chronological order of the clips, but correspond to their Id order. For example, if you have three clips, with ids 12, 45 and 150, they will receive row index 0,1 and 2. This is because the order actually doesn't matter since the clips are rendered based on their positions rather than their row order. The id order has been choosed because it is consistant with a valid ordering of the clips. The columns are never used, so the data is always in column 0 An ModelIndex in the ItemModel consists of a row number, a column number, and a parent index. In our case, tracks have always an empty parent, and the clip have a track index as parent. A ModelIndex can also store one additional integer, and we exploit this feature to store the unique ID of the object it corresponds to. */ class TimelineModel : public QAbstractItemModel_shared_from_this { Q_OBJECT protected: /* @brief this constructor should not be called. Call the static construct instead */ TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack); public: friend class TrackModel; template friend class MoveableItem; friend class ClipModel; friend class CompositionModel; friend class GroupsModel; friend class TimelineController; friend struct TimelineFunctions; /// Two level model: tracks and clips on track enum { NameRole = Qt::UserRole + 1, ResourceRole, /// clip only ServiceRole, /// clip only IsBlankRole, /// clip only StartRole, /// clip only BinIdRole, /// clip only MarkersRole, /// clip only StatusRole, /// clip only GroupDragRole, /// indicates if the clip is in current timeline selection, needed for group drag KeyframesRole, DurationRole, InPointRole, /// clip only OutPointRole, /// clip only FramerateRole, /// clip only GroupedRole, /// clip only HasAudio, /// clip only IsMuteRole, /// track only IsHiddenRole, /// track only IsAudioRole, SortRole, ShowKeyframesRole, AudioLevelsRole, /// clip only IsCompositeRole, /// track only IsLockedRole, /// track only HeightRole, /// track only TrackTagRole, /// track only FadeInRole, /// clip only FadeOutRole, /// clip only IsCompositionRole, /// clip only FileHashRole, /// clip only SpeedRole, /// clip only ReloadThumbRole, /// clip only ItemATrack, /// composition only ItemIdRole }; virtual ~TimelineModel(); Mlt::Tractor *tractor() const { return m_tractor.get(); } /* @brief Load tracks from the current tractor, used on project opening */ void loadTractor(); /* @brief Returns the current tractor's producer, useful fo control seeking, playing, etc */ Mlt::Producer *producer(); Mlt::Profile *getProfile(); /* @brief returns the number of tracks */ int getTracksCount() const; /* @brief returns the track index (id) from its position */ int getTrackIndexFromPosition(int pos) const; /* @brief returns the number of clips */ int getClipsCount() const; /* @brief returns the number of compositions */ int getCompositionsCount() const; /* @brief Returns the id of the track containing clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipTrackId(int clipId) const; /* @brief Returns the id of the track containing composition (-1 if it is not inserted) @param clipId Id of the composition to test */ Q_INVOKABLE int getCompositionTrackId(int compoId) const; /* @brief Convenience function that calls either of the previous ones based on item type*/ int getItemTrackId(int itemId) const; /* @brief Helper function that returns true if the given ID corresponds to a clip */ bool isClip(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a composition */ bool isComposition(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a track */ bool isTrack(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a track */ bool isGroup(int id) const; /* @brief Given a composition Id, returns its underlying parameter model */ std::shared_ptr getCompositionParameterModel(int compoId) const; /* @brief Given a clip Id, returns its underlying effect stack model */ std::shared_ptr getClipEffectStackModel(int clipId) const; /* @brief Returns the position of clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipPosition(int clipId) const; Q_INVOKABLE bool addClipEffect(int clipId, const QString &effectId); Q_INVOKABLE bool addTrackEffect(int trackId, const QString &effectId); double getClipSpeed(int clipId) const; bool removeFade(int clipId, bool fromStart); Q_INVOKABLE bool copyClipEffect(int clipId, const QString &sourceId); bool adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration); /* @brief Returns the closest snap point within snapDistance */ Q_INVOKABLE int suggestSnapPoint(int pos, int snapDistance); /* @brief Returns the in cut position of a clip @param clipId Id of the clip to test */ int getClipIn(int clipId) const; /* @brief Returns the bin id of the clip master @param clipId Id of the clip to test */ const QString getClipBinId(int clipId) const; /* @brief Returns the duration of a clip @param clipId Id of the clip to test */ int getClipPlaytime(int clipId) const; /* @brief Returns the duration of a clip @param clipId Id of the clip to test */ QSize getClipFrameSize(int clipId) const; /* @brief Returns the number of clips in a given track @param trackId Id of the track to test */ int getTrackClipsCount(int trackId) const; /* @brief Returns the number of compositions in a given track @param trackId Id of the track to test */ int getTrackCompositionsCount(int trackId) const; /* @brief Returns the position of the track in the order of the tracks @param trackId Id of the track to test */ int getTrackPosition(int trackId) const; /* @brief Returns the track's index in terms of mlt's internal representation */ int getTrackMltIndex(int trackId) const; /* @brief Returns the ids of the tracks below the given track in the order of the tracks Returns an empty list if no track available @param trackId Id of the track to test */ QList getLowerTracksId(int trackId, TrackType type = TrackType::AnyTrack) const; /* @brief Returns the MLT track index of the video track just below the given trackC @param trackId Id of the track to test */ int getPreviousVideoTrackPos(int trackId) const; /* @brief Move a clip to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param clipId is the ID of the clip @param trackId is the ID of the target track @param position is the position where we want to move @param updateView if set to false, no signal is sent to qml @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestClipMove(int clipId, int trackId, int position, bool updateView = true, bool logUndo = true, bool invalidateTimeline = false); /* @brief Move a composition to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param transid is the ID of the composition @param trackId is the ID of the track */ Q_INVOKABLE bool requestCompositionMove(int compoId, int trackId, int position, bool updateView = true, bool logUndo = true); /* Same function, but accumulates undo and redo, and doesn't check for group*/ bool requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo); bool requestCompositionMove(int transid, int trackId, int compositionTrack, int position, bool updateView, Fun &undo, Fun &redo); Q_INVOKABLE int getCompositionPosition(int compoId) const; int getCompositionPlaytime(int compoId) const; /* Returns an item position, item can be clip or composition */ int getItemPosition(int itemId) const; /* Returns an item duration, item can be clip or composition */ int getItemPlaytime(int itemId) const; /* @brief Given an intended move, try to suggest a more valid one (accounting for snaps and missing UI calls) @param clipId id of the clip to move @param trackId id of the target track @param position target position @param snapDistance the maximum distance for a snap result, -1 for no snapping of the clip */ Q_INVOKABLE int suggestClipMove(int clipId, int trackId, int position, int snapDistance = -1); Q_INVOKABLE int suggestCompositionMove(int compoId, int trackId, int position, int snapDistance = -1); /* @brief Request clip insertion at given position. This action is undoable Returns true on success. If it fails, nothing is modified. @param binClipId id of the clip in the bin @param track Id of the track where to insert @param position Requested position @param ID return parameter of the id of the inserted clip @param logUndo if set to false, no undo object is stored @param refreshView whether the view should be refreshed */ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo = true, bool refreshView = false); /* Same function, but accumulates undo and redo*/ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, Fun &undo, Fun &redo); /* @brief Creates a new clip instance without inserting it. This action is undoable, returns true on success @param binClipId: Bin id of the clip to insert @param id: return parameter for the id of the newly created clip. @param state: The desired clip state (original, audio/video only). */ - bool requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, Fun &undo, Fun &redo); + bool requestClipCreation(const QString &binClipId, int &id, PlaylistState state, Fun &undo, Fun &redo); /* @brief Deletes the given clip or composition from the timeline This action is undoable Returns true on success. If it fails, nothing is modified. If the clip/composition is in a group, the call is deferred to requestGroupDeletion @param clipId is the ID of the clip/composition @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestItemDeletion(int clipId, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestItemDeletion(int clipId, Fun &undo, Fun &redo); /* @brief Move a group to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clips in the group are not in inserted in a track yet, they get inserted for the first time. @param clipId is the id of the clip that triggers the group move @param groupId is the id of the group @param delta_track is the delta applied to the track index @param delta_pos is the requested position change @param updateView if set to false, no signal is sent to qml for the clip clipId @param logUndo if set to true, an undo object is created */ bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true); bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo); /* @brief Deletes all clips inside the group that contains the given clip. This action is undoable Note that if their is a hierarchy of groups, all of them will be deleted. Returns true on success. If it fails, nothing is modified. @param clipId is the id of the clip that triggers the group deletion */ Q_INVOKABLE bool requestGroupDeletion(int clipId, bool logUndo = true); bool requestGroupDeletion(int clipId, Fun &undo, Fun &redo); /* @brief Change the duration of an item (clip or composition) This action is undoable Returns the real size reached (can be different, if snapping occurs). If it fails, nothing is modified, and -1 is returned @param itemId is the ID of the item @param size is the new size of the item @param right is true if we change the right side of the item, false otherwise @param logUndo if set to true, an undo object is created @param snap if set to true, the resize order will be coerced to use the snapping grid */ Q_INVOKABLE int requestItemResize(int itemId, int size, bool right, bool logUndo = true, int snapDistance = -1, bool allowSingleResize = false); /* @brief Change the duration of an item (clip or composition) This action is undoable Returns true on success. If it fails, nothing is modified. @param itemId is the ID of the item @param position is the requested start or end position @param resizeStart is true if we want to resize clip start, false to resize clip end */ bool requestItemResizeToPos(int itemId, int position, bool right); /* Same function, but accumulates undo and redo and doesn't deal with snapping*/ bool requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo = false); /* @brief Group together a set of ids The ids are either a group ids or clip ids. The involved clip must already be inserted in a track This action is undoable Returns the group id on success, -1 if it fails and nothing is modified. Typically, ids would be ids of clips, but for convenience, some of them can be ids of groups as well. @param ids Set of ids to group */ int requestClipsGroup(const std::unordered_set &ids, bool logUndo = true, GroupType type = GroupType::Normal); int requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type = GroupType::Normal); /* @brief Destruct the topmost group containing clip This action is undoable Returns true on success. If it fails, nothing is modified. @param id of the clip to degroup (all clips belonging to the same group will be ungrouped as well) */ bool requestClipUngroup(int id, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestClipUngroup(int id, Fun &undo, Fun &redo); /* @brief Create a track at given position This action is undoable Returns true on success. If it fails, nothing is modified. @param Requested position (order). If set to -1, the track is inserted last. @param id is a return parameter that holds the id of the resulting track (-1 on failure) */ bool requestTrackInsertion(int pos, int &id, const QString &trackName = QString(), bool audioTrack = false); /* Same function, but accumulates undo and redo*/ bool requestTrackInsertion(int pos, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView = true); /* @brief Delete track with given id This also deletes all the clips contained in the track. This action is undoable Returns true on success. If it fails, nothing is modified. @param trackId id of the track to delete */ bool requestTrackDeletion(int trackId); /* Same function, but accumulates undo and redo*/ bool requestTrackDeletion(int trackId, Fun &undo, Fun &redo); /* @brief Get project duration Returns the duration in frames */ int duration() const; static int seekDuration; // Duration after project end where seeking is allowed /* @brief Get all the elements of the same group as the given clip. If there is a group hierarchy, only the topmost group is considered. @param clipId id of the clip to test */ std::unordered_set getGroupElements(int clipId); /* @brief Removes all the elements on the timeline (tracks and clips) */ bool requestReset(Fun &undo, Fun &redo); /* @brief Updates the current the pointer to the current undo_stack Must be called for example when the doc change */ void setUndoStack(std::weak_ptr undo_stack); /* @brief Requests the best snapped position for a clip @param pos is the clip's requested position @param length is the clip's duration @param pts snap points to ignore (for example currently moved clip) @param snapDistance the maximum distance for a snap result, -1 for no snapping @returns best snap position or -1 if no snap point is near */ int requestBestSnapPos(int pos, int length, const std::vector &pts = std::vector(), int snapDistance = -1); /* @brief Requests the next snapped point @param pos is the current position */ int requestNextSnapPos(int pos); /* @brief Requests the previous snapped point @param pos is the current position */ int requestPreviousSnapPos(int pos); /* @brief Add a new snap point @param pos is the current position */ void addSnap(int pos); /* @brief Remove snap point @param pos is the current position */ void removeSnap(int pos); /* @brief Request composition insertion at given position. This action is undoable Returns true on success. If it fails, nothing is modified. @param transitionId Identifier of the Mlt transition to insert (as given by repository) @param track Id of the track where to insert @param position Requested position @param length Requested initial length. @param id return parameter of the id of the inserted composition @param logUndo if set to false, no undo object is stored */ bool requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, Mlt::Properties *transProps, int &id, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, Mlt::Properties *transProps, int &id, Fun &undo, Fun &redo); /* @brief This function change the global (timeline-wise) enabled state of the effects It disables/enables track and clip effects (recursively) */ void setTimelineEffectsEnabled(bool enabled); /* @brief Get a timeline clip id by its position or -1 if not found */ int getClipByPosition(int trackId, int position) const; /* @brief Get a timeline composition id by its starting position or -1 if not found */ int getCompositionByPosition(int trackId, int position) const; /* @brief Returns a list of all items that are at or after a given position. * @param trackId is the id of the track for concerned items. Setting trackId to -1 returns items on all tracks * @param position is the position where we the items should start * @param end is the position after which items will not be selected, set to -1 to get all clips on track * @param listCompositions if enabled, the list will also contains composition ids */ std::unordered_set getItemsAfterPosition(int trackId, int position, int end = -1, bool listCompositions = true); /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Inform asset view of duration change */ virtual void adjustAssetRange(int clipId, int in, int out); void requestClipReload(int clipId); void requestClipUpdate(int clipId, const QVector &roles); /** @brief Returns the effectstack of a given clip. */ std::shared_ptr getClipEffectStack(int itemId); std::shared_ptr getTrackEffectStackModel(int trackId); /** @brief Add slowmotion effect to clip in timeline. */ bool requestClipTimeWarp(int clipId, int trackId, int blankSpace, double speed, Fun &undo, Fun &redo); bool changeItemSpeed(int clipId, int speed); void replugClip(int clipId); protected: /* @brief Register a new track. This is a call-back meant to be called from TrackModel @param pos indicates the number of the track we are adding. If this is -1, then we add at the end. */ void registerTrack(std::shared_ptr track, int pos = -1, bool doInsert = true, bool reloadView = true); /* @brief Register a new clip. This is a call-back meant to be called from ClipModel */ void registerClip(const std::shared_ptr &clip); /* @brief Register a new composition. This is a call-back meant to be called from CompositionModel */ void registerComposition(const std::shared_ptr &composition); /* @brief Register a new group. This is a call-back meant to be called from GroupsModel */ void registerGroup(int groupId); /* @brief Deregister and destruct the track with given id. @parame updateView Whether to send updates to the model. Must be false when called from a constructor/destructor */ Fun deregisterTrack_lambda(int id, bool updateView = false); /* @brief Return a lambda that deregisters and destructs the clip with given id. Note that the clip must already be deleted from its track and groups. */ Fun deregisterClip_lambda(int id); /* @brief Return a lambda that deregisters and destructs the composition with given id. */ Fun deregisterComposition_lambda(int compoId); /* @brief Deregister a group with given id */ void deregisterGroup(int id); /* @brief Helper function to get a pointer to the track, given its id */ std::shared_ptr getTrackById(int trackId); const std::shared_ptr getTrackById_const(int trackId) const; /*@brief Helper function to get a pointer to a clip, given its id*/ std::shared_ptr getClipPtr(int clipId) const; /*@brief Helper function to get a pointer to a composition, given its id*/ std::shared_ptr getCompositionPtr(int compoId) const; /* @brief Returns next valid unique id to create an object */ static int getNextId(); /* @brief unplant and the replant all the compositions in the correct order @param currentCompo is the id of a compo that have not yet been planted, if any. Otherwise send -1 */ bool replantCompositions(int currentCompo, bool updateView); /* @brief Unplant the composition with given Id */ bool unplantComposition(int compoId); /* Same function but accumulates undo and redo, and doesn't check for group*/ bool requestClipDeletion(int clipId, Fun &undo, Fun &redo); bool requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo); /** @brief Check tracks duration and update black track accordingly */ void updateDuration(); /** @brief Get a track tag (A1, V1, V2,...) through its id */ const QString getTrackTagById(int trackId) const; public: /* @brief Debugging function that checks consistency with Mlt objects */ bool checkConsistency(); protected: /* @brief Refresh project monitor if cursor was inside range */ void checkRefresh(int start, int end); /* @brief Send signal to require clearing effet/composition view */ void clearAssetView(int itemId); signals: /* @brief signal triggered by clearAssetView */ void requestClearAssetView(int); void requestMonitorRefresh(); /* @brief signal triggered by track operations */ void invalidateZone(int in, int out); /* @brief signal triggered when a track duration changed (insertion/deletion) */ void durationUpdated(); protected: std::unique_ptr m_tractor; std::list> m_allTracks; std::unordered_map>::iterator> m_iteratorTable; // this logs the iterator associated which each track id. This allows easy access of a track based on its id. std::unordered_map> m_allClips; // the keys are the clip id, and the values are the corresponding pointers std::unordered_map> m_allCompositions; // the keys are the composition id, and the values are the corresponding pointers static int next_id; // next valid id to assign std::unique_ptr m_groups; std::shared_ptr m_snaps; std::unordered_set m_allGroups; // ids of all the groups std::weak_ptr m_undoStack; Mlt::Profile *m_profile; // The black track producer. Its length / out should always be adjusted to the projects's length std::unique_ptr m_blackClip; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access #ifdef LOGGING std::ofstream m_logFile; // this is a temporary debug member to help reproduce issues #endif bool m_timelineEffectsEnabled; bool m_id; // id of the timeline itself // id of the currently selected group in timeline, should be destroyed on each new selection int m_temporarySelectionGroup; // The index of the temporary overlay track in tractor, or -1 if not connected int m_overlayTrackCount; // The preferred audio target for clip insertion or -1 if not defined int m_audioTarget; // The preferred video target for clip insertion or -1 if not defined int m_videoTarget; // what follows are some virtual function that corresponds to the QML. They are implemented in TimelineItemModel protected: virtual void _beginRemoveRows(const QModelIndex &, int, int) = 0; virtual void _beginInsertRows(const QModelIndex &, int, int) = 0; virtual void _endRemoveRows() = 0; virtual void _endInsertRows() = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) = 0; virtual QModelIndex makeClipIndexFromID(int) const = 0; virtual QModelIndex makeCompositionIndexFromID(int) const = 0; virtual QModelIndex makeTrackIndexFromID(int) const = 0; virtual void _resetView() = 0; }; #endif diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index 52e470b06..d8886e84d 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,1570 +1,1570 @@ /*************************************************************************** * Copyright (C) 2017 by 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 "timelinecontroller.h" #include "../model/timelinefunctions.hpp" #include "bin/bin.h" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/spacerdialog.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "previewmanager.h" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "timeline2/view/dialogs/trackdialog.h" #include "timelinewidget.h" #include "transitions/transitionsrepository.hpp" #include "utils/KoIconUtils.h" #include #include #include #include int TimelineController::m_duration = 0; TimelineController::TimelineController(KActionCollection *actionCollection, QObject *parent) : QObject(parent) , m_root(nullptr) , m_actionCollection(actionCollection) , m_usePreview(false) , m_position(0) , m_seekPosition(-1) , m_activeTrack(0) , m_scale(3.0) , m_timelinePreview(nullptr) , m_zone(-1, -1) { m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); m_disablePreview->setEnabled(false); } TimelineController::~TimelineController() { delete m_timelinePreview; m_timelinePreview = nullptr; } void TimelineController::setModel(std::shared_ptr model) { delete m_timelinePreview; m_zone = QPoint(-1, -1); m_timelinePreview = nullptr; m_model = std::move(model); connect(m_model.get(), &TimelineItemModel::requestClearAssetView, [&](int id) { pCore->clearAssetPanel(id); }); connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); }); connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection); connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration); } std::shared_ptr TimelineController::getModel() const { return m_model; } void TimelineController::setRoot(QQuickItem *root) { m_root = root; } Mlt::Tractor *TimelineController::tractor() { return m_model->tractor(); } void TimelineController::addSelection(int newSelection) { if (m_selection.selectedItems.contains(newSelection)) { return; } std::unordered_set previousSelection = getCurrentSelectionIds(); m_selection.selectedItems << newSelection; std::unordered_set ids; ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); std::unordered_set newIds; QVector roles; roles.push_back(TimelineModel::GroupDragRole); if (m_model->m_temporarySelectionGroup >= 0) { // new items were selected, inform model to prepare for group drag newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); for (int i : newIds) { if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } // Make sure to remove items from previous selection for (int i : previousSelection) { if (newIds.find(i) == newIds.end()) { // item not in selection anymore if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } emit selectionChanged(); if (!m_selection.selectedItems.isEmpty()) emitSelectedFromSelection(); else emit selected(nullptr); } int TimelineController::getCurrentItem() { // TODO: if selection is empty, return topmost clip under timeline cursor if (m_selection.selectedItems.isEmpty()) { return -1; } // TODO: if selection contains more than 1 clip, return topmost clip under timeline cursor in selection return m_selection.selectedItems.constFirst(); } double TimelineController::scaleFactor() const { return m_scale; } const QString TimelineController::getTrackNameFromMltIndex(int trackPos) { if (trackPos == -1) { return i18n("unknown"); } if (trackPos == 0) { return i18n("Black"); } return m_model->getTrackById(m_model->getTrackIndexFromPosition(trackPos - 1))->getProperty(QStringLiteral("kdenlive:track_name")).toString(); } const QString TimelineController::getTrackNameFromIndex(int trackIndex) { return m_model->getTrackById(trackIndex)->getProperty(QStringLiteral("kdenlive:track_name")).toString(); } QMap TimelineController::getTrackNames(bool videoOnly) { QMap names; for (const auto &track : m_model->m_iteratorTable) { if (videoOnly && m_model->getTrackById(track.first)->getProperty(QStringLiteral("kdenlive:audio_track")).toInt() == 1) { continue; } names[m_model->getTrackMltIndex(track.first)] = m_model->getTrackById(track.first)->getProperty("kdenlive:track_name").toString(); } return names; } void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) { /*if (m_duration * scale < width() - 160) { // Don't allow scaling less than full project's width scale = (width() - 160.0) / m_duration; }*/ m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMin(getMousePos(), m_duration - TimelineModel::seekDuration) : -1); m_scale = scale; emit scaleFactorChanged(); } void TimelineController::setScaleFactor(double scale) { /*if (m_duration * scale < width() - 160) { // Don't allow scaling less than full project's width scale = (width() - 160.0) / m_duration; }*/ m_scale = scale; emit scaleFactorChanged(); } int TimelineController::duration() const { return m_duration; } void TimelineController::checkDuration() { int currentLength = m_model->duration(); if (currentLength != m_duration) { m_duration = currentLength; emit durationChanged(); } } std::unordered_set TimelineController::getCurrentSelectionIds() const { std::unordered_set selection; if (m_model->m_temporarySelectionGroup >= 0 || (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { selection = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } else { for (int i : m_selection.selectedItems) { selection.insert(i); } } return selection; } void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent) { QList toSelect; int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, timelinePosition()) : m_model->getCompositionByPosition(m_activeTrack, timelinePosition()); if (currentClip == -1) { pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500); return; } if (addToCurrent || !select) { toSelect = m_selection.selectedItems; } if (select) { if (!toSelect.contains(currentClip)) { toSelect << currentClip; setSelection(toSelect); } } else if (toSelect.contains(currentClip)) { toSelect.removeAll(currentClip); setSelection(toSelect); } } void TimelineController::setSelection(const QList &newSelection, int trackIndex, bool isMultitrack) { if (newSelection != selection() || trackIndex != m_selection.selectedTrack || isMultitrack != m_selection.isMultitrackSelected) { qDebug() << "Changing selection to" << newSelection << " trackIndex" << trackIndex << "isMultitrack" << isMultitrack; std::unordered_set previousSelection = getCurrentSelectionIds(); m_selection.selectedItems = newSelection; m_selection.selectedTrack = trackIndex; m_selection.isMultitrackSelected = isMultitrack; if (m_model->m_temporarySelectionGroup > -1) { // CLear current selection m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } std::unordered_set newIds; QVector roles; roles.push_back(TimelineModel::GroupDragRole); if (m_selection.selectedItems.size() > 0) { std::unordered_set ids; ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); if (m_model->m_temporarySelectionGroup >= 0 || (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); for (int i : newIds) { if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } else { qDebug() << "// NON GROUPED SELCTUIIN: " << m_selection.selectedItems << " !!!!!!"; } emitSelectedFromSelection(); } else { // Empty selection emit selected(nullptr); emit showItemEffectStack(QString(), nullptr, QSize(), false); } for (int i : previousSelection) { // Clear previously selcted items if (newIds.find(i) == newIds.end()) { // item not in selection anymore if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } emit selectionChanged(); } } void TimelineController::emitSelectedFromSelection() { /*if (!m_model.trackList().count()) { if (m_model.tractor()) selectMultitrack(); else emit selected(0); return; } int trackIndex = currentTrack(); int clipIndex = selection().isEmpty()? 0 : selection().first(); Mlt::ClipInfo* info = getClipInfo(trackIndex, clipIndex); if (info && info->producer && info->producer->is_valid()) { delete m_updateCommand; m_updateCommand = new Timeline::UpdateCommand(*this, trackIndex, clipIndex, info->start); // We need to set these special properties so time-based filters // can get information about the cut while still applying filters // to the cut parent. info->producer->set(kFilterInProperty, info->frame_in); info->producer->set(kFilterOutProperty, info->frame_out); if (MLT.isImageProducer(info->producer)) info->producer->set("out", info->cut->get_int("out")); info->producer->set(kMultitrackItemProperty, 1); m_ignoreNextPositionChange = true; emit selected(info->producer); delete info; }*/ } QList TimelineController::selection() const { if (!m_root) return QList(); return m_selection.selectedItems; } void TimelineController::selectMultitrack() { setSelection(QList(), -1, true); QMetaObject::invokeMethod(m_root, "selectMultitrack"); // emit selected(m_model.tractor()); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } void TimelineController::snapChanged(bool snap) { m_root->setProperty("snapping", snap ? 10 / std::sqrt(m_scale) : -1); } bool TimelineController::ripple() { return false; } bool TimelineController::scrub() { return false; } int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView) { int id; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView)) { id = -1; } return id; } QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView) { QList clipIds; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView); // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids. return clipIds; } int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) { int id; if (!m_model->requestCompositionInsertion(transitionId, tid, position, 100, nullptr, id, logUndo)) { id = -1; } return id; } void TimelineController::deleteSelectedClips() { if (m_selection.selectedItems.isEmpty()) { return; } if (m_model->m_temporarySelectionGroup != -1) { // selection is grouped, delete group only m_model->requestGroupDeletion(m_model->m_temporarySelectionGroup); } else { for (int cid : m_selection.selectedItems) { m_model->requestItemDeletion(cid); } } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::copyItem() { int clipId = -1; if (!m_selection.selectedItems.isEmpty()) { clipId = m_selection.selectedItems.first(); } else { return; } m_root->setProperty("copiedClip", clipId); } bool TimelineController::pasteItem(int clipId, int tid, int position) { // TODO: copy multiple clips / groups if (clipId == -1) { clipId = m_root->property("copiedClip").toInt(); if (clipId == -1) { return -1; } } if (tid == -1 && position == -1) { tid = getMouseTrack(); position = getMousePos(); } if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } qDebug() << "PASTING CLIP: " << clipId << ", " << tid << ", " << position; return TimelineFunctions::requestItemCopy(m_model, clipId, tid, position); return false; } void TimelineController::triggerAction(const QString &name) { QAction *action = m_actionCollection->action(name); if (action) { action->trigger(); } } QString TimelineController::timecode(int frames) { return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); } bool TimelineController::showThumbnails() const { return KdenliveSettings::videothumbnails(); } bool TimelineController::showAudioThumbnails() const { return KdenliveSettings::audiothumbnails(); } bool TimelineController::showMarkers() const { return KdenliveSettings::showmarkers(); } bool TimelineController::audioThumbFormat() const { return KdenliveSettings::displayallchannels(); } bool TimelineController::showWaveforms() const { return KdenliveSettings::audiothumbnails(); } void TimelineController::addTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, m_model->getTrackMltIndex(tid), qApp->activeWindow()); if (d->exec() == QDialog::Accepted) { int mltIndex = d->selectedTrack(); int newTid; m_model->requestTrackInsertion(mltIndex, newTid, d->trackName(), d->addAudioTrack()); } } void TimelineController::deleteTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, m_model->getTrackMltIndex(tid), qApp->activeWindow(), true); if (d->exec() == QDialog::Accepted) { int mltIndex = d->selectedTrack(); m_model->requestTrackDeletion(mltIndex); } } void TimelineController::gotoNextSnap() { setPosition(m_model->requestNextSnapPos(timelinePosition())); } void TimelineController::gotoPreviousSnap() { setPosition(m_model->requestPreviousSnapPos(timelinePosition())); } void TimelineController::groupSelection() { if (m_selection.selectedItems.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } std::unordered_set clips; for (int id : m_selection.selectedItems) { clips.insert(id); } m_model->requestClipsGroup(clips); } void TimelineController::unGroupSelection(int cid) { if (cid == -1 && m_selection.selectedItems.isEmpty()) { pCore->displayMessage(i18n("Select at least 1 item to ungroup"), InformationMessage, 500); return; } if (cid == -1) { for (int id : m_selection.selectedItems) { if (m_model->m_groups->isInGroup(id) && !m_model->isInSelection(id)) { cid = id; break; } } } if (cid > -1) { m_model->requestClipUngroup(cid); } if (m_model->m_temporarySelectionGroup >= 0) { m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::setInPoint() { int cursorPos = timelinePosition(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { m_model->requestItemResizeToPos(id, cursorPos, false); } } } int TimelineController::timelinePosition() const { return m_seekPosition >= 0 ? m_seekPosition : m_position; } void TimelineController::setOutPoint() { int cursorPos = timelinePosition(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { m_model->requestItemResizeToPos(id, cursorPos, true); } } } void TimelineController::editMarker(const QString &cid, int frame) { std::shared_ptr clip = pCore->bin()->getBinClip(cid); GenTime pos(frame, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } void TimelineController::editGuide(int frame) { if (frame == -1) { frame = timelinePosition(); } auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); guideModel->editMarkerGui(pos, qApp->activeWindow(), false); } void TimelineController::moveGuide(int frame, int newFrame) { auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); GenTime newPos(newFrame, pCore->getCurrentFps()); guideModel->editMarker(pos, newPos); } void TimelineController::switchGuide(int frame, bool deleteOnly) { bool markerFound = false; if (frame == -1) { frame = timelinePosition(); } CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound); if (!markerFound) { if (deleteOnly) { pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500); return; } GenTime pos(frame, pCore->getCurrentFps()); pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide")); } else { pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time()); } } void TimelineController::addAsset(const QVariantMap data) { QString effect = data.value(QStringLiteral("kdenlive/effect")).toString(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { if (m_model->isClip(id)) { m_model->addClipEffect(id, effect); } } } else { pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::requestRefresh() { pCore->requestMonitorRefresh(); } void TimelineController::showAsset(int id) { if (m_model->isComposition(id)) { emit showTransitionModel(id, m_model->getCompositionParameterModel(id)); } else if (m_model->isClip(id)) { QModelIndex clipIx = m_model->makeClipIndexFromID(id); QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString(); bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt(); qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes; emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes); } } void TimelineController::showTrackAsset(int trackId) { emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false); } void TimelineController::setPosition(int position) { setSeekPosition(position); emit seeked(position); } void TimelineController::setAudioTarget(int track) { m_model->m_audioTarget = track; emit audioTargetChanged(); } void TimelineController::setVideoTarget(int track) { m_model->m_videoTarget = track; emit videoTargetChanged(); } void TimelineController::setActiveTrack(int track) { m_activeTrack = track; emit activeTrackChanged(); } void TimelineController::setSeekPosition(int position) { m_seekPosition = position; emit seekPositionChanged(); } void TimelineController::onSeeked(int position) { m_position = position; emit positionChanged(); if (m_seekPosition > -1 && position == m_seekPosition) { m_seekPosition = -1; emit seekPositionChanged(); } } void TimelineController::setZone(const QPoint &zone) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y()); } if (zone.x() > 0) { m_model->addSnap(zone.x()); } if (zone.y() > 0) { m_model->addSnap(zone.y()); } m_zone = zone; emit zoneChanged(); } void TimelineController::setZoneIn(int inPoint) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (inPoint > 0) { m_model->addSnap(inPoint); } m_zone.setX(inPoint); emit zoneMoved(m_zone); } void TimelineController::setZoneOut(int outPoint) { if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y()); } if (outPoint > 0) { m_model->addSnap(outPoint); } m_zone.setY(outPoint); emit zoneMoved(m_zone); } void TimelineController::selectItems(QVariantList arg, int startFrame, int endFrame, bool addToSelect) { std::unordered_set previousSelection = getCurrentSelectionIds(); std::unordered_set itemsToSelect; if (addToSelect) { for (int cid : m_selection.selectedItems) { itemsToSelect.insert(cid); } } m_selection.selectedItems.clear(); for (int i = 0; i < arg.count(); i++) { std::unordered_set trackClips = m_model->getTrackById(arg.at(i).toInt())->getClipsAfterPosition(startFrame, endFrame); itemsToSelect.insert(trackClips.begin(), trackClips.end()); std::unordered_set trackCompos = m_model->getTrackById(arg.at(i).toInt())->getCompositionsAfterPosition(startFrame, endFrame); itemsToSelect.insert(trackCompos.begin(), trackCompos.end()); } if (itemsToSelect.size() > 0) { for (int x : itemsToSelect) { m_selection.selectedItems << x; } qDebug() << "// GROUPING ITEMS: " << m_selection.selectedItems; m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(itemsToSelect, true, GroupType::Selection); } else if (m_model->m_temporarySelectionGroup > -1) { m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } std::unordered_set newIds; QVector roles; roles.push_back(TimelineModel::GroupDragRole); if (m_model->m_temporarySelectionGroup >= 0) { newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); for (int i : newIds) { if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } for (int i : previousSelection) { if (newIds.find(i) == newIds.end()) { // item not in selection anymore if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } emit selectionChanged(); } void TimelineController::requestClipCut(int clipId, int position) { if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestClipCut(m_model, clipId, position); } void TimelineController::cutClipUnderCursor(int position, int track) { if (position == -1) { position = timelinePosition(); } bool foundClip = false; for (int cid : m_selection.selectedItems) { if (m_model->isClip(cid)) { if (TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } else { qDebug() << "//// TODO: COMPOSITION CUT!!!"; } } if (!foundClip) { if (track == -1) { track = m_activeTrack; } if (track >= 0) { int cid = m_model->getClipByPosition(track, position); if (cid >= 0) { TimelineFunctions::requestClipCut(m_model, cid, position); foundClip = true; } } } if (!foundClip) { // TODO: display warning, no clip found } } int TimelineController::requestSpacerStartOperation(int trackId, int position) { return TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position); } bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition) { return TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition); } void TimelineController::seekCurrentClip(bool seekToEnd) { for (int cid : m_selection.selectedItems) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); break; } } void TimelineController::seekToClip(int cid, bool seekToEnd) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } void TimelineController::seekToMouse() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); int mousePos = returnedValue.toInt(); setPosition(mousePos); } int TimelineController::getMousePos() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } int TimelineController::getMouseTrack() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } void TimelineController::refreshItem(int id) { int in = m_model->getItemPosition(id); if (in > m_position) { return; } if (m_position <= in + m_model->getItemPlaytime(id)) { pCore->requestMonitorRefresh(); } } QPoint TimelineController::getTracksCount() const { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue)); QVariantList tracks = returnedValue.toList(); QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt()); return p; } QStringList TimelineController::extractCompositionLumas() const { return m_model->extractCompositionLumas(); } void TimelineController::addEffectToCurrentClip(const QStringList &effectData) { QList activeClips; for (int track = m_model->getTracksCount() - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); int cid = m_model->getClipByPosition(trackIx, timelinePosition()); if (cid > -1) { activeClips << cid; } } if (!activeClips.isEmpty()) { if (effectData.count() == 4) { QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3); m_model->copyClipEffect(activeClips.first(), effectString); } else { m_model->addClipEffect(activeClips.first(), effectData.constFirst()); } } } void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration) { if (duration <= 0) { // remove fade m_model->removeFade(cid, effectId == QLatin1String("fadein")); } else { m_model->adjustEffectLength(cid, effectId, duration, initialDuration); } } QPair TimelineController::getCompositionATrack(int cid) const { QPair result; std::shared_ptr compo = m_model->getCompositionPtr(cid); if (compo) { result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId())); } return result; } void TimelineController::setCompositionATrack(int cid, int aTrack) { TimelineFunctions::setCompositionATrack(m_model, cid, aTrack); } bool TimelineController::compositionAutoTrack(int cid) const { std::shared_ptr compo = m_model->getCompositionPtr(cid); return compo && compo->getForcedTrack() == -1; } const QString TimelineController::getClipBinId(int clipId) const { return m_model->getClipBinId(clipId); } void TimelineController::focusItem(int itemId) { int start = m_model->getItemPosition(itemId); setPosition(start); } int TimelineController::headerWidth() const { return qMax(10, KdenliveSettings::headerwidth()); } void TimelineController::setHeaderWidth(int width) { KdenliveSettings::setHeaderwidth(width); } bool TimelineController::createSplitOverlay(Mlt::Filter *filter) { if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) { return true; } int clipId = getCurrentItem(); if (clipId == -1) { pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500); return false; } std::shared_ptr clip = m_model->getClipPtr(clipId); const QString binId = clip->binId(); // Get clean bin copy of the clip std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut())); // Get copy of timeline producer Mlt::Producer *clipProducer = new Mlt::Producer(*clip); // Built tractor and compositing Mlt::Tractor trac(*m_model->m_tractor->profile()); Mlt::Playlist play(*m_model->m_tractor->profile()); Mlt::Playlist play2(*m_model->m_tractor->profile()); play.append(*clipProducer); play2.append(*binProd); trac.set_track(play, 0); trac.set_track(play2, 1); play2.attach(*filter); QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); int startPos = m_model->getClipPosition(clipId); // plug in overlay playlist Mlt::Playlist *overlay = new Mlt::Playlist(*m_model->m_tractor->profile()); overlay->insert_blank(0, startPos); Mlt::Producer split(trac.get_producer()); overlay->insert_at(startPos, &split, 1); // insert in tractor if (!m_timelinePreview) { initializePreview(); } m_timelinePreview->setOverlayTrack(overlay); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); return true; } void TimelineController::removeSplitOverlay() { if (m_timelinePreview && !m_timelinePreview->hasOverlayTrack()) { return; } // disconnect m_timelinePreview->removeOverlayTrack(); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } void TimelineController::addPreviewRange(bool add) { if (m_timelinePreview && !m_zone.isNull()) { m_timelinePreview->addPreviewRange(m_zone, add); } } void TimelineController::clearPreviewRange() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); } } void TimelineController::startPreviewRender() { // Timeline preview stuff if (!m_timelinePreview) { initializePreview(); } else if (m_disablePreview->isChecked()) { m_disablePreview->setChecked(false); disablePreview(false); } if (m_timelinePreview) { if (!m_usePreview) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->startPreviewRender(); } } void TimelineController::stopPreviewRender() { if (m_timelinePreview) { m_timelinePreview->abortRendering(); } } void TimelineController::initializePreview() { if (m_timelinePreview) { // Update parameters if (!m_timelinePreview->loadParams()) { if (m_usePreview) { // Disconnect preview track m_timelinePreview->disconnectTrack(); m_usePreview = false; } delete m_timelinePreview; m_timelinePreview = nullptr; } } else { m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get()); if (!m_timelinePreview->initialize()) { // TODO warn user delete m_timelinePreview; m_timelinePreview = nullptr; } else { } } QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone")); if (previewRender) { previewRender->setEnabled(m_timelinePreview != nullptr); } m_disablePreview->setEnabled(m_timelinePreview != nullptr); m_disablePreview->blockSignals(true); m_disablePreview->setChecked(false); m_disablePreview->blockSignals(false); } void TimelineController::disablePreview(bool disable) { if (disable) { m_timelinePreview->deletePreviewTrack(); m_usePreview = false; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } else { if (!m_usePreview) { if (!m_timelinePreview->buildPreviewTrack()) { // preview track already exists, reconnect m_model->m_tractor->lock(); m_timelinePreview->reconnectTrack(); m_model->m_tractor->unlock(); } m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime()); m_usePreview = true; } } m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } QVariantList TimelineController::dirtyChunks() const { return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList(); } QVariantList TimelineController::renderedChunks() const { return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList(); } int TimelineController::workingPreview() const { return m_timelinePreview ? m_timelinePreview->workingPreview : -1; } bool TimelineController::useRuler() const { return KdenliveSettings::useTimelineZoneToEdit(); } void TimelineController::resetPreview() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); initializePreview(); } } void TimelineController::loadPreview(QString chunks, QString dirty, const QDateTime &documentDate, int enable) { if (chunks.isEmpty() && dirty.isEmpty()) { return; } if (!m_timelinePreview) { initializePreview(); } QVariantList renderedChunks; QVariantList dirtyChunks; QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts); QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts); for (const QString &frame : chunksList) { renderedChunks << frame.toInt(); } for (const QString &frame : dirtyList) { dirtyChunks << frame.toInt(); } m_disablePreview->blockSignals(true); m_disablePreview->setChecked(enable); m_disablePreview->blockSignals(false); if (!enable) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate); } QMap TimelineController::documentProperties() { QMap props = pCore->currentDoc()->documentProperties(); // TODO // props.insert(QStringLiteral("audiotargettrack"), QString::number(audioTarget)); // props.insert(QStringLiteral("videotargettrack"), QString::number(videoTarget)); if (m_timelinePreview) { QPair chunks = m_timelinePreview->previewChunks(); props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(','))); props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(','))); } props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked())); return props; } void TimelineController::insertSpace(int trackId, int frame) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow()); if (d->exec() != QDialog::Accepted) { delete d; return; } int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame); int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps()); delete d; if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start + spaceDuration); } void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } // find blank duration int spaceDuration = m_model->getTrackById(trackId)->getBlankSizeAtPos(frame); int cid = requestSpacerStartOperation(affectAllTracks ? -1 : trackId, frame); if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start - spaceDuration); } void TimelineController::invalidateClip(int cid) { if (!m_timelinePreview) { return; } int start = m_model->getItemPosition(cid); int end = start + m_model->getItemPlaytime(cid); m_timelinePreview->invalidatePreview(start, end); } void TimelineController::invalidateZone(int in, int out) { if (!m_timelinePreview) { return; } m_timelinePreview->invalidatePreview(in, out); } void TimelineController::changeItemSpeed(int clipId, int speed) { if (speed == -1) { speed = 100 * m_model->m_allClips[clipId]->getSpeed(); bool ok; speed = QInputDialog::getInt(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), speed, -100000, 100000, 1, &ok); if (!ok) { return; } } m_model->changeItemSpeed(clipId, speed); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); qDebug() << "//// SWITCH COMPO: " << mode; QScopedPointer service(m_model->m_tractor->field()); Mlt::Field *field = m_model->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } if (mode > 0) { const QString compositeGeometry = QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height()); // Loop through tracks for (int track = 1; track < m_model->getTracksCount(); track++) { if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) { // This is a video track Mlt::Transition t(*m_model->m_tractor->profile(), mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData()); t.set("always_active", 1); t.set("a_track", 0); t.set("b_track", track + 1); if (mode == 1) { t.set("valign", "middle"); t.set("halign", "centre"); t.set("fill", 1); t.set("geometry", compositeGeometry.toUtf8().constData()); } t.set("internal_added", 237); field->plant_transition(t, 0, track + 1); } } } field->unlock(); delete field; pCore->requestMonitorRefresh(); } void TimelineController::extractZone(QPoint zone) { QVector tracks; if (audioTarget() >= 0) { tracks << audioTarget(); } if (videoTarget() >= 0) { tracks << videoTarget(); } if (tracks.isEmpty()) { tracks << m_activeTrack; } if (m_zone == QPoint()) { // Use current timeline position and clip zone length zone.setY(timelinePosition() + zone.y() - zone.x()); zone.setX(timelinePosition()); } TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, false); } void TimelineController::extract(int clipId) { // TODO: grouped clips? int in = m_model->getClipPosition(clipId); QPoint zone(in, in + m_model->getClipPlaytime(clipId)); int track = m_model->getClipTrackId(clipId); TimelineFunctions::extractZone(m_model, QVector() << track, zone, false); } void TimelineController::liftZone(QPoint zone) { QVector tracks; if (audioTarget() >= 0) { tracks << audioTarget(); } if (videoTarget() >= 0) { tracks << videoTarget(); } if (tracks.isEmpty()) { tracks << m_activeTrack; } if (m_zone == QPoint()) { // Use current timeline position and clip zone length zone.setY(timelinePosition() + zone.y() - zone.x()); zone.setX(timelinePosition()); } TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, true); } bool TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); int targetTrack = -1; if (clip->clipType() == ClipType::Audio) { targetTrack = audioTarget(); } else { targetTrack = videoTarget(); } if (targetTrack == -1) { targetTrack = m_activeTrack; } int insertPoint; QPoint sourceZone; if (KdenliveSettings::useTimelineZoneToEdit() && m_zone != QPoint()) { // We want to use timeline zone for in/out insert points insertPoint = m_zone.x(); sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x()); } else { // Use curent timeline pos and clip zone for in/out insertPoint = timelinePosition(); sourceZone = zone; } return TimelineFunctions::insertZone(m_model, targetTrack, binId, insertPoint, sourceZone, overwrite); } void TimelineController::updateClip(int clipId, QVector roles) { QModelIndex ix = m_model->makeClipIndexFromID(clipId); if (ix.isValid()) { m_model->dataChanged(ix, ix, roles); } } void TimelineController::showClipKeyframes(int clipId, bool value) { TimelineFunctions::showClipKeyframes(m_model, clipId, value); } void TimelineController::showCompositionKeyframes(int clipId, bool value) { TimelineFunctions::showCompositionKeyframes(m_model, clipId, value); } -void TimelineController::setClipStatus(int clipId, int status) +void TimelineController::setClipStatus(int clipId, PlaylistState status) { - TimelineFunctions::changeClipState(m_model, clipId, (PlaylistState::ClipState)status); + TimelineFunctions::changeClipState(m_model, clipId, status); } void TimelineController::splitAudio(int clipId) { TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget()); } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:locked_track").toInt() == 1; m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:locked_track"), locked ? QStringLiteral("0") : QStringLiteral("1")); } else { // Invert track lock // Get track states first QMap trackLockState; int unlockedTracksCount = 0; int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); bool isLocked = m_model->getTrackById_const(trackIx)->getProperty("kdenlive:locked_track").toInt() == 1; if (!isLocked) { unlockedTracksCount++; } trackLockState.insert(trackIx, isLocked); } if (unlockedTracksCount == tracksCount) { // do not lock all tracks, leave active track unlocked trackLockState.insert(m_activeTrack, true); } QMapIterator i(trackLockState); while (i.hasNext()) { i.next(); m_model->setTrackProperty(i.key(), QStringLiteral("kdenlive:locked_track"), i.value() ? QStringLiteral("0") : QStringLiteral("1")); } } } void TimelineController::switchTargetTrack() { bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1; if (isAudio) { setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack); } else { setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); } } int TimelineController::audioTarget() const { return m_model->m_audioTarget; } int TimelineController::videoTarget() const { return m_model->m_videoTarget; } void TimelineController::resetTrackHeight() { int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); } QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } int TimelineController::groupClips(const QList &clipIds) { std::unordered_set theSet(clipIds.begin(), clipIds.end()); return m_model->requestClipsGroup(theSet, false, GroupType::Selection); } bool TimelineController::ungroupClips(int clipId) { return m_model->requestClipUngroup(clipId); } void TimelineController::clearSelection() { if (m_model->m_temporarySelectionGroup >= 0) { m_model->m_groups->destructGroupItem(m_model->m_temporarySelectionGroup); m_model->m_temporarySelectionGroup = -1; } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::pasteEffects(int targetId, int sourceId) { if (!m_model->isClip(targetId) || !m_model->isClip(sourceId)) { return; } std::shared_ptr sourceStack = m_model->getClipEffectStackModel(sourceId); std::shared_ptr destStack = m_model->getClipEffectStackModel(targetId); destStack->importEffects(sourceStack); } diff --git a/src/timeline2/view/timelinecontroller.h b/src/timeline2/view/timelinecontroller.h index 34955f474..c66e0e81d 100644 --- a/src/timeline2/view/timelinecontroller.h +++ b/src/timeline2/view/timelinecontroller.h @@ -1,438 +1,438 @@ /*************************************************************************** * Copyright (C) 2017 by 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 . * ***************************************************************************/ #ifndef TIMELINECONTROLLER_H #define TIMELINECONTROLLER_H #include "definitions.h" #include "timeline2/model/timelineitemmodel.hpp" #include "timelinewidget.h" class PreviewManager; class QAction; // see https://bugreports.qt.io/browse/QTBUG-57714, don't expose a QWidget as a context property class TimelineController : public QObject { Q_OBJECT /* @brief holds a list of currently selected clips (list of clipId's) */ Q_PROPERTY(QList selection READ selection WRITE setSelection NOTIFY selectionChanged) /* @brief holds the timeline zoom factor */ Q_PROPERTY(double scaleFactor READ scaleFactor WRITE setScaleFactor NOTIFY scaleFactorChanged) /* @brief holds the current project duration */ Q_PROPERTY(int duration READ duration NOTIFY durationChanged) Q_PROPERTY(bool audioThumbFormat READ audioThumbFormat NOTIFY audioThumbFormatChanged) /* @brief holds the current timeline position */ Q_PROPERTY(int position READ position WRITE setPosition NOTIFY positionChanged) Q_PROPERTY(int zoneIn READ zoneIn WRITE setZoneIn NOTIFY zoneChanged) Q_PROPERTY(int zoneOut READ zoneOut WRITE setZoneOut NOTIFY zoneChanged) Q_PROPERTY(int seekPosition READ seekPosition WRITE setSeekPosition NOTIFY seekPositionChanged) Q_PROPERTY(bool ripple READ ripple NOTIFY rippleChanged) Q_PROPERTY(bool scrub READ scrub NOTIFY scrubChanged) Q_PROPERTY(bool showThumbnails READ showThumbnails NOTIFY showThumbnailsChanged) Q_PROPERTY(bool showMarkers READ showMarkers NOTIFY showMarkersChanged) Q_PROPERTY(bool showAudioThumbnails READ showAudioThumbnails NOTIFY showAudioThumbnailsChanged) Q_PROPERTY(QVariantList dirtyChunks READ dirtyChunks NOTIFY dirtyChunksChanged) Q_PROPERTY(QVariantList renderedChunks READ renderedChunks NOTIFY renderedChunksChanged) Q_PROPERTY(int workingPreview READ workingPreview NOTIFY workingPreviewChanged) Q_PROPERTY(bool useRuler READ useRuler NOTIFY useRulerChanged) Q_PROPERTY(int activeTrack READ activeTrack WRITE setActiveTrack NOTIFY activeTrackChanged) Q_PROPERTY(int audioTarget READ audioTarget WRITE setAudioTarget NOTIFY audioTargetChanged) Q_PROPERTY(int videoTarget READ videoTarget WRITE setVideoTarget NOTIFY videoTargetChanged) public: TimelineController(KActionCollection *actionCollection, QObject *parent); virtual ~TimelineController(); /* @brief Sets the model that this widgets displays */ void setModel(std::shared_ptr model); std::shared_ptr getModel() const; void setRoot(QQuickItem *root); Q_INVOKABLE bool isMultitrackSelected() const { return m_selection.isMultitrackSelected; } Q_INVOKABLE int selectedTrack() const { return m_selection.selectedTrack; } /* @brief Add a clip id to current selection */ Q_INVOKABLE void addSelection(int newSelection); /* @brief Clear current selection and inform the view */ void clearSelection(); /* @brief returns current timeline's zoom factor */ Q_INVOKABLE double scaleFactor() const; /* @brief set current timeline's zoom factor */ void setScaleFactorOnMouse(double scale, bool zoomOnMouse); Q_INVOKABLE void setScaleFactor(double scale); /* @brief Returns the project's duration (tractor) */ Q_INVOKABLE int duration() const; /* @brief Returns the current cursor position (frame currently displayed by MLT) */ Q_INVOKABLE int position() const { return m_position; } /* @brief Returns the seek request position (-1 = no seek pending) */ Q_INVOKABLE int seekPosition() const { return m_seekPosition; } Q_INVOKABLE int audioTarget() const; Q_INVOKABLE int videoTarget() const; Q_INVOKABLE int activeTrack() const { return m_activeTrack; } /* @brief Request a seek operation @param position is the desired new timeline position */ Q_INVOKABLE int zoneIn() const { return m_zone.x(); } Q_INVOKABLE int zoneOut() const { return m_zone.y(); } Q_INVOKABLE void setZoneIn(int inPoint); Q_INVOKABLE void setZoneOut(int outPoint); void setZone(const QPoint &zone); /* @brief Request a seek operation @param position is the desired new timeline position */ Q_INVOKABLE void setPosition(int position); Q_INVOKABLE bool snap(); Q_INVOKABLE bool ripple(); Q_INVOKABLE bool scrub(); Q_INVOKABLE QString timecode(int frames); /* @brief Request inserting a new clip in timeline (dragged from bin or monitor) @param tid is the destination track @param position is the timeline position @param xml is the data describing the dropped clip @param logUndo if set to false, no undo object is stored @return the id of the inserted clip */ Q_INVOKABLE int insertClip(int tid, int position, const QString &xml, bool logUndo, bool refreshView); /* @brief Request inserting multiple clips into the timeline (dragged from bin or monitor) * @param tid is the destination track * @param position is the timeline position * @param binIds the IDs of the bins being dropped * @param logUndo if set to false, no undo object is stored * @return the ids of the inserted clips */ Q_INVOKABLE QList insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView); /* @brief Request the grouping of the given clips * @param clipIds the ids to be grouped * @return the group id or -1 in case of faiure */ Q_INVOKABLE int groupClips(const QList &clipIds); /* @brief Request the ungrouping of clips * @param clipId the id of a clip belonging to the group * @return true in case of success, false otherwise */ Q_INVOKABLE bool ungroupClips(int clipId); Q_INVOKABLE void copyItem(); Q_INVOKABLE bool pasteItem(int clipId = -1, int tid = -1, int position = -1); /* @brief Request inserting a new composition in timeline (dragged from compositions list) @param tid is the destination track @param position is the timeline position @param transitionId is the data describing the dropped composition @param logUndo if set to false, no undo object is stored @return the id of the inserted composition */ Q_INVOKABLE int insertComposition(int tid, int position, const QString &transitionId, bool logUndo); /* @brief Request deletion of the currently selected clips */ Q_INVOKABLE void deleteSelectedClips(); Q_INVOKABLE void triggerAction(const QString &name); /* @brief Do we want to display video thumbnails */ bool showThumbnails() const; bool showAudioThumbnails() const; bool showMarkers() const; bool audioThumbFormat() const; /* @brief Do we want to display audio thumbnails */ Q_INVOKABLE bool showWaveforms() const; /* @brief Insert a timeline track */ Q_INVOKABLE void addTrack(int tid); /* @brief Remove a timeline track */ Q_INVOKABLE void deleteTrack(int tid); /* @brief Group selected items in timeline */ Q_INVOKABLE void groupSelection(); /* @brief Ungroup selected items in timeline */ Q_INVOKABLE void unGroupSelection(int cid = -1); /* @brief Ask for edit marker dialog */ Q_INVOKABLE void editMarker(const QString &cid, int frame); /* @brief Ask for edit timeline guide dialog */ Q_INVOKABLE void editGuide(int frame = -1); Q_INVOKABLE void moveGuide(int frame, int newFrame); /* @brief Add a timeline guide */ Q_INVOKABLE void switchGuide(int frame = -1, bool deleteOnly = false); /* @brief Request monitor refresh */ Q_INVOKABLE void requestRefresh(); /* @brief Show the asset of the given item in the AssetPanel If the id corresponds to a clip, we show the corresponding effect stack If the id corresponds to a composition, we show its properties */ Q_INVOKABLE void showAsset(int id); Q_INVOKABLE void showTrackAsset(int trackId); Q_INVOKABLE void selectItems(QVariantList arg, int startFrame, int endFrame, bool addToSelect); Q_INVOKABLE int headerWidth() const; Q_INVOKABLE void setHeaderWidth(int width); /* @brief Seek to next snap point */ void gotoNextSnap(); /* @brief Seek to previous snap point */ void gotoPreviousSnap(); /* @brief Set current item's start point to cursor position */ void setInPoint(); /* @brief Set current item's end point to cursor position */ void setOutPoint(); /* @brief Return the project's tractor */ Mlt::Tractor *tractor(); /* @brief Sets the list of currently selected clips @param selection is the list of id's @param trackIndex is current clip's track @param isMultitrack is true if we want to select the whole tractor (currently unused) */ void setSelection(const QList &selection = QList(), int trackIndex = -1, bool isMultitrack = false); /* @brief Get the list of currenly selected clip id's */ QList selection() const; /* @brief Add an asset (effect, composition) */ void addAsset(const QVariantMap data); /* @brief Cuts the clip on current track at timeline position */ Q_INVOKABLE void cutClipUnderCursor(int position = -1, int track = -1); /* @brief Request a spacer operation */ Q_INVOKABLE int requestSpacerStartOperation(int trackId, int position); /* @brief Request a spacer operation */ Q_INVOKABLE bool requestSpacerEndOperation(int clipId, int startPosition, int endPosition); /* @brief Request a Fade in effect for clip */ Q_INVOKABLE void adjustFade(int cid, const QString &effectId, int duration, int initialDuration); Q_INVOKABLE const QString getTrackNameFromMltIndex(int trackPos); /* @brief Request inserting space in a track */ Q_INVOKABLE void insertSpace(int trackId = -1, int frame = -1); Q_INVOKABLE void removeSpace(int trackId = -1, int frame = -1, bool affectAllTracks = false); /* @brief Change a clip status (normal / audio only / video only) */ - Q_INVOKABLE void setClipStatus(int clipId, int status); + Q_INVOKABLE void setClipStatus(int clipId, PlaylistState status); Q_INVOKABLE void requestClipCut(int clipId, int position); Q_INVOKABLE void extract(int clipId); Q_INVOKABLE void splitAudio(int clipId); /* @brief Seeks to selected clip start / end */ Q_INVOKABLE void pasteEffects(int targetId, int sourceId); void switchTrackLock(bool applyToAll = false); void switchTargetTrack(); const QString getTrackNameFromIndex(int trackIndex); /* @brief Seeks to selected clip start / end */ void seekCurrentClip(bool seekToEnd = false); /* @brief Seeks to a clip start (or end) based on it's clip id */ void seekToClip(int cid, bool seekToEnd); /* @brief Returns the number of tracks (audioTrakcs, videoTracks) */ QPoint getTracksCount() const; /* @brief Request monitor refresh if item (clip or composition) is under timeline cursor */ void refreshItem(int id); /* @brief Seek timeline to mouse position */ void seekToMouse(); /* @brief User enabled / disabled snapping, update timeline behavior */ void snapChanged(bool snap); /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Get the frame where mouse is positionned */ int getMousePos(); /* @brief Get the frame where mouse is positionned */ int getMouseTrack(); /* @brief Returns a map of track ids/track names */ QMap getTrackNames(bool videoOnly); /* @brief Returns the transition a track index for a composition (MLT index / Track id) */ QPair getCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); /* @brief Return true if composition's a_track is automatic (no forced track) */ bool compositionAutoTrack(int cid) const; const QString getClipBinId(int clipId) const; void focusItem(int itemId); /* @brief Create and display a split clip view to compare effect */ bool createSplitOverlay(Mlt::Filter *filter); /* @brief Delete the split clip view to compare effect */ void removeSplitOverlay(); /* @brief Add current timeline zone to preview rendering */ void addPreviewRange(bool add); /* @brief Clear current timeline zone from preview rendering */ void clearPreviewRange(); void startPreviewRender(); void stopPreviewRender(); QVariantList dirtyChunks() const; QVariantList renderedChunks() const; /* @brief returns the frame currently processed by timeline preview, -1 if none */ int workingPreview() const; /** @brief Return true if we want to use timeline ruler zone for editing */ bool useRuler() const; /* @brief Load timeline preview from saved doc */ void loadPreview(QString chunks, QString dirty, const QDateTime &documentDate, int enable); /* @brief Return document properties with added settings from timeline */ QMap documentProperties(); /** @brief Change track compsiting mode */ void switchCompositing(int mode); /** @brief Change a clip item's speed in timeline */ Q_INVOKABLE void changeItemSpeed(int clipId, int speed); /** @brief Delete selected zone and fill gap by moving following clips*/ void extractZone(QPoint zone); /** @brief Delete selected zone */ void liftZone(QPoint zone); bool insertZone(const QString &binId, QPoint zone, bool overwrite); void updateClip(int clipId, QVector roles); void showClipKeyframes(int clipId, bool value); void showCompositionKeyframes(int clipId, bool value); /** @brief Returns last usable timeline position (seek request or current pos) */ int timelinePosition() const; /** @brief Adjust all timeline tracks height */ void resetTrackHeight(); /** @brief timeline preview params changed, reset */ void resetPreview(); /** @brief Select the clip in active track under cursor */ void selectCurrentItem(ObjectType type, bool select, bool addToCurrent = false); public slots: void selectMultitrack(); Q_INVOKABLE void setSeekPosition(int position); Q_INVOKABLE void setAudioTarget(int track); Q_INVOKABLE void setVideoTarget(int track); Q_INVOKABLE void setActiveTrack(int track); void onSeeked(int position); void addEffectToCurrentClip(const QStringList &effectData); /** @brief Dis / enable timeline preview. */ void disablePreview(bool disable); void invalidateClip(int cid); void invalidateZone(int in, int out); void checkDuration(); private: QQuickItem *m_root; KActionCollection *m_actionCollection; std::shared_ptr m_model; bool m_usePreview; struct Selection { QList selectedItems; int selectedTrack; bool isMultitrackSelected; }; int m_position; int m_seekPosition; int m_audioTarget; int m_videoTarget; int m_activeTrack; QPoint m_zone; double m_scale; static int m_duration; Selection m_selection; Selection m_savedSelection; PreviewManager *m_timelinePreview; QAction *m_disablePreview; void emitSelectedFromSelection(); int getCurrentItem(); void initializePreview(); // Get a list of currently selected items, including clips grouped with selection std::unordered_set getCurrentSelectionIds() const; signals: void selected(Mlt::Producer *producer); void selectionChanged(); void frameFormatChanged(); void trackHeightChanged(); void scaleFactorChanged(); void audioThumbFormatChanged(); void durationChanged(); void positionChanged(); void seekPositionChanged(); void audioTargetChanged(); void videoTargetChanged(); void activeTrackChanged(); void showThumbnailsChanged(); void showAudioThumbnailsChanged(); void showMarkersChanged(); void rippleChanged(); void scrubChanged(); void seeked(int position); void zoneChanged(); void zoneMoved(const QPoint &zone); /* @brief Requests that a given parameter model is displayed in the asset panel */ void showTransitionModel(int tid, std::shared_ptr); void showItemEffectStack(const QString &clipName, std::shared_ptr, QSize frameSize, bool showKeyframes); /* @brief notify of chunks change */ void dirtyChunksChanged(); void renderedChunksChanged(); void workingPreviewChanged(); void useRulerChanged(); }; #endif diff --git a/tests/groupstest.cpp b/tests/groupstest.cpp index 126455a85..270d386f2 100644 --- a/tests/groupstest.cpp +++ b/tests/groupstest.cpp @@ -1,1194 +1,1194 @@ #include "bin/model/markerlistmodel.hpp" #include "catch.hpp" #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #pragma GCC diagnostic push #include "fakeit.hpp" #include #include #define private public #define protected public #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/docundostack.hpp" #include "project/projectmanager.h" #include "test_utils.hpp" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/timelinemodel.hpp" #include "timeline2/model/trackmodel.hpp" #include #include Mlt::Profile profile_group; TEST_CASE("Functional test of the group hierarchy", "[GroupsModel]") { 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; std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack); GroupsModel groups(timeline); std::function undo = []() { return true; }; std::function redo = []() { return true; }; for (int i = 0; i < 10; i++) { groups.createGroupItem(i); } SECTION("Test Basic Creation") { for (int i = 0; i < 10; i++) { REQUIRE(groups.getRootId(i) == i); REQUIRE(groups.isLeaf(i)); REQUIRE(groups.getLeaves(i).size() == 1); REQUIRE(groups.getSubtree(i).size() == 1); } } groups.setGroup(0, 1); groups.setGroup(1, 2); groups.setGroup(3, 2); groups.setGroup(9, 3); groups.setGroup(6, 3); groups.setGroup(4, 3); groups.setGroup(7, 3); groups.setGroup(8, 5); SECTION("Test leaf nodes") { std::unordered_set nodes = {1, 2, 3, 5}; for (int i = 0; i < 10; i++) { REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0)); if (nodes.count(i) == 0) { REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } } } SECTION("Test leaves retrieving") { REQUIRE(groups.getLeaves(2) == std::unordered_set({0, 4, 6, 7, 9})); REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(1) == std::unordered_set({0})); REQUIRE(groups.getLeaves(5) == std::unordered_set({8})); } SECTION("Test subtree retrieving") { REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(5) == std::unordered_set({5, 8})); } SECTION("Test root retieving") { std::set first_tree = {0, 1, 2, 3, 4, 6, 7, 9}; for (int n : first_tree) { CAPTURE(n); REQUIRE(groups.getRootId(n) == 2); } std::unordered_set second_tree = {5, 8}; for (int n : second_tree) { REQUIRE(groups.getRootId(n) == 5); } } groups.setGroup(3, 8); SECTION("Test leaf nodes 2") { std::unordered_set nodes = {1, 2, 3, 5, 8}; for (int i = 0; i < 10; i++) { REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0)); if (nodes.count(i) == 0) { REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } } } SECTION("Test leaves retrieving 2") { REQUIRE(groups.getLeaves(1) == std::unordered_set({0})); REQUIRE(groups.getLeaves(2) == std::unordered_set({0})); REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(5) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(8) == std::unordered_set({4, 6, 7, 9})); } SECTION("Test subtree retrieving 2") { REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2})); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(5) == std::unordered_set({5, 8, 3, 4, 6, 7, 9})); } SECTION("Test root retieving 2") { std::set first_tree = {0, 1, 2}; for (int n : first_tree) { CAPTURE(n); REQUIRE(groups.getRootId(n) == 2); } std::unordered_set second_tree = {5, 8, 3, 4, 6, 7, 9}; for (int n : second_tree) { REQUIRE(groups.getRootId(n) == 5); } } groups.setGroup(5, 2); SECTION("Test leaf nodes 3") { std::unordered_set nodes = {1, 2, 3, 5, 8}; for (int i = 0; i < 10; i++) { REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0)); if (nodes.count(i) == 0) { REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } } } SECTION("Test leaves retrieving 3") { REQUIRE(groups.getLeaves(1) == std::unordered_set({0})); REQUIRE(groups.getLeaves(2) == std::unordered_set({0, 4, 6, 7, 9})); REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(5) == std::unordered_set({4, 6, 7, 9})); REQUIRE(groups.getLeaves(8) == std::unordered_set({4, 6, 7, 9})); } SECTION("Test subtree retrieving 3") { REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(5) == std::unordered_set({5, 8, 3, 4, 6, 7, 9})); } SECTION("Test root retieving 3") { for (int i = 0; i < 10; i++) { CAPTURE(i); REQUIRE(groups.getRootId(i) == 2); } } groups.destructGroupItem(8, false, undo, redo); SECTION("Test leaf nodes 4") { std::unordered_set nodes = {1, 2, 3}; for (int i = 0; i < 10; i++) { if (i == 8) continue; REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0)); if (nodes.count(i) == 0) { REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } } } SECTION("Test leaves retrieving 4") { REQUIRE(groups.getLeaves(1) == std::unordered_set({0})); REQUIRE(groups.getLeaves(2) == std::unordered_set({0, 5})); REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9})); } SECTION("Test subtree retrieving 4") { REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 5})); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9})); REQUIRE(groups.getSubtree(5) == std::unordered_set({5})); } SECTION("Test root retieving 4") { std::set first_tree = {0, 1, 2, 5}; for (int n : first_tree) { CAPTURE(n); REQUIRE(groups.getRootId(n) == 2); } std::unordered_set second_tree = {3, 4, 6, 7, 9}; for (int n : second_tree) { CAPTURE(n); REQUIRE(groups.getRootId(n) == 3); } } } TEST_CASE("Interface test of the group hierarchy", "[GroupsModel]") { 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; std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack); GroupsModel groups(timeline); std::function undo = []() { return true; }; std::function redo = []() { return true; }; for (int i = 0; i < 10; i++) { groups.createGroupItem(i); // the following call shouldn't do anything, but we test that behaviour too. groups.ungroupItem(i, undo, redo); REQUIRE(groups.getRootId(i) == i); REQUIRE(groups.isLeaf(i)); REQUIRE(groups.getLeaves(i).size() == 1); REQUIRE(groups.getSubtree(i).size() == 1); } auto g1 = std::unordered_set({4, 6, 7, 9}); int gid1 = groups.groupItems(g1, undo, redo); SECTION("One single group") { for (int i = 0; i < 10; i++) { if (g1.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid1); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); auto g1b = g1; g1b.insert(gid1); REQUIRE(groups.getSubtree(gid1) == g1b); } SECTION("Twice the same group") { int old_gid1 = gid1; gid1 = groups.groupItems(g1, undo, redo); // recreate the same group (will create a parent with the old group as only element) for (int i = 0; i < 10; i++) { if (g1.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid1); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); REQUIRE(groups.getLeaves(old_gid1) == g1); auto g1b = g1; g1b.insert(old_gid1); REQUIRE(groups.getSubtree(old_gid1) == g1b); g1b.insert(gid1); REQUIRE(groups.getSubtree(gid1) == g1b); } auto g2 = std::unordered_set({3, 5, 7}); int gid2 = groups.groupItems(g2, undo, redo); auto all_g2 = g2; all_g2.insert(4); all_g2.insert(6); all_g2.insert(9); SECTION("Heterogeneous group") { for (int i = 0; i < 10; i++) { CAPTURE(i); if (all_g2.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid2); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); REQUIRE(groups.getLeaves(gid2) == all_g2); } auto g3 = std::unordered_set({0, 1}); int gid3 = groups.groupItems(g3, undo, redo); auto g4 = std::unordered_set({0, 4}); int gid4 = groups.groupItems(g4, undo, redo); auto all_g4 = all_g2; for (int i : g3) all_g4.insert(i); SECTION("Group of group") { for (int i = 0; i < 10; i++) { CAPTURE(i); if (all_g4.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid4); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); REQUIRE(groups.getLeaves(gid2) == all_g2); REQUIRE(groups.getLeaves(gid3) == g3); REQUIRE(groups.getLeaves(gid4) == all_g4); } // the following should delete g4 groups.ungroupItem(3, undo, redo); SECTION("Ungroup") { for (int i = 0; i < 10; i++) { CAPTURE(i); if (all_g2.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid2); } else if (g3.count(i) > 0) { REQUIRE(groups.getRootId(i) == gid3); } else { REQUIRE(groups.getRootId(i) == i); } REQUIRE(groups.getSubtree(i) == std::unordered_set({i})); REQUIRE(groups.getLeaves(i) == std::unordered_set({i})); } REQUIRE(groups.getLeaves(gid1) == g1); REQUIRE(groups.getLeaves(gid2) == all_g2); REQUIRE(groups.getLeaves(gid3) == g3); } } TEST_CASE("Orphan groups deletion", "[GroupsModel]") { 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; std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack); GroupsModel groups(timeline); std::function undo = []() { return true; }; std::function redo = []() { return true; }; for (int i = 0; i < 4; i++) { groups.createGroupItem(i); } auto g1 = std::unordered_set({0, 1}); int gid1 = groups.groupItems(g1, undo, redo); auto g2 = std::unordered_set({2, 3}); int gid2 = groups.groupItems(g2, undo, redo); auto g3 = std::unordered_set({0, 3}); int gid3 = groups.groupItems(g3, undo, redo); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({0, 1, 2, 3})); groups.destructGroupItem(0, true, undo, redo); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({1, 2, 3})); SECTION("Normal deletion") { groups.destructGroupItem(1, false, undo, redo); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({gid1, 2, 3})); groups.destructGroupItem(gid1, true, undo, redo); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({2, 3})); } SECTION("Cascade deletion") { groups.destructGroupItem(1, true, undo, redo); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({2, 3})); groups.destructGroupItem(2, true, undo, redo); REQUIRE(groups.getLeaves(gid3) == std::unordered_set({3})); REQUIRE(groups.m_downLink.count(gid3) > 0); groups.destructGroupItem(3, true, undo, redo); REQUIRE(groups.m_downLink.count(gid3) == 0); REQUIRE(groups.m_downLink.size() == 0); } } TEST_CASE("Undo/redo", "[GroupsModel]") { qDebug() << "STARTING PASS"; 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; TimelineItemModel tim(new Mlt::Profile(), undoStack); Mock timMock(tim); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); TimelineItemModel tim2(new Mlt::Profile(), undoStack); Mock timMock2(tim2); TimelineItemModel &tt2 = timMock2.get(); auto timeline2 = std::shared_ptr(&timMock2.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline2, guideModel); RESET(timMock); RESET(timMock2); Mlt::Profile *pr = new Mlt::Profile(); QString binId = createProducer(*pr, "red", binModel); int length = binModel->getClipByBinID(binId)->frameDuration(); GroupsModel groups(timeline); std::vector clips; for (int i = 0; i < 4; i++) { - clips.push_back(ClipModel::construct(timeline, binId)); + clips.push_back(ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly)); } std::vector clips2; for (int i = 0; i < 4; i++) { - clips2.push_back(ClipModel::construct(timeline2, binId)); + clips2.push_back(ClipModel::construct(timeline2, binId, -1, PlaylistState::VideoOnly)); } int tid1 = TrackModel::construct(timeline); int tid2 = TrackModel::construct(timeline); int tid1_2 = TrackModel::construct(timeline2); int tid2_2 = TrackModel::construct(timeline2); int init_index = undoStack->index(); SECTION("Basic Creation and export/import from json") { auto check_roots = [&](int r1, int r2, int r3, int r4) { REQUIRE(timeline->m_groups->getRootId(clips[0]) == r1); REQUIRE(timeline->m_groups->getRootId(clips[1]) == r2); REQUIRE(timeline->m_groups->getRootId(clips[2]) == r3); REQUIRE(timeline->m_groups->getRootId(clips[3]) == r4); }; // the following function is a recursive function to check the correctness of a json import // Basically, it takes as input a groupId in the imported (target) group hierarchy, and outputs the corresponding groupId from the original one. If no // match is found, it returns -1 std::function rec_check; rec_check = [&](int gid) { // we first check if the gid is a leaf if (timeline2->m_groups->isLeaf(gid)) { // then it must be a clip/composition int found = -1; for (int i = 0; i < 4; i++) { if (clips2[i] == gid) { found = i; break; } } if (found != -1) { return clips[found]; } else { qDebug() << "ERROR: did not find correspondance for group" << gid; } } else { // we find correspondances of all the children auto children = timeline2->m_groups->getDirectChildren(gid); std::unordered_set corresp; for (int c : children) { corresp.insert(rec_check(c)); } if (corresp.count(-1) > 0) { return -1; // something went wrong } std::unordered_set parents; for (int c : corresp) { // we find the parents of the corresponding groups in the original hierarchy parents.insert(timeline->m_groups->m_upLink[c]); } // if the matching is correct, we should have found only one parent if (parents.size() != 1) { return -1; // something went wrong } return *parents.begin(); } return -1; }; auto checkJsonParsing = [&]() { // we first destroy all groups in target timeline Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (int i = 0; i < 4; i++) { while (timeline2->m_groups->getRootId(clips2[i]) != clips2[i]) { timeline2->m_groups->ungroupItem(clips2[i], undo, redo); } } // we do the export then import REQUIRE(timeline2->m_groups->fromJson(timeline->m_groups->toJson())); std::unordered_map roots; for (int i = 0; i < 4; i++) { int r = timeline2->m_groups->getRootId(clips2[0]); if (roots.count(r) == 0) { roots[r] = rec_check(r); REQUIRE(roots[r] != -1); } } for (int i = 0; i < 4; i++) { int r = timeline->m_groups->getRootId(clips[0]); int r2 = timeline2->m_groups->getRootId(clips2[0]); REQUIRE(roots[r2] == r); } }; auto g1 = std::unordered_set({clips[0], clips[1]}); int gid1, gid2, gid3; // this fails because clips are not inserted REQUIRE(timeline->requestClipsGroup(g1) == -1); for (int i = 0; i < 4; i++) { REQUIRE(timeline->requestClipMove(clips[i], (i % 2 == 0) ? tid1 : tid2, i * length)); } for (int i = 0; i < 4; i++) { REQUIRE(timeline2->requestClipMove(clips2[i], (i % 2 == 0) ? tid1_2 : tid2_2, i * length)); } init_index = undoStack->index(); REQUIRE(timeline->requestClipsGroup(g1, true, GroupType::Normal) > 0); auto state1 = [&]() { gid1 = timeline->m_groups->getRootId(clips[0]); check_roots(gid1, gid1, clips[2], clips[3]); REQUIRE(timeline->m_groups->getType(gid1) == GroupType::Normal); REQUIRE(timeline->m_groups->getSubtree(gid1) == std::unordered_set({gid1, clips[0], clips[1]})); REQUIRE(timeline->m_groups->getLeaves(gid1) == std::unordered_set({clips[0], clips[1]})); REQUIRE(undoStack->index() == init_index + 1); }; INFO("Test 1"); checkJsonParsing(); state1(); auto g2 = std::unordered_set({clips[2], clips[3]}); REQUIRE(timeline->requestClipsGroup(g2, true, GroupType::AVSplit) > 0); auto state2 = [&]() { gid2 = timeline->m_groups->getRootId(clips[2]); check_roots(gid1, gid1, gid2, gid2); REQUIRE(timeline->m_groups->getType(gid1) == GroupType::Normal); REQUIRE(timeline->m_groups->getType(gid2) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getSubtree(gid2) == std::unordered_set({gid2, clips[2], clips[3]})); REQUIRE(timeline->m_groups->getLeaves(gid2) == std::unordered_set({clips[2], clips[3]})); REQUIRE(timeline->m_groups->getSubtree(gid1) == std::unordered_set({gid1, clips[0], clips[1]})); REQUIRE(timeline->m_groups->getLeaves(gid1) == std::unordered_set({clips[0], clips[1]})); REQUIRE(undoStack->index() == init_index + 2); }; INFO("Test 2"); checkJsonParsing(); state2(); auto g3 = std::unordered_set({clips[0], clips[3]}); REQUIRE(timeline->requestClipsGroup(g3, true, GroupType::Normal) > 0); auto state3 = [&]() { REQUIRE(undoStack->index() == init_index + 3); gid3 = timeline->m_groups->getRootId(clips[0]); check_roots(gid3, gid3, gid3, gid3); REQUIRE(timeline->m_groups->getType(gid1) == GroupType::Normal); REQUIRE(timeline->m_groups->getType(gid2) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(gid3) == GroupType::Normal); REQUIRE(timeline->m_groups->getSubtree(gid3) == std::unordered_set({gid1, clips[0], clips[1], gid3, gid2, clips[2], clips[3]})); REQUIRE(timeline->m_groups->getLeaves(gid3) == std::unordered_set({clips[2], clips[3], clips[0], clips[1]})); REQUIRE(timeline->m_groups->getSubtree(gid2) == std::unordered_set({gid2, clips[2], clips[3]})); REQUIRE(timeline->m_groups->getLeaves(gid2) == std::unordered_set({clips[2], clips[3]})); REQUIRE(timeline->m_groups->getSubtree(gid1) == std::unordered_set({gid1, clips[0], clips[1]})); REQUIRE(timeline->m_groups->getLeaves(gid1) == std::unordered_set({clips[0], clips[1]})); }; INFO("Test 3"); checkJsonParsing(); state3(); undoStack->undo(); INFO("Test 4"); checkJsonParsing(); state2(); undoStack->redo(); INFO("Test 5"); checkJsonParsing(); state3(); undoStack->undo(); INFO("Test 6"); checkJsonParsing(); state2(); undoStack->undo(); INFO("Test 8"); checkJsonParsing(); state1(); undoStack->undo(); INFO("Test 9"); checkJsonParsing(); check_roots(clips[0], clips[1], clips[2], clips[3]); undoStack->redo(); INFO("Test 10"); checkJsonParsing(); state1(); undoStack->redo(); INFO("Test 11"); checkJsonParsing(); state2(); REQUIRE(timeline->requestClipsGroup(g3) > 0); checkJsonParsing(); state3(); undoStack->undo(); checkJsonParsing(); state2(); undoStack->undo(); checkJsonParsing(); state1(); undoStack->undo(); checkJsonParsing(); check_roots(clips[0], clips[1], clips[2], clips[3]); } SECTION("Group deletion undo") { int tid1 = TrackModel::construct(timeline); CAPTURE(clips[0]); CAPTURE(clips[1]); CAPTURE(clips[2]); CAPTURE(clips[3]); REQUIRE(timeline->requestClipMove(clips[0], tid1, 10)); REQUIRE(timeline->requestClipMove(clips[1], tid1, 10 + length)); REQUIRE(timeline->requestClipMove(clips[2], tid1, 15 + 2 * length)); REQUIRE(timeline->requestClipMove(clips[3], tid1, 50 + 3 * length)); auto state0 = [&]() { REQUIRE(timeline->getTrackById(tid1)->checkConsistency()); REQUIRE(timeline->getTrackClipsCount(tid1) == 4); for (int i = 0; i < 4; i++) { REQUIRE(timeline->getClipTrackId(clips[i]) == tid1); } REQUIRE(timeline->getClipPosition(clips[0]) == 10); REQUIRE(timeline->getClipPosition(clips[1]) == 10 + length); REQUIRE(timeline->getClipPosition(clips[2]) == 15 + 2 * length); REQUIRE(timeline->getClipPosition(clips[3]) == 50 + 3 * length); }; auto state = [&](int gid1, int gid2, int gid3) { state0(); REQUIRE(timeline->m_groups->getType(gid1) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(gid2) == GroupType::AVSplit); REQUIRE(timeline->m_groups->getType(gid3) == GroupType::Normal); }; state0(); auto g1 = std::unordered_set({clips[0], clips[1]}); int gid1, gid2, gid3; gid1 = timeline->requestClipsGroup(g1, true, GroupType::AVSplit); REQUIRE(gid1 > 0); auto g2 = std::unordered_set({clips[2], clips[3]}); gid2 = timeline->requestClipsGroup(g2, true, GroupType::AVSplit); REQUIRE(gid2 > 0); auto g3 = std::unordered_set({clips[0], clips[3]}); gid3 = timeline->requestClipsGroup(g3, true, GroupType::Normal); REQUIRE(gid3 > 0); state(gid1, gid2, gid3); for (int i = 0; i < 4; i++) { REQUIRE(timeline->requestItemDeletion(clips[i])); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getClipsCount() == 0); REQUIRE(timeline->getTrackById(tid1)->checkConsistency()); undoStack->undo(); state(gid1, gid2, gid3); undoStack->redo(); REQUIRE(timeline->getTrackClipsCount(tid1) == 0); REQUIRE(timeline->getClipsCount() == 0); REQUIRE(timeline->getTrackById(tid1)->checkConsistency()); undoStack->undo(); state(gid1, gid2, gid3); } // we undo the three grouping operations undoStack->undo(); state0(); undoStack->undo(); state0(); undoStack->undo(); state0(); } SECTION("Group creation and query from timeline") { REQUIRE(timeline->requestClipMove(clips[0], tid1, 10)); REQUIRE(timeline->requestClipMove(clips[1], tid1, 10 + length)); REQUIRE(timeline->requestClipMove(clips[2], tid1, 15 + 2 * length)); REQUIRE(timeline->requestClipMove(clips[3], tid1, 50 + 3 * length)); auto state1 = [&]() { REQUIRE(timeline->getGroupElements(clips[2]) == std::unordered_set({clips[2]})); REQUIRE(timeline->getGroupElements(clips[1]) == std::unordered_set({clips[1]})); REQUIRE(timeline->getGroupElements(clips[3]) == std::unordered_set({clips[3]})); REQUIRE(timeline->getGroupElements(clips[0]) == std::unordered_set({clips[0]})); }; state1(); auto g1 = std::unordered_set({clips[0], clips[3]}); int gid1, gid2, gid3; REQUIRE(timeline->requestClipsGroup(g1) > 0); auto state2 = [&]() { REQUIRE(timeline->getGroupElements(clips[0]) == g1); REQUIRE(timeline->getGroupElements(clips[3]) == g1); REQUIRE(timeline->getGroupElements(clips[2]) == std::unordered_set({clips[2]})); REQUIRE(timeline->getGroupElements(clips[1]) == std::unordered_set({clips[1]})); }; state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); undoStack->undo(); state1(); undoStack->redo(); state2(); auto g2 = std::unordered_set({clips[2], clips[1]}); REQUIRE(timeline->requestClipsGroup(g2) > 0); auto state3 = [&]() { REQUIRE(timeline->getGroupElements(clips[0]) == g1); REQUIRE(timeline->getGroupElements(clips[3]) == g1); REQUIRE(timeline->getGroupElements(clips[2]) == g2); REQUIRE(timeline->getGroupElements(clips[1]) == g2); }; state3(); undoStack->undo(); state2(); undoStack->redo(); state3(); auto g3 = std::unordered_set({clips[0], clips[1]}); REQUIRE(timeline->requestClipsGroup(g3) > 0); auto all_g = std::unordered_set({clips[0], clips[1], clips[2], clips[3]}); auto state4 = [&]() { REQUIRE(timeline->getGroupElements(clips[0]) == all_g); REQUIRE(timeline->getGroupElements(clips[3]) == all_g); REQUIRE(timeline->getGroupElements(clips[2]) == all_g); REQUIRE(timeline->getGroupElements(clips[1]) == all_g); }; state4(); undoStack->undo(); state3(); undoStack->redo(); state4(); REQUIRE(timeline->requestClipUngroup(clips[0])); state3(); undoStack->undo(); state4(); REQUIRE(timeline->requestClipUngroup(clips[1])); state3(); undoStack->undo(); state4(); undoStack->redo(); state3(); REQUIRE(timeline->requestClipUngroup(clips[0])); REQUIRE(timeline->getGroupElements(clips[2]) == g2); REQUIRE(timeline->getGroupElements(clips[1]) == g2); REQUIRE(timeline->getGroupElements(clips[3]) == std::unordered_set({clips[3]})); REQUIRE(timeline->getGroupElements(clips[0]) == std::unordered_set({clips[0]})); REQUIRE(timeline->requestClipUngroup(clips[1])); state1(); } SECTION("MergeSingleGroups") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(groups.m_upLink.size() == 0); for (int i = 0; i < 6; i++) { groups.createGroupItem(i); } groups.setGroup(0, 3); groups.setGroup(2, 4); groups.setGroup(3, 1); groups.setGroup(4, 1); groups.setGroup(5, 0); auto test_tree = [&]() { REQUIRE(groups.getSubtree(1) == std::unordered_set({0, 1, 2, 3, 4, 5})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({5})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({3, 4})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({0})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({2})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); }; test_tree(); REQUIRE(groups.mergeSingleGroups(1, undo, redo)); auto test_tree2 = [&]() { REQUIRE(groups.getSubtree(1) == std::unordered_set({1, 2, 5})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({2, 5})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("MergeSingleGroups2") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(groups.m_upLink.size() == 0); for (int i = 0; i < 3; i++) { groups.createGroupItem(i); } groups.setGroup(1, 0); groups.setGroup(2, 1); auto test_tree = [&]() { REQUIRE(groups.getSubtree(0) == std::unordered_set({0, 1, 2})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({2})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); }; test_tree(); REQUIRE(groups.mergeSingleGroups(0, undo, redo)); auto test_tree2 = [&]() { REQUIRE(groups.getSubtree(2) == std::unordered_set({2})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getRootId(2) == 2); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("MergeSingleGroups3") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(groups.m_upLink.size() == 0); for (int i = 0; i < 6; i++) { groups.createGroupItem(i); } groups.setGroup(0, 2); groups.setGroup(1, 0); groups.setGroup(3, 1); groups.setGroup(4, 1); groups.setGroup(5, 4); auto test_tree = [&]() { for (int i = 0; i < 6; i++) { REQUIRE(groups.getRootId(i) == 2); } REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 5})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({4, 3})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({0})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({5})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); }; test_tree(); REQUIRE(groups.mergeSingleGroups(2, undo, redo)); auto test_tree2 = [&]() { REQUIRE(groups.getRootId(1) == 1); REQUIRE(groups.getRootId(3) == 1); REQUIRE(groups.getRootId(5) == 1); REQUIRE(groups.getSubtree(1) == std::unordered_set({1, 3, 5})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({3, 5})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("Split leaf") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(groups.m_upLink.size() == 0); // This is a dummy split criterion auto criterion = [](int a) { return a % 2 == 0; }; auto criterion2 = [](int a) { return a % 2 != 0; }; // We create a leaf groups.createGroupItem(1); auto test_leaf = [&]() { REQUIRE(groups.getRootId(1) == 1); REQUIRE(groups.isLeaf(1)); REQUIRE(groups.m_upLink.size() == 1); }; test_leaf(); REQUIRE(groups.split(1, criterion, undo, redo)); test_leaf(); undo(); test_leaf(); redo(); REQUIRE(groups.split(1, criterion2, undo, redo)); test_leaf(); undo(); test_leaf(); redo(); } SECTION("Simple split Tree") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(groups.m_upLink.size() == 0); // This is a dummy split criterion auto criterion = [](int a) { return a % 2 == 0; }; // We create a very simple tree for (int i = 0; i < 3; i++) { groups.createGroupItem(i); } groups.setGroup(1, 0); groups.setGroup(2, 0); auto test_tree = [&]() { REQUIRE(groups.getRootId(0) == 0); REQUIRE(groups.getRootId(1) == 0); REQUIRE(groups.getRootId(2) == 0); REQUIRE(groups.getSubtree(0) == std::unordered_set({0, 1, 2})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1, 2})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); }; test_tree(); REQUIRE(groups.split(0, criterion, undo, redo)); auto test_tree2 = [&]() { REQUIRE(groups.getRootId(1) == 1); REQUIRE(groups.getRootId(2) == 2); REQUIRE(groups.getSubtree(2) == std::unordered_set({2})); REQUIRE(groups.getSubtree(1) == std::unordered_set({1})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("complex split Tree") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(groups.m_upLink.size() == 0); // This is a dummy split criterion auto criterion = [](int a) { return a % 2 != 0; }; for (int i = 0; i < 9; i++) { groups.createGroupItem(i); } groups.setGroup(0, 3); groups.setGroup(1, 0); groups.setGroup(3, 2); groups.setGroup(4, 3); groups.setGroup(5, 8); groups.setGroup(6, 0); groups.setGroup(7, 8); groups.setGroup(8, 2); auto test_tree = [&]() { for (int i = 0; i < 9; i++) { REQUIRE(groups.getRootId(i) == 2); } REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 5, 6, 7, 8})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1, 6})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({3, 8})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({0, 4})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(6) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(7) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(8) == std::unordered_set({5, 7})); }; test_tree(); REQUIRE(groups.split(2, criterion, undo, redo)); auto test_tree2 = [&]() { REQUIRE(groups.getRootId(6) == 3); REQUIRE(groups.getRootId(3) == 3); REQUIRE(groups.getRootId(4) == 3); REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6})); REQUIRE(groups.getDirectChildren(6) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({6, 4})); // new tree int newRoot = groups.getRootId(1); REQUIRE(groups.getRootId(1) == newRoot); REQUIRE(groups.getRootId(5) == newRoot); REQUIRE(groups.getRootId(7) == newRoot); int other = -1; REQUIRE(groups.getDirectChildren(newRoot).size() == 2); for (int c : groups.getDirectChildren(newRoot)) if (c != 1) other = c; REQUIRE(other != -1); REQUIRE(groups.getSubtree(newRoot) == std::unordered_set({1, 5, 7, newRoot, other})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(7) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(newRoot) == std::unordered_set({1, other})); REQUIRE(groups.getDirectChildren(other) == std::unordered_set({5, 7})); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); } SECTION("Splitting preserves group type") { Fun undo = []() { return true; }; Fun redo = []() { return true; }; REQUIRE(groups.m_upLink.size() == 0); // This is a dummy split criterion auto criterion = [](int a) { return a % 2 == 0; }; // We create a very simple tree for (int i = 0; i <= 6; i++) { groups.createGroupItem(i); } groups.setGroup(0, 4); groups.setGroup(2, 4); groups.setGroup(1, 5); groups.setGroup(3, 5); groups.setGroup(4, 6); groups.setGroup(5, 6); groups.setType(4, GroupType::AVSplit); groups.setType(5, GroupType::AVSplit); groups.setType(6, GroupType::Normal); auto test_tree = [&]() { REQUIRE(groups.m_upLink.size() == 7); for (int i = 0; i <= 6; i++) { REQUIRE(groups.getRootId(i) == 6); } REQUIRE(groups.getSubtree(6) == std::unordered_set({0, 1, 2, 3, 4, 5, 6})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(4) == std::unordered_set({0, 2})); REQUIRE(groups.getDirectChildren(5) == std::unordered_set({1, 3})); REQUIRE(groups.getDirectChildren(6) == std::unordered_set({4, 5})); REQUIRE(groups.getType(4) == GroupType::AVSplit); REQUIRE(groups.getType(5) == GroupType::AVSplit); REQUIRE(groups.getType(6) == GroupType::Normal); }; test_tree(); qDebug() << " done testing"; REQUIRE(groups.split(6, criterion, undo, redo)); qDebug() << " done spliting"; auto test_tree2 = [&]() { // REQUIRE(groups.m_upLink.size() == 6); int r1 = groups.getRootId(0); int r2 = groups.getRootId(1); bool ok = r1 == 4 || r2 == 5; REQUIRE(ok); REQUIRE(groups.getRootId(2) == r1); REQUIRE(groups.getRootId(3) == r2); REQUIRE(groups.getSubtree(r1) == std::unordered_set({r1, 0, 2})); REQUIRE(groups.getSubtree(r2) == std::unordered_set({r2, 1, 3})); REQUIRE(groups.getDirectChildren(0) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(1) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(2) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(3) == std::unordered_set({})); REQUIRE(groups.getDirectChildren(r1) == std::unordered_set({0, 2})); REQUIRE(groups.getDirectChildren(r2) == std::unordered_set({1, 3})); REQUIRE(groups.getType(r1) == GroupType::AVSplit); REQUIRE(groups.getType(r2) == GroupType::AVSplit); }; test_tree2(); undo(); test_tree(); redo(); test_tree2(); undo(); test_tree(); redo(); test_tree2(); REQUIRE_FALSE(true); } } diff --git a/tests/modeltest.cpp b/tests/modeltest.cpp index 363de3cf6..fbc42e62c 100644 --- a/tests/modeltest.cpp +++ b/tests/modeltest.cpp @@ -1,2000 +1,2000 @@ #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]") { 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); TimelineItemModel &tt = timMock.get(); 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()); } } 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); + int id1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 1); REQUIRE(timeline->checkConsistency()); - int id2 = ClipModel::construct(timeline, binId2); + int id2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly); REQUIRE(timeline->getClipsCount() == 2); REQUIRE(timeline->checkConsistency()); - int id3 = ClipModel::construct(timeline, binId); + 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); } 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); TimelineItemModel &tt = timMock.get(); 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); + 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); - int cid3 = ClipModel::construct(timeline, binId3); - int cid4 = ClipModel::construct(timeline, binId2); + 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); + int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); 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("Clip copy") { - int cid6 = ClipModel::construct(timeline, binId); + 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::Original, undo, redo)); + REQUIRE(TimelineFunctions::copyClip(timeline, cid6, newId, PlaylistState::VideoOnly, undo, redo)); REQUIRE(timeline->m_allClips[cid6]->binId() == timeline->m_allClips[newId]->binId()); // TODO check effects } } 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); TimelineItemModel &tt = timMock.get(); 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); + 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()); } 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); TimelineItemModel &tt = timMock.get(); 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); + 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); + 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 shouln't get created { int temp; Fun undo = []() { return true; }; Fun redo = []() { return true; }; - REQUIRE_FALSE(timeline->requestClipCreation("impossible bin id", temp, PlaylistState::Original, undo, redo)); + REQUIRE_FALSE(timeline->requestClipCreation("impossible bin id", temp, PlaylistState::VideoOnly, 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::Original, undo, redo)); + REQUIRE(timeline->requestClipCreation(binId3, cid3, PlaylistState::VideoOnly, 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::Original, undo, redo)); + REQUIRE(timeline->requestClipCreation(binId4, cid4, PlaylistState::VideoOnly, 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); CHECK_MOVE(Once); 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(Once); 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(Once); 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(Once); 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(Once); 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(Once); 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(Once); undoStack->undo(); CHECK_MOVE(Once); 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") { int nb_tracks = timeline->getTracksCount(); 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::Original, undo, redo)); + REQUIRE(timeline->requestClipCreation(binId, cid6, PlaylistState::VideoOnly, 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(); } } 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); TimelineItemModel &tt = timMock.get(); 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); + int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly); int tid2 = TrackModel::construct(timeline); - int cid2 = ClipModel::construct(timeline, binId2); - int cid3 = ClipModel::construct(timeline, binId2); + 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) { check_snap(beg, perturb, snap); check_snap(beg + length, perturb, snap); check_snap(beg - length2, perturb, snap); check_snap(beg + length - length2, perturb, snap); REQUIRE(timeline->checkConsistency()); REQUIRE(timeline->getClipPosition(cid2) == 0); } } } } 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); TimelineItemModel &tt = timMock.get(); 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); + 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); - int cid3 = ClipModel::construct(timeline, binId); - int cid4 = ClipModel::construct(timeline, binId); - int cid5 = ClipModel::construct(timeline, binId); - int cid6 = ClipModel::construct(timeline, binId); - int cid7 = ClipModel::construct(timeline, binId); - - int audio1 = ClipModel::construct(timeline, binId3); - int audio2 = ClipModel::construct(timeline, binId3); - int audio3 = ClipModel::construct(timeline, binId3); + 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, tid2)); int splitted1 = timeline->getClipByPosition(tid2, 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) == tid2); REQUIRE(timeline->getTrackClipsCount(tid1) == 1); REQUIRE(timeline->getTrackClipsCount(tid2) == 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, 30)); // This is a color clip, shouldn't be splittable REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, cid1, tid2)); REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, splitted1, 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, tid2)); int splitted1 = timeline->getClipByPosition(tid2, 0); int splitted2 = timeline->getClipByPosition(tid2, l); int splitted3 = timeline->getClipByPosition(tid2, 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) == tid2); REQUIRE(timeline->getClipTrackId(splitted2) == tid2); REQUIRE(timeline->getClipTrackId(splitted3) == tid2); REQUIRE(timeline->getTrackClipsCount(tid1) == 3); REQUIRE(timeline->getTrackClipsCount(tid2) == 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(); } } diff --git a/tests/regressions.cpp b/tests/regressions.cpp index 1b755ab30..9d84262e6 100644 --- a/tests/regressions.cpp +++ b/tests/regressions.cpp @@ -1,417 +1,417 @@ #include "test_utils.hpp" Mlt::Profile reg_profile; TEST_CASE("Regression") { 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); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); TimelineModel::next_id = 0; undoStack->undo(); undoStack->redo(); undoStack->redo(); undoStack->undo(); QString binId0 = createProducer(reg_profile, "red", binModel); - int c = ClipModel::construct(timeline, binId0); + int c = ClipModel::construct(timeline, binId0, -1, PlaylistState::VideoOnly); timeline->m_allClips[c]->m_endlessResize = false; TrackModel::construct(timeline); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->requestClipMove(0, 1, 0)); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->requestItemResize(0, 16, false)); REQUIRE(timeline->getTrackById(1)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(1)->checkConsistency()); undoStack->redo(); REQUIRE(timeline->getTrackById(1)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(1)->checkConsistency()); undoStack->redo(); REQUIRE(timeline->getTrackById(1)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(1)->checkConsistency()); undoStack->redo(); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE_FALSE(timeline->requestItemResize(0, 0, false)); REQUIRE(timeline->getTrackById(1)->checkConsistency()); TrackModel::construct(timeline); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); undoStack->redo(); REQUIRE(timeline->getTrackById(1)->checkConsistency()); } TEST_CASE("Regression2") { 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); TimelineItemModel &tt = timMock.get(); auto timeline = std::shared_ptr(&timMock.get(), [](...) {}); TimelineItemModel::finishConstruct(timeline, guideModel); RESET(timMock); TimelineModel::next_id = 0; int dummy_id; undoStack->undo(); undoStack->undo(); undoStack->redo(); TrackModel::construct(timeline); REQUIRE(timeline->getTrackById(0)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(0)->checkConsistency()); { QString binId0 = createProducer(reg_profile, "red", binModel); bool ok = timeline->requestClipInsertion(binId0, 0, 10, dummy_id); timeline->m_allClips[dummy_id]->m_endlessResize = false; REQUIRE(ok); } REQUIRE(timeline->getTrackById(0)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(0)->checkConsistency()); undoStack->redo(); REQUIRE(timeline->getTrackById(0)->checkConsistency()); TrackModel::construct(timeline); REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); { QString binId0 = createProducer(reg_profile, "red", binModel); bool ok = timeline->requestClipInsertion(binId0, 2, 10, dummy_id); timeline->m_allClips[3]->m_endlessResize = false; REQUIRE(ok); } REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); { bool ok = timeline->requestClipMove(1, 0, 10); REQUIRE(ok); } REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); undoStack->redo(); timeline->m_allClips[3]->m_endlessResize = false; REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); { bool ok = timeline->requestItemResize(3, 0, false); REQUIRE_FALSE(ok); } REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); TrackModel::construct(timeline); REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); { QString binId0 = createProducer(reg_profile, "red", binModel); - int c = ClipModel::construct(timeline, binId0); + int c = ClipModel::construct(timeline, binId0, -1, PlaylistState::VideoOnly); timeline->m_allClips[c]->m_endlessResize = false; } REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); TrackModel::construct(timeline); REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); { bool ok = timeline->requestItemResize(3, 15, true); REQUIRE(ok); } REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); { bool ok = timeline->requestClipMove(3, 0, 0); REQUIRE_FALSE(ok); } REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); { bool ok = timeline->requestItemResize(3, 16, false); REQUIRE_FALSE(ok); } REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); { bool ok = timeline->requestItemResize(3, 16, true); REQUIRE(ok); } REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); { QString binId0 = createProducer(reg_profile, "red", binModel); bool ok = timeline->requestClipInsertion(binId0, 0, 1, dummy_id); REQUIRE_FALSE(ok); } REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); undoStack->undo(); REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); undoStack->redo(); REQUIRE(timeline->getTrackById(0)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(4)->checkConsistency()); REQUIRE(timeline->getTrackById(6)->checkConsistency()); undoStack->redo(); } /* TEST_CASE("Regression 3") { Mlt::Profile profile; std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), undoStack); TimelineModel::next_id = 0; int dummy_id; std::shared_ptr producer0 = std::make_shared(profile, "color", "red"); producer0->set("length", 20); producer0->set("out", 19); ClipModel::construct(timeline, producer0 ); { bool ok = timeline->requestTrackInsertion(-1, dummy_id); REQUIRE(ok); } TrackModel::construct(timeline); TrackModel::construct(timeline); std::shared_ptr producer1 = std::make_shared(profile, "color", "red"); producer1->set("length", 20); producer1->set("out", 19); ClipModel::construct(timeline, producer1 ); std::shared_ptr producer2 = std::make_shared(profile, "color", "red"); producer2->set("length", 20); producer2->set("out", 19); ClipModel::construct(timeline, producer2 ); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); std::shared_ptr producer3 = std::make_shared(profile, "color", "red"); producer3->set("length", 20); producer3->set("out", 19); ClipModel::construct(timeline, producer3 ); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); TrackModel::construct(timeline); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); TrackModel::construct(timeline); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); TrackModel::construct(timeline); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); REQUIRE(timeline->getTrackById(9)->checkConsistency()); std::shared_ptr producer4 = std::make_shared(profile, "color", "red"); producer4->set("length", 20); producer4->set("out", 19); ClipModel::construct(timeline, producer4 ); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); REQUIRE(timeline->getTrackById(9)->checkConsistency()); std::shared_ptr producer5 = std::make_shared(profile, "color", "red"); producer5->set("length", 20); producer5->set("out", 19); ClipModel::construct(timeline, producer5 ); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); REQUIRE(timeline->getTrackById(9)->checkConsistency()); std::shared_ptr producer6 = std::make_shared(profile, "color", "red"); producer6->set("length", 20); producer6->set("out", 19); ClipModel::construct(timeline, producer6 ); REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); REQUIRE(timeline->getTrackById(9)->checkConsistency()); { bool ok = timeline->requestClipMove(0,1 ,10 ); REQUIRE(ok); } REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); REQUIRE(timeline->getTrackById(9)->checkConsistency()); { bool ok = timeline->requestClipMove(4,2 ,12 ); REQUIRE(ok); } REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); REQUIRE(timeline->getTrackById(9)->checkConsistency()); { auto group = {4, 0}; bool ok = timeline->requestClipsGroup(group); REQUIRE(ok); } REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); REQUIRE(timeline->getTrackById(9)->checkConsistency()); { bool ok = timeline->requestClipMove(4,1 ,10 ); REQUIRE_FALSE(ok); } REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); REQUIRE(timeline->getTrackById(9)->checkConsistency()); { bool ok = timeline->requestClipMove(4,1 ,100 ); REQUIRE_FALSE(ok); } REQUIRE(timeline->getTrackById(1)->checkConsistency()); REQUIRE(timeline->getTrackById(2)->checkConsistency()); REQUIRE(timeline->getTrackById(3)->checkConsistency()); REQUIRE(timeline->getTrackById(7)->checkConsistency()); REQUIRE(timeline->getTrackById(8)->checkConsistency()); REQUIRE(timeline->getTrackById(9)->checkConsistency()); { bool ok = timeline->requestClipMove(0,3 ,100 ); REQUIRE(ok); } std::shared_ptr producer7 = std::make_shared(profile, "color", "red"); producer7->set("length", 20); producer7->set("out", 19); ClipModel::construct(timeline, producer7 ); { bool ok = timeline->requestTrackInsertion(-1, dummy_id); REQUIRE(ok); } undoStack->undo(); { bool ok = timeline->requestClipMove(0,1 ,5 ); REQUIRE(ok); } { bool ok = timeline->requestTrackDeletion(1); REQUIRE(ok); } } TEST_CASE("Regression 4") { Mlt::Profile profile; std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), undoStack); TimelineModel::next_id = 0; int dummy_id; timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); timeline->requestTrackInsertion(-1, dummy_id ); { std::shared_ptr producer = std::make_shared(profile, "color", "red"); producer->set("length", 62); producer->set("out", 61); timeline->requestClipInsertion(producer,10 ,453, dummy_id ); } timeline->requestClipMove(11,10 ,453, true, true ); { std::shared_ptr producer = std::make_shared(profile, "color", "red"); producer->set("length", 62); producer->set("out", 61); timeline->requestClipInsertion(producer,9 ,590, dummy_id ); } timeline->requestItemResize(11,62 ,true, false, true ); timeline->requestItemResize(11,62 ,true, true, true ); timeline->requestClipMove(11,10 ,507, true, true ); timeline->requestClipMove(12,10 ,583, false, false ); timeline->requestClipMove(12,9 ,521, true, true ); } */