diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index 09c08c9fe..057f6b127 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1426 +1,1430 @@ /* 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/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "model/markerlistmodel.hpp" #include "profiles/profilemodel.hpp" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectfolder.h" #include "projectitemmodel.h" #include "projectsubclip.h" #include "timecode.h" #include "timeline2/model/snapmodel.hpp" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" #include #include #include #include "kdenlive_debug.h" #include "logger.hpp" #include #include #include #include #include #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("ProjectClip"); } ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr &model, std::shared_ptr producer) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id, std::move(producer)) , m_thumbsProducer(nullptr) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); m_clipStatus = StatusReady; m_name = clipName(); m_duration = getStringDuration(); m_inPoint = 0; m_date = date; m_description = ClipController::description(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } // Make sure we have a hash for this clip hash(); connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); QString markers = getProducerProperty(QStringLiteral("kdenlive:markers")); if (!markers.isEmpty()) { QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true), Q_ARG(bool, false)); } connectEffectStack(); } // static std::shared_ptr ProjectClip::construct(const QString &id, const QIcon &thumb, const std::shared_ptr &model, const std::shared_ptr &producer) { std::shared_ptr self(new ProjectClip(id, thumb, model, producer)); baseFinishConstruct(self); QMetaObject::invokeMethod(model.get(), "loadSubClips", Qt::QueuedConnection, Q_ARG(const QString&, id), Q_ARG(const stringMap&, self->getPropertiesFromPrefix(QStringLiteral("kdenlive:clipzone.")))); return self; } void ProjectClip::importEffects(const std::shared_ptr &producer) { m_effectStack->importEffects(producer, PlaylistState::Disabled, true); } ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) , m_thumbsProducer(nullptr) { m_clipStatus = StatusWaiting; m_thumbnail = thumb; m_markerModel = std::make_shared(m_binId, pCore->projectManager()->undoStack()); if (description.hasAttribute(QStringLiteral("type"))) { m_clipType = (ClipType::ProducerType)description.attribute(QStringLiteral("type")).toInt(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } } m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource")); QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname")); if (!clipName.isEmpty()) { m_name = clipName; } else if (!m_temporaryUrl.isEmpty()) { m_name = QFileInfo(m_temporaryUrl).fileName(); } else { m_name = i18n("Untitled"); } connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); } std::shared_ptr ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) { std::shared_ptr self(new ProjectClip(id, description, thumb, std::move(model))); baseFinishConstruct(self); return self; } ProjectClip::~ProjectClip() { // controller is deleted in bincontroller m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); audioFrameCache.clear(); } void ProjectClip::connectEffectStack() { connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&]() { if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } }); } 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(QList audioLevels) { audioFrameCache = audioLevels; m_audioThumbCreated = true; if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshAudioThumbs(m_binId); } } bool ProjectClip::audioThumbCreated() const { return (m_audioThumbCreated); } ClipType::ProducerType ProjectClip::clipType() const { return m_clipType; } bool ProjectClip::hasParent(const QString &id) const { std::shared_ptr par = parent(); while (par) { if (par->clipId() == id) { return true; } par = par->parent(); } return false; } std::shared_ptr ProjectClip::clip(const QString &id) { if (id == m_binId) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } std::shared_ptr ProjectClip::folder(const QString &id) { Q_UNUSED(id) return std::shared_ptr(); } std::shared_ptr ProjectClip::getSubClip(int in, int out) { for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i))->subClip(in, out); if (clip) { return clip; } } return std::shared_ptr(); } QStringList ProjectClip::subClipIds() const { QStringList subIds; for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { subIds << clip->clipId(); } } return subIds; } std::shared_ptr ProjectClip::clipAt(int ix) { if (ix == row()) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } /*bool ProjectClip::isValid() const { return m_controller->isValid(); }*/ bool ProjectClip::hasUrl() const { if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) { return (!clipUrl().isEmpty()); } return false; } const QString ProjectClip::url() const { return clipUrl(); } GenTime ProjectClip::duration() const { return getPlaytime(); } size_t ProjectClip::frameDuration() const { GenTime d = duration(); return (size_t)d.frames(pCore->getCurrentFps()); } void ProjectClip::reloadProducer(bool refreshOnly, bool audioStreamChanged) { // we find if there are some loading job on that clip int loadjobId = -1; pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::LOADJOB, &loadjobId); QMutexLocker lock(&m_thumbMutex); if (refreshOnly) { // In that case, we only want a new thumbnail. // We thus set up a thumb job. We must make sure that there is no pending LOADJOB // Clear cache first ThumbnailCache::get()->invalidateThumbsForClip(clipId()); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), 150, -1, true, true); } else { // If another load job is running? if (loadjobId > -1) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::LOADJOB); } + if (QFile::exists(m_path) && !hasProxy()) { + clearBackupProperties(); + } QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); ThumbnailCache::get()->invalidateThumbsForClip(clipId()); int loadJob = pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), xml); pCore->jobManager()->startJob({clipId()}, loadJob, QString(), 150, -1, true, true); if (audioStreamChanged) { discardAudioThumb(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString()); } } } } QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta, bool includeProfile) { getProducerXML(document, includeMeta, includeProfile); QDomElement prod = document.documentElement().firstChildElement(QStringLiteral("producer")); if (m_clipType != ClipType::Unknown) { prod.setAttribute(QStringLiteral("type"), (int)m_clipType); } return prod; } void ProjectClip::setThumbnail(const QImage &img) { QPixmap thumb = roundedPixmap(QPixmap::fromImage(img)); if (hasProxy() && !thumb.isNull()) { // Overlay proxy icon QPainter p(&thumb); QColor c(220, 220, 10, 200); QRect r(0, 0, thumb.height() / 2.5, thumb.height() / 2.5); p.fillRect(r, c); QFont font = p.font(); font.setPixelSize(r.height()); font.setBold(true); p.setFont(font); p.setPen(Qt::black); p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P")); } m_thumbnail = QIcon(thumb); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataThumbnail); } } bool ProjectClip::hasAudioAndVideo() const { return hasAudio() && hasVideo() && m_masterProducer->get_int("set.test_image") == 0 && m_masterProducer->get_int("set.test_audio") == 0; } bool ProjectClip::isCompatible(PlaylistState::ClipState state) const { switch (state) { case PlaylistState::AudioOnly: return hasAudio() && (m_masterProducer->get_int("set.test_audio") == 0); case PlaylistState::VideoOnly: return hasVideo() && (m_masterProducer->get_int("set.test_image") == 0); default: return true; } } QPixmap ProjectClip::thumbnail(int width, int height) { return m_thumbnail.pixmap(width, height); } bool ProjectClip::setProducer(std::shared_ptr producer, bool replaceProducer) { Q_UNUSED(replaceProducer) qDebug() << "################### ProjectClip::setproducer"; QMutexLocker locker(&m_producerMutex); updateProducer(producer); m_thumbsProducer.reset(); connectEffectStack(); // Update info if (m_name.isEmpty()) { m_name = clipName(); } m_date = date; m_description = ClipController::description(); m_temporaryUrl.clear(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else if (m_clipType == ClipType::Image) { if (producer->get_int("meta.media.width") < 8 || producer->get_int("meta.media.height") < 8) { KMessageBox::information(QApplication::activeWindow(), i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework.")); } } m_duration = getStringDuration(); m_clipStatus = StatusReady; if (!hasProxy()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->refreshPanel(m_binId); } if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); std::static_pointer_cast(ptr)->updateWatcher(std::static_pointer_cast(shared_from_this())); } // Make sure we have a hash for this clip getFileHash(); // set parent again (some info need to be stored in producer) updateParent(parentItem().lock()); if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() == 1) { QList> clipList; // automatic proxy generation enabled if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { clipList << std::static_pointer_cast(shared_from_this()); } } else if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1 && (m_clipType == ClipType::AV || m_clipType == ClipType::Video) && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { bool skipProducer = false; if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableexternalproxy")).toInt() == 1) { QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';')); // We have a camcorder profile, check if we have opened a proxy clip if (externalParams.count() >= 6) { QFileInfo info(m_path); QDir dir = info.absoluteDir(); dir.cd(externalParams.at(3)); QString fileName = info.fileName(); if (!externalParams.at(2).isEmpty()) { fileName.chop(externalParams.at(2).size()); } fileName.append(externalParams.at(5)); if (dir.exists(fileName)) { setProducerProperty(QStringLiteral("kdenlive:proxy"), m_path); m_path = dir.absoluteFilePath(fileName); setProducerProperty(QStringLiteral("kdenlive:originalurl"), m_path); getFileHash(); skipProducer = true; } } } if (!skipProducer && getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize()) { clipList << std::static_pointer_cast(shared_from_this()); } } if (!clipList.isEmpty()) { pCore->currentDoc()->slotProxyCurrentItem(true, clipList, false); } } pCore->bin()->reloadMonitorIfActive(clipId()); for (auto &p : m_audioProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_videoProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_timewarpProducers) { m_effectStack->removeService(p.second); } // Release audio producers m_audioProducers.clear(); m_videoProducers.clear(); m_timewarpProducers.clear(); emit refreshPropertiesPanel(); replaceInTimeline(); return true; } std::shared_ptr ProjectClip::thumbProducer() { if (m_thumbsProducer) { return m_thumbsProducer; } if (clipType() == ClipType::Unknown) { return nullptr; } QMutexLocker lock(&m_thumbMutex); std::shared_ptr prod = originalProducer(); if (!prod->is_valid()) { return nullptr; } if (KdenliveSettings::gpu_accel()) { // TODO: when the original producer changes, we must reload this thumb producer m_thumbsProducer = softClone(ClipController::getPassPropertiesList()); Mlt::Filter converter(*prod->profile(), "avcolor_space"); m_thumbsProducer->attach(converter); } else { QString mltService = m_masterProducer->get("mlt_service"); const QString mltResource = m_masterProducer->get("resource"); if (mltService == QLatin1String("avformat")) { mltService = QStringLiteral("avformat-novalidate"); } m_thumbsProducer.reset(new Mlt::Producer(*pCore->thumbProfile(), mltService.toUtf8().constData(), mltResource.toUtf8().constData())); if (m_thumbsProducer->is_valid()) { Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(m_thumbsProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList()); Mlt::Filter scaler(*pCore->thumbProfile(), "swscale"); Mlt::Filter padder(*pCore->thumbProfile(), "resize"); Mlt::Filter converter(*pCore->thumbProfile(), "avcolor_space"); m_thumbsProducer->set("audio_index", -1); // Required to make get_playtime() return > 1 m_thumbsProducer->set("out", m_thumbsProducer->get_length() -1); m_thumbsProducer->attach(scaler); m_thumbsProducer->attach(padder); m_thumbsProducer->attach(converter); } } return m_thumbsProducer; } void ProjectClip::createDisabledMasterProducer() { if (!m_disabledProducer) { m_disabledProducer = cloneProducer(); m_disabledProducer->set("set.test_audio", 1); m_disabledProducer->set("set.test_image", 1); m_effectStack->addService(m_disabledProducer); } } std::shared_ptr ProjectClip::getTimelineProducer(int trackId, int clipId, PlaylistState::ClipState state, double speed) { if (!m_masterProducer) { return nullptr; } if (qFuzzyCompare(speed, 1.0)) { // we are requesting a normal speed producer if (trackId == -1 || (state == PlaylistState::VideoOnly && (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text))) { // Temporary copy, return clone of master int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_masterProducer->cut(-1, duration > 0 ? duration : -1)); } if (m_timewarpProducers.count(clipId) > 0) { m_effectStack->removeService(m_timewarpProducers[clipId]); m_timewarpProducers.erase(clipId); } if (state == PlaylistState::AudioOnly) { // We need to get an audio producer, if none exists if (m_audioProducers.count(trackId) == 0) { m_audioProducers[trackId] = cloneProducer(true); m_audioProducers[trackId]->set("set.test_audio", 0); m_audioProducers[trackId]->set("set.test_image", 1); m_effectStack->addService(m_audioProducers[trackId]); } return std::shared_ptr(m_audioProducers[trackId]->cut()); } if (m_audioProducers.count(trackId) > 0) { m_effectStack->removeService(m_audioProducers[trackId]); m_audioProducers.erase(trackId); } if (state == PlaylistState::VideoOnly) { // we return the video producer // We need to get an audio producer, if none exists if (m_videoProducers.count(trackId) == 0) { m_videoProducers[trackId] = cloneProducer(true); m_videoProducers[trackId]->set("set.test_audio", 1); m_videoProducers[trackId]->set("set.test_image", 0); m_effectStack->addService(m_videoProducers[trackId]); } int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_videoProducers[trackId]->cut(-1, duration > 0 ? duration : -1)); } if (m_videoProducers.count(trackId) > 0) { m_effectStack->removeService(m_videoProducers[trackId]); m_videoProducers.erase(trackId); } Q_ASSERT(state == PlaylistState::Disabled); createDisabledMasterProducer(); int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_disabledProducer->cut(-1, duration > 0 ? duration : -1)); } // For timewarp clips, we keep one separate producer for each clip. std::shared_ptr warpProducer; if (m_timewarpProducers.count(clipId) > 0) { // remove in all cases, we add it unconditionally anyways m_effectStack->removeService(m_timewarpProducers[clipId]); if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) { // the producer we have is good, use it ! warpProducer = m_timewarpProducers[clipId]; qDebug() << "Reusing producer!"; } else { m_timewarpProducers.erase(clipId); } } if (!warpProducer) { QLocale locale; QString resource(originalProducer()->get("resource")); if (resource.isEmpty() || resource == QLatin1String("")) { resource = m_service; } QString url = QString("timewarp:%1:%2").arg(locale.toString(speed)).arg(resource); warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), url.toUtf8().constData())); qDebug() << "new producer: " << url; qDebug() << "warp LENGTH before" << warpProducer->get_length(); int original_length = originalProducer()->get_length(); // this is a workaround to cope with Mlt erroneous rounding Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(warpProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList(false)); warpProducer->set("length", double(original_length) / std::abs(speed)); } qDebug() << "warp LENGTH" << warpProducer->get_length(); warpProducer->set("set.test_audio", 1); warpProducer->set("set.test_image", 1); if (state == PlaylistState::AudioOnly) { warpProducer->set("set.test_audio", 0); } if (state == PlaylistState::VideoOnly) { warpProducer->set("set.test_image", 0); } m_timewarpProducers[clipId] = warpProducer; m_effectStack->addService(m_timewarpProducers[clipId]); return std::shared_ptr(warpProducer->cut()); } std::pair, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state) { int in = master->get_in(); int out = master->get_out(); if (master->parent().is_valid()) { // in that case, we have a cut // check whether it's a timewarp double speed = 1.0; bool timeWarp = false; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->parent().get_double("warp_speed"); timeWarp = true; } if (master->parent().get_int("_loaded") == 1) { // we already have a clip that shares the same master if (state != PlaylistState::Disabled || timeWarp) { // In that case, we must create copies std::shared_ptr prod(getTimelineProducer(-1, clipId, state, speed)->cut(in, out)); return {prod, false}; } if (state == PlaylistState::Disabled) { if (!m_disabledProducer) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet"; createDisabledMasterProducer(); } return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } // We have a good id, this clip can be used return {master, true}; } else { master->parent().set("_loaded", 1); if (timeWarp) { m_timewarpProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_timewarpProducers[clipId]); return {master, true}; } if (state == PlaylistState::AudioOnly) { m_audioProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_audioProducers[clipId]); return {master, true}; } if (state == PlaylistState::VideoOnly) { // good, we found a master video producer, and we didn't have any m_videoProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_videoProducers[clipId]); return {master, true}; } if (state == PlaylistState::Disabled) { if (!m_disabledProducer) { createDisabledMasterProducer(); } return {std::make_shared(m_disabledProducer->cut(master->get_in(), master->get_out())), true}; } qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master"; Q_ASSERT(false); } } else if (master->is_valid()) { // in that case, we have a master qDebug() << "Warning: weird, we received a master clip in lieue of a cut"; double speed = 1.0; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->get_double("warp_speed"); } return {getTimelineProducer(-1, 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(bool removeEffects) { Mlt::Consumer c(pCore->getCurrentProfile()->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.run(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod; prod.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); } // we pass some properties that wouldn't be passed because of the novalidate const char *prefix = "meta."; const size_t prefix_len = strlen(prefix); for (int i = 0; i < m_masterProducer->count(); ++i) { char *current = m_masterProducer->get_name(i); if (strlen(current) >= prefix_len && strncmp(current, prefix, prefix_len) == 0) { prod->set(current, m_masterProducer->get(i)); } } if (removeEffects) { int ct = 0; Mlt::Filter *filter = prod->filter(ct); while (filter) { qDebug() << "// EFFECT " << ct << " : " << filter->get("mlt_service"); QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { qDebug() << "/ + + DELTING"; if (prod->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = prod->filter(ct); } } prod->set("id", (char *)nullptr); return prod; } std::shared_ptr ProjectClip::cloneProducer(const std::shared_ptr &producer) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.start(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod(new Mlt::Producer(*producer->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); } return prod; } std::shared_ptr ProjectClip::softClone(const char *list) { QString service = QString::fromLatin1(m_masterProducer->get("mlt_service")); QString resource = QString::fromLatin1(m_masterProducer->get("resource")); std::shared_ptr clone(new Mlt::Producer(*m_masterProducer->profile(), service.toUtf8().constData(), resource.toUtf8().constData())); Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(clone->get_properties()); cloneProps.pass_list(original, list); return clone; } bool ProjectClip::isReady() const { return m_clipStatus == StatusReady; } QPoint ProjectClip::zone() const { return ClipController::zone(); } const QString ProjectClip::hash() { QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash")); if (!clipHash.isEmpty()) { return clipHash; } return getFileHash(); } const QString ProjectClip::getFileHash() { QByteArray fileData; QByteArray fileHash; switch (m_clipType) { case ClipType::SlideShow: fileData = clipUrl().toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Text: case ClipType::TextTemplate: fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::QText: fileData = getProducerProperty(QStringLiteral("text")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Color: fileData = getProducerProperty(QStringLiteral("resource")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; default: QFile file(clipUrl()); if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 2000000) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(file.size())); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); } break; } if (fileHash.isEmpty()) { qDebug() << "// WARNING EMPTY CLIP HASH: "; return QString(); } QString result = fileHash.toHex(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result); return result; } double ProjectClip::getOriginalFps() const { return originalFps(); } bool ProjectClip::hasProxy() const { QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); return proxy.size() > 2; } void ProjectClip::setProperties(const QMap &properties, bool refreshPanel) { qDebug() << "// SETTING CLIP PROPERTIES: " << properties; QMapIterator i(properties); QMap passProperties; bool refreshAnalysis = false; bool reload = false; bool refreshOnly = true; if (properties.contains(QStringLiteral("templatetext"))) { m_description = properties.value(QStringLiteral("templatetext")); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::ClipStatus); refreshPanel = true; } // Some properties also need to be passed to track producers QStringList timelineProperties{ QStringLiteral("force_aspect_ratio"), QStringLiteral("set.force_full_luma"), QStringLiteral("full_luma"), QStringLiteral("threads"), QStringLiteral("force_colorspace"), QStringLiteral("force_tff"), QStringLiteral("force_progressive"), }; QStringList forceReloadProperties{QStringLiteral("autorotate"), QStringLiteral("templatetext"), QStringLiteral("resource"), QStringLiteral("force_fps"), QStringLiteral("set.test_image"), QStringLiteral("set.test_audio"), QStringLiteral("audio_index"), QStringLiteral("video_index")}; QStringList keys{QStringLiteral("luma_duration"), QStringLiteral("luma_file"), QStringLiteral("fade"), QStringLiteral("ttl"), QStringLiteral("softness"), QStringLiteral("crop"), QStringLiteral("animation")}; QVector updateRoles; while (i.hasNext()) { i.next(); setProducerProperty(i.key(), i.value()); if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) { reload = true; refreshOnly = false; } if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) { refreshAnalysis = true; } if (timelineProperties.contains(i.key())) { passProperties.insert(i.key(), i.value()); } } if (properties.contains(QStringLiteral("kdenlive:proxy"))) { QString value = properties.value(QStringLiteral("kdenlive:proxy")); // If value is "-", that means user manually disabled proxy on this clip if (value.isEmpty() || value == QLatin1String("-")) { // reset proxy int id; if (pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::PROXYJOB, &id)) { // The proxy clip is being created, abort pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); } else { reload = true; refreshOnly = false; } } else { // A proxy was requested, make sure to keep original url setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); + backupOriginalProperties(); pCore->jobManager()->startJob({clipId()}, -1, QString()); } } else if (!reload) { const QList propKeys = properties.keys(); for (const QString &k : propKeys) { if (forceReloadProperties.contains(k)) { if (m_clipType != ClipType::Color) { reload = true; refreshOnly = false; } else { // Clip resource changed, update thumbnail reload = true; refreshPanel = true; updateRoles << TimelineModel::ResourceRole; } break; } } } if (!reload && (properties.contains(QStringLiteral("xmldata")) || !passProperties.isEmpty())) { reload = true; } if (refreshAnalysis) { emit refreshAnalysisPanel(); } if (properties.contains(QStringLiteral("length")) || properties.contains(QStringLiteral("kdenlive:duration"))) { m_duration = getStringDuration(); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); refreshOnly = false; reload = true; } if (properties.contains(QStringLiteral("kdenlive:clipname"))) { m_name = properties.value(QStringLiteral("kdenlive:clipname")); refreshPanel = true; if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataName); } // update timeline clips updateTimelineClips(QVector() << TimelineModel::NameRole); } if (refreshPanel) { // Some of the clip properties have changed through a command, update properties panel emit refreshPropertiesPanel(); } if (reload) { // producer has changed, refresh monitor and thumbnail if (hasProxy()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); setProducerProperty(QStringLiteral("_overwriteproxy"), 1); pCore->jobManager()->startJob({clipId()}, -1, QString()); } else { reloadProducer(refreshOnly, properties.contains(QStringLiteral("audio_index"))); } if (refreshOnly) { if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshClip(m_binId); } } if (!updateRoles.isEmpty()) { updateTimelineClips(updateRoles); } } if (!passProperties.isEmpty() && (!reload || refreshOnly)) { 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); auto *panel = new ClipPropertiesController(static_cast(this), parent); connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties); connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData); connect(panel, &ClipPropertiesController::requestProxy, [this](bool doProxy) { QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(doProxy, clipList); }); connect(panel, &ClipPropertiesController::deleteProxy, this, &ProjectClip::deleteProxy); return panel; } void ProjectClip::deleteProxy() { // Disable proxy file QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(false, clipList); // Delete bool ok; QDir dir = pCore->currentDoc()->getCacheDir(CacheProxy, &ok); if (ok && proxy.length() > 2) { proxy = QFileInfo(proxy).fileName(); if (dir.exists(proxy)) { dir.remove(proxy); } } } void ProjectClip::updateParent(std::shared_ptr parent) { if (parent) { auto item = std::static_pointer_cast(parent); ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId()); } AbstractProjectItem::updateParent(parent); } bool ProjectClip::matches(const QString &condition) { // TODO Q_UNUSED(condition) return true; } bool ProjectClip::rename(const QString &name, int column) { QMap newProperites; QMap oldProperites; bool edited = false; switch (column) { case 0: if (m_name == name) { return false; } // Rename clip oldProperites.insert(QStringLiteral("kdenlive:clipname"), m_name); newProperites.insert(QStringLiteral("kdenlive:clipname"), name); m_name = name; edited = true; break; case 2: if (m_description == name) { return false; } // Rename clip if (m_clipType == ClipType::TextTemplate) { oldProperites.insert(QStringLiteral("templatetext"), m_description); newProperites.insert(QStringLiteral("templatetext"), name); } else { oldProperites.insert(QStringLiteral("kdenlive:description"), m_description); newProperites.insert(QStringLiteral("kdenlive:description"), name); } m_description = name; edited = true; break; } if (edited) { pCore->bin()->slotEditClipCommand(m_binId, oldProperites, newProperites); } return edited; } QVariant ProjectClip::getData(DataType type) const { switch (type) { case AbstractProjectItem::IconOverlay: return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant(); default: return AbstractProjectItem::getData(type); } } void ProjectClip::slotExtractImage(const QList &frames) { QMutexLocker lock(&m_thumbMutex); for (int i = 0; i < frames.count(); i++) { if (!m_requestedThumbs.contains(frames.at(i))) { m_requestedThumbs << frames.at(i); } } qSort(m_requestedThumbs); if (!m_thumbThread.isRunning()) { m_thumbThread = QtConcurrent::run(this, &ProjectClip::doExtractImage); } } void ProjectClip::doExtractImage() { // TODO refac: we can probably move that into a ThumbJob std::shared_ptr prod = thumbProducer(); if (prod == nullptr || !prod->is_valid()) { return; } int frameWidth = 150 * prod->profile()->dar() + 0.5; bool ok = false; auto ptr = m_model.lock(); Q_ASSERT(ptr); QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheThumbs, &ok); int max = prod->get_length(); while (!m_requestedThumbs.isEmpty()) { m_thumbMutex.lock(); int pos = m_requestedThumbs.takeFirst(); m_thumbMutex.unlock(); if (ok && thumbFolder.exists(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png"))) { emit thumbReady(pos, QImage(thumbFolder.absoluteFilePath(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png")))); continue; } if (pos >= max) { pos = max - 1; } const QString path = url() + QLatin1Char('_') + QString::number(pos); QImage img; if (ThumbnailCache::get()->hasThumbnail(clipId(), pos, true)) { img = ThumbnailCache::get()->getThumbnail(clipId(), pos, true); } if (!img.isNull()) { emit thumbReady(pos, img); continue; } prod->seek(pos); Mlt::Frame *frame = prod->get_frame(); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (frame->is_valid()) { img = KThumb::getFrame(frame, frameWidth, 150, !qFuzzyCompare(prod->profile()->sar(), 1)); ThumbnailCache::get()->storeThumbnail(clipId(), pos, img, false); emit thumbReady(pos, img); } delete frame; } } int ProjectClip::audioChannels() const { if (!audioInfo()) { return 0; } return audioInfo()->channels(); } void ProjectClip::discardAudioThumb() { QString audioThumbPath = getAudioThumbPath(); if (!audioThumbPath.isEmpty()) { QFile::remove(audioThumbPath); } audioFrameCache.clear(); qCDebug(KDENLIVE_LOG) << "//////////////////// DISCARD AUIIO THUMBNS"; m_audioThumbCreated = false; refreshAudioInfo(); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB); } const QString ProjectClip::getAudioThumbPath() { if (audioInfo() == nullptr) { return QString(); } int audioStream = audioInfo()->ffmpeg_audio_index(); QString clipHash = hash(); if (clipHash.isEmpty()) { return QString(); } bool ok = false; QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheAudio, &ok); if (!ok) { return QString(); } QString audioPath = thumbFolder.absoluteFilePath(clipHash); if (audioStream > 0) { audioPath.append(QLatin1Char('_') + QString::number(audioInfo()->audio_index())); } int roundedFps = (int)pCore->getCurrentFps(); audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps)); return audioPath; } QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset) { if (data.isEmpty()) { // Remove data return QStringList() << QString("kdenlive:clipanalysis." + name) << QString(); // m_controller->resetProperty("kdenlive:clipanalysis." + name); } QString current = getProducerProperty("kdenlive:clipanalysis." + name); if (!current.isEmpty()) { if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(), KGuiItem(i18n("Merge")), KGuiItem(i18n("Add"))) == KMessageBox::Yes) { // Merge data auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (newGeometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; geometry.insert(item); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise(); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise()); } // Add data with another name int i = 1; QString previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); while (!previous.isEmpty()) { ++i; previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); } return QStringList() << QString("kdenlive:clipanalysis." + name + QString::number(i)) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name + QLatin1Char(' ') + QString::number(i), geometryWithOffset(data, offset)); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset)); } QMap ProjectClip::analysisData(bool withPrefix) { return getPropertiesFromPrefix(QStringLiteral("kdenlive:clipanalysis."), withPrefix); } const QString ProjectClip::geometryWithOffset(const QString &data, int offset) { if (offset == 0) { return data; } auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newgeometry(nullptr, duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (geometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; newgeometry.insert(item); } return newgeometry.serialise(); } bool ProjectClip::isSplittable() const { return (m_clipType == ClipType::AV || m_clipType == ClipType::Playlist); } void ProjectClip::setBinEffectsEnabled(bool enabled) { ClipController::setBinEffectsEnabled(enabled); } void ProjectClip::registerService(std::weak_ptr timeline, int clipId, const std::shared_ptr &service, bool forceRegister) { if (!service->is_cut() || forceRegister) { int hasAudio = service->get_int("set.test_audio") == 0; int hasVideo = service->get_int("set.test_image") == 0; if (hasVideo && m_videoProducers.count(clipId) == 0) { // This is an undo producer, register it! m_videoProducers[clipId] = service; m_effectStack->addService(m_videoProducers[clipId]); } else if (hasAudio && m_audioProducers.count(clipId) == 0) { // This is an undo producer, register it! m_audioProducers[clipId] = service; m_effectStack->addService(m_audioProducers[clipId]); } } registerTimelineClip(std::move(timeline), clipId); } void ProjectClip::registerTimelineClip(std::weak_ptr timeline, int clipId) { Q_ASSERT(m_registeredClips.count(clipId) == 0); Q_ASSERT(!timeline.expired()); m_registeredClips[clipId] = std::move(timeline); setRefCount((uint)m_registeredClips.size()); } void ProjectClip::deregisterTimelineClip(int clipId) { qDebug() << " ** * DEREGISTERING TIMELINE CLIP: " << clipId; Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.erase(clipId); if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } setRefCount((uint)m_registeredClips.size()); } QList ProjectClip::timelineInstances() const { QList ids; for (const auto &m_registeredClip : m_registeredClips) { ids.push_back(m_registeredClip.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::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(const 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/dialogs/renderwidget.cpp b/src/dialogs/renderwidget.cpp index e69c0a85b..64eaa453c 100644 --- a/src/dialogs/renderwidget.cpp +++ b/src/dialogs/renderwidget.cpp @@ -1,3432 +1,3436 @@ /*************************************************************************** * Copyright (C) 2008 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 "renderwidget.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" #include "timecode.h" #include "ui_saveprofile_ui.h" #include "xml/xml.hpp" #include "klocalizedstring.h" #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 #include #include #include #ifdef KF5_USE_PURPOSE #include #include #endif #include #ifdef Q_OS_MAC #include #endif // Render profiles roles enum { GroupRole = Qt::UserRole, ExtensionRole, StandardRole, RenderRole, ParamsRole, EditableRole, ExtraRole, BitratesRole, DefaultBitrateRole, AudioBitratesRole, DefaultAudioBitrateRole, SpeedsRole, FieldRole, ErrorRole }; // Render job roles const int ParametersRole = Qt::UserRole + 1; const int TimeRole = Qt::UserRole + 2; const int ProgressRole = Qt::UserRole + 3; const int ExtraInfoRole = Qt::UserRole + 5; // Running job status enum JOBSTATUS { WAITINGJOB = 0, STARTINGJOB, RUNNINGJOB, FINISHEDJOB, FAILEDJOB, ABORTEDJOB }; static QStringList acodecsList; static QStringList vcodecsList; static QStringList supportedFormats; RenderJobItem::RenderJobItem(QTreeWidget *parent, const QStringList &strings, int type) : QTreeWidgetItem(parent, strings, type) , m_status(-1) { setSizeHint(1, QSize(parent->columnWidth(1), parent->fontMetrics().height() * 3)); setStatus(WAITINGJOB); } void RenderJobItem::setStatus(int status) { if (m_status == status) { return; } m_status = status; switch (status) { case WAITINGJOB: setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause"))); setData(1, Qt::UserRole, i18n("Waiting...")); break; case FINISHEDJOB: setData(1, Qt::UserRole, i18n("Rendering finished")); setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); setData(1, ProgressRole, 100); break; case FAILEDJOB: setData(1, Qt::UserRole, i18n("Rendering crashed")); setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); setData(1, ProgressRole, 100); break; case ABORTEDJOB: setData(1, Qt::UserRole, i18n("Rendering aborted")); setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-cancel"))); setData(1, ProgressRole, 100); default: break; } } int RenderJobItem::status() const { return m_status; } void RenderJobItem::setMetadata(const QString &data) { m_data = data; } const QString RenderJobItem::metadata() const { return m_data; } RenderWidget::RenderWidget(bool enableProxy, QWidget *parent) : QDialog(parent) , m_blockProcessing(false) { m_view.setupUi(this); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); setWindowTitle(i18n("Rendering")); m_view.buttonDelete->setIconSize(iconSize); m_view.buttonEdit->setIconSize(iconSize); m_view.buttonSave->setIconSize(iconSize); m_view.buttonFavorite->setIconSize(iconSize); m_view.buttonDownload->setIconSize(iconSize); m_view.buttonDelete->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty"))); m_view.buttonDelete->setToolTip(i18n("Delete profile")); m_view.buttonDelete->setEnabled(false); m_view.buttonEdit->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); m_view.buttonEdit->setToolTip(i18n("Edit profile")); m_view.buttonEdit->setEnabled(false); m_view.buttonSave->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_view.buttonSave->setToolTip(i18n("Create new profile")); m_view.hide_log->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); m_view.buttonFavorite->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); m_view.buttonFavorite->setToolTip(i18n("Copy profile to favorites")); m_view.buttonDownload->setIcon(QIcon::fromTheme(QStringLiteral("edit-download"))); m_view.buttonDownload->setToolTip(i18n("Download New Render Profiles...")); m_view.out_file->button()->setToolTip(i18n("Select output destination")); m_view.advanced_params->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 5); m_view.optionsGroup->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.optionsGroup, &QWidget::setVisible); m_view.videoLabel->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.videoLabel, &QWidget::setVisible); m_view.video->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.video, &QWidget::setVisible); m_view.audioLabel->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.audioLabel, &QWidget::setVisible); m_view.audio->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.audio, &QWidget::setVisible); connect(m_view.quality, &QAbstractSlider::valueChanged, this, &RenderWidget::adjustAVQualities); connect(m_view.video, static_cast(&QSpinBox::valueChanged), this, &RenderWidget::adjustQuality); connect(m_view.speed, &QAbstractSlider::valueChanged, this, &RenderWidget::adjustSpeed); m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); setRescaleEnabled(false); m_view.guides_box->setVisible(false); m_view.open_dvd->setVisible(false); m_view.create_chapter->setVisible(false); m_view.open_browser->setVisible(false); m_view.error_box->setVisible(false); m_view.tc_type->setEnabled(false); m_view.checkTwoPass->setEnabled(false); m_view.proxy_render->setHidden(!enableProxy); connect(m_view.proxy_render, &QCheckBox::toggled, this, &RenderWidget::slotProxyWarn); KColorScheme scheme(palette().currentColorGroup(), KColorScheme::Window); QColor bg = scheme.background(KColorScheme::NegativeBackground).color(); m_view.errorBox->setStyleSheet( QStringLiteral("QGroupBox { background-color: rgb(%1, %2, %3); border-radius: 5px;}; ").arg(bg.red()).arg(bg.green()).arg(bg.blue())); int height = QFontInfo(font()).pixelSize(); m_view.errorIcon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(height, height)); m_view.errorBox->setHidden(true); m_infoMessage = new KMessageWidget; m_view.info->addWidget(m_infoMessage); m_infoMessage->setCloseButtonVisible(false); m_infoMessage->hide(); m_jobInfoMessage = new KMessageWidget; m_view.jobInfo->addWidget(m_jobInfoMessage); m_jobInfoMessage->setCloseButtonVisible(false); m_jobInfoMessage->hide(); m_view.encoder_threads->setMinimum(0); m_view.encoder_threads->setMaximum(QThread::idealThreadCount()); m_view.encoder_threads->setToolTip(i18n("Encoding threads (0 is automatic)")); m_view.encoder_threads->setValue(KdenliveSettings::encodethreads()); connect(m_view.encoder_threads, static_cast(&QSpinBox::valueChanged), this, &RenderWidget::slotUpdateEncodeThreads); m_view.rescale_keep->setChecked(KdenliveSettings::rescalekeepratio()); connect(m_view.rescale_width, static_cast(&QSpinBox::valueChanged), this, &RenderWidget::slotUpdateRescaleWidth); connect(m_view.rescale_height, static_cast(&QSpinBox::valueChanged), this, &RenderWidget::slotUpdateRescaleHeight); m_view.rescale_keep->setIcon(QIcon::fromTheme(QStringLiteral("edit-link"))); m_view.rescale_keep->setToolTip(i18n("Preserve aspect ratio")); connect(m_view.rescale_keep, &QAbstractButton::clicked, this, &RenderWidget::slotSwitchAspectRatio); connect(m_view.buttonRender, SIGNAL(clicked()), this, SLOT(slotPrepareExport())); connect(m_view.buttonGenerateScript, &QAbstractButton::clicked, this, &RenderWidget::slotGenerateScript); m_view.abort_job->setEnabled(false); m_view.start_script->setEnabled(false); m_view.delete_script->setEnabled(false); connect(m_view.export_audio, &QCheckBox::stateChanged, this, &RenderWidget::slotUpdateAudioLabel); m_view.export_audio->setCheckState(Qt::PartiallyChecked); checkCodecs(); parseProfiles(); parseScriptFiles(); m_view.running_jobs->setUniformRowHeights(false); m_view.scripts_list->setUniformRowHeights(false); connect(m_view.start_script, &QAbstractButton::clicked, this, &RenderWidget::slotStartScript); connect(m_view.delete_script, &QAbstractButton::clicked, this, &RenderWidget::slotDeleteScript); connect(m_view.scripts_list, &QTreeWidget::itemSelectionChanged, this, &RenderWidget::slotCheckScript); connect(m_view.running_jobs, &QTreeWidget::itemSelectionChanged, this, &RenderWidget::slotCheckJob); connect(m_view.running_jobs, &QTreeWidget::itemDoubleClicked, this, &RenderWidget::slotPlayRendering); connect(m_view.buttonSave, &QAbstractButton::clicked, this, &RenderWidget::slotSaveProfile); connect(m_view.buttonEdit, &QAbstractButton::clicked, this, &RenderWidget::slotEditProfile); connect(m_view.buttonDelete, &QAbstractButton::clicked, this, &RenderWidget::slotDeleteProfile); connect(m_view.buttonFavorite, &QAbstractButton::clicked, this, &RenderWidget::slotCopyToFavorites); connect(m_view.buttonDownload, &QAbstractButton::clicked, this, &RenderWidget::slotDownloadNewRenderProfiles); connect(m_view.abort_job, &QAbstractButton::clicked, this, &RenderWidget::slotAbortCurrentJob); connect(m_view.start_job, &QAbstractButton::clicked, this, &RenderWidget::slotStartCurrentJob); connect(m_view.clean_up, &QAbstractButton::clicked, this, &RenderWidget::slotCLeanUpJobs); connect(m_view.hide_log, &QAbstractButton::clicked, this, &RenderWidget::slotHideLog); connect(m_view.buttonClose, &QAbstractButton::clicked, this, &QWidget::hide); connect(m_view.buttonClose2, &QAbstractButton::clicked, this, &QWidget::hide); connect(m_view.buttonClose3, &QAbstractButton::clicked, this, &QWidget::hide); connect(m_view.rescale, &QAbstractButton::toggled, this, &RenderWidget::setRescaleEnabled); connect(m_view.out_file, &KUrlRequester::textChanged, this, static_cast(&RenderWidget::slotUpdateButtons)); connect(m_view.out_file, &KUrlRequester::urlSelected, this, static_cast(&RenderWidget::slotUpdateButtons)); connect(m_view.formats, &QTreeWidget::currentItemChanged, this, &RenderWidget::refreshParams); connect(m_view.formats, &QTreeWidget::itemDoubleClicked, this, &RenderWidget::slotEditItem); connect(m_view.render_guide, &QAbstractButton::clicked, this, &RenderWidget::slotUpdateGuideBox); connect(m_view.render_zone, &QAbstractButton::clicked, this, &RenderWidget::slotUpdateGuideBox); connect(m_view.render_full, &QAbstractButton::clicked, this, &RenderWidget::slotUpdateGuideBox); connect(m_view.guide_end, static_cast(&KComboBox::activated), this, &RenderWidget::slotCheckStartGuidePosition); connect(m_view.guide_start, static_cast(&KComboBox::activated), this, &RenderWidget::slotCheckEndGuidePosition); connect(m_view.tc_overlay, &QAbstractButton::toggled, m_view.tc_type, &QWidget::setEnabled); // m_view.splitter->setStretchFactor(1, 5); // m_view.splitter->setStretchFactor(0, 2); m_view.out_file->setMode(KFile::File); #if KIO_VERSION >= QT_VERSION_CHECK(5, 33, 0) m_view.out_file->setAcceptMode(QFileDialog::AcceptSave); #elif !defined(KIOWIDGETS_DEPRECATED) m_view.out_file->fileDialog()->setAcceptMode(QFileDialog::AcceptSave); #endif m_view.out_file->setFocusPolicy(Qt::ClickFocus); m_jobsDelegate = new RenderViewDelegate(this); m_view.running_jobs->setHeaderLabels(QStringList() << QString() << i18n("File")); m_view.running_jobs->setItemDelegate(m_jobsDelegate); QHeaderView *header = m_view.running_jobs->header(); header->setSectionResizeMode(0, QHeaderView::Fixed); header->resizeSection(0, size + 4); header->setSectionResizeMode(1, QHeaderView::Interactive); m_view.scripts_list->setHeaderLabels(QStringList() << QString() << i18n("Stored Playlists")); m_scriptsDelegate = new RenderViewDelegate(this); m_view.scripts_list->setItemDelegate(m_scriptsDelegate); header = m_view.scripts_list->header(); header->setSectionResizeMode(0, QHeaderView::Fixed); header->resizeSection(0, size + 4); // Find path for Kdenlive renderer #ifdef Q_OS_WIN m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render.exe"); #else m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render"); #endif if (!QFile::exists(m_renderer)) { m_renderer = QStandardPaths::findExecutable(QStringLiteral("kdenlive_render")); if (m_renderer.isEmpty()) { m_renderer = QStringLiteral("kdenlive_render"); } } QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); if ((interface == nullptr) || (!interface->isServiceRegistered(QStringLiteral("org.kde.ksmserver")) && !interface->isServiceRegistered(QStringLiteral("org.gnome.SessionManager")))) { m_view.shutdown->setEnabled(false); } #ifdef KF5_USE_PURPOSE m_shareMenu = new Purpose::Menu(); m_view.shareButton->setMenu(m_shareMenu); m_view.shareButton->setIcon(QIcon::fromTheme(QStringLiteral("document-share"))); connect(m_shareMenu, &Purpose::Menu::finished, this, &RenderWidget::slotShareActionFinished); #else m_view.shareButton->setEnabled(false); #endif m_view.parallel_process->setChecked(KdenliveSettings::parallelrender()); connect(m_view.parallel_process, &QCheckBox::stateChanged, [](int state) { KdenliveSettings::setParallelrender(state == Qt::Checked); }); + if (KdenliveSettings::gpu_accel()) { + // Disable parallel rendering for movit + m_view.parallel_process->setEnabled(false); + } m_view.field_order->setEnabled(false); connect(m_view.scanning_list, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { m_view.field_order->setEnabled(index == 2); }); refreshView(); focusFirstVisibleItem(); adjustSize(); } void RenderWidget::slotShareActionFinished(const QJsonObject &output, int error, const QString &message) { #ifdef KF5_USE_PURPOSE m_jobInfoMessage->hide(); if (error) { KMessageBox::error(this, i18n("There was a problem sharing the document: %1", message), i18n("Share")); } else { const QString url = output["url"].toString(); if (url.isEmpty()) { m_jobInfoMessage->setMessageType(KMessageWidget::Positive); m_jobInfoMessage->setText(i18n("Document shared successfully")); m_jobInfoMessage->show(); } else { KMessageBox::information(this, i18n("You can find the shared document at: %1", url), i18n("Share"), QString(), KMessageBox::Notify | KMessageBox::AllowLink); } } #else Q_UNUSED(output); Q_UNUSED(error); Q_UNUSED(message); #endif } QSize RenderWidget::sizeHint() const { // Make sure the widget has minimum size on opening return {200, 200}; } RenderWidget::~RenderWidget() { m_view.running_jobs->blockSignals(true); m_view.scripts_list->blockSignals(true); m_view.running_jobs->clear(); m_view.scripts_list->clear(); delete m_jobsDelegate; delete m_scriptsDelegate; delete m_infoMessage; delete m_jobInfoMessage; } void RenderWidget::slotEditItem(QTreeWidgetItem *item) { if (item->parent() == nullptr) { // This is a top level item - group - don't edit return; } const QString edit = item->data(0, EditableRole).toString(); if (edit.isEmpty() || !edit.endsWith(QLatin1String("customprofiles.xml"))) { slotSaveProfile(); } else { slotEditProfile(); } } void RenderWidget::showInfoPanel() { if (m_view.advanced_params->isVisible()) { m_view.advanced_params->setVisible(false); KdenliveSettings::setShowrenderparams(false); } else { m_view.advanced_params->setVisible(true); KdenliveSettings::setShowrenderparams(true); } } void RenderWidget::updateDocumentPath() { if (m_view.out_file->url().isEmpty()) { return; } const QString fileName = m_view.out_file->url().fileName(); m_view.out_file->setUrl(QUrl::fromLocalFile(QDir(pCore->currentDoc()->projectDataFolder()).absoluteFilePath(fileName))); parseScriptFiles(); } void RenderWidget::slotUpdateGuideBox() { m_view.guides_box->setVisible(m_view.render_guide->isChecked()); } void RenderWidget::slotCheckStartGuidePosition() { if (m_view.guide_start->currentIndex() > m_view.guide_end->currentIndex()) { m_view.guide_start->setCurrentIndex(m_view.guide_end->currentIndex()); } } void RenderWidget::slotCheckEndGuidePosition() { if (m_view.guide_end->currentIndex() < m_view.guide_start->currentIndex()) { m_view.guide_end->setCurrentIndex(m_view.guide_start->currentIndex()); } } void RenderWidget::setGuides(const QList &guidesList, double duration) { m_view.guide_start->clear(); m_view.guide_end->clear(); if (!guidesList.isEmpty()) { m_view.guide_start->addItem(i18n("Beginning"), "0"); m_view.render_guide->setEnabled(true); m_view.create_chapter->setEnabled(true); } else { m_view.render_guide->setEnabled(false); m_view.create_chapter->setEnabled(false); } double fps = pCore->getCurrentProfile()->fps(); for (int i = 0; i < guidesList.count(); i++) { const CommentedTime &c = guidesList.at(i); GenTime pos = c.time(); const QString guidePos = Timecode::getStringTimecode(pos.frames(fps), fps); m_view.guide_start->addItem(c.comment() + QLatin1Char('/') + guidePos, pos.seconds()); m_view.guide_end->addItem(c.comment() + QLatin1Char('/') + guidePos, pos.seconds()); } if (!guidesList.isEmpty()) { m_view.guide_end->addItem(i18n("End"), QString::number(duration)); } } /** * Will be called when the user selects an output file via the file dialog. * File extension will be added automatically. */ void RenderWidget::slotUpdateButtons(const QUrl &url) { if (m_view.out_file->url().isEmpty()) { m_view.buttonGenerateScript->setEnabled(false); m_view.buttonRender->setEnabled(false); } else { updateButtons(); // This also checks whether the selected format is available } if (url.isValid()) { QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || (item->parent() == nullptr)) { // categories have no parent m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); return; } const QString extension = item->data(0, ExtensionRole).toString(); m_view.out_file->setUrl(filenameWithExtension(url, extension)); } } /** * Will be called when the user changes the output file path in the text line. * File extension must NOT be added, would make editing impossible! */ void RenderWidget::slotUpdateButtons() { if (m_view.out_file->url().isEmpty()) { m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); } else { updateButtons(); // This also checks whether the selected format is available } } void RenderWidget::slotSaveProfile() { Ui::SaveProfile_UI ui; QPointer d = new QDialog(this); ui.setupUi(d); QString customGroup; QStringList arguments = m_view.advanced_params->toPlainText().split(' ', QString::SkipEmptyParts); if (!arguments.isEmpty()) { ui.parameters->setText(arguments.join(QLatin1Char(' '))); } ui.profile_name->setFocus(); QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item != nullptr) && (item->parent() != nullptr)) { // not a category // Duplicate current item settings customGroup = item->parent()->text(0); ui.extension->setText(item->data(0, ExtensionRole).toString()); if (ui.parameters->toPlainText().contains(QStringLiteral("%bitrate")) || ui.parameters->toPlainText().contains(QStringLiteral("%quality"))) { if (ui.parameters->toPlainText().contains(QStringLiteral("%quality"))) { ui.vbitrates_label->setText(i18n("Qualities")); ui.default_vbitrate_label->setText(i18n("Default quality")); } else { ui.vbitrates_label->setText(i18n("Bitrates")); ui.default_vbitrate_label->setText(i18n("Default bitrate")); } if (item->data(0, BitratesRole).canConvert(QVariant::StringList) && (item->data(0, BitratesRole).toStringList().count() != 0)) { QStringList bitrates = item->data(0, BitratesRole).toStringList(); ui.vbitrates_list->setText(bitrates.join(QLatin1Char(','))); if (item->data(0, DefaultBitrateRole).canConvert(QVariant::String)) { ui.default_vbitrate->setValue(item->data(0, DefaultBitrateRole).toInt()); } } } else { ui.vbitrates->setHidden(true); } if (ui.parameters->toPlainText().contains(QStringLiteral("%audiobitrate")) || ui.parameters->toPlainText().contains(QStringLiteral("%audioquality"))) { if (ui.parameters->toPlainText().contains(QStringLiteral("%audioquality"))) { ui.abitrates_label->setText(i18n("Qualities")); ui.default_abitrate_label->setText(i18n("Default quality")); } else { ui.abitrates_label->setText(i18n("Bitrates")); ui.default_abitrate_label->setText(i18n("Default bitrate")); } if ((item != nullptr) && item->data(0, AudioBitratesRole).canConvert(QVariant::StringList) && (item->data(0, AudioBitratesRole).toStringList().count() != 0)) { QStringList bitrates = item->data(0, AudioBitratesRole).toStringList(); ui.abitrates_list->setText(bitrates.join(QLatin1Char(','))); if (item->data(0, DefaultAudioBitrateRole).canConvert(QVariant::String)) { ui.default_abitrate->setValue(item->data(0, DefaultAudioBitrateRole).toInt()); } } } else { ui.abitrates->setHidden(true); } if (item->data(0, SpeedsRole).canConvert(QVariant::StringList) && (item->data(0, SpeedsRole).toStringList().count() != 0)) { QStringList speeds = item->data(0, SpeedsRole).toStringList(); ui.speeds_list->setText(speeds.join('\n')); } } if (customGroup.isEmpty()) { customGroup = i18nc("Group Name", "Custom"); } ui.group_name->setText(customGroup); if (d->exec() == QDialog::Accepted && !ui.profile_name->text().simplified().isEmpty()) { QString newProfileName = ui.profile_name->text().simplified(); QString newGroupName = ui.group_name->text().simplified(); if (newGroupName.isEmpty()) { newGroupName = i18nc("Group Name", "Custom"); } QDomDocument doc; QDomElement profileElement = doc.createElement(QStringLiteral("profile")); profileElement.setAttribute(QStringLiteral("name"), newProfileName); profileElement.setAttribute(QStringLiteral("category"), newGroupName); profileElement.setAttribute(QStringLiteral("extension"), ui.extension->text().simplified()); QString args = ui.parameters->toPlainText().simplified(); profileElement.setAttribute(QStringLiteral("args"), args); if (args.contains(QStringLiteral("%bitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultbitrate"), QString::number(ui.default_vbitrate->value())); profileElement.setAttribute(QStringLiteral("bitrates"), ui.vbitrates_list->text()); } else if (args.contains(QStringLiteral("%quality"))) { profileElement.setAttribute(QStringLiteral("defaultquality"), QString::number(ui.default_vbitrate->value())); profileElement.setAttribute(QStringLiteral("qualities"), ui.vbitrates_list->text()); } if (args.contains(QStringLiteral("%audiobitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudiobitrate"), QString::number(ui.default_abitrate->value())); profileElement.setAttribute(QStringLiteral("audiobitrates"), ui.abitrates_list->text()); } else if (args.contains(QStringLiteral("%audioquality"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudioquality"), QString::number(ui.default_abitrate->value())); profileElement.setAttribute(QStringLiteral("audioqualities"), ui.abitrates_list->text()); } QString speeds_list_str = ui.speeds_list->toPlainText(); if (!speeds_list_str.isEmpty()) { profileElement.setAttribute(QStringLiteral("speeds"), speeds_list_str.replace('\n', ';').simplified()); } doc.appendChild(profileElement); saveProfile(doc.documentElement()); parseProfiles(); } delete d; } bool RenderWidget::saveProfile(QDomElement newprofile) { QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/export/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } QDomDocument doc; QFile file(dir.absoluteFilePath(QStringLiteral("customprofiles.xml"))); doc.setContent(&file, false); file.close(); QDomElement documentElement; QDomElement profiles = doc.documentElement(); if (profiles.isNull() || profiles.tagName() != QLatin1String("profiles")) { doc.clear(); profiles = doc.createElement(QStringLiteral("profiles")); profiles.setAttribute(QStringLiteral("version"), 1); doc.appendChild(profiles); } int version = profiles.attribute(QStringLiteral("version"), nullptr).toInt(); if (version < 1) { doc.clear(); profiles = doc.createElement(QStringLiteral("profiles")); profiles.setAttribute(QStringLiteral("version"), 1); doc.appendChild(profiles); } QDomNodeList profilelist = doc.elementsByTagName(QStringLiteral("profile")); QString newProfileName = newprofile.attribute(QStringLiteral("name")); // Check existing profiles QStringList existingProfileNames; int i = 0; while (!profilelist.item(i).isNull()) { documentElement = profilelist.item(i).toElement(); QString profileName = documentElement.attribute(QStringLiteral("name")); existingProfileNames << profileName; i++; } // Check if a profile with that same name already exists bool ok; while (existingProfileNames.contains(newProfileName)) { QString updatedProfileName = QInputDialog::getText(this, i18n("Profile already exists"), i18n("This profile name already exists. Change the name if you do not want to overwrite it."), QLineEdit::Normal, newProfileName, &ok); if (!ok) { return false; } if (updatedProfileName == newProfileName) { // remove previous profile profiles.removeChild(profilelist.item(existingProfileNames.indexOf(newProfileName))); break; } else { newProfileName = updatedProfileName; newprofile.setAttribute(QStringLiteral("name"), newProfileName); } } profiles.appendChild(newprofile); // QCString save = doc.toString().utf8(); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", dir.absoluteFilePath("customprofiles.xml"))); return false; } QTextStream out(&file); out << doc.toString(); if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", dir.absoluteFilePath("customprofiles.xml"))); file.close(); return false; } file.close(); return true; } void RenderWidget::slotCopyToFavorites() { QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || (item->parent() == nullptr)) { return; } QString params = item->data(0, ParamsRole).toString(); QString extension = item->data(0, ExtensionRole).toString(); QString currentProfile = item->text(0); QDomDocument doc; QDomElement profileElement = doc.createElement(QStringLiteral("profile")); profileElement.setAttribute(QStringLiteral("name"), currentProfile); profileElement.setAttribute(QStringLiteral("category"), i18nc("Category Name", "Custom")); profileElement.setAttribute(QStringLiteral("destinationid"), QStringLiteral("favorites")); profileElement.setAttribute(QStringLiteral("extension"), extension); profileElement.setAttribute(QStringLiteral("args"), params); if (params.contains(QStringLiteral("%bitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultbitrate"), item->data(0, DefaultBitrateRole).toString()); profileElement.setAttribute(QStringLiteral("bitrates"), item->data(0, BitratesRole).toStringList().join(QLatin1Char(','))); } else if (params.contains(QStringLiteral("%quality"))) { profileElement.setAttribute(QStringLiteral("defaultquality"), item->data(0, DefaultBitrateRole).toString()); profileElement.setAttribute(QStringLiteral("qualities"), item->data(0, BitratesRole).toStringList().join(QLatin1Char(','))); } if (params.contains(QStringLiteral("%audiobitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudiobitrate"), item->data(0, DefaultAudioBitrateRole).toString()); profileElement.setAttribute(QStringLiteral("audiobitrates"), item->data(0, AudioBitratesRole).toStringList().join(QLatin1Char(','))); } else if (params.contains(QStringLiteral("%audioquality"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudioquality"), item->data(0, DefaultAudioBitrateRole).toString()); profileElement.setAttribute(QStringLiteral("audioqualities"), item->data(0, AudioBitratesRole).toStringList().join(QLatin1Char(','))); } if (item->data(0, SpeedsRole).canConvert(QVariant::StringList) && (item->data(0, SpeedsRole).toStringList().count() != 0)) { // profile has a variable speed profileElement.setAttribute(QStringLiteral("speeds"), item->data(0, SpeedsRole).toStringList().join(QLatin1Char(';'))); } doc.appendChild(profileElement); if (saveProfile(doc.documentElement())) { parseProfiles(profileElement.attribute(QStringLiteral("name"))); } } void RenderWidget::slotDownloadNewRenderProfiles() { if (getNewStuff(QStringLiteral(":data/kdenlive_renderprofiles.knsrc")) > 0) { reloadProfiles(); } } int RenderWidget::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 RenderWidget::slotEditProfile() { QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || (item->parent() == nullptr)) { return; } QString params = item->data(0, ParamsRole).toString(); Ui::SaveProfile_UI ui; QPointer d = new QDialog(this); ui.setupUi(d); QString customGroup = item->parent()->text(0); if (customGroup.isEmpty()) { customGroup = i18nc("Group Name", "Custom"); } ui.group_name->setText(customGroup); ui.profile_name->setText(item->text(0)); ui.extension->setText(item->data(0, ExtensionRole).toString()); ui.parameters->setText(params); ui.profile_name->setFocus(); if (params.contains(QStringLiteral("%bitrate")) || ui.parameters->toPlainText().contains(QStringLiteral("%quality"))) { if (params.contains(QStringLiteral("%quality"))) { ui.vbitrates_label->setText(i18n("Qualities")); ui.default_vbitrate_label->setText(i18n("Default quality")); } else { ui.vbitrates_label->setText(i18n("Bitrates")); ui.default_vbitrate_label->setText(i18n("Default bitrate")); } if (item->data(0, BitratesRole).canConvert(QVariant::StringList) && (item->data(0, BitratesRole).toStringList().count() != 0)) { QStringList bitrates = item->data(0, BitratesRole).toStringList(); ui.vbitrates_list->setText(bitrates.join(QLatin1Char(','))); if (item->data(0, DefaultBitrateRole).canConvert(QVariant::String)) { ui.default_vbitrate->setValue(item->data(0, DefaultBitrateRole).toInt()); } } } else { ui.vbitrates->setHidden(true); } if (params.contains(QStringLiteral("%audiobitrate")) || ui.parameters->toPlainText().contains(QStringLiteral("%audioquality"))) { if (params.contains(QStringLiteral("%audioquality"))) { ui.abitrates_label->setText(i18n("Qualities")); ui.default_abitrate_label->setText(i18n("Default quality")); } else { ui.abitrates_label->setText(i18n("Bitrates")); ui.default_abitrate_label->setText(i18n("Default bitrate")); } if (item->data(0, AudioBitratesRole).canConvert(QVariant::StringList) && (item->data(0, AudioBitratesRole).toStringList().count() != 0)) { QStringList bitrates = item->data(0, AudioBitratesRole).toStringList(); ui.abitrates_list->setText(bitrates.join(QLatin1Char(','))); if (item->data(0, DefaultAudioBitrateRole).canConvert(QVariant::String)) { ui.default_abitrate->setValue(item->data(0, DefaultAudioBitrateRole).toInt()); } } } else { ui.abitrates->setHidden(true); } if (item->data(0, SpeedsRole).canConvert(QVariant::StringList) && (item->data(0, SpeedsRole).toStringList().count() != 0)) { QStringList speeds = item->data(0, SpeedsRole).toStringList(); ui.speeds_list->setText(speeds.join('\n')); } d->setWindowTitle(i18n("Edit Profile")); if (d->exec() == QDialog::Accepted) { slotDeleteProfile(true); QString exportFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/export/customprofiles.xml"); QDomDocument doc; QFile file(exportFile); doc.setContent(&file, false); file.close(); QDomElement documentElement; QDomElement profiles = doc.documentElement(); if (profiles.isNull() || profiles.tagName() != QLatin1String("profiles")) { doc.clear(); profiles = doc.createElement(QStringLiteral("profiles")); profiles.setAttribute(QStringLiteral("version"), 1); doc.appendChild(profiles); } int version = profiles.attribute(QStringLiteral("version"), nullptr).toInt(); if (version < 1) { doc.clear(); profiles = doc.createElement(QStringLiteral("profiles")); profiles.setAttribute(QStringLiteral("version"), 1); doc.appendChild(profiles); } QString newProfileName = ui.profile_name->text().simplified(); QString newGroupName = ui.group_name->text().simplified(); if (newGroupName.isEmpty()) { newGroupName = i18nc("Group Name", "Custom"); } QDomNodeList profilelist = doc.elementsByTagName(QStringLiteral("profile")); int i = 0; while (!profilelist.item(i).isNull()) { // make sure a profile with same name doesn't exist documentElement = profilelist.item(i).toElement(); QString profileName = documentElement.attribute(QStringLiteral("name")); if (profileName == newProfileName) { // a profile with that same name already exists bool ok; newProfileName = QInputDialog::getText(this, i18n("Profile already exists"), i18n("This profile name already exists. Change the name if you do not want to overwrite it."), QLineEdit::Normal, newProfileName, &ok); if (!ok) { return; } if (profileName == newProfileName) { profiles.removeChild(profilelist.item(i)); break; } } ++i; } QDomElement profileElement = doc.createElement(QStringLiteral("profile")); profileElement.setAttribute(QStringLiteral("name"), newProfileName); profileElement.setAttribute(QStringLiteral("category"), newGroupName); profileElement.setAttribute(QStringLiteral("extension"), ui.extension->text().simplified()); QString args = ui.parameters->toPlainText().simplified(); profileElement.setAttribute(QStringLiteral("args"), args); if (args.contains(QStringLiteral("%bitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultbitrate"), QString::number(ui.default_vbitrate->value())); profileElement.setAttribute(QStringLiteral("bitrates"), ui.vbitrates_list->text()); } else if (args.contains(QStringLiteral("%quality"))) { profileElement.setAttribute(QStringLiteral("defaultquality"), QString::number(ui.default_vbitrate->value())); profileElement.setAttribute(QStringLiteral("qualities"), ui.vbitrates_list->text()); } if (args.contains(QStringLiteral("%audiobitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudiobitrate"), QString::number(ui.default_abitrate->value())); profileElement.setAttribute(QStringLiteral("audiobitrates"), ui.abitrates_list->text()); } else if (args.contains(QStringLiteral("%audioquality"))) { profileElement.setAttribute(QStringLiteral("defaultaudioquality"), QString::number(ui.default_abitrate->value())); profileElement.setAttribute(QStringLiteral("audioqualities"), ui.abitrates_list->text()); } QString speeds_list_str = ui.speeds_list->toPlainText(); if (!speeds_list_str.isEmpty()) { // profile has a variable speed profileElement.setAttribute(QStringLiteral("speeds"), speeds_list_str.replace('\n', ';').simplified()); } profiles.appendChild(profileElement); // QCString save = doc.toString().utf8(); delete d; if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::error(this, i18n("Cannot write to file %1", exportFile)); return; } QTextStream out(&file); out << doc.toString(); if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", exportFile)); file.close(); return; } file.close(); parseProfiles(); } else { delete d; } } void RenderWidget::slotDeleteProfile(bool dontRefresh) { // TODO: delete a profile installed by KNewStuff the easy way /* QString edit = m_view.formats->currentItem()->data(EditableRole).toString(); if (!edit.endsWith(QLatin1String("customprofiles.xml"))) { // This is a KNewStuff installed file, process through KNS KNS::Engine engine(0); if (engine.init("kdenlive_render.knsrc")) { KNS::Entry::List entries; } return; }*/ QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || (item->parent() == nullptr)) { return; } QString currentProfile = item->text(0); QString exportFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/export/customprofiles.xml"); QDomDocument doc; QFile file(exportFile); doc.setContent(&file, false); file.close(); QDomElement documentElement; QDomNodeList profiles = doc.elementsByTagName(QStringLiteral("profile")); int i = 0; QString profileName; while (!profiles.item(i).isNull()) { documentElement = profiles.item(i).toElement(); profileName = documentElement.attribute(QStringLiteral("name")); if (profileName == currentProfile) { doc.documentElement().removeChild(profiles.item(i)); break; } ++i; } if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", exportFile)); return; } QTextStream out(&file); out << doc.toString(); if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", exportFile)); file.close(); return; } file.close(); if (dontRefresh) { return; } parseProfiles(); focusFirstVisibleItem(); } void RenderWidget::updateButtons() { if ((m_view.formats->currentItem() == nullptr) || m_view.formats->currentItem()->isHidden()) { m_view.buttonSave->setEnabled(false); m_view.buttonDelete->setEnabled(false); m_view.buttonEdit->setEnabled(false); m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); } else { m_view.buttonSave->setEnabled(true); m_view.buttonRender->setEnabled(m_view.formats->currentItem()->data(0, ErrorRole).isNull()); m_view.buttonGenerateScript->setEnabled(m_view.formats->currentItem()->data(0, ErrorRole).isNull()); QString edit = m_view.formats->currentItem()->data(0, EditableRole).toString(); if (edit.isEmpty() || !edit.endsWith(QLatin1String("customprofiles.xml"))) { m_view.buttonDelete->setEnabled(false); m_view.buttonEdit->setEnabled(false); } else { m_view.buttonDelete->setEnabled(true); m_view.buttonEdit->setEnabled(true); } } } void RenderWidget::focusFirstVisibleItem(const QString &profile) { QTreeWidgetItem *item = nullptr; if (!profile.isEmpty()) { QList items = m_view.formats->findItems(profile, Qt::MatchExactly | Qt::MatchRecursive); if (!items.isEmpty()) { item = items.constFirst(); } } if (!item) { // searched profile not found in any category, select 1st available profile for (int i = 0; i < m_view.formats->topLevelItemCount(); ++i) { item = m_view.formats->topLevelItem(i); if (item->childCount() > 0) { item = item->child(0); break; } } } if (item) { m_view.formats->setCurrentItem(item); item->parent()->setExpanded(true); refreshParams(); } updateButtons(); } void RenderWidget::slotPrepareExport(bool delayedRendering, const QString &scriptPath) { Q_UNUSED(scriptPath); if (!QFile::exists(KdenliveSettings::rendererpath())) { KMessageBox::sorry(this, i18n("Cannot find the melt program required for rendering (part of Mlt)")); return; } if (QFile::exists(m_view.out_file->url().toLocalFile())) { if (KMessageBox::warningYesNo(this, i18n("Output file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { return; } } QString chapterFile; if (m_view.create_chapter->isChecked()) { chapterFile = m_view.out_file->url().toLocalFile() + QStringLiteral(".dvdchapter"); } // mantisbt 1051 QDir dir(m_view.out_file->url().adjusted(QUrl::RemoveFilename).toLocalFile()); if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { KMessageBox::sorry(this, i18n("The directory %1, could not be created.\nPlease make sure you have the required permissions.", m_view.out_file->url().adjusted(QUrl::RemoveFilename).toLocalFile())); return; } prepareRendering(delayedRendering, chapterFile); } void RenderWidget::prepareRendering(bool delayedRendering, const QString &chapterFile) { KdenliveDoc *project = pCore->currentDoc(); QString playlistPath; QString mltSuffix(QStringLiteral(".mlt")); QList playlistPaths; QList trackNames; QString renderName; if (delayedRendering) { bool ok; renderName = QFileInfo(pCore->currentDoc()->url().toLocalFile()).fileName(); if (renderName.isEmpty()) { renderName = i18n("export") + QStringLiteral(".mlt"); } QDir projectFolder(pCore->currentDoc()->projectDataFolder()); projectFolder.mkpath(QStringLiteral("kdenlive-renderqueue")); projectFolder.cd(QStringLiteral("kdenlive-renderqueue")); if (projectFolder.exists(renderName)) { int ix = 1; while (projectFolder.exists(renderName)) { if (renderName.contains(QLatin1Char('-'))) { renderName = renderName.section(QLatin1Char('-'), 0, -2); } else { renderName = renderName.section(QLatin1Char('.'), 0, -2); } renderName.append(QString("-%1.mlt").arg(ix)); ix++; } } renderName = renderName.section(QLatin1Char('.'), 0, -2); renderName = QInputDialog::getText(this, i18n("Delayed rendering"), i18n("Select a name for this rendering."), QLineEdit::Normal, renderName, &ok); if (!ok) { return; } if (!renderName.endsWith(QStringLiteral(".mlt"))) { renderName.append(QStringLiteral(".mlt")); } if (projectFolder.exists(renderName)) { if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", renderName)) == KMessageBox::No) { return; } } playlistPath = projectFolder.absoluteFilePath(renderName); } else { QTemporaryFile tmp(QDir::tempPath() + "/kdenlive-XXXXXX.mlt"); if (!tmp.open()) { // Something went wrong return; } tmp.close(); playlistPath = tmp.fileName(); } int in = 0; int out; Monitor *pMon = pCore->getMonitor(Kdenlive::ProjectMonitor); bool zoneOnly = m_view.render_zone->isChecked(); if (zoneOnly) { in = pMon->getZoneStart(); out = pMon->getZoneEnd() - 1; } else { out = pCore->projectDuration() - 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++) { const 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(); } } } // 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() && !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 = Xml::getXmlProperty(e, QStringLiteral("resource")); producerService = Xml::getXmlProperty(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); Xml::setXmlProperty(e, QStringLiteral("resource"), prefix + replacementResource + suffix); if (producerService == QLatin1String("timewarp")) { Xml::setXmlProperty(e, QStringLiteral("warp_resource"), replacementResource); } // We need to delete the "aspect_ratio" property because proxy clips // sometimes have different ratio than original clips Xml::removeXmlProperty(e, QStringLiteral("aspect_ratio")); Xml::removeMetaProperties(e); } } } } generateRenderFiles(doc, playlistPath, in, out, delayedRendering); } void RenderWidget::generateRenderFiles(QDomDocument doc, const QString &playlistPath, int in, int out, bool delayedRendering) { QDomDocument clone; KdenliveDoc *project = pCore->currentDoc(); int passes = m_view.checkTwoPass->isChecked() ? 2 : 1; QString renderArgs = m_view.advanced_params->toPlainText().simplified(); QDomElement consumer = doc.createElement(QStringLiteral("consumer")); QDomNodeList profiles = doc.elementsByTagName(QStringLiteral("profile")); if (profiles.isEmpty()) { doc.documentElement().insertAfter(consumer, doc.documentElement()); } else { doc.documentElement().insertAfter(consumer, profiles.at(profiles.length() - 1)); } // Check for fps change double forcedfps = 0; if (renderArgs.startsWith(QLatin1String("r="))) { QString sub = renderArgs.section(QLatin1Char(' '), 0, 0).toLower(); sub = sub.section(QLatin1Char('='), 1, 1); forcedfps = sub.toDouble(); } else if (renderArgs.contains(QStringLiteral(" r="))) { QString sub = renderArgs.section(QStringLiteral(" r="), 1, 1); sub = sub.section(QLatin1Char(' '), 0, 0).toLower(); forcedfps = sub.toDouble(); } else if (renderArgs.contains(QStringLiteral("mlt_profile="))) { QString sub = renderArgs.section(QStringLiteral("mlt_profile="), 1, 1); sub = sub.section(QLatin1Char(' '), 0, 0).toLower(); forcedfps = ProfileRepository::get()->getProfile(sub)->fps(); } bool resizeProfile = false; std::unique_ptr &profile = pCore->getCurrentProfile(); if (renderArgs.contains(QLatin1String("%dv_standard"))) { QString dvstd; if (fmod((double)profile->frame_rate_num() / profile->frame_rate_den(), 30.01) > 27) { dvstd = QStringLiteral("ntsc"); if (!(profile->frame_rate_num() == 30000 && profile->frame_rate_den() == 1001)) { forcedfps = 30000.0 / 1001; } if (!(profile->width() == 720 && profile->height() == 480)) { resizeProfile = true; } } else { dvstd = QStringLiteral("pal"); if (!(profile->frame_rate_num() == 25 && profile->frame_rate_den() == 1)) { forcedfps = 25; } if (!(profile->width() == 720 && profile->height() == 576)) { resizeProfile = true; } } if ((double)profile->display_aspect_num() / profile->display_aspect_den() > 1.5) { dvstd += QLatin1String("_wide"); } renderArgs.replace(QLatin1String("%dv_standard"), dvstd); } QStringList args = renderArgs.split(QLatin1Char(' ')); for (auto ¶m : args) { if (param.contains(QLatin1Char('='))) { QString paramValue = param.section(QLatin1Char('='), 1); if (paramValue.startsWith(QLatin1Char('%'))) { if (paramValue.startsWith(QStringLiteral("%bitrate")) || paramValue == QStringLiteral("%quality")) { if (paramValue.contains("+'k'")) paramValue = QString::number(m_view.video->value()) + 'k'; else paramValue = QString::number(m_view.video->value()); } if (paramValue.startsWith(QStringLiteral("%audiobitrate")) || paramValue == QStringLiteral("%audioquality")) { if (paramValue.contains("+'k'")) paramValue = QString::number(m_view.audio->value()) + 'k'; else paramValue = QString::number(m_view.audio->value()); } if (paramValue == QStringLiteral("%dar")) paramValue = '@' + QString::number(profile->display_aspect_num()) + QLatin1Char('/') + QString::number(profile->display_aspect_den()); if (paramValue == QStringLiteral("%passes")) paramValue = QString::number(static_cast(m_view.checkTwoPass->isChecked()) + 1); } consumer.setAttribute(param.section(QLatin1Char('='), 0, 0), paramValue); } } // Check for movit if (KdenliveSettings::gpu_accel()) { consumer.setAttribute(QStringLiteral("glsl."), 1); } // in/out points if (m_view.render_guide->isChecked()) { double fps = profile->fps(); double guideStart = m_view.guide_start->itemData(m_view.guide_start->currentIndex()).toDouble(); double guideEnd = m_view.guide_end->itemData(m_view.guide_end->currentIndex()).toDouble(); consumer.setAttribute(QStringLiteral("in"), (int)GenTime(guideStart).frames(fps)); consumer.setAttribute(QStringLiteral("out"), (int)GenTime(guideEnd).frames(fps)); } else { consumer.setAttribute(QStringLiteral("in"), in); consumer.setAttribute(QStringLiteral("out"), out); } // Check if the rendering profile is different from project profile, // in which case we need to use the producer_comsumer from MLT QString subsize; if (renderArgs.startsWith(QLatin1String("s="))) { subsize = renderArgs.section(QLatin1Char(' '), 0, 0).toLower(); subsize = subsize.section(QLatin1Char('='), 1, 1); } else if (renderArgs.contains(QStringLiteral(" s="))) { subsize = renderArgs.section(QStringLiteral(" s="), 1, 1); subsize = subsize.section(QLatin1Char(' '), 0, 0).toLower(); } else if (m_view.rescale->isChecked() && m_view.rescale->isEnabled()) { subsize = QStringLiteral("%1x%2").arg(m_view.rescale_width->value()).arg(m_view.rescale_height->value()); } if (!subsize.isEmpty()) { consumer.setAttribute(QStringLiteral("s"), subsize); } // Check if we need to embed the playlist into the producer consumer // That is required if PAR != 1 if (profile->sample_aspect_num() != profile->sample_aspect_den() && subsize.isEmpty()) { resizeProfile = true; } // Project metadata if (m_view.export_meta->isChecked()) { QMap metadata = project->metadata(); QMap::const_iterator i = metadata.constBegin(); while (i != metadata.constEnd()) { consumer.setAttribute(i.key(), QString(QUrl::toPercentEncoding(i.value()))); ++i; } } // Adjust scanning switch (m_view.scanning_list->currentIndex()) { case 1: consumer.setAttribute(QStringLiteral("progressive"), 1); break; case 2: // Interlaced rendering consumer.setAttribute(QStringLiteral("progressive"), 0); // Adjust field order consumer.setAttribute(QStringLiteral("top_field_first"), m_view.field_order->currentIndex()); break; default: // leave as is break; } // check if audio export is selected bool exportAudio; if (automaticAudioExport()) { // TODO check if projact contains audio // exportAudio = pCore->projectManager()->currentTimeline()->checkProjectAudio(); exportAudio = true; } else { exportAudio = selectedAudioExport(); } if (renderArgs.contains(QLatin1String("pix_fmt=argb")) || renderArgs.contains(QLatin1String("pix_fmt=abgr")) || renderArgs.contains(QLatin1String("pix_fmt=bgra")) || renderArgs.contains(QLatin1String("pix_fmt=gbra")) || renderArgs.contains(QLatin1String("pix_fmt=rgba")) || renderArgs.contains(QLatin1String("pix_fmt=yuva")) || renderArgs.contains(QLatin1String("pix_fmt=ya" )) || renderArgs.contains(QLatin1String("pix_fmt=ayuv"))) { auto prods = doc.elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < prods.count(); ++i) { auto prod = prods.at(i).toElement(); if (prod.attribute(QStringLiteral("id")) == QStringLiteral("black_track")) { auto props = prod.elementsByTagName(QStringLiteral("property")); for (int j = 0; j < props.count(); ++j) { auto prop = props.at(i).toElement(); if (prop.attribute(QStringLiteral("name")) == QStringLiteral("resource")) { prop.firstChild().setNodeValue(QStringLiteral("transparent")); break; } } break; } } } // disable audio if requested if (!exportAudio) { consumer.setAttribute(QStringLiteral("an"), 1); } int threadCount = QThread::idealThreadCount(); - if (threadCount < 2 || !m_view.parallel_process->isChecked()) { + if (threadCount < 2 || !m_view.parallel_process->isChecked() || !m_view.parallel_process->isEnabled()) { threadCount = 1; } // Set the thread counts if (!renderArgs.contains(QStringLiteral("threads="))) { consumer.setAttribute(QStringLiteral("threads"), KdenliveSettings::encodethreads()); } consumer.setAttribute(QStringLiteral("real_time"), -threadCount); // 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++; } } }*/ if (m_view.checkTwoPass->isChecked()) { // We will generate 2 files, one for each pass. clone = doc.cloneNode(true).toDocument(); } QStringList playlists; QString renderedFile = m_view.out_file->url().toLocalFile(); for (int i = 0; i < passes; i++) { // Append consumer settings QDomDocument final = i > 0 ? clone : doc; QDomNodeList cons = final.elementsByTagName(QStringLiteral("consumer")); QDomElement myConsumer = cons.at(0).toElement(); QString mytarget = renderedFile; QString playlistName = playlistPath; myConsumer.setAttribute(QStringLiteral("mlt_service"), QStringLiteral("avformat")); if (passes == 2 && i == 1) { playlistName = playlistName.section(QLatin1Char('.'), 0, -2) + QString("-pass%1.").arg(i + 1) + playlistName.section(QLatin1Char('.'), -1); } playlists << playlistName; myConsumer.setAttribute(QStringLiteral("target"), mytarget); // Prepare rendering args int pass = passes == 2 ? i + 1 : 0; if (renderArgs.contains(QStringLiteral("libx265"))) { if (pass == 1 || pass == 2) { QString x265params = myConsumer.attribute("x265-params"); x265params = QString("pass=%1:stats=%2:%3").arg(pass).arg(mytarget.replace(":", "\\:") + "_2pass.log").arg(x265params); myConsumer.setAttribute("x265-params", x265params); } } else { if (pass == 1 || pass == 2) { myConsumer.setAttribute("pass", pass); myConsumer.setAttribute("passlogfile", mytarget + "_2pass.log"); } if (pass == 1) { myConsumer.setAttribute("fastfirstpass", 1); myConsumer.removeAttribute("acodec"); myConsumer.setAttribute("an", 1); } else { myConsumer.removeAttribute("fastfirstpass"); } } QFile file(playlistName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { pCore->displayMessage(i18n("Cannot write to file %1", playlistName), ErrorMessage); return; } file.write(final.toString().toUtf8()); if (file.error() != QFile::NoError) { pCore->displayMessage(i18n("Cannot write to file %1", playlistName), ErrorMessage); file.close(); return; } file.close(); } // Create job RenderJobItem *renderItem = nullptr; QList existing = m_view.running_jobs->findItems(renderedFile, Qt::MatchExactly, 1); if (!existing.isEmpty()) { renderItem = static_cast(existing.at(0)); if (renderItem->status() == RUNNINGJOB || renderItem->status() == WAITINGJOB || renderItem->status() == STARTINGJOB) { KMessageBox::information( this, i18n("There is already a job writing file:
%1
Abort the job if you want to overwrite it...", renderedFile), i18n("Already running")); return; } if (delayedRendering || playlists.size() > 1) { delete renderItem; renderItem = nullptr; } else { renderItem->setData(1, ProgressRole, 0); renderItem->setStatus(WAITINGJOB); renderItem->setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause"))); renderItem->setData(1, Qt::UserRole, i18n("Waiting...")); QStringList argsJob = {KdenliveSettings::rendererpath(), playlistPath, renderedFile, QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid())}; renderItem->setData(1, ParametersRole, argsJob); renderItem->setData(1, TimeRole, QDateTime::currentDateTime()); if (!exportAudio) { renderItem->setData(1, ExtraInfoRole, i18n("Video without audio track")); } else { renderItem->setData(1, ExtraInfoRole, QString()); } m_view.running_jobs->setCurrentItem(renderItem); m_view.tabWidget->setCurrentIndex(1); checkRenderStatus(); return; } } if (delayedRendering) { parseScriptFiles(); return; } QList jobList; for (const QString &pl : playlists) { renderItem = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << renderedFile); renderItem->setData(1, TimeRole, QDateTime::currentDateTime()); QStringList argsJob = {KdenliveSettings::rendererpath(), pl, renderedFile, QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid())}; renderItem->setData(1, ParametersRole, argsJob); qDebug() << "* CREATED JOB WITH ARGS: " << argsJob; if (!exportAudio) { renderItem->setData(1, ExtraInfoRole, i18n("Video without audio track")); } else { renderItem->setData(1, ExtraInfoRole, QString()); } jobList << renderItem; } m_view.running_jobs->setCurrentItem(jobList.at(0)); m_view.tabWidget->setCurrentIndex(1); // check render status checkRenderStatus(); // 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)) { pCore->displayMessage(i18n("Cannot write to file %1", plPath), ErrorMessage); return; } file.write(docList.at(i).toString().toUtf8()); if (file.error() != QFile::NoError) { pCore->displayMessage(i18n("Cannot write to file %1", plPath), ErrorMessage); file.close(); return; } file.close(); }*/ // slotExport(delayedRendering, in, out, project->metadata(), playlistPaths, trackNames, renderName, exportAudio); } void RenderWidget::slotExport(bool scriptExport, int zoneIn, int zoneOut, const QMap &metadata, const QList &playlistPaths, const QList &trackNames, const QString &scriptPath, bool exportAudio) { // DEPRECATED QTreeWidgetItem *item = m_view.formats->currentItem(); if (!item) { return; } QString destBase = m_view.out_file->url().toLocalFile().trimmed(); if (destBase.isEmpty()) { return; } // script file QFile file(scriptPath); int stemCount = playlistPaths.count(); bool stemExport = (!trackNames.isEmpty()); for (int stemIdx = 0; stemIdx < stemCount; stemIdx++) { QString dest(destBase); // on stem export append track name to each filename if (stemExport) { QFileInfo dfi(dest); QStringList filePath; // construct the full file path filePath << dfi.absolutePath() << QDir::separator() << dfi.completeBaseName() + QLatin1Char('_') << QString(trackNames.at(stemIdx)).replace(QLatin1Char(' '), QLatin1Char('_')) << QStringLiteral(".") << dfi.suffix(); dest = filePath.join(QString()); } // Check whether target file has an extension. // If not, ask whether extension should be added or not. QString extension = item->data(0, ExtensionRole).toString(); if (!dest.endsWith(extension, Qt::CaseInsensitive)) { if (KMessageBox::questionYesNo(this, i18n("File has no extension. Add extension (%1)?", extension)) == KMessageBox::Yes) { dest.append('.' + extension); } } // Checks for image sequence QStringList imageSequences; imageSequences << QStringLiteral("jpg") << QStringLiteral("png") << QStringLiteral("bmp") << QStringLiteral("dpx") << QStringLiteral("ppm") << QStringLiteral("tga") << QStringLiteral("tif"); if (imageSequences.contains(extension)) { // format string for counter? if (!QRegExp(QStringLiteral(".*%[0-9]*d.*")).exactMatch(dest)) { dest = dest.section(QLatin1Char('.'), 0, -2) + QStringLiteral("_%05d.") + extension; } } if (QFile::exists(dest)) { if (KMessageBox::warningYesNo(this, i18n("Output file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { for (const QString &playlistFilePath : playlistPaths) { QFile playlistFile(playlistFilePath); if (playlistFile.exists()) { playlistFile.remove(); } } return; } } // Generate script file QStringList overlayargs; if (m_view.tc_overlay->isChecked()) { QString filterFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("metadata.properties")); overlayargs << QStringLiteral("meta.attr.timecode=1") << "meta.attr.timecode.markup=#" + QString(m_view.tc_type->currentIndex() != 0 ? "frame" : "timecode"); overlayargs << QStringLiteral("-attach") << QStringLiteral("data_feed:attr_check") << QStringLiteral("-attach"); overlayargs << "data_show:" + filterFile << QStringLiteral("_loader=1") << QStringLiteral("dynamic=1"); } QStringList render_process_args; if (!scriptExport) { render_process_args << QStringLiteral("-erase"); } #ifndef Q_OS_WIN if (KdenliveSettings::usekuiserver()) { render_process_args << QStringLiteral("-kuiserver"); } // get process id render_process_args << QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid()); #endif // Set locale for render process if required if (QLocale().decimalPoint() != QLocale::system().decimalPoint()) { ; #ifndef Q_OS_MAC const QString currentLocale = setlocale(LC_NUMERIC, nullptr); #else const QString currentLocale = setlocale(LC_NUMERIC_MASK, nullptr); #endif render_process_args << QStringLiteral("-locale:%1").arg(currentLocale); } QString renderArgs = m_view.advanced_params->toPlainText().simplified(); QString std = renderArgs; // Check for fps change double forcedfps = 0; if (std.startsWith(QLatin1String("r="))) { QString sub = std.section(QLatin1Char(' '), 0, 0).toLower(); sub = sub.section(QLatin1Char('='), 1, 1); forcedfps = sub.toDouble(); } else if (std.contains(QStringLiteral(" r="))) { QString sub = std.section(QStringLiteral(" r="), 1, 1); sub = sub.section(QLatin1Char(' '), 0, 0).toLower(); forcedfps = sub.toDouble(); } else if (std.contains(QStringLiteral("mlt_profile="))) { QString sub = std.section(QStringLiteral("mlt_profile="), 1, 1); sub = sub.section(QLatin1Char(' '), 0, 0).toLower(); forcedfps = ProfileRepository::get()->getProfile(sub)->fps(); } bool resizeProfile = false; std::unique_ptr &profile = pCore->getCurrentProfile(); if (renderArgs.contains(QLatin1String("%dv_standard"))) { QString dvstd; if (fmod((double)profile->frame_rate_num() / profile->frame_rate_den(), 30.01) > 27) { dvstd = QStringLiteral("ntsc"); if (!(profile->frame_rate_num() == 30000 && profile->frame_rate_den() == 1001)) { forcedfps = 30000.0 / 1001; } if (!(profile->width() == 720 && profile->height() == 480)) { resizeProfile = true; } } else { dvstd = QStringLiteral("pal"); if (!(profile->frame_rate_num() == 25 && profile->frame_rate_den() == 1)) { forcedfps = 25; } if (!(profile->width() == 720 && profile->height() == 576)) { resizeProfile = true; } } if ((double)profile->display_aspect_num() / profile->display_aspect_den() > 1.5) { dvstd += QLatin1String("_wide"); } renderArgs.replace(QLatin1String("%dv_standard"), dvstd); } // If there is an fps change, we need to use the producer consumer AND update the in/out points if (forcedfps > 0 && qAbs((int)100 * forcedfps - ((int)100 * profile->frame_rate_num() / profile->frame_rate_den())) > 2) { resizeProfile = true; double ratio = profile->frame_rate_num() / profile->frame_rate_den() / forcedfps; if (ratio > 0) { zoneIn /= ratio; zoneOut /= ratio; } } if (m_view.render_guide->isChecked()) { double fps = profile->fps(); double guideStart = m_view.guide_start->itemData(m_view.guide_start->currentIndex()).toDouble(); double guideEnd = m_view.guide_end->itemData(m_view.guide_end->currentIndex()).toDouble(); render_process_args << "in=" + QString::number((int)GenTime(guideStart).frames(fps)) << "out=" + QString::number((int)GenTime(guideEnd).frames(fps)); } else { render_process_args << "in=" + QString::number(zoneIn) << "out=" + QString::number(zoneOut); } if (!overlayargs.isEmpty()) { render_process_args << "preargs=" + overlayargs.join(QLatin1Char(' ')); } render_process_args << profile->path() << item->data(0, RenderRole).toString(); if (!scriptExport && m_view.play_after->isChecked()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(dest); KService::Ptr serv = KMimeTypeTrader::self()->preferredService(mime.name()); if (serv) { KIO::DesktopExecParser parser(*serv, QList() << QUrl::fromLocalFile(QUrl::toPercentEncoding(dest))); render_process_args << parser.resultingArguments().join(QLatin1Char(' ')); } else { // no service found to play MIME type // TODO: inform user // errorMessage(PlaybackError, i18n("No service found to play %1", mime.name())); render_process_args << QStringLiteral("-"); } } else { render_process_args << QStringLiteral("-"); } if (m_view.speed->isEnabled()) { renderArgs.append(QChar(' ') + item->data(0, SpeedsRole).toStringList().at(m_view.speed->value())); } // Project metadata if (m_view.export_meta->isChecked()) { QMap::const_iterator i = metadata.constBegin(); while (i != metadata.constEnd()) { renderArgs.append(QStringLiteral(" %1=%2").arg(i.key(), QString(QUrl::toPercentEncoding(i.value())))); ++i; } } // Adjust frame scale int width; int height; if (m_view.rescale->isChecked() && m_view.rescale->isEnabled()) { width = m_view.rescale_width->value(); height = m_view.rescale_height->value(); } else { width = profile->width(); height = profile->height(); } // Adjust scanning if (m_view.scanning_list->currentIndex() == 1) { renderArgs.append(QStringLiteral(" progressive=1")); } else if (m_view.scanning_list->currentIndex() == 2) { renderArgs.append(QStringLiteral(" progressive=0")); } // disable audio if requested if (!exportAudio) { renderArgs.append(QStringLiteral(" an=1 ")); } int threadCount = QThread::idealThreadCount(); - if (threadCount > 2 && m_view.parallel_process->isChecked()) { + if (threadCount > 2 && m_view.parallel_process->isChecked() && m_view.parallel_process->isEnabled()) { threadCount = qMin(threadCount - 1, 4); } else { threadCount = 1; } // Set the thread counts if (!renderArgs.contains(QStringLiteral("threads="))) { renderArgs.append(QStringLiteral(" threads=%1").arg(KdenliveSettings::encodethreads())); } renderArgs.append(QStringLiteral(" real_time=-%1").arg(threadCount)); // Check if the rendering profile is different from project profile, // in which case we need to use the producer_consumer from MLT QString subsize; if (std.startsWith(QLatin1String("s="))) { subsize = std.section(QLatin1Char(' '), 0, 0).toLower(); subsize = subsize.section(QLatin1Char('='), 1, 1); } else if (std.contains(QStringLiteral(" s="))) { subsize = std.section(QStringLiteral(" s="), 1, 1); subsize = subsize.section(QLatin1Char(' '), 0, 0).toLower(); } else if (m_view.rescale->isChecked() && m_view.rescale->isEnabled()) { subsize = QStringLiteral(" s=%1x%2").arg(width).arg(height); // Add current size parameter renderArgs.append(subsize); } // Check if we need to embed the playlist into the producer consumer // That is required if PAR != 1 if (profile->sample_aspect_num() != profile->sample_aspect_den() && subsize.isEmpty()) { resizeProfile = true; } QStringList paramsList = renderArgs.split(' ', QString::SkipEmptyParts); for (int i = 0; i < paramsList.count(); ++i) { QString paramName = paramsList.at(i).section(QLatin1Char('='), 0, -2); QString paramValue = paramsList.at(i).section(QLatin1Char('='), -1); // If the profiles do not match we need to use the consumer tag if (paramName == QLatin1String("mlt_profile") && paramValue != profile->path()) { resizeProfile = true; } // evaluate expression if (paramValue.startsWith(QLatin1Char('%'))) { if (paramValue.startsWith(QStringLiteral("%bitrate")) || paramValue == QStringLiteral("%quality")) { if (paramValue.contains("+'k'")) paramValue = QString::number(m_view.video->value()) + 'k'; else paramValue = QString::number(m_view.video->value()); } if (paramValue.startsWith(QStringLiteral("%audiobitrate")) || paramValue == QStringLiteral("%audioquality")) { if (paramValue.contains("+'k'")) paramValue = QString::number(m_view.audio->value()) + 'k'; else paramValue = QString::number(m_view.audio->value()); } if (paramValue == QStringLiteral("%dar")) paramValue = '@' + QString::number(profile->display_aspect_num()) + QLatin1Char('/') + QString::number(profile->display_aspect_den()); if (paramValue == QStringLiteral("%passes")) paramValue = QString::number(static_cast(m_view.checkTwoPass->isChecked()) + 1); paramsList[i] = paramName + QLatin1Char('=') + paramValue; } } /*if (resizeProfile && !KdenliveSettings::gpu_accel()) { render_process_args << "consumer:" + (scriptExport ? ScriptGetVar("SOURCE_" + QString::number(stemIdx)) : QUrl::fromLocalFile(playlistPaths.at(stemIdx)).toEncoded()); } else { render_process_args << (scriptExport ? ScriptGetVar("SOURCE_" + QString::number(stemIdx)) : QUrl::fromLocalFile(playlistPaths.at(stemIdx)).toEncoded()); } render_process_args << (scriptExport ? ScriptGetVar("TARGET_" + QString::number(stemIdx)) : QUrl::fromLocalFile(dest).toEncoded());*/ if (KdenliveSettings::gpu_accel()) { render_process_args << QStringLiteral("glsl.=1"); } render_process_args << paramsList; if (scriptExport) { QTextStream outStream(&file); QString stemIdxStr(QString::number(stemIdx)); /*outStream << ScriptSetVar("SOURCE_" + stemIdxStr, QUrl::fromLocalFile(playlistPaths.at(stemIdx)).toEncoded()) << '\n'; outStream << ScriptSetVar("TARGET_" + stemIdxStr, QUrl::fromLocalFile(dest).toEncoded()) << '\n'; outStream << ScriptSetVar("PARAMETERS_" + stemIdxStr, render_process_args.join(QLatin1Char(' '))) << '\n'; outStream << ScriptGetVar("RENDERER") + " " + ScriptGetVar("PARAMETERS_" + stemIdxStr) << "\n";*/ if (stemIdx == (stemCount - 1)) { if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", scriptPath)); file.close(); return; } file.close(); QFile::setPermissions(scriptPath, file.permissions() | QFile::ExeUser); QTimer::singleShot(400, this, &RenderWidget::parseScriptFiles); m_view.tabWidget->setCurrentIndex(2); return; } continue; } // Save rendering profile to document QMap renderProps; renderProps.insert(QStringLiteral("rendercategory"), m_view.formats->currentItem()->parent()->text(0)); renderProps.insert(QStringLiteral("renderprofile"), m_view.formats->currentItem()->text(0)); renderProps.insert(QStringLiteral("renderurl"), destBase); renderProps.insert(QStringLiteral("renderzone"), QString::number(static_cast(m_view.render_zone->isChecked()))); renderProps.insert(QStringLiteral("renderguide"), QString::number(static_cast(m_view.render_guide->isChecked()))); renderProps.insert(QStringLiteral("renderstartguide"), QString::number(m_view.guide_start->currentIndex())); renderProps.insert(QStringLiteral("renderendguide"), QString::number(m_view.guide_end->currentIndex())); renderProps.insert(QStringLiteral("renderscanning"), QString::number(m_view.scanning_list->currentIndex())); renderProps.insert(QStringLiteral("renderfield"), QString::number(m_view.field_order->currentIndex())); int export_audio = 0; if (m_view.export_audio->checkState() == Qt::Checked) { export_audio = 2; } else if (m_view.export_audio->checkState() == Qt::Unchecked) { export_audio = 1; } renderProps.insert(QStringLiteral("renderexportaudio"), QString::number(export_audio)); renderProps.insert(QStringLiteral("renderrescale"), QString::number(static_cast(m_view.rescale->isChecked()))); renderProps.insert(QStringLiteral("renderrescalewidth"), QString::number(m_view.rescale_width->value())); renderProps.insert(QStringLiteral("renderrescaleheight"), QString::number(m_view.rescale_height->value())); renderProps.insert(QStringLiteral("rendertcoverlay"), QString::number(static_cast(m_view.tc_overlay->isChecked()))); renderProps.insert(QStringLiteral("rendertctype"), QString::number(m_view.tc_type->currentIndex())); renderProps.insert(QStringLiteral("renderratio"), QString::number(static_cast(m_view.rescale_keep->isChecked()))); renderProps.insert(QStringLiteral("renderplay"), QString::number(static_cast(m_view.play_after->isChecked()))); renderProps.insert(QStringLiteral("rendertwopass"), QString::number(static_cast(m_view.checkTwoPass->isChecked()))); renderProps.insert(QStringLiteral("renderquality"), QString::number(m_view.video->value())); renderProps.insert(QStringLiteral("renderaudioquality"), QString::number(m_view.audio->value())); renderProps.insert(QStringLiteral("renderspeed"), QString::number(m_view.speed->value())); emit selectedRenderProfile(renderProps); // insert item in running jobs list RenderJobItem *renderItem = nullptr; QList existing = m_view.running_jobs->findItems(dest, Qt::MatchExactly, 1); if (!existing.isEmpty()) { renderItem = static_cast(existing.at(0)); if (renderItem->status() == RUNNINGJOB || renderItem->status() == WAITINGJOB || renderItem->status() == STARTINGJOB) { KMessageBox::information(this, i18n("There is already a job writing file:
%1
Abort the job if you want to overwrite it...", dest), i18n("Already running")); return; } /*if (renderItem->type() != DirectRenderType) { delete renderItem; renderItem = nullptr; } else { renderItem->setData(1, ProgressRole, 0); renderItem->setStatus(WAITINGJOB); renderItem->setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause"))); renderItem->setData(1, Qt::UserRole, i18n("Waiting...")); renderItem->setData(1, ParametersRole, dest); }*/ } if (!renderItem) { renderItem = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << dest); } renderItem->setData(1, TimeRole, QDateTime::currentDateTime()); // Set rendering type /*if (group == QLatin1String("dvd")) { if (m_view.open_dvd->isChecked()) { renderItem->setData(0, Qt::UserRole, group); if (renderArgs.contains(QStringLiteral("mlt_profile="))) { //TODO: probably not valid anymore (no more MLT profiles in args) // rendering profile contains an MLT profile, so pass it to the running jog item, useful for dvd QString prof = renderArgs.section(QStringLiteral("mlt_profile="), 1, 1); prof = prof.section(QLatin1Char(' '), 0, 0); qCDebug(KDENLIVE_LOG) << "// render profile: " << prof; renderItem->setMetadata(prof); } } } else { if (group == QLatin1String("websites") && m_view.open_browser->isChecked()) { renderItem->setData(0, Qt::UserRole, group); // pass the url QString url = m_view.formats->currentItem()->data(ExtraRole).toString(); renderItem->setMetadata(url); } }*/ renderItem->setData(1, ParametersRole, render_process_args); if (!exportAudio) { renderItem->setData(1, ExtraInfoRole, i18n("Video without audio track")); } else { renderItem->setData(1, ExtraInfoRole, QString()); } m_view.running_jobs->setCurrentItem(renderItem); m_view.tabWidget->setCurrentIndex(1); // check render status checkRenderStatus(); } // end loop } void RenderWidget::checkRenderStatus() { // check if we have a job waiting to render if (m_blockProcessing) { return; } auto *item = static_cast(m_view.running_jobs->topLevelItem(0)); // Make sure no other rendering is running while (item != nullptr) { if (item->status() == RUNNINGJOB) { return; } item = static_cast(m_view.running_jobs->itemBelow(item)); } item = static_cast(m_view.running_jobs->topLevelItem(0)); bool waitingJob = false; // Find first waiting job while (item != nullptr) { if (item->status() == WAITINGJOB) { item->setData(1, TimeRole, QDateTime::currentDateTime()); waitingJob = true; startRendering(item); // Check for 2 pass encoding QStringList jobData = item->data(1, ParametersRole).toStringList(); if (jobData.size() > 2 && jobData.at(1).endsWith(QStringLiteral("-pass2.mlt"))) { // Find and remove 1st pass job QTreeWidgetItem *above = m_view.running_jobs->itemAbove(item); QString firstPassName = jobData.at(1).section(QLatin1Char('-'), 0, -2) + QStringLiteral(".mlt"); while (above) { QStringList aboveData = above->data(1, ParametersRole).toStringList(); qDebug() << "// GOT JOB: " << aboveData.at(1); if (aboveData.size() > 2 && aboveData.at(1) == firstPassName) { delete above; break; } above = m_view.running_jobs->itemAbove(above); } } item->setStatus(STARTINGJOB); break; } item = static_cast(m_view.running_jobs->itemBelow(item)); } if (!waitingJob && m_view.shutdown->isChecked()) { emit shutdown(); } } void RenderWidget::startRendering(RenderJobItem *item) { auto rendererArgs = item->data(1, ParametersRole).toStringList(); qDebug() << "starting kdenlive_render process using: " << m_renderer; if (!QProcess::startDetached(m_renderer, rendererArgs)) { item->setStatus(FAILEDJOB); } else { KNotification::event(QStringLiteral("RenderStarted"), i18n("Rendering %1 started", item->text(1)), QPixmap(), this); } } int RenderWidget::waitingJobsCount() const { int count = 0; auto *item = static_cast(m_view.running_jobs->topLevelItem(0)); while (item != nullptr) { if (item->status() == WAITINGJOB || item->status() == STARTINGJOB) { count++; } item = static_cast(m_view.running_jobs->itemBelow(item)); } return count; } void RenderWidget::adjustViewToProfile() { m_view.scanning_list->setCurrentIndex(0); m_view.rescale_width->setValue(KdenliveSettings::defaultrescalewidth()); if (!m_view.rescale_keep->isChecked()) { m_view.rescale_height->blockSignals(true); m_view.rescale_height->setValue(KdenliveSettings::defaultrescaleheight()); m_view.rescale_height->blockSignals(false); } refreshView(); } void RenderWidget::refreshView() { m_view.formats->blockSignals(true); QIcon brokenIcon = QIcon::fromTheme(QStringLiteral("dialog-close")); QIcon warningIcon = QIcon::fromTheme(QStringLiteral("dialog-warning")); KColorScheme scheme(palette().currentColorGroup(), KColorScheme::Window); const QColor disabled = scheme.foreground(KColorScheme::InactiveText).color(); const QColor disabledbg = scheme.background(KColorScheme::NegativeBackground).color(); // We borrow a reference to the profile's pointer to query it more easily std::unique_ptr &profile = pCore->getCurrentProfile(); double project_framerate = (double)profile->frame_rate_num() / profile->frame_rate_den(); for (int i = 0; i < m_view.formats->topLevelItemCount(); ++i) { QTreeWidgetItem *group = m_view.formats->topLevelItem(i); for (int j = 0; j < group->childCount(); ++j) { QTreeWidgetItem *item = group->child(j); QString std = item->data(0, StandardRole).toString(); if (std.isEmpty() || (std.contains(QStringLiteral("PAL"), Qt::CaseInsensitive) && profile->frame_rate_num() == 25 && profile->frame_rate_den() == 1) || (std.contains(QStringLiteral("NTSC"), Qt::CaseInsensitive) && profile->frame_rate_num() == 30000 && profile->frame_rate_den() == 1001)) { // Standard OK } else { item->setData(0, ErrorRole, i18n("Standard (%1) not compatible with project profile (%2)", std, project_framerate)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); continue; } QString params = item->data(0, ParamsRole).toString(); // Make sure the selected profile uses the same frame rate as project profile if (params.contains(QStringLiteral("mlt_profile="))) { QString profile_str = params.section(QStringLiteral("mlt_profile="), 1, 1).section(QLatin1Char(' '), 0, 0); std::unique_ptr &target_profile = ProfileRepository::get()->getProfile(profile_str); if (target_profile->frame_rate_den() > 0) { double profile_rate = (double)target_profile->frame_rate_num() / target_profile->frame_rate_den(); if ((int)(1000.0 * profile_rate) != (int)(1000.0 * project_framerate)) { item->setData(0, ErrorRole, i18n("Frame rate (%1) not compatible with project profile (%2)", profile_rate, project_framerate)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); continue; } } } // Make sure the selected profile uses an installed avformat codec / format if (!supportedFormats.isEmpty()) { QString format; if (params.startsWith(QLatin1String("f="))) { format = params.section(QStringLiteral("f="), 1, 1); } else if (params.contains(QStringLiteral(" f="))) { format = params.section(QStringLiteral(" f="), 1, 1); } if (!format.isEmpty()) { format = format.section(QLatin1Char(' '), 0, 0).toLower(); if (!supportedFormats.contains(format)) { item->setData(0, ErrorRole, i18n("Unsupported video format: %1", format)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); continue; } } } if (!acodecsList.isEmpty()) { QString format; if (params.startsWith(QLatin1String("acodec="))) { format = params.section(QStringLiteral("acodec="), 1, 1); } else if (params.contains(QStringLiteral(" acodec="))) { format = params.section(QStringLiteral(" acodec="), 1, 1); } if (!format.isEmpty()) { format = format.section(QLatin1Char(' '), 0, 0).toLower(); if (!acodecsList.contains(format)) { item->setData(0, ErrorRole, i18n("Unsupported audio codec: %1", format)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); item->setBackground(0, disabledbg); } } } if (!vcodecsList.isEmpty()) { QString format; if (params.startsWith(QLatin1String("vcodec="))) { format = params.section(QStringLiteral("vcodec="), 1, 1); } else if (params.contains(QStringLiteral(" vcodec="))) { format = params.section(QStringLiteral(" vcodec="), 1, 1); } if (!format.isEmpty()) { format = format.section(QLatin1Char(' '), 0, 0).toLower(); if (!vcodecsList.contains(format)) { item->setData(0, ErrorRole, i18n("Unsupported video codec: %1", format)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); continue; } } } if (params.contains(QStringLiteral(" profile=")) || params.startsWith(QLatin1String("profile="))) { // changed in MLT commit d8a3a5c9190646aae72048f71a39ee7446a3bd45 // (http://www.mltframework.org/gitweb/mlt.git?p=mltframework.org/mlt.git;a=commit;h=d8a3a5c9190646aae72048f71a39ee7446a3bd45) item->setData(0, ErrorRole, i18n("This render profile uses a 'profile' parameter.
Unless you know what you are doing you will probably " "have to change it to 'mlt_profile'.")); item->setIcon(0, warningIcon); continue; } } } focusFirstVisibleItem(); m_view.formats->blockSignals(false); refreshParams(); } QUrl RenderWidget::filenameWithExtension(QUrl url, const QString &extension) { if (!url.isValid()) { url = QUrl::fromLocalFile(pCore->currentDoc()->projectDataFolder() + QDir::separator()); } QString directory = url.adjusted(QUrl::RemoveFilename).toLocalFile(); QString filename = url.fileName(); QString ext; if (extension.at(0) == '.') { ext = extension; } else { ext = '.' + extension; } if (filename.isEmpty()) { filename = i18n("untitled"); } int pos = filename.lastIndexOf('.'); if (pos == 0) { filename.append(ext); } else { if (!filename.endsWith(ext, Qt::CaseInsensitive)) { filename = filename.left(pos) + ext; } } return QUrl::fromLocalFile(directory + filename); } void RenderWidget::refreshParams() { // Format not available (e.g. codec not installed); Disable start button QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || item->parent() == nullptr) { // This is a category item, not a real profile m_view.buttonBox->setEnabled(false); } else { m_view.buttonBox->setEnabled(true); } QString extension; if (item) { extension = item->data(0, ExtensionRole).toString(); } if ((item == nullptr) || item->isHidden() || extension.isEmpty()) { if (!item) { errorMessage(ProfileError, i18n("No matching profile")); } else if (!item->parent()) // category ; else if (extension.isEmpty()) { errorMessage(ProfileError, i18n("Invalid profile")); } m_view.advanced_params->clear(); m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); return; } QString params = item->data(0, ParamsRole).toString(); errorMessage(ProfileError, item->data(0, ErrorRole).toString()); m_view.advanced_params->setPlainText(params); if (params.contains(QStringLiteral(" s=")) || params.startsWith(QLatin1String("s=")) || params.contains(QLatin1String("%dv_standard"))) { // profile has a fixed size, do not allow resize m_view.rescale->setEnabled(false); setRescaleEnabled(false); } else { m_view.rescale->setEnabled(true); setRescaleEnabled(m_view.rescale->isChecked()); } QUrl url = filenameWithExtension(m_view.out_file->url(), extension); m_view.out_file->setUrl(url); // if (!url.isEmpty()) { // QString path = url.path(); // int pos = path.lastIndexOf('.') + 1; // if (pos == 0) path.append('.' + extension); // else path = path.left(pos) + extension; // m_view.out_file->setUrl(QUrl(path)); // } else { // m_view.out_file->setUrl(QUrl(QDir::homePath() + QStringLiteral("/untitled.") + extension)); // } m_view.out_file->setFilter("*." + extension); QString edit = item->data(0, EditableRole).toString(); if (edit.isEmpty() || !edit.endsWith(QLatin1String("customprofiles.xml"))) { m_view.buttonDelete->setEnabled(false); m_view.buttonEdit->setEnabled(false); } else { m_view.buttonDelete->setEnabled(true); m_view.buttonEdit->setEnabled(true); } // video quality control m_view.video->blockSignals(true); bool quality = false; if ((params.contains(QStringLiteral("%quality")) || params.contains(QStringLiteral("%bitrate"))) && item->data(0, BitratesRole).canConvert(QVariant::StringList)) { // bitrates or quantizers list QStringList qs = item->data(0, BitratesRole).toStringList(); if (qs.count() > 1) { quality = true; int qmax = qs.constFirst().toInt(); int qmin = qs.last().toInt(); if (qmax < qmin) { // always show best quality on right m_view.video->setRange(qmax, qmin); m_view.video->setProperty("decreasing", true); } else { m_view.video->setRange(qmin, qmax); m_view.video->setProperty("decreasing", false); } } } m_view.video->setEnabled(quality); m_view.quality->setEnabled(quality); m_view.qualityLabel->setEnabled(quality); m_view.video->blockSignals(false); // audio quality control quality = false; m_view.audio->blockSignals(true); if ((params.contains(QStringLiteral("%audioquality")) || params.contains(QStringLiteral("%audiobitrate"))) && item->data(0, AudioBitratesRole).canConvert(QVariant::StringList)) { // bitrates or quantizers list QStringList qs = item->data(0, AudioBitratesRole).toStringList(); if (qs.count() > 1) { quality = true; int qmax = qs.constFirst().toInt(); int qmin = qs.last().toInt(); if (qmax < qmin) { m_view.audio->setRange(qmax, qmin); m_view.audio->setProperty("decreasing", true); } else { m_view.audio->setRange(qmin, qmax); m_view.audio->setProperty("decreasing", false); } if (params.contains(QStringLiteral("%audiobitrate"))) { m_view.audio->setSingleStep(32); // 32kbps step } else { m_view.audio->setSingleStep(1); } } } m_view.audio->setEnabled(quality); m_view.audio->blockSignals(false); if (m_view.quality->isEnabled()) { adjustAVQualities(m_view.quality->value()); } if (item->data(0, SpeedsRole).canConvert(QVariant::StringList) && (item->data(0, SpeedsRole).toStringList().count() != 0)) { int speed = item->data(0, SpeedsRole).toStringList().count() - 1; m_view.speed->setEnabled(true); m_view.speed->setMaximum(speed); m_view.speed->setValue(speed * 3 / 4); // default to intermediate speed } else { m_view.speed->setEnabled(false); } if (!item->data(0, FieldRole).isNull()) { m_view.field_order->setCurrentIndex(item->data(0, FieldRole).toInt()); } adjustSpeed(m_view.speed->value()); bool passes = params.contains(QStringLiteral("passes")); m_view.checkTwoPass->setEnabled(passes); m_view.checkTwoPass->setChecked(passes && params.contains(QStringLiteral("passes=2"))); m_view.encoder_threads->setEnabled(!params.contains(QStringLiteral("threads="))); m_view.buttonRender->setEnabled(m_view.formats->currentItem()->data(0, ErrorRole).isNull()); m_view.buttonGenerateScript->setEnabled(m_view.formats->currentItem()->data(0, ErrorRole).isNull()); } void RenderWidget::reloadProfiles() { parseProfiles(); } void RenderWidget::parseProfiles(const QString &selectedProfile) { m_view.formats->clear(); // Parse our xml profile QString exportFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("export/profiles.xml")); parseFile(exportFile, false); // Parse some MLT's profiles parseMltPresets(); QString exportFolder = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/export/"); QDir directory(exportFolder); QStringList filter; filter << QStringLiteral("*.xml"); QStringList fileList = directory.entryList(filter, QDir::Files); // We should parse customprofiles.xml in last position, so that user profiles // can also override profiles installed by KNewStuff fileList.removeAll(QStringLiteral("customprofiles.xml")); for (const QString &filename : fileList) { parseFile(directory.absoluteFilePath(filename), true); } if (QFile::exists(exportFolder + QStringLiteral("customprofiles.xml"))) { parseFile(exportFolder + QStringLiteral("customprofiles.xml"), true); } focusFirstVisibleItem(selectedProfile); } void RenderWidget::parseMltPresets() { QDir root(KdenliveSettings::mltpath()); if (!root.cd(QStringLiteral("../presets/consumer/avformat"))) { // Cannot find MLT's presets directory qCWarning(KDENLIVE_LOG) << " / / / WARNING, cannot find MLT's preset folder"; return; } if (root.cd(QStringLiteral("lossless"))) { QString groupName = i18n("Lossless/HQ"); QList foundGroup = m_view.formats->findItems(groupName, Qt::MatchExactly); QTreeWidgetItem *groupItem; if (!foundGroup.isEmpty()) { groupItem = foundGroup.takeFirst(); } else { groupItem = new QTreeWidgetItem(QStringList(groupName)); m_view.formats->addTopLevelItem(groupItem); groupItem->setExpanded(true); } const QStringList profiles = root.entryList(QDir::Files, QDir::Name); for (const QString &prof : profiles) { KConfig config(root.absoluteFilePath(prof), KConfig::SimpleConfig); KConfigGroup group = config.group(QByteArray()); QString vcodec = group.readEntry("vcodec"); QString acodec = group.readEntry("acodec"); QString extension = group.readEntry("meta.preset.extension"); QString note = group.readEntry("meta.preset.note"); QString profileName = prof; if (!vcodec.isEmpty() || !acodec.isEmpty()) { profileName.append(" ("); if (!vcodec.isEmpty()) { profileName.append(vcodec); if (!acodec.isEmpty()) { profileName.append("+" + acodec); } } else if (!acodec.isEmpty()) { profileName.append(acodec); } profileName.append(QLatin1Char(')')); } QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(profileName)); item->setData(0, ExtensionRole, extension); item->setData(0, RenderRole, "avformat"); item->setData(0, ParamsRole, QString("properties=lossless/" + prof)); if (!note.isEmpty()) { item->setToolTip(0, note); } groupItem->addChild(item); } } if (root.cd(QStringLiteral("../stills"))) { QString groupName = i18nc("Category Name", "Images sequence"); QList foundGroup = m_view.formats->findItems(groupName, Qt::MatchExactly); QTreeWidgetItem *groupItem; if (!foundGroup.isEmpty()) { groupItem = foundGroup.takeFirst(); } else { groupItem = new QTreeWidgetItem(QStringList(groupName)); m_view.formats->addTopLevelItem(groupItem); groupItem->setExpanded(true); } QStringList profiles = root.entryList(QDir::Files, QDir::Name); for (const QString &prof : profiles) { QTreeWidgetItem *item = loadFromMltPreset(groupName, root.absoluteFilePath(prof), prof); if (!item) { continue; } item->setData(0, ParamsRole, QString("properties=stills/" + prof)); groupItem->addChild(item); } // Add GIF as image sequence root.cdUp(); QTreeWidgetItem *item = loadFromMltPreset(groupName, root.absoluteFilePath(QStringLiteral("GIF")), QStringLiteral("GIF")); if (item) { item->setData(0, ParamsRole, QStringLiteral("properties=GIF")); groupItem->addChild(item); } } } QTreeWidgetItem *RenderWidget::loadFromMltPreset(const QString &groupName, const QString &path, const QString &profileName) { KConfig config(path, KConfig::SimpleConfig); KConfigGroup group = config.group(QByteArray()); QString extension = group.readEntry("meta.preset.extension"); QString note = group.readEntry("meta.preset.note"); if (extension.isEmpty()) { return nullptr; } QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(profileName)); item->setData(0, GroupRole, groupName); item->setData(0, ExtensionRole, extension); item->setData(0, RenderRole, "avformat"); if (!note.isEmpty()) { item->setToolTip(0, note); } return item; } void RenderWidget::parseFile(const QString &exportFile, bool editable) { QDomDocument doc; QFile file(exportFile); doc.setContent(&file, false); file.close(); QDomElement documentElement; QDomElement profileElement; QString extension; QDomNodeList groups = doc.elementsByTagName(QStringLiteral("group")); QTreeWidgetItem *item = nullptr; bool replaceVorbisCodec = false; if (acodecsList.contains(QStringLiteral("libvorbis"))) { replaceVorbisCodec = true; } bool replaceLibfaacCodec = false; if (acodecsList.contains(QStringLiteral("libfaac"))) { replaceLibfaacCodec = true; } if (editable || groups.isEmpty()) { QDomElement profiles = doc.documentElement(); if (editable && profiles.attribute(QStringLiteral("version"), nullptr).toInt() < 1) { // this is an old profile version, update it QDomDocument newdoc; QDomElement newprofiles = newdoc.createElement(QStringLiteral("profiles")); newprofiles.setAttribute(QStringLiteral("version"), 1); newdoc.appendChild(newprofiles); QDomNodeList profilelist = doc.elementsByTagName(QStringLiteral("profile")); for (int i = 0; i < profilelist.count(); ++i) { QString category = i18nc("Category Name", "Custom"); QString ext; QDomNode parent = profilelist.at(i).parentNode(); if (!parent.isNull()) { QDomElement parentNode = parent.toElement(); if (parentNode.hasAttribute(QStringLiteral("name"))) { category = parentNode.attribute(QStringLiteral("name")); } ext = parentNode.attribute(QStringLiteral("extension")); } if (!profilelist.at(i).toElement().hasAttribute(QStringLiteral("category"))) { profilelist.at(i).toElement().setAttribute(QStringLiteral("category"), category); } if (!ext.isEmpty()) { profilelist.at(i).toElement().setAttribute(QStringLiteral("extension"), ext); } QDomNode n = profilelist.at(i).cloneNode(); newprofiles.appendChild(newdoc.importNode(n, true)); } if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", exportFile)); return; } QTextStream out(&file); out << newdoc.toString(); file.close(); parseFile(exportFile, editable); return; } QDomNode node = doc.elementsByTagName(QStringLiteral("profile")).at(0); if (node.isNull()) { return; } int count = 1; while (!node.isNull()) { QDomElement profile = node.toElement(); QString profileName = profile.attribute(QStringLiteral("name")); QString standard = profile.attribute(QStringLiteral("standard")); QTextDocument docConvert; docConvert.setHtml(profile.attribute(QStringLiteral("args"))); QString params = docConvert.toPlainText().simplified(); if (replaceVorbisCodec && params.contains(QStringLiteral("acodec=vorbis"))) { // replace vorbis with libvorbis params = params.replace(QLatin1String("=vorbis"), QLatin1String("=libvorbis")); } if (replaceLibfaacCodec && params.contains(QStringLiteral("acodec=aac"))) { // replace libfaac with aac params = params.replace(QLatin1String("aac"), QLatin1String("libfaac")); } QString prof_extension = profile.attribute(QStringLiteral("extension")); if (!prof_extension.isEmpty()) { extension = prof_extension; } QString groupName = profile.attribute(QStringLiteral("category"), i18nc("Category Name", "Custom")); QList foundGroup = m_view.formats->findItems(groupName, Qt::MatchExactly); QTreeWidgetItem *groupItem; if (!foundGroup.isEmpty()) { groupItem = foundGroup.takeFirst(); } else { groupItem = new QTreeWidgetItem(QStringList(groupName)); if (editable) { m_view.formats->insertTopLevelItem(0, groupItem); } else { m_view.formats->addTopLevelItem(groupItem); groupItem->setExpanded(true); } } // Check if item with same name already exists and replace it, // allowing to override default profiles QTreeWidgetItem *childitem = nullptr; for (int j = 0; j < groupItem->childCount(); ++j) { if (groupItem->child(j)->text(0) == profileName) { childitem = groupItem->child(j); break; } } if (!childitem) { childitem = new QTreeWidgetItem(QStringList(profileName)); } childitem->setData(0, GroupRole, groupName); childitem->setData(0, ExtensionRole, extension); childitem->setData(0, RenderRole, "avformat"); childitem->setData(0, StandardRole, standard); childitem->setData(0, ParamsRole, params); if (params.contains(QLatin1String("%quality"))) { childitem->setData(0, BitratesRole, profile.attribute(QStringLiteral("qualities")).split(QLatin1Char(','), QString::SkipEmptyParts)); } else if (params.contains(QLatin1String("%bitrate"))) { childitem->setData(0, BitratesRole, profile.attribute(QStringLiteral("bitrates")).split(QLatin1Char(','), QString::SkipEmptyParts)); } if (params.contains(QLatin1String("%audioquality"))) { childitem->setData(0, AudioBitratesRole, profile.attribute(QStringLiteral("audioqualities")).split(QLatin1Char(','), QString::SkipEmptyParts)); } else if (params.contains(QLatin1String("%audiobitrate"))) { childitem->setData(0, AudioBitratesRole, profile.attribute(QStringLiteral("audiobitrates")).split(QLatin1Char(','), QString::SkipEmptyParts)); } if (profile.hasAttribute(QStringLiteral("speeds"))) { childitem->setData(0, SpeedsRole, profile.attribute(QStringLiteral("speeds")).split(QLatin1Char(';'), QString::SkipEmptyParts)); } if (profile.hasAttribute(QStringLiteral("url"))) { childitem->setData(0, ExtraRole, profile.attribute(QStringLiteral("url"))); } if (profile.hasAttribute(QStringLiteral("top_field_first"))) { childitem->setData(0, FieldRole, profile.attribute(QStringLiteral("top_field_first"))); } if (editable) { childitem->setData(0, EditableRole, exportFile); if (exportFile.endsWith(QLatin1String("customprofiles.xml"))) { childitem->setIcon(0, QIcon::fromTheme(QStringLiteral("favorite"))); } else { childitem->setIcon(0, QIcon::fromTheme(QStringLiteral("applications-internet"))); } } groupItem->addChild(childitem); node = doc.elementsByTagName(QStringLiteral("profile")).at(count); count++; } return; } int i = 0; QString groupName; QString profileName; QString prof_extension; QString renderer; QString params; QString standard; while (!groups.item(i).isNull()) { documentElement = groups.item(i).toElement(); QDomNode gname = documentElement.elementsByTagName(QStringLiteral("groupname")).at(0); groupName = documentElement.attribute(QStringLiteral("name"), i18nc("Attribute Name", "Custom")); extension = documentElement.attribute(QStringLiteral("extension"), QString()); renderer = documentElement.attribute(QStringLiteral("renderer"), QString()); QList foundGroup = m_view.formats->findItems(groupName, Qt::MatchExactly); QTreeWidgetItem *groupItem; if (!foundGroup.isEmpty()) { groupItem = foundGroup.takeFirst(); } else { groupItem = new QTreeWidgetItem(QStringList(groupName)); m_view.formats->addTopLevelItem(groupItem); groupItem->setExpanded(true); } QDomNode n = groups.item(i).firstChild(); while (!n.isNull()) { if (n.toElement().tagName() != QLatin1String("profile")) { n = n.nextSibling(); continue; } profileElement = n.toElement(); profileName = profileElement.attribute(QStringLiteral("name")); standard = profileElement.attribute(QStringLiteral("standard")); params = profileElement.attribute(QStringLiteral("args")).simplified(); if (replaceVorbisCodec && params.contains(QStringLiteral("acodec=vorbis"))) { // replace vorbis with libvorbis params = params.replace(QLatin1String("=vorbis"), QLatin1String("=libvorbis")); } if (replaceLibfaacCodec && params.contains(QStringLiteral("acodec=aac"))) { // replace libfaac with aac params = params.replace(QLatin1String("aac"), QLatin1String("libfaac")); } prof_extension = profileElement.attribute(QStringLiteral("extension")); if (!prof_extension.isEmpty()) { extension = prof_extension; } item = new QTreeWidgetItem(QStringList(profileName)); item->setData(0, GroupRole, groupName); item->setData(0, ExtensionRole, extension); item->setData(0, RenderRole, renderer); item->setData(0, StandardRole, standard); item->setData(0, ParamsRole, params); if (params.contains(QLatin1String("%quality"))) { item->setData(0, BitratesRole, profileElement.attribute(QStringLiteral("qualities")).split(QLatin1Char(','), QString::SkipEmptyParts)); } else if (params.contains(QLatin1String("%bitrate"))) { item->setData(0, BitratesRole, profileElement.attribute(QStringLiteral("bitrates")).split(QLatin1Char(','), QString::SkipEmptyParts)); } if (params.contains(QLatin1String("%audioquality"))) { item->setData(0, AudioBitratesRole, profileElement.attribute(QStringLiteral("audioqualities")).split(QLatin1Char(','), QString::SkipEmptyParts)); } else if (params.contains(QLatin1String("%audiobitrate"))) { item->setData(0, AudioBitratesRole, profileElement.attribute(QStringLiteral("audiobitrates")).split(QLatin1Char(','), QString::SkipEmptyParts)); } if (profileElement.hasAttribute(QStringLiteral("speeds"))) { item->setData(0, SpeedsRole, profileElement.attribute(QStringLiteral("speeds")).split(QLatin1Char(';'), QString::SkipEmptyParts)); } if (profileElement.hasAttribute(QStringLiteral("url"))) { item->setData(0, ExtraRole, profileElement.attribute(QStringLiteral("url"))); } groupItem->addChild(item); n = n.nextSibling(); } ++i; } } void RenderWidget::setRenderJob(const QString &dest, int progress) { RenderJobItem *item; QList existing = m_view.running_jobs->findItems(dest, Qt::MatchExactly, 1); if (!existing.isEmpty()) { item = static_cast(existing.at(0)); } else { item = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << dest); if (progress == 0) { item->setStatus(WAITINGJOB); } } item->setData(1, ProgressRole, progress); item->setStatus(RUNNINGJOB); if (progress == 0) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("media-record"))); item->setData(1, TimeRole, QDateTime::currentDateTime()); slotCheckJob(); } else { QDateTime startTime = item->data(1, TimeRole).toDateTime(); qint64 elapsedTime = startTime.secsTo(QDateTime::currentDateTime()); qint64 remaining = elapsedTime * (100 - progress) / progress; int days = static_cast(remaining / 86400); int remainingSecs = static_cast(remaining % 86400); QTime when = QTime(0, 0, 0, 0); when = when.addSecs(remainingSecs); QString est = (days > 0) ? i18np("%1 day ", "%1 days ", days) : QString(); est.append(when.toString(QStringLiteral("hh:mm:ss"))); QString t = i18n("Remaining time %1", est); item->setData(1, Qt::UserRole, t); } } void RenderWidget::setRenderStatus(const QString &dest, int status, const QString &error) { RenderJobItem *item; QList existing = m_view.running_jobs->findItems(dest, Qt::MatchExactly, 1); if (!existing.isEmpty()) { item = static_cast(existing.at(0)); } else { item = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << dest); } if (!item) { return; } if (status == -1) { // Job finished successfully item->setStatus(FINISHEDJOB); QDateTime startTime = item->data(1, TimeRole).toDateTime(); qint64 elapsedTime = startTime.secsTo(QDateTime::currentDateTime()); int days = static_cast(elapsedTime / 86400); int secs = static_cast(elapsedTime % 86400); QTime when = QTime(0, 0, 0, 0); when = when.addSecs(secs); QString est = (days > 0) ? i18np("%1 day ", "%1 days ", days) : QString(); est.append(when.toString(QStringLiteral("hh:mm:ss"))); QString t = i18n("Rendering finished in %1", est); item->setData(1, Qt::UserRole, t); #ifdef KF5_USE_PURPOSE m_shareMenu->model()->setInputData(QJsonObject{{QStringLiteral("mimeType"), QMimeDatabase().mimeTypeForFile(item->text(1)).name()}, {QStringLiteral("urls"), QJsonArray({item->text(1)})}}); m_shareMenu->model()->setPluginType(QStringLiteral("Export")); m_shareMenu->reload(); #endif QString notif = i18n("Rendering of %1 finished in %2", item->text(1), est); KNotification *notify = new KNotification(QStringLiteral("RenderFinished")); notify->setText(notif); #if KNOTIFICATIONS_VERSION >= QT_VERSION_CHECK(5, 29, 0) notify->setUrls({QUrl::fromLocalFile(dest)}); #endif notify->sendEvent(); QString itemGroup = item->data(0, Qt::UserRole).toString(); if (itemGroup == QLatin1String("dvd")) { emit openDvdWizard(item->text(1)); } else if (itemGroup == QLatin1String("websites")) { QString url = item->metadata(); if (!url.isEmpty()) { new KRun(QUrl::fromLocalFile(url), this); } } } else if (status == -2) { // Rendering crashed item->setStatus(FAILEDJOB); m_view.error_log->append(i18n("Rendering of %1 crashed
", dest)); m_view.error_log->append(error); m_view.error_log->append(QStringLiteral("
")); m_view.error_box->setVisible(true); } else if (status == -3) { // User aborted job item->setStatus(ABORTEDJOB); } else { delete item; } slotCheckJob(); checkRenderStatus(); } void RenderWidget::slotAbortCurrentJob() { auto *current = static_cast(m_view.running_jobs->currentItem()); if (current) { if (current->status() == RUNNINGJOB) { emit abortProcess(current->text(1)); } else { delete current; slotCheckJob(); checkRenderStatus(); } } } void RenderWidget::slotStartCurrentJob() { auto *current = static_cast(m_view.running_jobs->currentItem()); if ((current != nullptr) && current->status() == WAITINGJOB) { startRendering(current); } m_view.start_job->setEnabled(false); } void RenderWidget::slotCheckJob() { bool activate = false; auto *current = static_cast(m_view.running_jobs->currentItem()); if (current) { if (current->status() == RUNNINGJOB || current->status() == STARTINGJOB) { m_view.abort_job->setText(i18n("Abort Job")); m_view.start_job->setEnabled(false); } else { m_view.abort_job->setText(i18n("Remove Job")); m_view.start_job->setEnabled(current->status() == WAITINGJOB); } activate = true; #ifdef KF5_USE_PURPOSE if (current->status() == FINISHEDJOB) { m_shareMenu->model()->setInputData(QJsonObject{{QStringLiteral("mimeType"), QMimeDatabase().mimeTypeForFile(current->text(1)).name()}, {QStringLiteral("urls"), QJsonArray({current->text(1)})}}); m_shareMenu->model()->setPluginType(QStringLiteral("Export")); m_shareMenu->reload(); m_view.shareButton->setEnabled(true); } else { m_view.shareButton->setEnabled(false); } #endif } m_view.abort_job->setEnabled(activate); /* for (int i = 0; i < m_view.running_jobs->topLevelItemCount(); ++i) { current = static_cast(m_view.running_jobs->topLevelItem(i)); if (current == static_cast (m_view.running_jobs->currentItem())) { current->setSizeHint(1, QSize(m_view.running_jobs->columnWidth(1), fontMetrics().height() * 3)); } else current->setSizeHint(1, QSize(m_view.running_jobs->columnWidth(1), fontMetrics().height() * 2)); }*/ } void RenderWidget::slotCLeanUpJobs() { int ix = 0; auto *current = static_cast(m_view.running_jobs->topLevelItem(ix)); while (current != nullptr) { if (current->status() == FINISHEDJOB || current->status() == ABORTEDJOB) { delete current; } else { ix++; } current = static_cast(m_view.running_jobs->topLevelItem(ix)); } slotCheckJob(); } void RenderWidget::parseScriptFiles() { QStringList scriptsFilter; scriptsFilter << QStringLiteral("*.mlt"); m_view.scripts_list->clear(); QTreeWidgetItem *item; // List the project scripts QDir projectFolder(pCore->currentDoc()->projectDataFolder()); projectFolder.mkpath(QStringLiteral("kdenlive-renderqueue")); projectFolder.cd(QStringLiteral("kdenlive-renderqueue")); QStringList scriptFiles = projectFolder.entryList(scriptsFilter, QDir::Files); for (int i = 0; i < scriptFiles.size(); ++i) { QUrl scriptpath = QUrl::fromLocalFile(projectFolder.absoluteFilePath(scriptFiles.at(i))); QFile f(scriptpath.toLocalFile()); QDomDocument doc; doc.setContent(&f, false); f.close(); QDomElement consumer = doc.documentElement().firstChildElement(QStringLiteral("consumer")); if (consumer.isNull()) { continue; } QString target = consumer.attribute(QStringLiteral("target")); if (target.isEmpty()) { continue; } item = new QTreeWidgetItem(m_view.scripts_list, QStringList() << QString() << scriptpath.fileName()); auto icon = QFileIconProvider().icon(QFileInfo(f)); item->setIcon(0, icon.isNull() ? QIcon::fromTheme(QStringLiteral("application-x-executable-script")) : icon); item->setSizeHint(0, QSize(m_view.scripts_list->columnWidth(0), fontMetrics().height() * 2)); item->setData(1, Qt::UserRole, QUrl(QUrl::fromEncoded(target.toUtf8())).url(QUrl::PreferLocalFile)); item->setData(1, Qt::UserRole + 1, scriptpath.toLocalFile()); } QTreeWidgetItem *script = m_view.scripts_list->topLevelItem(0); if (script) { m_view.scripts_list->setCurrentItem(script); script->setSelected(true); } } void RenderWidget::slotCheckScript() { QTreeWidgetItem *current = m_view.scripts_list->currentItem(); if (current == nullptr) { return; } m_view.start_script->setEnabled(current->data(0, Qt::UserRole).toString().isEmpty()); m_view.delete_script->setEnabled(true); for (int i = 0; i < m_view.scripts_list->topLevelItemCount(); ++i) { current = m_view.scripts_list->topLevelItem(i); if (current == m_view.scripts_list->currentItem()) { current->setSizeHint(1, QSize(m_view.scripts_list->columnWidth(1), fontMetrics().height() * 3)); } else { current->setSizeHint(1, QSize(m_view.scripts_list->columnWidth(1), fontMetrics().height() * 2)); } } } void RenderWidget::slotStartScript() { auto *item = static_cast(m_view.scripts_list->currentItem()); if (item) { QString destination = item->data(1, Qt::UserRole).toString(); if (QFile::exists(destination)) { if (KMessageBox::warningYesNo(this, i18n("Output file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { return; } } QString path = item->data(1, Qt::UserRole + 1).toString(); // Insert new job in queue RenderJobItem *renderItem = nullptr; QList existing = m_view.running_jobs->findItems(destination, Qt::MatchExactly, 1); if (!existing.isEmpty()) { renderItem = static_cast(existing.at(0)); if (renderItem->status() == RUNNINGJOB || renderItem->status() == WAITINGJOB || renderItem->status() == STARTINGJOB) { KMessageBox::information( this, i18n("There is already a job writing file:
%1
Abort the job if you want to overwrite it...", destination), i18n("Already running")); return; } delete renderItem; renderItem = nullptr; } if (!renderItem) { renderItem = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << destination); } renderItem->setData(1, ProgressRole, 0); renderItem->setStatus(WAITINGJOB); renderItem->setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause"))); renderItem->setData(1, Qt::UserRole, i18n("Waiting...")); renderItem->setData(1, TimeRole, QDateTime::currentDateTime()); QStringList argsJob = {KdenliveSettings::rendererpath(), path, destination, QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid())}; renderItem->setData(1, ParametersRole, argsJob); checkRenderStatus(); m_view.tabWidget->setCurrentIndex(1); } } void RenderWidget::slotDeleteScript() { QTreeWidgetItem *item = m_view.scripts_list->currentItem(); if (item) { QString path = item->data(1, Qt::UserRole + 1).toString(); bool success = true; success &= static_cast(QFile::remove(path)); if (!success) { qCWarning(KDENLIVE_LOG) << "// Error removing script or playlist: " << path << ", " << path << ".mlt"; } parseScriptFiles(); } } void RenderWidget::slotGenerateScript() { slotPrepareExport(true); } void RenderWidget::slotHideLog() { m_view.error_box->setVisible(false); } void RenderWidget::setRenderProfile(const QMap &props) { m_view.scanning_list->setCurrentIndex(props.value(QStringLiteral("renderscanning")).toInt()); m_view.field_order->setCurrentIndex(props.value(QStringLiteral("renderfield")).toInt()); int exportAudio = props.value(QStringLiteral("renderexportaudio")).toInt(); switch (exportAudio) { case 1: m_view.export_audio->setCheckState(Qt::Unchecked); break; case 2: m_view.export_audio->setCheckState(Qt::Checked); break; default: m_view.export_audio->setCheckState(Qt::PartiallyChecked); } if (props.contains(QStringLiteral("renderrescale"))) { m_view.rescale->setChecked(props.value(QStringLiteral("renderrescale")).toInt() != 0); } if (props.contains(QStringLiteral("renderrescalewidth"))) { m_view.rescale_width->setValue(props.value(QStringLiteral("renderrescalewidth")).toInt()); } if (props.contains(QStringLiteral("renderrescaleheight"))) { m_view.rescale_height->setValue(props.value(QStringLiteral("renderrescaleheight")).toInt()); } if (props.contains(QStringLiteral("rendertcoverlay"))) { m_view.tc_overlay->setChecked(props.value(QStringLiteral("rendertcoverlay")).toInt() != 0); } if (props.contains(QStringLiteral("rendertctype"))) { m_view.tc_type->setCurrentIndex(props.value(QStringLiteral("rendertctype")).toInt()); } if (props.contains(QStringLiteral("renderratio"))) { m_view.rescale_keep->setChecked(props.value(QStringLiteral("renderratio")).toInt() != 0); } if (props.contains(QStringLiteral("renderplay"))) { m_view.play_after->setChecked(props.value(QStringLiteral("renderplay")).toInt() != 0); } if (props.contains(QStringLiteral("rendertwopass"))) { m_view.checkTwoPass->setChecked(props.value(QStringLiteral("rendertwopass")).toInt() != 0); } if (props.value(QStringLiteral("renderzone")) == QLatin1String("1")) { m_view.render_zone->setChecked(true); } else if (props.value(QStringLiteral("renderguide")) == QLatin1String("1")) { m_view.render_guide->setChecked(true); m_view.guide_start->setCurrentIndex(props.value(QStringLiteral("renderstartguide")).toInt()); m_view.guide_end->setCurrentIndex(props.value(QStringLiteral("renderendguide")).toInt()); } else { m_view.render_full->setChecked(true); } slotUpdateGuideBox(); QString url = props.value(QStringLiteral("renderurl")); if (!url.isEmpty()) { m_view.out_file->setUrl(QUrl::fromLocalFile(url)); } if (props.contains(QStringLiteral("renderprofile")) || props.contains(QStringLiteral("rendercategory"))) { focusFirstVisibleItem(props.value(QStringLiteral("renderprofile"))); } if (props.contains(QStringLiteral("renderquality"))) { m_view.video->setValue(props.value(QStringLiteral("renderquality")).toInt()); } else if (props.contains(QStringLiteral("renderbitrate"))) { m_view.video->setValue(props.value(QStringLiteral("renderbitrate")).toInt()); } else { m_view.quality->setValue(m_view.quality->maximum() * 3 / 4); } if (props.contains(QStringLiteral("renderaudioquality"))) { m_view.audio->setValue(props.value(QStringLiteral("renderaudioquality")).toInt()); } if (props.contains(QStringLiteral("renderaudiobitrate"))) { m_view.audio->setValue(props.value(QStringLiteral("renderaudiobitrate")).toInt()); } if (props.contains(QStringLiteral("renderspeed"))) { m_view.speed->setValue(props.value(QStringLiteral("renderspeed")).toInt()); } } bool RenderWidget::startWaitingRenderJobs() { m_blockProcessing = true; #ifdef Q_OS_WIN const QLatin1String ScriptFormat(".bat"); #else const QLatin1String ScriptFormat(".sh"); #endif QTemporaryFile tmp(QDir::tempPath() + QStringLiteral("/kdenlive-XXXXXX") + ScriptFormat); if (!tmp.open()) { // Something went wrong return false; } tmp.close(); QString autoscriptFile = tmp.fileName(); QFile file(autoscriptFile); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: " << autoscriptFile; KMessageBox::error(nullptr, i18n("Cannot write to file %1", autoscriptFile)); return false; } QTextStream outStream(&file); #ifndef Q_OS_WIN outStream << "#! /bin/sh" << '\n' << '\n'; #endif auto *item = static_cast(m_view.running_jobs->topLevelItem(0)); while (item != nullptr) { if (item->status() == WAITINGJOB) { // Add render process for item const QString params = item->data(1, ParametersRole).toStringList().join(QLatin1Char(' ')); outStream << '\"' << m_renderer << "\" " << params << '\n'; } item = static_cast(m_view.running_jobs->itemBelow(item)); } // erase itself when rendering is finished #ifndef Q_OS_WIN outStream << "rm \"" << autoscriptFile << "\"\n"; #else outStream << "del \"" << autoscriptFile << "\"\n"; #endif if (file.error() != QFile::NoError) { KMessageBox::error(nullptr, i18n("Cannot write to file %1", autoscriptFile)); file.close(); m_blockProcessing = false; return false; } file.close(); QFile::setPermissions(autoscriptFile, file.permissions() | QFile::ExeUser); QProcess::startDetached(autoscriptFile, QStringList()); return true; } void RenderWidget::slotPlayRendering(QTreeWidgetItem *item, int) { auto *renderItem = static_cast(item); if (renderItem->status() != FINISHEDJOB) { return; } new KRun(QUrl::fromLocalFile(item->text(1)), this); } void RenderWidget::errorMessage(RenderError type, const QString &message) { QString fullMessage; m_errorMessages.insert(type, message); QMapIterator i(m_errorMessages); while (i.hasNext()) { i.next(); if (!i.value().isEmpty()) { if (!fullMessage.isEmpty()) { fullMessage.append(QLatin1Char('\n')); } fullMessage.append(i.value()); } } if (!fullMessage.isEmpty()) { m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(fullMessage); m_infoMessage->show(); } else { m_infoMessage->hide(); } } void RenderWidget::slotUpdateEncodeThreads(int val) { KdenliveSettings::setEncodethreads(val); } void RenderWidget::slotUpdateRescaleWidth(int val) { KdenliveSettings::setDefaultrescalewidth(val); if (!m_view.rescale_keep->isChecked()) { return; } m_view.rescale_height->blockSignals(true); std::unique_ptr &profile = pCore->getCurrentProfile(); m_view.rescale_height->setValue(val * profile->height() / profile->width()); KdenliveSettings::setDefaultrescaleheight(m_view.rescale_height->value()); m_view.rescale_height->blockSignals(false); } void RenderWidget::slotUpdateRescaleHeight(int val) { KdenliveSettings::setDefaultrescaleheight(val); if (!m_view.rescale_keep->isChecked()) { return; } m_view.rescale_width->blockSignals(true); std::unique_ptr &profile = pCore->getCurrentProfile(); m_view.rescale_width->setValue(val * profile->width() / profile->height()); KdenliveSettings::setDefaultrescaleheight(m_view.rescale_width->value()); m_view.rescale_width->blockSignals(false); } void RenderWidget::slotSwitchAspectRatio() { KdenliveSettings::setRescalekeepratio(m_view.rescale_keep->isChecked()); if (m_view.rescale_keep->isChecked()) { slotUpdateRescaleWidth(m_view.rescale_width->value()); } } void RenderWidget::slotUpdateAudioLabel(int ix) { if (ix == Qt::PartiallyChecked) { m_view.export_audio->setText(i18n("Export audio (automatic)")); } else { m_view.export_audio->setText(i18n("Export audio")); } m_view.stemAudioExport->setEnabled(ix != Qt::Unchecked); } bool RenderWidget::automaticAudioExport() const { return (m_view.export_audio->checkState() == Qt::PartiallyChecked); } bool RenderWidget::selectedAudioExport() const { return (m_view.export_audio->checkState() != Qt::Unchecked); } void RenderWidget::updateProxyConfig(bool enable) { m_view.proxy_render->setHidden(!enable); } bool RenderWidget::proxyRendering() { return m_view.proxy_render->isChecked(); } bool RenderWidget::isStemAudioExportEnabled() const { return (m_view.stemAudioExport->isChecked() && m_view.stemAudioExport->isVisible() && m_view.stemAudioExport->isEnabled()); } void RenderWidget::setRescaleEnabled(bool enable) { for (int i = 0; i < m_view.rescale_box->layout()->count(); ++i) { if (m_view.rescale_box->itemAt(i)->widget()) { m_view.rescale_box->itemAt(i)->widget()->setEnabled(enable); } } } void RenderWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { switch (m_view.tabWidget->currentIndex()) { case 1: if (m_view.start_job->isEnabled()) { slotStartCurrentJob(); } break; case 2: if (m_view.start_script->isEnabled()) { slotStartScript(); } break; default: if (m_view.buttonRender->isEnabled()) { slotPrepareExport(); } break; } } else { QDialog::keyPressEvent(e); } } void RenderWidget::adjustAVQualities(int quality) { // calculate video/audio quality indexes from the general quality cursor // taking into account decreasing/increasing video/audio quality parameter double q = (double)quality / m_view.quality->maximum(); int dq = q * (m_view.video->maximum() - m_view.video->minimum()); // prevent video spinbox to update quality cursor (loop) m_view.video->blockSignals(true); m_view.video->setValue(m_view.video->property("decreasing").toBool() ? m_view.video->maximum() - dq : m_view.video->minimum() + dq); m_view.video->blockSignals(false); dq = q * (m_view.audio->maximum() - m_view.audio->minimum()); dq -= dq % m_view.audio->singleStep(); // keep a 32 pitch for bitrates m_view.audio->setValue(m_view.audio->property("decreasing").toBool() ? m_view.audio->maximum() - dq : m_view.audio->minimum() + dq); } void RenderWidget::adjustQuality(int videoQuality) { int dq = videoQuality * m_view.quality->maximum() / (m_view.video->maximum() - m_view.video->minimum()); m_view.quality->blockSignals(true); m_view.quality->setValue(m_view.video->property("decreasing").toBool() ? m_view.video->maximum() - dq : m_view.video->minimum() + dq); m_view.quality->blockSignals(false); } void RenderWidget::adjustSpeed(int speedIndex) { if (m_view.formats->currentItem()) { QStringList speeds = m_view.formats->currentItem()->data(0, SpeedsRole).toStringList(); if (speedIndex < speeds.count()) { m_view.speed->setToolTip(i18n("Codec speed parameters:\n%1", speeds.at(speedIndex))); } } } void RenderWidget::checkCodecs() { Mlt::Profile p; auto *consumer = new Mlt::Consumer(p, "avformat"); if (consumer) { consumer->set("vcodec", "list"); consumer->set("acodec", "list"); consumer->set("f", "list"); consumer->start(); vcodecsList.clear(); Mlt::Properties vcodecs((mlt_properties)consumer->get_data("vcodec")); vcodecsList.reserve(vcodecs.count()); for (int i = 0; i < vcodecs.count(); ++i) { vcodecsList << QString(vcodecs.get(i)); } acodecsList.clear(); Mlt::Properties acodecs((mlt_properties)consumer->get_data("acodec")); acodecsList.reserve(acodecs.count()); for (int i = 0; i < acodecs.count(); ++i) { acodecsList << QString(acodecs.get(i)); } supportedFormats.clear(); Mlt::Properties formats((mlt_properties)consumer->get_data("f")); supportedFormats.reserve(formats.count()); for (int i = 0; i < formats.count(); ++i) { supportedFormats << QString(formats.get(i)); } delete consumer; } } void RenderWidget::slotProxyWarn(bool enableProxy) { errorMessage(ProxyWarning, enableProxy ? i18n("Rendering using low quality proxy") : QString()); } diff --git a/src/mltcontroller/clipcontroller.cpp b/src/mltcontroller/clipcontroller.cpp index bfe813cce..2c3dafdd3 100644 --- a/src/mltcontroller/clipcontroller.cpp +++ b/src/mltcontroller/clipcontroller.cpp @@ -1,895 +1,954 @@ /* 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 "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "profiles/profilemodel.hpp" #include "core.h" #include "kdenlive_debug.h" #include #include #include std::shared_ptr ClipController::mediaUnavailable; ClipController::ClipController(const QString &clipId, const 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_videoIndex(0) , m_clipType(ClipType::Unknown) , m_hasLimitedDuration(true) , m_effectStack(producer ? EffectStackModel::construct(producer, {ObjectType::BinClip, clipId.toInt()}, pCore->undoStack()) : nullptr) , m_hasAudio(false) , m_hasVideo(false) , m_controllerBinId(clipId) { if (m_masterProducer && !m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; return; } if (m_masterProducer) { 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 { 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, bool includeProfile) { 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); } if (!includeProfile) { c.set("no_profile", 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, bool includeProfile) { // TODO refac this is a probable duplicate with Clip::xml if (m_masterProducer) { QString xml = producerXml(m_masterProducer, includeMeta, includeProfile); document.setContent(xml); } else { qCDebug(KDENLIVE_LOG) << " + + ++ NO MASTER PROD"; } } void ClipController::getInfoForProducer() { date = QFileInfo(m_path).lastModified(); m_videoIndex = -1; int audioIndex = -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")) { audioIndex = getProducerIntProperty(QStringLiteral("audio_index")); m_videoIndex = getProducerIntProperty(QStringLiteral("video_index")); if (m_videoIndex == -1) { m_clipType = ClipType::Audio; } else { if (audioIndex == -1) { m_clipType = ClipType::Video; } else { m_clipType = ClipType::AV; } if (m_service == QLatin1String("avformat")) { m_properties->set("mlt_service", "avformat-novalidate"); } } } else if (m_service == QLatin1String("qimage") || m_service == QLatin1String("pixbuf")) { if (m_path.contains(QLatin1Char('%')) || m_path.contains(QStringLiteral("/.all."))) { m_clipType = ClipType::SlideShow; m_hasLimitedDuration = true; } 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 if (m_service == QLatin1String("blipflash")) { // Mostly used for testing m_clipType = ClipType::AV; m_hasLimitedDuration = true; } else { m_clipType = ClipType::Unknown; } if (audioIndex > -1 || m_clipType == ClipType::Playlist) { m_audioInfo = std::make_unique(m_masterProducer, audioIndex); } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime <= 0) { // Fix clips having missing kdenlive:duration m_masterProducer->set("kdenlive:duration", m_masterProducer->frames_to_time(m_masterProducer->get_playtime(), mlt_time_clock)); m_masterProducer->set("out", m_masterProducer->frames_to_time(m_masterProducer->get_length() - 1, mlt_time_clock)); } } } 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,xmldata,video_index,audio_index,set.test_image,set.test_audio"; } 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,video_index,audio_index,set.test_image,set.test_audio"; } 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; } // When resetting profile, duration can change so we invalidate it to 0 in that case int length = m_properties->get_int("length"); const char *passList = getPassPropertiesList(m_usesProxy && length > 0); // This is necessary as some properties like set.test_audio are reset on producer creation passProperties.pass_list(*m_properties, passList); delete m_properties; *m_masterProducer = producer.get(); checkAudioVideo(); m_properties = new Mlt::Properties(m_masterProducer->get_properties()); // Pass properties from previous producer m_properties->pass_list(passProperties, passList); if (!m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { m_effectStack->resetService(m_masterProducer); emitProducerChanged(m_controllerBinId, producer); // URL and name should 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"); } const QString ClipController::getStringDuration() { if (m_masterProducer) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("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"); } int ClipController::getProducerDuration() const { if (m_masterProducer) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime <= 0) { return playtime = m_masterProducer->get_length(); } return playtime; } return -1; } char *ClipController::framesToTime(int frames) const { if (m_masterProducer) { return m_masterProducer->frames_to_time(frames, mlt_time_clock); } return nullptr; } GenTime ClipController::getPlaytime() const { if (!m_masterProducer || !m_masterProducer->is_valid()) { return GenTime(); } double fps = pCore->getCurrentFps(); if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return GenTime(playtime == 0 ? m_masterProducer->get_playtime() : playtime, fps); } return {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->time_to_frames(m_masterProducer->get("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 {}; } 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_properties->get_int("audio_index") : 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 if (!m_masterProducer) return; m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } ClipType::ProducerType 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::checkAudioVideo() { m_masterProducer->seek(0); if (m_masterProducer->get_int("_placeholder") == 1 || m_masterProducer->get("text") == QLatin1String("INVALID")) { // This is a placeholder file, try to guess from its properties QString orig_service = m_masterProducer->get("kdenlive:orig_service"); if (orig_service.startsWith(QStringLiteral("avformat")) || (m_masterProducer->get_int("audio_index") + m_masterProducer->get_int("video_index") > 0)) { m_hasAudio = m_masterProducer->get_int("audio_index") >= 0; m_hasVideo = m_masterProducer->get_int("video_index") >= 0; } else { // Assume image or text producer m_hasAudio = false; m_hasVideo = true; } return; } QScopedPointer frame(m_masterProducer->get_frame()); // 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::ClipState 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::backupOriginalProperties() +{ + if (m_properties->get_int("kdenlive:original.backup") == 1) { + return; + } + int propsCount = m_properties->count(); + // store original props + for (int j = 0; j < propsCount; j++) { + QString propName = m_properties->get_name(j); + if (!propName.startsWith(QLatin1Char('_'))) { + propName.prepend(QStringLiteral("kdenlive:original.")); + m_properties->set(propName.toUtf8().constData(), m_properties->get(j)); + } + } + m_properties->set("kdenlive:original.backup", 1); +} + +void ClipController::clearBackupProperties() +{ + if (m_properties->get_int("kdenlive:original.backup") == 0) { + return; + } + int propsCount = m_properties->count(); + // clear original props + QStringList passProps; + for (int j = 0; j < propsCount; j++) { + QString propName = m_properties->get_name(j); + if (propName.startsWith(QLatin1String("kdenlive:original."))) { + passProps << propName; + } + } + for (const QString &p : passProps) { + m_properties->set(p.toUtf8().constData(), (char *)nullptr); + } + m_properties->set("kdenlive:original.backup", (char *)nullptr); +} + void ClipController::mirrorOriginalProperties(Mlt::Properties &props) { if (m_usesProxy && QFileInfo(m_properties->get("resource")).fileName() == QFileInfo(m_properties->get("kdenlive:proxy")).fileName()) { - // We have a proxy clip, load original source producer - std::shared_ptr prod = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, m_path.toUtf8().constData()); - // Get frame to make sure we retrieve all original props - std::shared_ptr fr(prod->get_frame()); - if (!prod->is_valid()) { - return; + // This is a proxy, we need to use the real source properties + if (m_properties->get_int("kdenlive:original.backup") == 0) { + // We have a proxy clip, load original source producer + std::shared_ptr prod = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, m_path.toUtf8().constData()); + // Get frame to make sure we retrieve all original props + std::shared_ptr fr(prod->get_frame()); + if (!prod->is_valid()) { + return; + } + int width = 0; + int height = 0; + mlt_image_format format = mlt_image_none; + fr->get_image(format, width, height); + Mlt::Properties sourceProps(prod->get_properties()); + props.inherit(sourceProps); + int propsCount = sourceProps.count(); + // store original props + for (int i = 0; i < propsCount; i++) { + QString propName = sourceProps.get_name(i); + if (!propName.startsWith(QLatin1Char('_'))) { + propName.prepend(QStringLiteral("kdenlive:original.")); + m_properties->set(propName.toUtf8().constData(), sourceProps.get(i)); + } + } + m_properties->set("kdenlive:original.backup", 1); } - Mlt::Properties sourceProps(prod->get_properties()); + // Properties were fetched in the past, reuse + Mlt::Properties sourceProps; + sourceProps.pass_values(*m_properties, "kdenlive:original."); props.inherit(sourceProps); } else { if (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Audio) { // Make sure that a frame / image was fetched to initialize all meta properties QString progressive = m_properties->get("meta.media.progressive"); if (progressive.isEmpty()) { // Fetch a frame to initialize required properties QScopedPointer tmpProd(nullptr); if (KdenliveSettings::gpu_accel()) { QString service = m_masterProducer->get("mlt_service"); tmpProd.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), service.toUtf8().constData(), m_masterProducer->get("resource"))); } std::shared_ptr fr(tmpProd ? tmpProd->get_frame() : m_masterProducer->get_frame()); mlt_image_format format = mlt_image_none; int width = 0; int height = 0; fr->get_image(format, width, height); } } props.inherit(*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); } */ } 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; } 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; } bool ClipController::addEffect(const QString &effectId) { return m_effectStack->appendEffect(effectId, true); } bool ClipController::copyEffect(const std::shared_ptr &stackModel, int rowId) { m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), !m_hasAudio ? PlaylistState::VideoOnly : !m_hasVideo ? PlaylistState::AudioOnly : PlaylistState::Disabled); return true; } std::shared_ptr ClipController::getMarkerModel() const { return m_markerModel; } void ClipController::refreshAudioInfo() { if (m_audioInfo && m_masterProducer) { m_audioInfo->setAudioIndex(m_masterProducer, m_properties->get_int("audio_index")); } } diff --git a/src/mltcontroller/clipcontroller.h b/src/mltcontroller/clipcontroller.h index 187feeee7..5a2572a1b 100644 --- a/src/mltcontroller/clipcontroller.h +++ b/src/mltcontroller/clipcontroller.h @@ -1,235 +1,237 @@ /* 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, const std::shared_ptr &producer = nullptr); public: virtual ~ClipController(); QMutex producerMutex; /** @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, bool includeProfile = true); /** @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::ProducerType 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(); int getProducerDuration() const; char *framesToTime(int frames) const; /** * @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 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 mirrorOriginalProperties(Mlt::Properties &props); void addEffect(QDomElement &xml); bool copyEffect(const std::shared_ptr &stackModel, int rowId); void removeEffect(int effectIndex, bool delayRefresh = false); /** @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::ClipState 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 example 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 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 */ bool addEffect(const QString &effectId); protected: virtual void emitProducerChanged(const QString & /*unused*/, const std::shared_ptr & /*unused*/){}; virtual void connectEffectStack(){}; // This is the helper function that checks if the clip has audio and video and stores the result void checkAudioVideo(); // Update audio stream info void refreshAudioInfo(); + void backupOriginalProperties(); + void clearBackupProperties(); 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_videoIndex; ClipType::ProducerType 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/project/projectmanager.cpp b/src/project/projectmanager.cpp index cafeeb88c..49c6a6470 100644 --- a/src/project/projectmanager.cpp +++ b/src/project/projectmanager.cpp @@ -1,965 +1,969 @@ /* Copyright (C) 2014 Till Theato 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 3 of the License, or (at your option) any later version. */ #include "projectmanager.h" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "jobs/jobmanager.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "project/dialogs/archivewidget.h" #include "project/dialogs/backupwidget.h" #include "project/dialogs/noteswidget.h" #include "project/dialogs/projectsettings.h" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" // Temporary for testing #include "bin/model/markerlistmodel.hpp" #include "profiles/profilerepository.hpp" #include "project/notesplugin.h" #include "timeline2/model/builders/meltBuilder.hpp" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinewidget.h" #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include ProjectManager::ProjectManager(QObject *parent) : QObject(parent) + , m_mainTimelineModel(nullptr) { m_fileRevert = KStandardAction::revert(this, SLOT(slotRevert()), pCore->window()->actionCollection()); m_fileRevert->setIcon(QIcon::fromTheme(QStringLiteral("document-revert"))); m_fileRevert->setEnabled(false); QAction *a = KStandardAction::open(this, SLOT(openFile()), pCore->window()->actionCollection()); a->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); a = KStandardAction::saveAs(this, SLOT(saveFileAs()), pCore->window()->actionCollection()); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); a = KStandardAction::openNew(this, SLOT(newFile()), pCore->window()->actionCollection()); a->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_recentFilesAction = KStandardAction::openRecent(this, SLOT(openFile(QUrl)), pCore->window()->actionCollection()); QAction *backupAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Open Backup File"), this); pCore->window()->addAction(QStringLiteral("open_backup"), backupAction); connect(backupAction, SIGNAL(triggered(bool)), SLOT(slotOpenBackup())); m_notesPlugin = new NotesPlugin(this); m_autoSaveTimer.setSingleShot(true); connect(&m_autoSaveTimer, &QTimer::timeout, this, &ProjectManager::slotAutoSave); // Ensure the default data folder exist QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); dir.mkpath(QStringLiteral(".backup")); dir.mkdir(QStringLiteral("titles")); } ProjectManager::~ProjectManager() = default; void ProjectManager::slotLoadOnOpen() { if (m_startUrl.isValid()) { openFile(); } else if (KdenliveSettings::openlastproject()) { openLastFile(); } else { newFile(false); } if (!m_loadClipsOnOpen.isEmpty() && (m_project != nullptr)) { const QStringList list = m_loadClipsOnOpen.split(QLatin1Char(',')); QList urls; urls.reserve(list.count()); for (const QString &path : list) { // qCDebug(KDENLIVE_LOG) << QDir::current().absoluteFilePath(path); urls << QUrl::fromLocalFile(QDir::current().absoluteFilePath(path)); } pCore->bin()->droppedUrls(urls); } m_loadClipsOnOpen.clear(); } void ProjectManager::init(const QUrl &projectUrl, const QString &clipList) { m_startUrl = projectUrl; m_loadClipsOnOpen = clipList; } void ProjectManager::newFile(bool showProjectSettings) { QString profileName = KdenliveSettings::default_profile(); if (profileName.isEmpty()) { profileName = pCore->getCurrentProfile()->path(); } newFile(profileName, showProjectSettings); } void ProjectManager::newFile(QString profileName, bool showProjectSettings) { // fix mantis#3160 QUrl startFile = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder() + QStringLiteral("/_untitled.kdenlive")); if (checkForBackupFile(startFile, true)) { return; } m_fileRevert->setEnabled(false); QString projectFolder; QMap documentProperties; QMap documentMetadata; QPoint projectTracks(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()); pCore->monitorManager()->resetDisplay(); QString documentId = QString::number(QDateTime::currentMSecsSinceEpoch()); documentProperties.insert(QStringLiteral("documentid"), documentId); if (!showProjectSettings) { if (!closeCurrentDocument()) { return; } if (KdenliveSettings::customprojectfolder()) { projectFolder = KdenliveSettings::defaultprojectfolder(); if (!projectFolder.endsWith(QLatin1Char('/'))) { projectFolder.append(QLatin1Char('/')); } documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId); } } else { QPointer w = new ProjectSettings(nullptr, QMap(), QStringList(), projectTracks.x(), projectTracks.y(), KdenliveSettings::defaultprojectfolder(), false, true, pCore->window()); connect(w.data(), &ProjectSettings::refreshProfiles, pCore->window(), &MainWindow::slotRefreshProfiles); if (w->exec() != QDialog::Accepted) { delete w; return; } if (!closeCurrentDocument()) { delete w; return; } if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) { pCore->window()->slotSwitchVideoThumbs(); } if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) { pCore->window()->slotSwitchAudioThumbs(); } profileName = w->selectedProfile(); projectFolder = w->storageFolder(); projectTracks = w->tracks(); documentProperties.insert(QStringLiteral("enableproxy"), QString::number((int)w->useProxy())); documentProperties.insert(QStringLiteral("generateproxy"), QString::number((int)w->generateProxy())); documentProperties.insert(QStringLiteral("proxyminsize"), QString::number(w->proxyMinSize())); documentProperties.insert(QStringLiteral("proxyparams"), w->proxyParams()); documentProperties.insert(QStringLiteral("proxyextension"), w->proxyExtension()); documentProperties.insert(QStringLiteral("generateimageproxy"), QString::number((int)w->generateImageProxy())); QString preview = w->selectedPreview(); if (!preview.isEmpty()) { documentProperties.insert(QStringLiteral("previewparameters"), preview.section(QLatin1Char(';'), 0, 0)); documentProperties.insert(QStringLiteral("previewextension"), preview.section(QLatin1Char(';'), 1, 1)); } documentProperties.insert(QStringLiteral("proxyimageminsize"), QString::number(w->proxyImageMinSize())); if (!projectFolder.isEmpty()) { if (!projectFolder.endsWith(QLatin1Char('/'))) { projectFolder.append(QLatin1Char('/')); } documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId); } documentMetadata = w->metadata(); delete w; } bool openBackup; m_notesPlugin->clear(); documentProperties.insert(QStringLiteral("decimalPoint"), QLocale().decimalPoint()); KdenliveDoc *doc = new KdenliveDoc(QUrl(), projectFolder, pCore->window()->m_commandStack, profileName, documentProperties, documentMetadata, projectTracks, &openBackup, pCore->window()); doc->m_autosave = new KAutoSaveFile(startFile, doc); pCore->bin()->setDocument(doc); m_project = doc; pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); updateTimeline(0); pCore->window()->connectDocument(); bool disabled = m_project->getDocumentProperty(QStringLiteral("disabletimelineeffects")) == QLatin1String("1"); QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_timeline_effects")); if (disableEffects) { if (disabled != disableEffects->isChecked()) { disableEffects->blockSignals(true); disableEffects->setChecked(disabled); disableEffects->blockSignals(false); } } emit docOpened(m_project); m_lastSave.start(); } bool ProjectManager::closeCurrentDocument(bool saveChanges, bool quit) { if ((m_project != nullptr) && m_project->isModified() && saveChanges) { QString message; if (m_project->url().fileName().isEmpty()) { message = i18n("Save changes to document?"); } else { message = i18n("The project \"%1\" has been changed.\nDo you want to save your changes?", m_project->url().fileName()); } switch (KMessageBox::warningYesNoCancel(pCore->window(), message)) { case KMessageBox::Yes: // save document here. If saving fails, return false; if (!saveFile()) { return false; } break; case KMessageBox::Cancel: return false; break; default: break; } } pCore->window()->getMainTimeline()->controller()->clipActions.clear(); + if (m_mainTimelineModel) { + m_mainTimelineModel->prepareClose(); + } if (!quit && !qApp->isSavingSession()) { m_autoSaveTimer.stop(); if (m_project) { pCore->jobManager()->slotCancelJobs(); pCore->bin()->abortOperations(); pCore->monitorManager()->clipMonitor()->slotOpenClip(nullptr); pCore->window()->clearAssetPanel(); delete m_project; m_project = nullptr; } pCore->monitorManager()->setDocument(m_project); } /* // Make sure to reset locale to system's default QString requestedLocale = QLocale::system().name(); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); if (env.contains(QStringLiteral("LC_NUMERIC"))) { requestedLocale = env.value(QStringLiteral("LC_NUMERIC")); } qDebug()<<"//////////// RESETTING LOCALE TO: "<decimal_point; if (QString::fromUtf8(separator) != QString(newLocale.decimalPoint())) { pCore->displayBinMessage(i18n("There is a locale conflict on your system, project might get corrupt"), KMessageWidget::Warning); } setlocale(LC_NUMERIC, requestedLocale.toUtf8().constData()); #endif QLocale::setDefault(newLocale);*/ return true; } bool ProjectManager::saveFileAs(const QString &outputFileName) { pCore->monitorManager()->pauseActiveMonitor(); // Sync document properties prepareSave(); QString saveFolder = QFileInfo(outputFileName).absolutePath(); QString scene = projectSceneList(saveFolder); if (!m_replacementPattern.isEmpty()) { QMapIterator i(m_replacementPattern); while (i.hasNext()) { i.next(); scene.replace(i.key(), i.value()); } } if (!m_project->saveSceneList(outputFileName, scene)) { return false; } QUrl url = QUrl::fromLocalFile(outputFileName); // Save timeline thumbnails QStringList thumbKeys = pCore->window()->getMainTimeline()->controller()->getThumbKeys(); ThumbnailCache::get()->saveCachedThumbs(thumbKeys); m_project->setUrl(url); // setting up autosave file in ~/.kde/data/stalefiles/kdenlive/ // saved under file name // actual saving by KdenliveDoc::slotAutoSave() called by a timer 3 seconds after the document has been edited // This timer is set by KdenliveDoc::setModified() const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex(); QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(outputFileName).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive"))); if (m_project->m_autosave == nullptr) { // The temporary file is not opened or created until actually needed. // The file filename does not have to exist for KAutoSaveFile to be constructed (if it exists, it will not be touched). m_project->m_autosave = new KAutoSaveFile(autosaveUrl, m_project); } else { m_project->m_autosave->setManagedFile(autosaveUrl); } pCore->window()->setWindowTitle(m_project->description()); m_project->setModified(false); m_recentFilesAction->addUrl(url); // remember folder for next project opening KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), saveFolder); saveRecentFiles(); m_fileRevert->setEnabled(true); pCore->window()->m_undoView->stack()->setClean(); return true; } void ProjectManager::saveRecentFiles() { KSharedConfigPtr config = KSharedConfig::openConfig(); m_recentFilesAction->saveEntries(KConfigGroup(config, "Recent Files")); config->sync(); } bool ProjectManager::saveFileAs() { QFileDialog fd(pCore->window()); fd.setDirectory(m_project->url().isValid() ? m_project->url().adjusted(QUrl::RemoveFilename).toLocalFile() : KdenliveSettings::defaultprojectfolder()); fd.setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlive")); fd.setAcceptMode(QFileDialog::AcceptSave); fd.setFileMode(QFileDialog::AnyFile); fd.setDefaultSuffix(QStringLiteral("kdenlive")); if (fd.exec() != QDialog::Accepted || fd.selectedFiles().isEmpty()) { return false; } QString outputFile = fd.selectedFiles().constFirst(); #if KXMLGUI_VERSION_MINOR < 23 && KXMLGUI_VERSION_MAJOR == 5 // Since Plasma 5.7 (release at same time as KF 5.23, // the file dialog manages the overwrite check if (QFile::exists(outputFile)) { // Show the file dialog again if the user does not want to overwrite the file if (KMessageBox::questionYesNo(pCore->window(), i18n("File %1 already exists.\nDo you want to overwrite it?", outputFile)) == KMessageBox::No) { return saveFileAs(); } } #endif bool ok = false; QDir cacheDir = m_project->getCacheDir(CacheBase, &ok); if (ok) { QFile file(cacheDir.absoluteFilePath(QString::fromLatin1(QUrl::toPercentEncoding(QStringLiteral(".") + outputFile)))); file.open(QIODevice::ReadWrite | QIODevice::Text); file.close(); } return saveFileAs(outputFile); } bool ProjectManager::saveFile() { if (!m_project) { // Calling saveFile before a project was created, something is wrong qCDebug(KDENLIVE_LOG) << "SaveFile called without project"; return false; } if (m_project->url().isEmpty()) { return saveFileAs(); } bool result = saveFileAs(m_project->url().toLocalFile()); m_project->m_autosave->resize(0); return result; } void ProjectManager::openFile() { if (m_startUrl.isValid()) { openFile(m_startUrl); m_startUrl.clear(); return; } QUrl url = QFileDialog::getOpenFileUrl(pCore->window(), QString(), QUrl::fromLocalFile(KRecentDirs::dir(QStringLiteral(":KdenliveProjectsFolder"))), getMimeType()); if (!url.isValid()) { return; } KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), url.adjusted(QUrl::RemoveFilename).toLocalFile()); m_recentFilesAction->addUrl(url); saveRecentFiles(); openFile(url); } void ProjectManager::openLastFile() { if (m_recentFilesAction->selectableActionGroup()->actions().isEmpty()) { // No files in history newFile(false); return; } QAction *firstUrlAction = m_recentFilesAction->selectableActionGroup()->actions().last(); if (firstUrlAction) { firstUrlAction->trigger(); } else { newFile(false); } } // fix mantis#3160 separate check from openFile() so we can call it from newFile() // to find autosaved files (in ~/.local/share/stalefiles/kdenlive) and recover it bool ProjectManager::checkForBackupFile(const QUrl &url, bool newFile) { // Check for autosave file that belong to the url we passed in. const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex(); QUrl autosaveUrl = newFile ? url : QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive"))); QList staleFiles = KAutoSaveFile::staleFiles(autosaveUrl); KAutoSaveFile *orphanedFile = nullptr; // Check if we can have a lock on one of the file, // meaning it is not handled by any Kdenlive instance if (!staleFiles.isEmpty()) { for (KAutoSaveFile *stale : staleFiles) { if (stale->open(QIODevice::QIODevice::ReadWrite)) { // Found orphaned autosave file orphanedFile = stale; break; } else { // Another Kdenlive instance is probably handling this autosave file staleFiles.removeAll(stale); delete stale; continue; } } } if (orphanedFile) { if (KMessageBox::questionYesNo(nullptr, i18n("Auto-saved files exist. Do you want to recover them now?"), i18n("File Recovery"), KGuiItem(i18n("Recover")), KGuiItem(i18n("Do not recover"))) == KMessageBox::Yes) { doOpenFile(url, orphanedFile); return true; } // remove the stale files for (KAutoSaveFile *stale : staleFiles) { stale->open(QIODevice::ReadWrite); delete stale; } return false; } return false; } void ProjectManager::openFile(const QUrl &url) { QMimeDatabase db; // Make sure the url is a Kdenlive project file QMimeType mime = db.mimeTypeForUrl(url); if (mime.inherits(QStringLiteral("application/x-compressed-tar"))) { // Opening a compressed project file, we need to process it // qCDebug(KDENLIVE_LOG)<<"Opening archive, processing"; QPointer ar = new ArchiveWidget(url); if (ar->exec() == QDialog::Accepted) { openFile(QUrl::fromLocalFile(ar->extractedProjectFile())); } else if (m_startUrl.isValid()) { // we tried to open an invalid file from command line, init new project newFile(false); } delete ar; return; } /*if (!url.fileName().endsWith(".kdenlive")) { // This is not a Kdenlive project file, abort loading KMessageBox::sorry(pCore->window(), i18n("File %1 is not a Kdenlive project file", url.toLocalFile())); if (m_startUrl.isValid()) { // we tried to open an invalid file from command line, init new project newFile(false); } return; }*/ if ((m_project != nullptr) && m_project->url() == url) { return; } if (!closeCurrentDocument()) { return; } if (checkForBackupFile(url)) { return; } pCore->window()->slotGotProgressInfo(i18n("Opening file %1", url.toLocalFile()), 100, InformationMessage); doOpenFile(url, nullptr); } void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale) { Q_ASSERT(m_project == nullptr); m_fileRevert->setEnabled(true); delete m_progressDialog; pCore->monitorManager()->resetDisplay(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); m_progressDialog = new QProgressDialog(pCore->window()); m_progressDialog->setWindowTitle(i18n("Loading project")); m_progressDialog->setCancelButton(nullptr); m_progressDialog->setLabelText(i18n("Loading project")); m_progressDialog->setMaximum(0); m_progressDialog->show(); bool openBackup; m_notesPlugin->clear(); KdenliveDoc *doc = new KdenliveDoc(stale ? QUrl::fromLocalFile(stale->fileName()) : url, QString(), pCore->window()->m_commandStack, KdenliveSettings::default_profile().isEmpty() ? pCore->getCurrentProfile()->path() : KdenliveSettings::default_profile(), QMap(), QMap(), QPoint(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()), &openBackup, pCore->window()); if (stale == nullptr) { const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex(); QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive"))); stale = new KAutoSaveFile(autosaveUrl, doc); doc->m_autosave = stale; } else { doc->m_autosave = stale; stale->setParent(doc); // if loading from an autosave of unnamed file then keep unnamed if (url.fileName().contains(QStringLiteral("_untitled.kdenlive"))) { doc->setUrl(QUrl()); } else { doc->setUrl(url); } doc->setModified(true); stale->setParent(doc); } m_progressDialog->setLabelText(i18n("Loading clips")); // TODO refac delete this pCore->bin()->setDocument(doc); QList rulerActions; rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("set_render_timeline_zone")); rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("unset_render_timeline_zone")); rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("clear_render_timeline_zone")); // Set default target tracks to upper audio / lower video tracks m_project = doc; updateTimeline(m_project->getDocumentProperty(QStringLiteral("position")).toInt()); pCore->window()->connectDocument(); QDateTime documentDate = QFileInfo(m_project->url().toLocalFile()).lastModified(); pCore->window()->getMainTimeline()->controller()->loadPreview(m_project->getDocumentProperty(QStringLiteral("previewchunks")), m_project->getDocumentProperty(QStringLiteral("dirtypreviewchunks")), documentDate, m_project->getDocumentProperty(QStringLiteral("disablepreview")).toInt()); emit docOpened(m_project); pCore->window()->slotGotProgressInfo(QString(), 100); if (openBackup) { slotOpenBackup(url); } m_lastSave.start(); delete m_progressDialog; m_progressDialog = nullptr; } void ProjectManager::slotRevert() { if (m_project->isModified() && KMessageBox::warningContinueCancel(pCore->window(), i18n("This will delete all changes made since you last saved your project. Are you sure you want to continue?"), i18n("Revert to last saved version")) == KMessageBox::Cancel) { return; } QUrl url = m_project->url(); if (closeCurrentDocument(false)) { doOpenFile(url, nullptr); } } QString ProjectManager::getMimeType(bool open) { QString mimetype = i18n("Kdenlive project (*.kdenlive)"); if (open) { mimetype.append(QStringLiteral(";;") + i18n("Archived project (*.tar.gz)")); } return mimetype; } KdenliveDoc *ProjectManager::current() { return m_project; } void ProjectManager::slotOpenBackup(const QUrl &url) { QUrl projectFile; QUrl projectFolder; QString projectId; if (url.isValid()) { // we could not open the project file, guess where the backups are projectFolder = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder()); projectFile = url; } else { projectFolder = QUrl::fromLocalFile(m_project->projectTempFolder()); projectFile = m_project->url(); projectId = m_project->getDocumentProperty(QStringLiteral("documentid")); } QPointer dia = new BackupWidget(projectFile, projectFolder, projectId, pCore->window()); if (dia->exec() == QDialog::Accepted) { QString requestedBackup = dia->selectedFile(); m_project->backupLastSavedVersion(projectFile.toLocalFile()); closeCurrentDocument(false); doOpenFile(QUrl::fromLocalFile(requestedBackup), nullptr); if (m_project) { m_project->setUrl(projectFile); m_project->setModified(true); pCore->window()->setWindowTitle(m_project->description()); } } delete dia; } KRecentFilesAction *ProjectManager::recentFilesAction() { return m_recentFilesAction; } void ProjectManager::slotStartAutoSave() { if (m_lastSave.elapsed() > 300000) { // If the project was not saved in the last 5 minute, force save m_autoSaveTimer.stop(); slotAutoSave(); } else { m_autoSaveTimer.start(3000); // will trigger slotAutoSave() in 3 seconds } } void ProjectManager::slotAutoSave() { prepareSave(); QString saveFolder = m_project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); QString scene = projectSceneList(saveFolder); if (!m_replacementPattern.isEmpty()) { QMapIterator i(m_replacementPattern); while (i.hasNext()) { i.next(); scene.replace(i.key(), i.value()); } } m_project->slotAutoSave(scene); m_lastSave.start(); } QString ProjectManager::projectSceneList(const QString &outputFolder) { // TODO: re-implement overlay and all // TODO refac: repair this return pCore->monitorManager()->projectMonitor()->sceneList(outputFolder); /*bool multitrackEnabled = m_trackView->multitrackView; if (multitrackEnabled) { // Multitrack view was enabled, disable for auto save m_trackView->slotMultitrackView(false); } m_trackView->connectOverlayTrack(false); QString scene = pCore->monitorManager()->projectMonitor()->sceneList(outputFolder); m_trackView->connectOverlayTrack(true); if (multitrackEnabled) { // Multitrack view was enabled, re-enable for auto save m_trackView->slotMultitrackView(true); } return scene; */ } void ProjectManager::setDocumentNotes(const QString ¬es) { m_notesPlugin->widget()->setHtml(notes); } QString ProjectManager::documentNotes() const { QString text = m_notesPlugin->widget()->toPlainText().simplified(); if (text.isEmpty()) { return QString(); } return m_notesPlugin->widget()->toHtml(); } void ProjectManager::slotAddProjectNote() { m_notesPlugin->widget()->raise(); m_notesPlugin->widget()->setFocus(); m_notesPlugin->widget()->addProjectNote(); } void ProjectManager::prepareSave() { pCore->projectItemModel()->saveDocumentProperties(pCore->window()->getMainTimeline()->controller()->documentProperties(), m_project->metadata(), m_project->getGuideModel()); pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:documentnotes"), documentNotes()); pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:docproperties.groups"), m_mainTimelineModel->groupsData()); } void ProjectManager::slotResetProfiles() { m_project->resetProfile(); pCore->monitorManager()->resetProfiles(m_project->timecode()); pCore->monitorManager()->updateScopeSource(); } void ProjectManager::slotResetConsumers(bool fullReset) { pCore->monitorManager()->resetConsumers(fullReset); } void ProjectManager::slotExpandClip() { // TODO refac // m_trackView->projectView()->expandActiveClip(); } void ProjectManager::disableBinEffects(bool disable) { if (m_project) { if (disable) { m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString::number((int)true)); } else { m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString()); } } pCore->monitorManager()->refreshProjectMonitor(); pCore->monitorManager()->refreshClipMonitor(); } void ProjectManager::slotDisableTimelineEffects(bool disable) { if (disable) { m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString::number((int)true)); } else { m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString()); } m_mainTimelineModel->setTimelineEffectsEnabled(!disable); pCore->monitorManager()->refreshProjectMonitor(); } void ProjectManager::slotSwitchTrackLock() { pCore->window()->getMainTimeline()->controller()->switchTrackLock(); } void ProjectManager::slotSwitchTrackActive() { pCore->window()->getMainTimeline()->controller()->switchTrackActive(); } void ProjectManager::slotSwitchAllTrackLock() { pCore->window()->getMainTimeline()->controller()->switchTrackLock(true); } void ProjectManager::slotSwitchTrackTarget() { pCore->window()->getMainTimeline()->controller()->switchTargetTrack(); } QString ProjectManager::getDefaultProjectFormat() { // On first run, lets use an HD1080p profile with fps related to timezone country. Then, when the first video is added to a project, if it does not match // our profile, propose a new default. QTimeZone zone; zone = QTimeZone::systemTimeZone(); QList ntscCountries; ntscCountries << QLocale::Canada << QLocale::Chile << QLocale::CostaRica << QLocale::Cuba << QLocale::DominicanRepublic << QLocale::Ecuador; ntscCountries << QLocale::Japan << QLocale::Mexico << QLocale::Nicaragua << QLocale::Panama << QLocale::Peru << QLocale::Philippines; ntscCountries << QLocale::PuertoRico << QLocale::SouthKorea << QLocale::Taiwan << QLocale::UnitedStates; bool ntscProject = ntscCountries.contains(zone.country()); if (!ntscProject) { return QStringLiteral("atsc_1080p_25"); } return QStringLiteral("atsc_1080p_2997"); } void ProjectManager::saveZone(const QStringList &info, const QDir &dir) { pCore->bin()->saveZone(info, dir); } void ProjectManager::moveProjectData(const QString &src, const QString &dest) { // Move tmp folder (thumbnails, timeline preview) KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest)); connect(copyJob, &KJob::result, this, &ProjectManager::slotMoveFinished); connect(copyJob, SIGNAL(percent(KJob *, ulong)), this, SLOT(slotMoveProgress(KJob *, ulong))); m_project->moveProjectData(src, dest); } void ProjectManager::slotMoveProgress(KJob *, unsigned long progress) { pCore->window()->slotGotProgressInfo(i18n("Moving project folder"), static_cast(progress), ProcessingJobMessage); } void ProjectManager::slotMoveFinished(KJob *job) { if (job->error() == 0) { pCore->window()->slotGotProgressInfo(QString(), 100, InformationMessage); auto *copyJob = static_cast(job); QString newFolder = copyJob->destUrl().toLocalFile(); // Check if project folder is inside document folder, in which case, paths will be relative QDir projectDir(m_project->url().toString(QUrl::RemoveFilename | QUrl::RemoveScheme)); QDir srcDir(m_project->projectTempFolder()); if (srcDir.absolutePath().startsWith(projectDir.absolutePath())) { m_replacementPattern.insert(QStringLiteral(">proxy/"), QStringLiteral(">") + newFolder + QStringLiteral("/proxy/")); } else { m_replacementPattern.insert(m_project->projectTempFolder() + QStringLiteral("/proxy/"), newFolder + QStringLiteral("/proxy/")); } m_project->setProjectFolder(QUrl::fromLocalFile(newFolder)); saveFile(); m_replacementPattern.clear(); slotRevert(); } else { KMessageBox::sorry(pCore->window(), i18n("Error moving project folder: %1", job->errorText())); } } void ProjectManager::updateTimeline(int pos, int scrollPos) { Q_UNUSED(scrollPos); pCore->jobManager()->slotCancelJobs(); /*qDebug() << "Loading xml"<getProjectXml().constData(); QFile file("/tmp/data.xml"); if (file.open(QIODevice::ReadWrite)) { QTextStream stream(&file); stream << m_project->getProjectXml() << endl; }*/ pCore->window()->getMainTimeline()->loading = true; pCore->window()->slotSwitchTimelineZone(m_project->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1); QScopedPointer xmlProd(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", m_project->getProjectXml().constData())); Mlt::Service s(*xmlProd); Mlt::Tractor tractor(s); m_mainTimelineModel = TimelineItemModel::construct(&pCore->getCurrentProfile()->profile(), m_project->getGuideModel(), m_project->commandStack()); pCore->window()->getMainTimeline()->setModel(m_mainTimelineModel); if (!constructTimelineFromMelt(m_mainTimelineModel, tractor)) { //TODO: act on project load failure qDebug()<<"// Project failed to load!!"; } const QString groupsData = m_project->getDocumentProperty(QStringLiteral("groups")); if (!groupsData.isEmpty()) { m_mainTimelineModel->loadGroups(groupsData); } pCore->monitorManager()->projectMonitor()->setProducer(m_mainTimelineModel->producer(), pos); pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_mainTimelineModel->duration() - 1, m_project->getGuideModel()); pCore->window()->getMainTimeline()->controller()->setZone(m_project->zone()); pCore->window()->getMainTimeline()->controller()->setTargetTracks(m_project->targetTracks()); pCore->window()->getMainTimeline()->controller()->setScrollPos(m_project->getDocumentProperty(QStringLiteral("scrollPos")).toInt()); int activeTrackPosition = m_project->getDocumentProperty(QStringLiteral("activeTrack"), QString::number( - 1)).toInt(); if (activeTrackPosition > -1 && activeTrackPosition < m_mainTimelineModel->getTracksCount()) { pCore->window()->getMainTimeline()->controller()->setActiveTrack(m_mainTimelineModel->getTrackIndexFromPosition(activeTrackPosition)); } m_mainTimelineModel->setUndoStack(m_project->commandStack()); } void ProjectManager::adjustProjectDuration() { pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_mainTimelineModel->duration() - 1, nullptr); } void ProjectManager::activateAsset(const QVariantMap &effectData) { if (effectData.contains(QStringLiteral("kdenlive/effect"))) { pCore->window()->addEffect(effectData.value(QStringLiteral("kdenlive/effect")).toString()); } else { pCore->window()->getMainTimeline()->controller()->addAsset(effectData); } } std::shared_ptr ProjectManager::getGuideModel() { return current()->getGuideModel(); } std::shared_ptr ProjectManager::undoStack() { return current()->commandStack(); } void ProjectManager::saveWithUpdatedProfile(const QString &updatedProfile) { // First backup current project with fps appended QString message; if (m_project && m_project->isModified()) { switch ( KMessageBox::warningYesNoCancel(pCore->window(), i18n("The project \"%1\" has been changed.\nDo you want to save your changes?", m_project->url().fileName().isEmpty() ? i18n("Untitled") : m_project->url().fileName()))) { case KMessageBox::Yes: // save document here. If saving fails, return false; if (!saveFile()) { pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; } break; default: pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; break; } } if (!m_project || m_project->isModified()) { pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; } const QString currentFile = m_project->url().toLocalFile(); // Now update to new profile auto &newProfile = ProfileRepository::get()->getProfile(updatedProfile); QString convertedFile = currentFile.section(QLatin1Char('.'), 0, -2); convertedFile.append(QString("-%1.kdenlive").arg((int)(newProfile->fps() * 100))); QFile f(currentFile); QDomDocument doc; doc.setContent(&f, false); f.close(); QDomElement mltProfile = doc.documentElement().firstChildElement(QStringLiteral("profile")); if (!mltProfile.isNull()) { mltProfile.setAttribute(QStringLiteral("frame_rate_num"), newProfile->frame_rate_num()); mltProfile.setAttribute(QStringLiteral("frame_rate_den"), newProfile->frame_rate_den()); mltProfile.setAttribute(QStringLiteral("display_aspect_num"), newProfile->display_aspect_num()); mltProfile.setAttribute(QStringLiteral("display_aspect_den"), newProfile->display_aspect_den()); mltProfile.setAttribute(QStringLiteral("sample_aspect_num"), newProfile->sample_aspect_num()); mltProfile.setAttribute(QStringLiteral("sample_aspect_den"), newProfile->sample_aspect_den()); mltProfile.setAttribute(QStringLiteral("colorspace"), newProfile->colorspace()); mltProfile.setAttribute(QStringLiteral("progressive"), newProfile->progressive()); mltProfile.setAttribute(QStringLiteral("description"), newProfile->description()); mltProfile.setAttribute(QStringLiteral("width"), newProfile->width()); mltProfile.setAttribute(QStringLiteral("height"), newProfile->height()); } QDomNodeList playlists = doc.documentElement().elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < playlists.count(); ++i) { QDomElement e = playlists.at(i).toElement(); if (e.attribute(QStringLiteral("id")) == QLatin1String("main_bin")) { Xml::setXmlProperty(e, QStringLiteral("kdenlive:docproperties.profile"), updatedProfile); break; } } QFile file(convertedFile); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; } QTextStream out(&file); out << doc.toString(); if (file.error() != QFile::NoError) { KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", convertedFile)); file.close(); return; } file.close(); openFile(QUrl::fromLocalFile(convertedFile)); pCore->displayBinMessage(i18n("Project profile changed"), KMessageWidget::Information); } diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp index e3cd548d9..6cc4a2254 100644 --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -1,3124 +1,3139 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "logger.hpp" #include "snapmodel.hpp" #include "timelinefunctions.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include #include #include "macros.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("TimelineModel") .method("requestClipMove", select_overload(&TimelineModel::requestClipMove))( parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline")) .method("requestCompositionMove", select_overload(&TimelineModel::requestCompositionMove))( parameter_names("compoId", "trackId", "position", "updateView", "logUndo")) .method("requestClipInsertion", select_overload(&TimelineModel::requestClipInsertion))( parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets")) .method("requestItemDeletion", select_overload(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo")) .method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove))( parameter_names("itemId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo")) .method("requestGroupDeletion", select_overload(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo")) .method("requestItemResize", select_overload(&TimelineModel::requestItemResize))( parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize")) .method("requestClipsGroup", select_overload &, bool, GroupType)>(&TimelineModel::requestClipsGroup))( parameter_names("itemIds", "logUndo", "type")) .method("requestClipUngroup", select_overload(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo")) .method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo")) .method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion))( parameter_names("pos", "id", "trackName", "audioTrack")) .method("requestTrackDeletion", select_overload(&TimelineModel::requestTrackDeletion))(parameter_names("trackId")) .method("requestClearSelection", select_overload(&TimelineModel::requestClearSelection))(parameter_names("onDeletion")) .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear")) .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId")) .method("requestSetSelection", select_overload &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds")) .method("requestFakeClipMove", select_overload(&TimelineModel::requestFakeClipMove))( parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline")) .method("requestFakeGroupMove", select_overload(&TimelineModel::requestFakeGroupMove))( parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo")) // (parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView" "logUndo")) .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance")) .method("suggestCompositionMove", &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance")) // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos")) // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos")) // .method("requestCompositionInsertion", select_overload, int &, bool)>( // &TimelineModel::requestCompositionInsertion))( // parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo")) .method("requestClipTimeWarp", select_overload(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed")); } 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(std::move(undo_stack)) , m_profile(profile) , m_blackClip(new Mlt::Producer(*profile, "color:black")) , m_lock(QReadWriteLock::Recursive) , m_timelineEffectsEnabled(true) , m_id(getNextId()) , m_overlayTrackCount(-1) , m_audioTarget(-1) , m_videoTarget(-1) , m_editMode(TimelineMode::NormalEdit) , m_blockRefresh(false) + , m_closing(false) { // 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_in_and_out(0, TimelineModel::seekDuration); m_tractor->insert_track(*m_blackClip, 0); TRACE_CONSTR(this); } +void TimelineModel::prepareClose() +{ + // Unlock all tracks to allow delting clip from tracks + m_closing = true; + auto it = m_allTracks.begin(); + while (it != m_allTracks.end()) { + (*it)->setProperty(QStringLiteral("kdenlive:locked_track"), (char*) nullptr); + ++it; + } +} + 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(isItem(itemId)); if (isComposition(itemId)) { return getCompositionTrackId(itemId); } return getClipTrackId(itemId); } int TimelineModel::getClipPosition(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getPosition(); return pos; } double TimelineModel::getClipSpeed(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->getSpeed(); } int TimelineModel::getClipSplitPartner(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_groups->getSplitPartner(clipId); } int TimelineModel::getClipIn(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getIn(); } PlaylistState::ClipState TimelineModel::getClipState(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->clipState(); } const QString TimelineModel::getClipBinId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); QString id = clip->binId(); return id; } int TimelineModel::getClipPlaytime(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); int playtime = clip->getPlaytime(); return playtime; } QSize TimelineModel::getClipFrameSize(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); return clip->getFrameSize(); } int TimelineModel::getTrackClipsCount(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); int count = getTrackById_const(trackId)->getClipsCount(); return count; } int TimelineModel::getClipByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getClipByPosition(position); } int TimelineModel::getCompositionByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionByPosition(position); } int TimelineModel::getTrackPosition(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_allTracks.begin(); int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId)); return pos; } int TimelineModel::getTrackMltIndex(int trackId) const { READ_LOCK(); // Because of the black track that we insert in first position, the mlt index is the position + 1 return getTrackPosition(trackId) + 1; } int TimelineModel::getTrackSortValue(int trackId, bool separated) const { if (separated) { return getTrackPosition(trackId) + 1; } auto it = m_allTracks.end(); int aCount = 0; int vCount = 0; bool isAudio = false; int trackPos = 0; while (it != m_allTracks.begin()) { --it; bool audioTrack = (*it)->isAudioTrack(); if (audioTrack) { aCount++; } else { vCount++; } if (trackId == (*it)->getId()) { isAudio = audioTrack; trackPos = audioTrack ? aCount : vCount; } } int trackDiff = aCount - vCount; if (trackDiff > 0) { // more audio tracks if (!isAudio) { trackPos -= trackDiff; } else if (trackPos > vCount) { return -trackPos; } } return isAudio ? ((aCount * trackPos) - 1) : (vCount + 1 - trackPos) * 2; } QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); QList results; auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (type == TrackType::AnyTrack) { results << (*it)->getId(); continue; } bool audioTrack = (*it)->isAudioTrack(); if (type == TrackType::AudioTrack && audioTrack) { results << (*it)->getId(); } else if (type == TrackType::VideoTrack && !audioTrack) { results << (*it)->getId(); } } return results; } int TimelineModel::getPreviousVideoTrackIndex(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) { break; } } return it == m_allTracks.begin() ? 0 : (*it)->getId(); } int TimelineModel::getPreviousVideoTrackPos(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) { break; } } return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId()); } int TimelineModel::getMirrorVideoTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if (!(*it)->isAudioTrack()) { // we expected an audio track... return -1; } int count = 0; if (it != m_allTracks.end()) { ++it; } while (it != m_allTracks.end()) { if ((*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } ++it; } if (it != m_allTracks.end() && !(*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } int TimelineModel::getMirrorTrackId(int trackId) const { if (isAudioTrack(trackId)) { return getMirrorVideoTrackId(trackId); } return getMirrorAudioTrackId(trackId); } int TimelineModel::getMirrorAudioTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if ((*it)->isAudioTrack()) { // we expected a video track... return -1; } int count = 0; if (it != m_allTracks.begin()) { --it; } while (it != m_allTracks.begin()) { if (!(*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } --it; } if ((*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } void TimelineModel::setEditMode(TimelineMode::EditMode mode) { m_editMode = mode; } bool TimelineModel::normalEdit() const { return m_editMode == TimelineMode::NormalEdit; } bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { Q_UNUSED(updateView); Q_UNUSED(invalidateTimeline); Q_UNUSED(undo); Q_UNUSED(redo); Q_ASSERT(isClip(clipId)); m_allClips[clipId]->setFakePosition(position); bool trackChanged = false; if (trackId > -1) { if (trackId != m_allClips[clipId]->getFakeTrackId()) { if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) { m_allClips[clipId]->setFakeTrackId(trackId); trackChanged = true; } } } QModelIndex modelIndex = makeClipIndexFromID(clipId); if (modelIndex.isValid()) { QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } notifyChange(modelIndex, modelIndex, roles); return true; } return false; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo) { // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<clipState() == PlaylistState::Disabled) { if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) { return false; } if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) { return false; } } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) { // Move not allowed (audio / video mismatch) qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState(); return false; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool ok = true; int old_trackId = getClipTrackId(clipId); bool notifyViewOnly = false; // qDebug()<<"MOVING CLIP FROM: "< 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { TRACE_RES(true); return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); TRACE_RES(res); return res; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } TRACE_RES(res); return res; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { QWriteLocker locker(&m_lock); TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { TRACE_RES(true); return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } TRACE_RES(res); return res; } bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = true; if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false); } else { res = requestClipMove(clipId, trackId, position, false, false, false, undo, redo); } if (res) { undo(); } return res; } int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance) { if (isClip(itemId)) { return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance); } return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance); } int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance) { QWriteLocker locker(&m_lock); TRACE(clipId, trackId, position, cursorPosition, snapDistance); Q_ASSERT(isClip(clipId)); Q_ASSERT(isTrack(trackId)); int currentPos = getClipPosition(clipId); int sourceTrackId = getClipTrackId(clipId); if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = sourceTrackId; } if (currentPos == position && sourceTrackId == trackId) { TRACE_RES(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_items = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_items = m_groups->getLeaves(groupId); } for (int current_clipId : all_items) { if (getItemTrackId(current_clipId) != -1) { int in = getItemPosition(current_clipId); int out = in + getItemPlaytime(current_clipId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } int snapped = getBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector(), cursorPosition, snapDistance); // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false) : requestFakeClipMove(clipId, trackId, position, true, false, false); /*} else { possible = requestClipMoveAttempt(clipId, trackId, position); }*/ if (possible) { TRACE_RES(position); return position; } if (sourceTrackId == -1) { // not clear what to do hear, if the current move doesn't work. We could try to find empty space, but it might end up being far away... TRACE_RES(currentPos); return currentPos; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Try same track move if (trackId != sourceTrackId && sourceTrackId != -1) { qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId; trackId = sourceTrackId; possible = requestClipMove(clipId, trackId, position, true, false, false); if (!possible) { qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position; } else { TRACE_RES(position); return position; } } int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { position = currentPos + blank_length; } else { position = currentPos - blank_length; } } else { TRACE_RES(currentPos); return currentPos; } possible = requestClipMove(clipId, trackId, position, true, false, false); TRACE_RES(possible ? position : currentPos); return possible ? position : currentPos; } // find best pos for groups int groupId = m_groups->getRootId(clipId); std::unordered_set all_items = m_groups->getLeaves(groupId); QMap trackPosition; // First pass, sort clips by track and keep only the first / last depending on move direction for (int current_clipId : all_items) { int clipTrack = getItemTrackId(current_clipId); if (clipTrack == -1) { continue; } int in = getItemPosition(current_clipId); if (trackPosition.contains(clipTrack)) { if (after) { // keep only last clip position for track int out = in + getItemPlaytime(current_clipId); if (trackPosition.value(clipTrack) < out) { trackPosition.insert(clipTrack, out); } } else { // keep only first clip position for track if (trackPosition.value(clipTrack) > in) { trackPosition.insert(clipTrack, in); } } } else { trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) : in); } } // Now check space on each track QMapIterator i(trackPosition); int blank_length = -1; while (i.hasNext()) { i.next(); int track_space; if (!after) { // Check space before the position track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1); if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } else { // Check space after the position track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1; 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, true, false, false); if (possible) { TRACE_RES(updatedPos); return updatedPos; } } TRACE_RES(currentPos); return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance) { QWriteLocker locker(&m_lock); TRACE(compoId, trackId, position, cursorPosition, snapDistance); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); int currentPos = getCompositionPosition(compoId); int currentTrack = getCompositionTrackId(compoId); if (getTrackById_const(trackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = currentTrack; } if (currentPos == position && currentTrack == trackId) { TRACE_RES(position); return position; } if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; if (m_groups->isInGroup(compoId)) { int groupId = m_groups->getRootId(compoId); auto all_items = m_groups->getLeaves(groupId); for (int current_compoId : all_items) { // TODO: fix for composition int in = getItemPosition(current_compoId); int out = in + getItemPlaytime(current_compoId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } else { int in = currentPos; int out = in + getCompositionPlaytime(compoId); qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out; ignored_pts.push_back(in); ignored_pts.push_back(out); } int snapped = getBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance); qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = requestCompositionMove(compoId, trackId, position, true, false); qDebug() << "Original move success" << possible; if (possible) { TRACE_RES(position); 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;*/ TRACE_RES(currentPos); return currentPos; } bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo) { qDebug() << "requestClipCreation " << binClipId; QString bid = binClipId; if (binClipId.contains(QLatin1Char('/'))) { bid = binClipId.section(QLatin1Char('/'), 0, 0); } if (!pCore->projectItemModel()->hasClip(bid)) { qDebug() << " / / / /MASTER CLIP NOT FOUND"; return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); if (!master->isReady() || !master->isCompatible(state)) { qDebug() << "// CLIP NOT READY OR NOT COMPATIBLE: " << state; return false; } int clipId = TimelineModel::getNextId(); id = clipId; Fun local_undo = deregisterClip_lambda(clipId); ClipModel::construct(shared_from_this(), bid, clipId, state, speed); auto clip = m_allClips[clipId]; Fun local_redo = [clip, this, state]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); clip->refreshProducerFromBin(state); return true; }; if (binClipId.contains(QLatin1Char('/'))) { int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt(); int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt(); int initLength = m_allClips[clipId]->getPlaytime(); bool res = true; if (in != 0) { res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo); } res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets) { QWriteLocker locker(&m_lock); TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Clip")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, Fun &redo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; qDebug() << "requestClipInsertion " << binClipId << " " << " " << trackId << " " << position; bool res = false; ClipType::ProducerType type = ClipType::Unknown; QString bid = binClipId.section(QLatin1Char('/'), 0, 0); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } if (!pCore->projectItemModel()->hasClip(bid)) { return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); type = master->clipType(); if (useTargets && m_audioTarget == -1 && m_videoTarget == -1) { useTargets = false; } if (dropType == PlaylistState::Disabled && (type == ClipType::AV || type == ClipType::Playlist)) { if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) { // If audio target is set but no video target, only insert audio trackId = m_audioTarget; if (trackId > -1 && getTrackById_const(trackId)->isLocked()) { trackId = -1; } } else if (useTargets && getTrackById_const(trackId)->isLocked()) { // Video target set but locked trackId = m_audioTarget; if (trackId > -1 && getTrackById_const(trackId)->isLocked()) { trackId = -1; } } if (trackId == -1) { pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage); return false; } bool audioDrop = getTrackById_const(trackId)->isAudioTrack(); res = requestClipCreation(binClipId, id, getTrackById_const(trackId)->trackType(), 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, logUndo, local_undo, local_redo); int target_track; if (audioDrop) { target_track = m_videoTarget == -1 ? -1 : getTrackById_const(m_videoTarget)->isLocked() ? -1 : m_videoTarget; } else { target_track = m_audioTarget == -1 ? -1 : getTrackById_const(m_audioTarget)->isLocked() ? -1 : m_audioTarget; } qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo(); int mirror = getMirrorTrackId(trackId); if (mirror > -1 && getTrackById_const(mirror)->isLocked()) { mirror = -1; } bool canMirrorDrop = !useTargets && mirror > -1; if (res && (canMirrorDrop || target_track > -1) && master->hasAudioAndVideo()) { if (!useTargets) { target_track = mirror; } // QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack); QList possibleTracks; qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets; if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) { possibleTracks << target_track; } if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage); res = false; } else { std::function audio_undo = []() { return true; }; std::function audio_redo = []() { return true; }; int newId; res = requestClipCreation(binClipId, newId, audioDrop ? PlaylistState::VideoOnly : PlaylistState::AudioOnly, 1.0, audio_undo, audio_redo); if (res) { bool move = false; while (!move && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); move = requestClipMove(newId, newTrack, position, true, true, true, audio_undo, audio_redo); } // use lazy evaluation to group only if move was successful res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit); if (!res || !move) { pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } else { UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo); } } else { pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } } } } else { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid); if (dropType == PlaylistState::Disabled) { dropType = getTrackById_const(trackId)->trackType(); } else if (dropType != getTrackById_const(trackId)->trackType()) { qDebug() << "// INCORRECT DRAG, ABORTING"; return false; } QString normalisedBinId = binClipId; if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) { normalisedBinId.remove(0, 1); } res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, 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 itemId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (m_groups->isInGroup(itemId)) { return requestGroupDeletion(itemId, undo, redo); } if (isClip(itemId)) { return requestClipDeletion(itemId, undo, redo); } if (isComposition(itemId)) { return requestCompositionDeletion(itemId, undo, redo); } Q_ASSERT(false); return false; } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); Q_ASSERT(isItem(itemId)); QString actionLabel; if (m_groups->isInGroup(itemId)) { actionLabel = i18n("Remove group"); } else { if (isClip(itemId)) { actionLabel = i18n("Delete Clip"); } else { actionLabel = i18n("Delete Composition"); } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestItemDeletion(itemId, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, actionLabel); } TRACE_RES(res); requestClearSelection(true); return res; } bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo) { int trackId = getClipTrackId(clipId); if (trackId != -1) { bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo); if (!res) { undo(); return false; } } auto operation = deregisterClip_lambda(clipId); auto clip = m_allClips[clipId]; Fun reverse = [this, clip]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo) { int trackId = getCompositionTrackId(compositionId); if (trackId != -1) { bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo); if (!res) { undo(); return false; } else { unplantComposition(compositionId); } } Fun operation = deregisterComposition_lambda(compositionId); auto composition = m_allCompositions[compositionId]; Fun reverse = [this, composition]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } std::unordered_set TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions) { Q_UNUSED(listCompositions) std::unordered_set allClips; if (trackId == -1) { for (const auto &track : m_allTracks) { if (track->isLocked()) { continue; } std::unordered_set clipTracks = getItemsInRange(track->getId(), start, end, listCompositions); allClips.insert(clipTracks.begin(), clipTracks.end()); } } else { std::unordered_set clipTracks = getTrackById(trackId)->getClipsInRange(start, end); allClips.insert(clipTracks.begin(), clipTracks.end()); if (listCompositions) { std::unordered_set compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end); allClips.insert(compoTracks.begin(), compoTracks.end()); } } return allClips; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } TRACE_RES(res); return res; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { Q_UNUSED(updateView); Q_UNUSED(finalMove); Q_UNUSED(undo); Q_UNUSED(redo); Q_UNUSED(allowViewRefresh); QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = []() { return true; }; // Check if there is a track move // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : all_items) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { if (isClip(item)) { old_position[item] = m_allClips[item]->getPosition(); } else { old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } } } // Second step, calculate delta int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } bool trackChanged = false; // Reverse sort. We need to insert from left to right to avoid confusing the view for (int item : all_items) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { qDebug() << "/// SETTING FAKE CLIP: " << target_track << ", POSITION: " << target_position; m_allClips[item]->setFakePosition(target_position); if (m_allClips[item]->getFakeTrackId() != target_track) { trackChanged = true; } m_allClips[item]->setFakeTrackId(target_track); } else { } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } QModelIndex modelIndex; QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } for (int item : all_items) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; } bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } TRACE_RES(res); return res; } bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); Q_ASSERT(isItem(itemId)); if (getGroupElements(groupId).count(itemId) == 0) { // this group doesn't contain the clip, abort return false; } bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; std::unordered_set all_clips; std::unordered_set all_compositions; // Separate clips from compositions to sort for (int affectedItemId : all_items) { if (isClip(affectedItemId)) { all_clips.insert(affectedItemId); } else { all_compositions.insert(affectedItemId); } } // Sort clips first std::vector sorted_clips(all_clips.begin(), all_clips.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_track, delta_pos](int clipId1, int clipId2) { int p1 = m_allClips[clipId1]->getPosition(); int p2 = m_allClips[clipId2]->getPosition(); return delta_pos > 0 ? p2 <= p1 : p1 <= p2; }); - + // Sort compositions. We need to delete in the move direction from top to bottom std::vector sorted_compositions(all_compositions.begin(), all_compositions.end()); std::sort(sorted_compositions.begin(), sorted_compositions.end(), [this, delta_track, delta_pos](int clipId1, int clipId2) { int p1 = delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition(); int p2 = delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition(); return delta_track == 0 ? (delta_pos > 0 ? p2 <= p1 : p1 <= p2) : p1 <= p2; }); sorted_clips.insert(sorted_clips.end(), sorted_compositions.begin(), sorted_compositions.end()); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = []() { return true; }; // Check if there is a track move bool updatePositionOnly = false; if (delta_track == 0 && updateView) { updateView = false; allowViewRefresh = false; updatePositionOnly = true; update_model = [sorted_clips, this]() { QModelIndex modelIndex; QVector roles{StartRole}; for (int item : sorted_clips) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; }; } // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : sorted_clips) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = allowViewRefresh; if (isClip(item)) { ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo); old_position[item] = m_allClips[item]->getPosition(); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, finalMove, local_undo, local_redo); old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } } // Second step, reinsert clips at correct positions int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[itemId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } // We need to insert depending on the move direction to avoid confusing the view // std::reverse(std::begin(sorted_clips), std::end(sorted_clips)); for (int item : sorted_clips) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; bool updateThisView = allowViewRefresh; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { ok = ok && requestClipMove(item, target_track, target_position, updateThisView, finalMove, finalMove, local_undo, local_redo); } else { ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, finalMove, local_undo, local_redo); } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } if (updatePositionOnly) { update_model(); PUSH_LAMBDA(update_model, local_redo); PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(clipId, logUndo); if (!m_groups->isInGroup(clipId)) { TRACE_RES(false); return false; } bool res = requestItemDeletion(clipId, logUndo); TRACE_RES(res); return res; } bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo) { // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves. std::queue group_queue; group_queue.push(m_groups->getRootId(clipId)); std::unordered_set all_items; std::unordered_set all_compositions; while (!group_queue.empty()) { int current_group = group_queue.front(); bool isSelection = m_currentSelection == current_group; if (isSelection) { m_currentSelection = -1; } group_queue.pop(); Q_ASSERT(isGroup(current_group)); auto children = m_groups->getDirectChildren(current_group); int one_child = -1; // we need the id on any of the indices of the elements of the group for (int c : children) { if (isClip(c)) { all_items.insert(c); one_child = c; } else if (isComposition(c)) { all_compositions.insert(c); one_child = c; } else { Q_ASSERT(isGroup(c)); one_child = c; group_queue.push(c); } } if (one_child != -1) { if (m_groups->getType(current_group) == GroupType::Selection) { Q_ASSERT(isSelection); // in the case of a selection group, we delete the group but don't log it in the undo object Fun tmp_undo = []() { return true; }; Fun tmp_redo = []() { return true; }; m_groups->ungroupItem(one_child, tmp_undo, tmp_redo); } else { bool res = m_groups->ungroupItem(one_child, undo, redo); if (!res) { undo(); return false; } } } } for (int clip : all_items) { bool res = requestClipDeletion(clip, undo, redo); if (!res) { undo(); return false; } } for (int compo : all_compositions) { bool res = requestCompositionDeletion(compo, undo, redo); if (!res) { undo(); return false; } } return true; } int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) { if (logUndo) { qDebug() << "---------------------\n---------------------\nRESIZE W/UNDO CALLED\n++++++++++++++++\n++++"; } QWriteLocker locker(&m_lock); TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize); Q_ASSERT(isItem(itemId)); if (size <= 0) { TRACE_RES(-1); return -1; } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); if (snapDistance > 0 && getItemTrackId(itemId) != -1) { Fun temp_undo = []() { return true; }; Fun temp_redo = []() { return true; }; if (right && size > out - in && isClip(itemId)) { int targetPos = in + size - 1; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos)) { size = getTrackById_const(trackId)->getBlankEnd(out + 1) - in; } } else if (!right && size > (out - in) && isClip(itemId)) { int targetPos = out - size; int trackId = getItemTrackId(itemId); if (!getTrackById_const(trackId)->isBlankAt(targetPos)) { size = out - getTrackById_const(trackId)->getBlankStart(in - 1); } } int timelinePos = pCore->getTimelinePosition(); m_snaps->addPoint(timelinePos); int proposed_size = m_snaps->proposeSize(in, out, size, right, snapDistance); m_snaps->removePoint(timelinePos); if (proposed_size > 0) { // only test move if proposed_size is valid bool success = false; if (isClip(itemId)) { success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } else { success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } if (success) { temp_undo(); // undo temp move size = proposed_size; } } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::unordered_set all_items; if (!allowSingleResize && m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); std::unordered_set items; if (m_groups->getType(groupId) == GroupType::AVSplit) { // Only resize group elements if it is an avsplit items = m_groups->getLeaves(groupId); } else { all_items.insert(itemId); } 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) { int tid = getItemTrackId(id); if (tid > -1 && getTrackById_const(tid)->isLocked()) { continue; } result = result && requestItemResize(id, size, right, logUndo, undo, redo); } if (!result) { bool undone = undo(); Q_ASSERT(undone); TRACE_RES(-1); return -1; } if (result && logUndo) { if (isClip(itemId)) { PUSH_UNDO(undo, redo, i18n("Resize clip")); } else { PUSH_UNDO(undo, redo, i18n("Resize composition")); } } int res = result ? size : -1; TRACE_RES(res); return res; } bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; Fun update_model = [itemId, right, logUndo, this]() { Q_ASSERT(isItem(itemId)); if (getItemTrackId(itemId) != -1) { qDebug() << "++++++++++\nRESIZING ITEM: " << itemId << "\n+++++++"; QModelIndex modelIndex = isClip(itemId) ? makeClipIndexFromID(itemId) : makeCompositionIndexFromID(itemId); notifyChange(modelIndex, modelIndex, !right, true, logUndo); } return true; }; bool result = false; if (isClip(itemId)) { result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } else { Q_ASSERT(isComposition(itemId)); result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } if (result) { if (!blockUndo) { PUSH_LAMBDA(update_model, local_undo); } PUSH_LAMBDA(update_model, local_redo); update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, bool logUndo, GroupType type) { QWriteLocker locker(&m_lock); TRACE(ids, logUndo, type); if (type == GroupType::Selection || type == GroupType::Leaf) { // Selections shouldn't be done here. Call requestSetSelection instead TRACE_RES(-1); return -1; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; int result = requestClipsGroup(ids, undo, redo, type); if (result > -1 && logUndo) { PUSH_UNDO(undo, redo, i18n("Group clips")); } TRACE_RES(result); return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type) { QWriteLocker locker(&m_lock); if (type != GroupType::Selection) { requestClearSelection(); } 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; } } if (type == GroupType::Selection && ids.size() == 1) { // only one element selected, no group created return -1; } int groupId = m_groups->groupItems(ids, undo, redo, type); if (type != GroupType::Selection) { // we make sure that the undo and the redo are going to unselect before doing anything else Fun unselect = [this]() { return requestClearSelection(); }; PUSH_FRONT_LAMBDA(unselect, undo); PUSH_FRONT_LAMBDA(unselect, redo); } return groupId; } bool TimelineModel::requestClipsUngroup(const std::unordered_set &itemIds, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemIds, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; requestClearSelection(); std::unordered_set roots; std::transform(itemIds.begin(), itemIds.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); for (int root : roots) { result = result && requestClipUngroup(root, undo, redo); } if (!result) { bool undone = undo(); Q_ASSERT(undone); } if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); requestClearSelection(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; result = requestClipUngroup(itemId, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int itemId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); bool isSelection = m_groups->getType(m_groups->getRootId(itemId)) == GroupType::Selection; if (!isSelection) { requestClearSelection(); } bool res = m_groups->ungroupItem(itemId, undo, redo); if (res && !isSelection) { // we make sure that the undo and the redo are going to unselect before doing anything else Fun unselect = [this]() { return requestClearSelection(); }; PUSH_FRONT_LAMBDA(unselect, undo); PUSH_FRONT_LAMBDA(unselect, redo); } return res; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack) { QWriteLocker locker(&m_lock); TRACE(position, id, trackName, audioTrack); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo, true); if (result) { PUSH_UNDO(undo, redo, i18n("Insert Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView) { // TODO: make sure we disable overlayTrack before inserting a track if (position == -1) { position = (int)(m_allTracks.size()); } if (position < 0 || position > (int)m_allTracks.size()) { return false; } int trackId = TimelineModel::getNextId(); id = trackId; Fun local_undo = deregisterTrack_lambda(trackId, true); TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack); if (updateView) { _resetView(); } 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, true); if (updateView) { _resetView(); } 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 QWriteLocker locker(&m_lock); TRACE(trackId); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackDeletion(trackId, undo, redo); if (result) { if (m_videoTarget == trackId) { m_videoTarget = -1; } if (m_audioTarget == trackId) { m_audioTarget = -1; } PUSH_UNDO(undo, redo, i18n("Delete Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo) { Q_ASSERT(isTrack(trackId)); std::vector clips_to_delete; for (const auto &it : getTrackById(trackId)->m_allClips) { clips_to_delete.push_back(it.first); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (int clip : clips_to_delete) { bool res = true; while (res && m_groups->isInGroup(clip)) { res = requestClipUngroup(clip, local_undo, local_redo); } if (res) { res = requestClipDeletion(clip, local_undo, local_redo); } if (!res) { bool u = local_undo(); Q_ASSERT(u); return false; } } int old_position = getTrackPosition(trackId); auto operation = deregisterTrack_lambda(trackId, true); std::shared_ptr track = getTrackById(trackId); Fun reverse = [this, track, old_position]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, old_position); _resetView(); 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) { // 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; } void TimelineModel::registerClip(const std::shared_ptr &clip, bool registerProducer) { int id = clip->getId(); qDebug() << " // /REQUEST TL CLP REGSTR: " << id << "\n--------\nCLIPS COUNT: " << m_allClips.size(); Q_ASSERT(m_allClips.count(id) == 0); m_allClips[id] = clip; clip->registerClipToBin(clip->getProducer(), registerProducer); m_groups->createGroupItem(id); clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled); } void TimelineModel::registerGroup(int groupId) { Q_ASSERT(m_allGroups.count(groupId) == 0); m_allGroups.insert(groupId); } Fun TimelineModel::deregisterTrack_lambda(int id, bool updateView) { return [this, id, updateView]() { // qDebug() << "DEREGISTER TRACK" << id; auto it = m_iteratorTable[id]; // iterator to the element int index = getTrackPosition(id); // compute index in list m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track // send update to the model m_allTracks.erase(it); // actual deletion of object m_iteratorTable.erase(id); // clean table if (updateView) { _resetView(); } return true; }; } Fun TimelineModel::deregisterClip_lambda(int clipId) { return [this, clipId]() { // qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size(); requestClearSelection(true); clearAssetView(clipId); Q_ASSERT(m_allClips.count(clipId) > 0); Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point auto clip = m_allClips[clipId]; m_allClips.erase(clipId); clip->deregisterClipToBin(); m_groups->destructGroupItem(clipId); return true; }; } void TimelineModel::deregisterGroup(int id) { Q_ASSERT(m_allGroups.count(id) > 0); m_allGroups.erase(id); } std::shared_ptr TimelineModel::getTrackById(int trackId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable[trackId]; } const std::shared_ptr TimelineModel::getTrackById_const(int trackId) const { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable.at(trackId); } bool TimelineModel::addTrackEffect(int trackId, const QString &effectId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500); return false; } return true; } bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_iteratorTable.count(trackId) > 0 && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) { pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500); return false; } return true; } std::shared_ptr TimelineModel::getClipPtr(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId); } bool TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify) { Q_ASSERT(m_allClips.count(clipId) > 0); bool result = m_allClips.at(clipId)->addEffect(effectId); if (!result && notify) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } return result; } bool TimelineModel::removeFade(int clipId, bool fromStart) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->removeFade(fromStart); } std::shared_ptr TimelineModel::getClipEffectStack(int itemId) { Q_ASSERT(m_allClips.count(itemId)); return m_allClips.at(itemId)->m_effectStack; } bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_allClips.count(clipId) && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); return m_allClips.at(clipId)->copyEffect(effectStack, itemRow); } bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration) { Q_ASSERT(m_allClips.count(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo); if (res && initialDuration > 0) { PUSH_UNDO(undo, redo, i18n("Adjust Fade")); } return res; } std::shared_ptr TimelineModel::getCompositionPtr(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); return m_allCompositions.at(compoId); } int TimelineModel::getNextId() { return TimelineModel::next_id++; } bool TimelineModel::isClip(int id) const { return m_allClips.count(id) > 0; } bool TimelineModel::isComposition(int id) const { return m_allCompositions.count(id) > 0; } bool TimelineModel::isItem(int id) const { return isClip(id) || isComposition(id); } 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() { + if (m_closing) { + return; + } int current = m_blackClip->get_playtime() - TimelineModel::seekDuration; int duration = 0; for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); duration = qMax(duration, track->trackDuration()); } if (duration != current) { // update black track length m_blackClip->set_in_and_out(0, duration + TimelineModel::seekDuration); emit durationUpdated(); } } int TimelineModel::duration() const { return m_tractor->get_playtime() - TimelineModel::seekDuration; } std::unordered_set TimelineModel::getGroupElements(int clipId) { int groupId = m_groups->getRootId(clipId); return m_groups->getLeaves(groupId); } Mlt::Profile *TimelineModel::getProfile() { return m_profile; } bool TimelineModel::requestReset(Fun &undo, Fun &redo) { std::vector all_ids; for (const auto &track : m_iteratorTable) { all_ids.push_back(track.first); } bool ok = true; for (int trackId : all_ids) { ok = ok && requestTrackDeletion(trackId, undo, redo); } return ok; } void TimelineModel::setUndoStack(std::weak_ptr undo_stack) { m_undoStack = std::move(undo_stack); } int TimelineModel::suggestSnapPoint(int pos, int snapDistance) { int snapped = m_snaps->getClosestPoint(pos); return (qAbs(snapped - pos) < snapDistance ? snapped : pos); } int TimelineModel::getBestSnapPos(int pos, int length, const std::vector &pts, int cursorPosition, int snapDistance) { if (!pts.empty()) { m_snaps->ignore(pts); } m_snaps->addPoint(cursorPosition); int snapped_start = m_snaps->getClosestPoint(pos); int snapped_end = m_snaps->getClosestPoint(pos + length); m_snaps->unIgnore(); m_snaps->removePoint(cursorPosition); int startDiff = qAbs(pos - snapped_start); int endDiff = qAbs(pos + length - snapped_end); if (startDiff < endDiff && startDiff <= snapDistance) { // snap to start return snapped_start; } if (endDiff <= snapDistance) { // snap to end return snapped_end - length; } return -1; } int TimelineModel::getNextSnapPos(int pos) { return m_snaps->getNextPoint(pos); } int TimelineModel::getPreviousSnapPos(int pos) { return m_snaps->getPreviousPoint(pos); } void TimelineModel::addSnap(int pos) { TRACE(pos); return m_snaps->addPoint(pos); } void TimelineModel::removeSnap(int pos) { TRACE(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, std::unique_ptr transProps, int &id, bool logUndo) { QWriteLocker locker(&m_lock); // TRACE(transitionId, trackId, position, length, transProps.get(), id, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, std::move(transProps), id, undo, redo, logUndo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Composition")); } // TRACE_RES(result); return result; } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, std::unique_ptr transProps, int &id, Fun &undo, Fun &redo, bool finalMove) { qDebug() << "Inserting compo track" << trackId << "pos" << position << "length" << length; int compositionId = TimelineModel::getNextId(); id = compositionId; Fun local_undo = deregisterComposition_lambda(compositionId); CompositionModel::construct(shared_from_this(), transitionId, compositionId, std::move(transProps)); auto composition = m_allCompositions[compositionId]; Fun local_redo = [composition, this]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo); qDebug() << "trying to move" << trackId << "pos" << position << "success " << res; if (res) { res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); qDebug() << "trying to resize" << compositionId << "length" << length << "success " << res; } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } Fun TimelineModel::deregisterComposition_lambda(int compoId) { return [this, compoId]() { Q_ASSERT(m_allCompositions.count(compoId) > 0); Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point requestClearSelection(true); clearAssetView(compoId); m_allCompositions.erase(compoId); m_groups->destructGroupItem(compoId); return true; }; } int TimelineModel::getCompositionPosition(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getPosition(); } int TimelineModel::getCompositionPlaytime(int compoId) const { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); int playtime = trans->getPlaytime(); return playtime; } int TimelineModel::getItemPosition(int itemId) const { if (isClip(itemId)) { return getClipPosition(itemId); } return getCompositionPosition(itemId); } int TimelineModel::getItemPlaytime(int itemId) const { if (isClip(itemId)) { return getClipPlaytime(itemId); } return getCompositionPlaytime(itemId); } int TimelineModel::getTrackCompositionsCount(int trackId) const { Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionsCount(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) { return true; } if (m_groups->isInGroup(compoId)) { // element is in a group. int groupId = m_groups->getRootId(compoId); int current_trackId = getCompositionTrackId(compoId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allCompositions[compoId]->getPosition(); return requestGroupMove(compoId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; int min = getCompositionPosition(compoId); int max = min + getCompositionPlaytime(compoId); int tk = getCompositionTrackId(compoId); bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo); if (tk > -1) { min = qMin(min, getCompositionPosition(compoId)); max = qMax(max, getCompositionPosition(compoId)); } else { min = getCompositionPosition(compoId); max = min + getCompositionPlaytime(compoId); } if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move composition")); checkRefresh(min, max); } return res; } bool TimelineModel::isAudioTrack(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); return (*it)->isAudioTrack(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) { // qDebug() << "// compo track: " << trackId << ", PREVIOUS TK: " << getPreviousVideoTrackPos(trackId); compositionTrack = getPreviousVideoTrackPos(trackId); } if (compositionTrack == -1) { // it doesn't make sense to insert a composition on the last track qDebug() << "Move failed because of last track"; return false; } qDebug() << "Requesting composition move" << trackId << "," << position << " ( " << compositionTrack << " / " << (compositionTrack > 0 ? getTrackIndexFromPosition(compositionTrack - 1) : 0); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool ok = true; int old_trackId = getCompositionTrackId(compoId); bool notifyViewOnly = false; Fun update_model = []() { return true; }; if (updateView && old_trackId == trackId) { // Move on same track, only send view update updateView = false; notifyViewOnly = true; update_model = [compoId, this]() { QModelIndex modelIndex = makeCompositionIndexFromID(compoId); notifyChange(modelIndex, modelIndex, StartRole); return true; }; } if (old_trackId != -1) { Fun delete_operation = []() { return true; }; Fun delete_reverse = []() { return true; }; if (old_trackId != trackId) { delete_operation = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; int oldAtrack = m_allCompositions[compoId]->getATrack(); delete_reverse = [this, compoId, oldAtrack, updateView]() { m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack <= 0 ? -1 : getTrackIndexFromPosition(oldAtrack - 1)); return replantCompositions(compoId, updateView); }; } ok = delete_operation(); if (!ok) qDebug() << "Move failed because of first delete operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo); } if (!ok) { qDebug() << "Move failed because of first deletion request"; bool undone = local_undo(); Q_ASSERT(undone); return false; } } ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo); if (!ok) qDebug() << "Move failed because of second insertion request"; if (ok) { Fun insert_operation = []() { return true; }; Fun insert_reverse = []() { return true; }; if (old_trackId != trackId) { insert_operation = [this, compoId, compositionTrack, updateView]() { qDebug() << "-------------- ATRACK ----------------\n" << compositionTrack << " = " << getTrackIndexFromPosition(compositionTrack); m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack <= 0 ? -1 : getTrackIndexFromPosition(compositionTrack - 1)); return replantCompositions(compoId, updateView); }; insert_reverse = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; } ok = insert_operation(); if (!ok) qDebug() << "Move failed because of second insert operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_redo); } UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::replantCompositions(int currentCompo, bool updateView) { // We ensure that the compositions are planted in a decreasing order of b_track. // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order. std::vector> compos; for (const auto &compo : m_allCompositions) { int trackId = compo.second->getCurrentTrackId(); if (trackId == -1 || compo.second->getATrack() == -1) { continue; } // Note: we need to retrieve the position of the track, that is its melt index. int trackPos = getTrackMltIndex(trackId); compos.emplace_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 && aTrack < m_tractor->count()); int ret = field->plant_transition(*m_allCompositions[compo.second].get(), aTrack, compo.first); qDebug() << "Planting composition " << compo.second << "in " << aTrack << "/" << compo.first << "IN = " << m_allCompositions[compo.second]->getIn() << "OUT = " << m_allCompositions[compo.second]->getOut() << "ret=" << ret; Mlt::Transition &transition = *m_allCompositions[compo.second].get(); transition.set_tracks(aTrack, compo.first); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); if (ret != 0) { field->unlock(); return false; } } // Replant last tracks compositing while (!trackCompositions.isEmpty()) { Mlt::Transition *firstTr = trackCompositions.takeFirst(); field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track()); } field->unlock(); if (updateView) { QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo); notifyChange(modelIndex, modelIndex, ItemATrack); } return true; } bool TimelineModel::unplantComposition(int compoId) { qDebug() << "Unplanting" << compoId; Mlt::Transition &transition = *m_allCompositions[compoId].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); QScopedPointer field(m_tractor->field()); field->lock(); field->disconnect_service(transition); int ret = transition.disconnect_all_producers(); mlt_service nextservice = mlt_service_get_producer(transition.get_service()); // mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(nextservice == nullptr); // Q_ASSERT(consumer == nullptr); field->unlock(); return ret != 0; } bool TimelineModel::checkConsistency() { for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); // Check parent/children link for tracks if (auto ptr = track->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for track" << tck.first; return false; } } else { qDebug() << "NULL parent for track" << tck.first; return false; } // check consistency of track if (!track->checkConsistency()) { qDebug() << "Consistency check failed for track" << tck.first; return false; } } // We store all in/outs of clips to check snap points std::map snaps; // Check parent/children link for clips for (const auto &cp : m_allClips) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for clip" << cp.first; return false; } } else { qDebug() << "NULL parent for clip" << cp.first; return false; } if (getClipTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } if (!clip->checkConsistency()) { qDebug() << "Consistency check failed for clip" << cp.first; return false; } } for (const auto &cp : m_allCompositions) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for compo" << cp.first; return false; } } else { qDebug() << "NULL parent for compo" << cp.first; return false; } if (getCompositionTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } } // Check snaps auto stored_snaps = m_snaps->_snaps(); if (snaps.size() != stored_snaps.size()) { qDebug() << "Wrong number of snaps: " << snaps.size() << " == " << stored_snaps.size(); return false; } for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) { if (*i != *j) { qDebug() << "Wrong snap info at point" << (*i).first; return false; } } // We check consistency with bin model auto binClips = pCore->projectItemModel()->getAllClipIds(); // First step: all clips referenced by the bin model exist and are inserted for (const auto &binClip : binClips) { auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); for (const auto &insertedClip : projClip->m_registeredClips) { if (auto ptr = insertedClip.second.lock()) { if (ptr.get() == this) { // check we are talking of this timeline if (!isClip(insertedClip.first)) { qDebug() << "Bin model registers a bad clip ID" << insertedClip.first; return false; } } } else { qDebug() << "Bin model registers a clip in a NULL timeline" << insertedClip.first; return false; } } } // Second step: all clips are referenced for (const auto &clip : m_allClips) { auto binId = clip.second->m_binClipId; auto projClip = pCore->projectItemModel()->getClipByBinID(binId); if (projClip->m_registeredClips.count(clip.first) == 0) { qDebug() << "Clip " << clip.first << "not registered in bin"; return false; } } // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our // m_allCompositions std::unordered_set remaining_compo; for (const auto &compo : m_allCompositions) { if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) { remaining_compo.insert(compo.first); // check validity of the consumer Mlt::Transition &transition = *m_allCompositions[compo.first].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); } } QScopedPointer field(m_tractor->field()); field->lock(); mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_service_type mlt_type = mlt_service_identify(nextservice); while (nextservice != nullptr) { if (mlt_type == transition_type) { auto tr = (mlt_transition)nextservice; int currentTrack = mlt_transition_get_b_track(tr); int currentATrack = mlt_transition_get_a_track(tr); int currentIn = (int)mlt_transition_get_in(tr); int currentOut = (int)mlt_transition_get_out(tr); qDebug() << "looking composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; int foundId = -1; // we iterate to try to find a matching compo for (int compoId : remaining_compo) { if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack && m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) { foundId = compoId; break; } } if (foundId == -1) { qDebug() << "Error, we didn't find matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; field->unlock(); return false; } qDebug() << "Found"; remaining_compo.erase(foundId); } nextservice = mlt_service_producer(nextservice); if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); } field->unlock(); if (!remaining_compo.empty()) { qDebug() << "Error: We found less compositions than expected. Compositions that have not been found:"; for (int compoId : remaining_compo) { qDebug() << compoId; } return false; } // We check consistency of groups if (!m_groups->checkConsistency(true, true)) { qDebug() << "== ERROR IN GROUP CONSISTENCY"; return false; } // Check that the selection is in a valid state: if (m_currentSelection != -1 && !isClip(m_currentSelection) && !isComposition(m_currentSelection) && !isGroup(m_currentSelection)) { qDebug() << "Selection is in inconsistent state"; return false; } return true; } void TimelineModel::setTimelineEffectsEnabled(bool enabled) { m_timelineEffectsEnabled = enabled; // propagate info to clips for (const auto &clip : m_allClips) { clip.second->setTimelineEffectsEnabled(enabled); } // TODO if we support track effects, they should be disabled here too } std::shared_ptr TimelineModel::producer() { return std::make_shared(tractor()); } void TimelineModel::checkRefresh(int start, int end) { if (m_blockRefresh) { return; } int currentPos = tractor()->position(); if (currentPos >= start && currentPos < end) { emit requestMonitorRefresh(); } } void TimelineModel::clearAssetView(int itemId) { emit requestClearAssetView(itemId); } std::shared_ptr TimelineModel::getCompositionParameterModel(int compoId) const { READ_LOCK(); Q_ASSERT(isComposition(compoId)); return std::static_pointer_cast(m_allCompositions.at(compoId)); } std::shared_ptr TimelineModel::getClipEffectStackModel(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); return std::static_pointer_cast(m_allClips.at(clipId)->m_effectStack); } std::shared_ptr TimelineModel::getTrackEffectStackModel(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById(trackId)->m_effectStack; } QStringList TimelineModel::extractCompositionLumas() const { QStringList urls; for (const auto &compo : m_allCompositions) { QString luma = compo.second->getProperty(QStringLiteral("resource")); if (!luma.isEmpty()) { urls << QUrl::fromLocalFile(luma).toLocalFile(); } } urls.removeDuplicates(); return urls; } void TimelineModel::adjustAssetRange(int clipId, int in, int out) { Q_UNUSED(clipId) Q_UNUSED(in) Q_UNUSED(out) // pCore->adjustAssetRange(clipId, in, out); } void TimelineModel::requestClipReload(int clipId) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; // in order to make the producer change effective, we need to unplant / replant the clip in int track int old_trackId = getClipTrackId(clipId); int oldPos = getClipPosition(clipId); int oldOut = getClipIn(clipId) + getClipPlaytime(clipId); // Check if clip out is longer than actual producer duration (if user forced duration) std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId)); bool refreshView = oldOut > (int)binClip->frameDuration(); if (old_trackId != -1) { getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo); } if (old_trackId != -1) { m_allClips[clipId]->refreshProducerFromBin(); getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo); } } void TimelineModel::replugClip(int clipId) { int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { getTrackById(old_trackId)->replugClip(clipId); } } void TimelineModel::requestClipUpdate(int clipId, const QVector &roles) { QModelIndex modelIndex = makeClipIndexFromID(clipId); if (roles.contains(TimelineModel::ReloadThumbRole)) { m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload; } notifyChange(modelIndex, modelIndex, roles); } bool TimelineModel::requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed())) { return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; int oldPos = getClipPosition(clipId); // in order to make the producer change effective, we need to unplant / replant the clip in int track bool success = true; int trackId = getClipTrackId(clipId); if (trackId != -1) { success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo); } if (success) { success = m_allClips[clipId]->useTimewarpProducer(speed, local_undo, local_redo); } if (trackId != -1) { success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); } if (!success) { local_undo(); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return success; } bool TimelineModel::requestClipTimeWarp(int clipId, double speed) { QWriteLocker locker(&m_lock); TRACE(clipId, speed); Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Get main clip info int trackId = getClipTrackId(clipId); bool result = true; if (trackId != -1) { // Check if clip has a split partner int splitId = m_groups->getSplitPartner(clipId); if (splitId > -1) { result = requestClipTimeWarp(splitId, speed / 100.0, undo, redo); } if (result) { result = requestClipTimeWarp(clipId, speed / 100.0, undo, redo); } else { pCore->displayMessage(i18n("Change speed failed"), ErrorMessage); undo(); TRACE_RES(false); return false; } } else { // If clip is not inserted on a track, we just change the producer result = m_allClips[clipId]->useTimewarpProducer(speed, undo, redo); } if (result) { PUSH_UNDO(undo, redo, i18n("Change clip speed")); } TRACE_RES(result); return result; } const QString TimelineModel::getTrackTagById(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); bool isAudio = getTrackById_const(trackId)->isAudioTrack(); int count = 1; int totalAudio = 2; auto it = m_allTracks.begin(); bool found = false; while ((isAudio || !found) && it != m_allTracks.end()) { if ((*it)->isAudioTrack()) { totalAudio++; if (isAudio && !found) { count++; } } else if (!isAudio) { count++; } if ((*it)->getId() == trackId) { found = true; } it++; } return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1); } void TimelineModel::updateProfile(Mlt::Profile *profile) { m_profile = profile; m_tractor->set_profile(*m_profile); } int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = getClipTrackId(clipId); if (trackId != -1) { return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); } return 0; } int TimelineModel::getPreviousTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.begin() ? trackId : (*it)->getId(); } int TimelineModel::getNextTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.end()) { ++it; if (it != m_allTracks.end() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.end() ? trackId : (*it)->getId(); } bool TimelineModel::requestClearSelection(bool onDeletion) { QWriteLocker locker(&m_lock); TRACE(); if (m_currentSelection == -1) { TRACE_RES(true); return true; } if (isGroup(m_currentSelection)) { if (m_groups->getType(m_currentSelection) == GroupType::Selection) { // Reset offset display on clips std::unordered_set items = getCurrentSelection(); for (auto &id : items) { if (isClip(id)) { m_allClips[id]->clearOffset(); } } m_groups->destructGroupItem(m_currentSelection); } } else { Q_ASSERT(onDeletion || isClip(m_currentSelection) || isComposition(m_currentSelection)); } m_currentSelection = -1; emit selectionChanged(); TRACE_RES(true); return true; } void TimelineModel::requestClearSelection(bool onDeletion, Fun &undo, Fun &redo) { Fun operation = [this, onDeletion]() { requestClearSelection(onDeletion); return true; }; Fun reverse = [this, clips = getCurrentSelection()]() { return requestSetSelection(clips); }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } } std::unordered_set TimelineModel::getCurrentSelection() const { READ_LOCK(); if (m_currentSelection == -1) { return {}; } if (isGroup(m_currentSelection)) { return m_groups->getLeaves(m_currentSelection); } else { Q_ASSERT(isClip(m_currentSelection) || isComposition(m_currentSelection)); return {m_currentSelection}; } } void TimelineModel::requestAddToSelection(int itemId, bool clear) { QWriteLocker locker(&m_lock); TRACE(itemId, clear); if (clear) { requestClearSelection(); } std::unordered_set selection = getCurrentSelection(); if (selection.count(itemId) == 0) { selection.insert(itemId); requestSetSelection(selection); } } void TimelineModel::requestRemoveFromSelection(int itemId) { QWriteLocker locker(&m_lock); TRACE(itemId); std::unordered_set all_items = {itemId}; int parentGroup = m_groups->getDirectAncestor(itemId); if (parentGroup > -1 && m_groups->getType(parentGroup) != GroupType::Selection) { all_items = m_groups->getLeaves(parentGroup); } std::unordered_set selection = getCurrentSelection(); for (int current_itemId : all_items) { if (selection.count(current_itemId) > 0) { selection.erase(current_itemId); } } requestSetSelection(selection); } bool TimelineModel::requestSetSelection(const std::unordered_set &ids) { QWriteLocker locker(&m_lock); TRACE(ids); requestClearSelection(); // if the items are in groups, we must retrieve their topmost containing groups std::unordered_set roots; std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); bool result = true; if (roots.size() == 0) { m_currentSelection = -1; } else if (roots.size() == 1) { m_currentSelection = *(roots.begin()); } else { Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (ids.size() == 2) { // Check if we selected 2 clips from the same master QList pairIds; for(auto &id : roots) { if (isClip(id)) { pairIds << id; } } if (pairIds.size() == 2 && getClipBinId(pairIds.at(0)) == getClipBinId(pairIds.at(1))) { // Check if they have same bin id // Both clips have same bin ID, display offset int pos1 = getClipPosition(pairIds.at(0)); int pos2 = getClipPosition(pairIds.at(1)); if (pos2 > pos1) { int offset = pos2 - getClipIn(pairIds.at(1)) - (pos1 - getClipIn(pairIds.at(0))); if (offset != 0) { m_allClips[pairIds.at(1)]->setOffset(offset); m_allClips[pairIds.at(0)]->setOffset(-offset); } } else { int offset = pos1 - getClipIn(pairIds.at(0)) - (pos2 - getClipIn(pairIds.at(1))); if (offset != 0) { m_allClips[pairIds.at(0)]->setOffset(offset); m_allClips[pairIds.at(1)]->setOffset(-offset); } } } } result = (m_currentSelection = m_groups->groupItems(ids, undo, redo, GroupType::Selection)) >= 0; Q_ASSERT(m_currentSelection >= 0); } emit selectionChanged(); return result; } bool TimelineModel::requestSetSelection(const std::unordered_set &ids, Fun &undo, Fun &redo) { Fun reverse = [this]() { requestClearSelection(false); return true; }; Fun operation = [this, ids]() { return requestSetSelection(ids); }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } diff --git a/src/timeline2/model/timelinemodel.hpp b/src/timeline2/model/timelinemodel.hpp index 29f8cd168..f56f2104b 100644 --- a/src/timeline2/model/timelinemodel.hpp +++ b/src/timeline2/model/timelinemodel.hpp @@ -1,793 +1,797 @@ /*************************************************************************** * 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 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 The undo/redo system is designed around lambda functions. Each time a function executes an elementary change to the model, it writes the corresponding operation and its reverse, respectively in the redo and the undo lambdas. This way, if an operation fails for some reason, we can easily cancel the steps that have been done so far without corrupting anything. The other advantage is that operations are easy to compose, and you get a undo/redo pair for free no matter in which way you combine them. Most of the modification functions are named requestObjectAction. Eg, if the object is a clip and we want to move it, we call requestClipMove. These functions always return a bool indicating success, and when they return false they should guarantee than nothing has been modified. Most of the time, these functions come in two versions: the first one is the entry point if you want to perform only the action (and not compose it with other actions). This version will generally automatically push and Undo object on the Application stack, in case the user later wants to cancel the operation. It also generally goes the extra mile to ensure the operation is done in a way that match the user's expectation: for example requestClipMove checks whether the clip belongs to a group and in that case actually mouves the full group. The other version of the function, if it exists, is intended for composition (using the action as part of a complex operation). It takes as input the undo/redo lambda corresponding to the action that is being performed and accumulates on them. Note that this version does the minimal job: in the example of the requestClipMove, it will not move the full group if the clip is in a group. 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 chosen because it is consistent 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 StartRole, /// clip only BinIdRole, /// clip only TrackIdRole, FakeTrackIdRole, FakePositionRole, MarkersRole, /// clip only StatusRole, /// clip only TypeRole, /// clip only KeyframesRole, DurationRole, InPointRole, /// clip only OutPointRole, /// clip only FramerateRole, /// clip only GroupedRole, /// clip only HasAudio, /// clip only CanBeAudioRole, /// clip only CanBeVideoRole, /// clip only IsDisabledRole, /// track only IsAudioRole, SortRole, ShowKeyframesRole, AudioLevelsRole, /// clip only AudioChannelsRole, /// clip only IsCompositeRole, /// track only IsLockedRole, /// track only HeightRole, /// track only TrackTagRole, /// track only FadeInRole, /// clip only FadeOutRole, /// clip only FileHashRole, /// clip only SpeedRole, /// clip only ReloadThumbRole, /// clip only PositionOffsetRole, /// clip only ItemATrack, /// composition only ItemIdRole, ThumbsFormatRole, /// track only EffectNamesRole, // track and clip only EffectsEnabledRole, // track and clip only GrabbedRole, /// clip+composition only TrackActiveRole, /// track only AudioRecordRole /// track only }; ~TimelineModel() override; 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 */ std::shared_ptr 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 track index (id) from its position */ Q_INVOKABLE bool isAudioTrack(int trackId) 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*/ Q_INVOKABLE int getItemTrackId(int itemId) const; Q_INVOKABLE int getCompositionPosition(int compoId) const; int getCompositionPlaytime(int compoId) const; /* Returns an item position, item can be clip or composition */ Q_INVOKABLE int getItemPosition(int itemId) const; /* Returns an item duration, item can be clip or composition */ int getItemPlaytime(int itemId) const; /* Returns the current speed of a clip */ double getClipSpeed(int clipId) const; /* @brief Helper function to query the amount of free space around a clip * @param clipId: the queried clip. If it is not inserted on a track, this functions returns 0 * @param after: if true, we return the blank after the clip, otherwise, before. */ int getBlankSizeNearClip(int clipId, bool after) const; /* @brief if the clip belongs to a AVSplit group, then return the id of the other corresponding clip. Otherwise, returns -1 */ int getClipSplitPartner(int clipId) const; /* @brief Helper function that returns true if the given ID corresponds to a clip */ Q_INVOKABLE bool isClip(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a composition */ Q_INVOKABLE bool isComposition(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a timeline item (composition or clip) */ Q_INVOKABLE bool isItem(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a track */ Q_INVOKABLE bool isTrack(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a group */ Q_INVOKABLE 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, bool notify = true); Q_INVOKABLE bool addTrackEffect(int trackId, const QString &effectId); bool removeFade(int clipId, bool fromStart); Q_INVOKABLE bool copyClipEffect(int clipId, const QString &sourceId); Q_INVOKABLE bool copyTrackEffect(int trackId, 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 Return the previous track of same type as source trackId, or trackId if no track found */ Q_INVOKABLE int getPreviousTrackId(int trackId); /** @brief Return the next track of same type as source trackId, or trackId if no track found */ Q_INVOKABLE int getNextTrackId(int trackId); /* @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 clip state (audio/video only) */ PlaylistState::ClipState getClipState(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 size of the clip's frame (widthxheight) @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 a sort position for tracks. * @param separated: if true, the tracks will be sorted like: V2,V1,A1,A2 * Otherwise, the tracks will be sorted like V2,A2,V1,A1 */ int getTrackSortValue(int trackId, bool separated) 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 track @param trackId Id of the track to test */ int getPreviousVideoTrackPos(int trackId) const; /* @brief Returns the Track id of the video track just below the given track @param trackId Id of the track to test */ int getPreviousVideoTrackIndex(int trackId) const; /* @brief Returns the Id of the corresponding audio track. If trackId corresponds to video1, this will return audio 1 and so on */ int getMirrorAudioTrackId(int trackId) const; int getMirrorVideoTrackId(int trackId) const; int getMirrorTrackId(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, bool finalMove, Fun &undo, Fun &redo); bool requestCompositionMove(int transid, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo); /* When timeline edit mode is insert or overwrite, we fake the move (as it will overlap existing clips, and only process the real move on drop */ bool requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo); bool requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline); bool requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true); bool requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh = true); /* @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 @param dontRefreshMasterClip when false, no view refresh is attempted */ Q_INVOKABLE int suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance = -1); Q_INVOKABLE int suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance = -1); Q_INVOKABLE int suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, 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 @param useTargets: if true, the Audio/video split will occur on the set targets. Otherwise, they will be computed as an offset from the middle line */ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo = true, bool refreshView = false, bool useTargets = true); /* Same function, but accumulates undo and redo*/ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, Fun &redo); protected: /* @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, double speed, Fun &undo, Fun &redo); public: /* @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 itemId, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestItemDeletion(int itemId, 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 @param allowViewRefresh if false, the view will never get updated (useful for suggestMove) */ bool requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true); bool requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh = true); /* @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 if @param allowSingleResize is false, then the resize will also be applied to any clip in the same AV group (allow resizing audio and video at the same time) */ Q_INVOKABLE int requestItemResize(int itemId, int size, bool right, bool logUndo = true, int snapDistance = -1, bool allowSingleResize = false); /* 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 itemId, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestClipUngroup(int itemId, Fun &undo, Fun &redo); // convenience functions for several ids at the same time bool requestClipsUngroup(const std::unordered_set &itemIds, bool logUndo = true); /* @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); protected: /* @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 getBestSnapPos(int pos, int length, const std::vector &pts = std::vector(), int cursorPosition = 0, int snapDistance = -1); public: /* @brief Requests the next snapped point @param pos is the current position */ int getNextSnapPos(int pos); /* @brief Requests the previous snapped point @param pos is the current position */ int getPreviousSnapPos(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, std::unique_ptr 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, std::unique_ptr transProps, int &id, Fun &undo, Fun &redo, bool finalMove = false); /* @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 intersect with a given range. * @param trackId is the id of the track for concerned items. Setting trackId to -1 returns items on all tracks * @param start 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 getItemsInRange(int trackId, int start, 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 define current edit mode (normal, insert, overwrite */ void setEditMode(TimelineMode::EditMode mode); Q_INVOKABLE bool normalEdit() const; /** @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. @param clipId id of the target clip @param speed: speed in percentage. 100 corresponds to original speed, 50 to half the speed This functions create an undo object and also apply the effect to the corresponding audio if there is any. Returns true on success, false otherwise (and nothing is modified) */ bool requestClipTimeWarp(int clipId, double speed); /* @brief Same function as above, but doesn't check for paired audio and accumulate undo/redo */ bool requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo); void replugClip(int clipId); /** @brief Refresh the tractor profile in case a change was requested. */ void updateProfile(Mlt::Profile *profile); /** @brief Clear the current selection @param onDeletion is true when the selection is cleared as a result of a deletion */ Q_INVOKABLE bool requestClearSelection(bool onDeletion = false); // same function with undo/redo accumulation void requestClearSelection(bool onDeletion, Fun &undo, Fun &redo); /** @brief Add the given item to the selection If @param clear is true, the selection is first cleared */ Q_INVOKABLE void requestAddToSelection(int itemId, bool clear = false); /** @brief Remove the given item from the selection */ Q_INVOKABLE void requestRemoveFromSelection(int itemId); /** @brief Set the selection to the set of given ids */ bool requestSetSelection(const std::unordered_set &ids); // same function with undo/redo bool requestSetSelection(const std::unordered_set &ids, Fun &undo, Fun &redo); /** @brief Returns a set containing all the items in the selection */ std::unordered_set getCurrentSelection() const; + /** @brief Do some cleanup before closing */ + void prepareClose(); + 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); /* @brief Register a new clip. This is a call-back meant to be called from ClipModel */ void registerClip(const std::shared_ptr &clip, bool registerProducer = false); /* @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); /* Internal functions to delete a clip or a composition. In general, you should call requestItemDeletion */ 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; /** @brief Attempt to make a clip move without ever updating the view */ bool requestClipMoveAttempt(int clipId, int trackId, int position); 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); bool m_blockRefresh; 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(); /* @brief Signal sent whenever the selection changes */ void selectionChanged(); 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 bool m_timelineEffectsEnabled; bool m_id; // id of the timeline itself // id of the selection. If -1, there is no selection, if positive, then it might either be the id of the selection group, or the id of an individual // item, or, finally, the id of a group which is not of type selection. The last case happens when the selection exactly matches an existing group // (in that case we cannot further group it because the selection would have only one child, which is prohibited by design) int m_currentSelection = -1; // 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; // Timeline editing mode TimelineMode::EditMode m_editMode; + bool m_closing; // 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 void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) = 0; virtual QModelIndex makeClipIndexFromID(int) const = 0; virtual QModelIndex makeCompositionIndexFromID(int) const = 0; virtual QModelIndex makeTrackIndexFromID(int) const = 0; virtual void _resetView() = 0; }; #endif