diff --git a/src/timeline2/model/clipmodel.cpp b/src/timeline2/model/clipmodel.cpp
index b91041982..2fdb4cc83 100644
--- a/src/timeline2/model/clipmodel.cpp
+++ b/src/timeline2/model/clipmodel.cpp
@@ -1,728 +1,749 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "clipmodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "clipsnapmodel.hpp"
#include "core.h"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "logger.hpp"
#include "macros.hpp"
#include "timelinemodel.hpp"
#include "trackmodel.hpp"
#include
#include
#include
#include
ClipModel::ClipModel(const std::shared_ptr &parent, std::shared_ptr prod, const QString &binClipId, int id,
PlaylistState::ClipState state, double speed)
: MoveableItem(parent, id)
, m_producer(std::move(prod))
, m_effectStack(EffectStackModel::construct(m_producer, {ObjectType::TimelineClip, m_id}, parent->m_undoStack))
, m_clipMarkerModel(new ClipSnapModel())
, m_binClipId(binClipId)
, forceThumbReload(false)
, m_currentState(state)
, m_speed(speed)
, m_fakeTrack(-1)
+ , m_positionOffset(0)
{
m_producer->set("kdenlive:id", binClipId.toUtf8().constData());
m_producer->set("_kdenlive_cid", m_id);
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
m_canBeVideo = binClip->hasVideo();
m_canBeAudio = binClip->hasAudio();
m_clipType = binClip->clipType();
if (binClip) {
m_endlessResize = !binClip->hasLimitedDuration();
} else {
m_endlessResize = false;
}
QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector roles) {
qDebug() << "// GOT CLIP STACK DATA CHANGE: " << roles;
if (m_currentTrackId != -1) {
if (auto ptr = m_parent.lock()) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
qDebug() << "// GOT CLIP STACK DATA CHANGE DONE: " << ix << " = " << roles;
ptr->dataChanged(ix, ix, roles);
}
}
});
}
int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState::ClipState state, double speed)
{
id = (id == -1 ? TimelineModel::getNextId() : id);
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId);
// We refine the state according to what the clip can actually produce
std::pair videoAudio = stateToBool(state);
videoAudio.first = videoAudio.first && binClip->hasVideo();
videoAudio.second = videoAudio.second && binClip->hasAudio();
state = stateFromBool(videoAudio);
std::shared_ptr cutProducer = binClip->getTimelineProducer(-1, id, state, speed);
std::shared_ptr clip(new ClipModel(parent, cutProducer, binClipId, id, state, speed));
TRACE_CONSTR(clip.get(), parent, binClipId, id, state, speed);
clip->setClipState_lambda(state)();
parent->registerClip(clip);
clip->m_clipMarkerModel->setReferenceModel(binClip->getMarkerModel());
return id;
}
int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, const std::shared_ptr &producer,
PlaylistState::ClipState state)
{
// we hand the producer to the bin clip, and in return we get a cut to a good master producer
// We might not be able to use directly the producer that we receive as an argument, because it cannot share the same master producer with any other
// clipModel (due to a mlt limitation, see ProjectClip doc)
int id = TimelineModel::getNextId();
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId);
// We refine the state according to what the clip can actually produce
std::pair videoAudio = stateToBool(state);
videoAudio.first = videoAudio.first && binClip->hasVideo();
videoAudio.second = videoAudio.second && binClip->hasAudio();
state = stateFromBool(videoAudio);
double speed = 1.0;
if (QString::fromUtf8(producer->parent().get("mlt_service")) == QLatin1String("timewarp")) {
speed = producer->parent().get_double("warp_speed");
}
auto result = binClip->giveMasterAndGetTimelineProducer(id, producer, state);
std::shared_ptr clip(new ClipModel(parent, result.first, binClipId, id, state, speed));
clip->setClipState_lambda(state)();
clip->m_effectStack->importEffects(producer, state, result.second);
parent->registerClip(clip);
clip->m_clipMarkerModel->setReferenceModel(binClip->getMarkerModel());
return id;
}
void ClipModel::registerClipToBin(std::shared_ptr service, bool registerProducer)
{
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
if (!binClip) {
qDebug() << "Error : Bin clip for id: " << m_binClipId << " NOT AVAILABLE!!!";
}
qDebug() << "REGISTRATION " << m_id << "ptr count" << m_parent.use_count();
binClip->registerService(m_parent, m_id, std::move(service), registerProducer);
}
void ClipModel::deregisterClipToBin()
{
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
binClip->deregisterTimelineClip(m_id);
}
ClipModel::~ClipModel() = default;
bool ClipModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo)
{
QWriteLocker locker(&m_lock);
// qDebug() << "RESIZE CLIP" << m_id << "target size=" << size << "right=" << right << "endless=" << m_endlessResize << "length" <<
// m_producer->get_length();
if (!m_endlessResize && (size <= 0 || size > m_producer->get_length())) {
return false;
}
int delta = getPlaytime() - size;
if (delta == 0) {
return true;
}
int in = m_producer->get_in();
int out = m_producer->get_out();
int old_in = in, old_out = out;
// check if there is enough space on the chosen side
if (!right && in + delta < 0 && !m_endlessResize) {
return false;
}
if (!m_endlessResize && right && (out - delta >= m_producer->get_length())) {
return false;
}
if (right) {
out -= delta;
} else {
in += delta;
}
// qDebug() << "Resize facts delta =" << delta << "old in" << old_in << "old_out" << old_out << "in" << in << "out" << out;
std::function track_operation = []() { return true; };
std::function track_reverse = []() { return true; };
int outPoint = out;
int inPoint = in;
int offset = 0;
if (m_endlessResize) {
offset = inPoint;
outPoint = out - in;
inPoint = 0;
}
if (m_currentTrackId != -1) {
if (auto ptr = m_parent.lock()) {
track_operation = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, inPoint, outPoint, right);
} else {
qDebug() << "Error : Moving clip failed because parent timeline is not available anymore";
Q_ASSERT(false);
}
} else {
// Ensure producer is long enough
if (m_endlessResize && outPoint > m_producer->parent().get_length()) {
m_producer->set("length", outPoint + 1);
}
}
Fun operation = [this, inPoint, outPoint, track_operation]() {
if (track_operation()) {
setInOut(inPoint, outPoint);
return true;
}
return false;
};
if (operation()) {
// Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here
if (m_currentTrackId != -1) {
QVector roles{TimelineModel::DurationRole};
if (!right) {
roles.push_back(TimelineModel::StartRole);
roles.push_back(TimelineModel::InPointRole);
} else {
roles.push_back(TimelineModel::OutPointRole);
}
if (auto ptr = m_parent.lock()) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
// TODO: integrate in undo
ptr->dataChanged(ix, ix, roles);
track_reverse = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, old_in, old_out, right);
}
}
Fun reverse = [this, old_in, old_out, track_reverse]() {
if (track_reverse()) {
setInOut(old_in, old_out);
return true;
}
return false;
};
qDebug() << "----------\n-----------\n// ADJUSTING EFFECT LENGTH, LOGUNDO " << logUndo << ", " << old_in << "/" << inPoint << ", "
<< m_producer->get_playtime();
if (logUndo) {
adjustEffectLength(right, old_in, inPoint, old_out - old_in, m_producer->get_playtime(), offset, reverse, operation, logUndo);
}
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
return false;
}
const QString ClipModel::getProperty(const QString &name) const
{
READ_LOCK();
if (service()->parent().is_valid()) {
return QString::fromUtf8(service()->parent().get(name.toUtf8().constData()));
}
return QString::fromUtf8(service()->get(name.toUtf8().constData()));
}
int ClipModel::getIntProperty(const QString &name) const
{
READ_LOCK();
if (service()->parent().is_valid()) {
return service()->parent().get_int(name.toUtf8().constData());
}
return service()->get_int(name.toUtf8().constData());
}
QSize ClipModel::getFrameSize() const
{
READ_LOCK();
if (service()->parent().is_valid()) {
return QSize(service()->parent().get_int("meta.media.width"), service()->parent().get_int("meta.media.height"));
}
return {service()->get_int("meta.media.width"), service()->get_int("meta.media.height")};
}
double ClipModel::getDoubleProperty(const QString &name) const
{
READ_LOCK();
if (service()->parent().is_valid()) {
return service()->parent().get_double(name.toUtf8().constData());
}
return service()->get_double(name.toUtf8().constData());
}
Mlt::Producer *ClipModel::service() const
{
READ_LOCK();
return m_producer.get();
}
std::shared_ptr ClipModel::getProducer()
{
READ_LOCK();
return m_producer;
}
int ClipModel::getPlaytime() const
{
READ_LOCK();
return m_producer->get_playtime();
}
void ClipModel::setTimelineEffectsEnabled(bool enabled)
{
QWriteLocker locker(&m_lock);
m_effectStack->setEffectStackEnabled(enabled);
}
bool ClipModel::addEffect(const QString &effectId)
{
QWriteLocker locker(&m_lock);
if (EffectsRepository::get()->getType(effectId) == EffectType::Audio) {
if (m_currentState == PlaylistState::VideoOnly) {
return false;
}
} else if (m_currentState == PlaylistState::AudioOnly) {
return false;
}
m_effectStack->appendEffect(effectId);
return true;
}
bool ClipModel::copyEffect(const std::shared_ptr &stackModel, int rowId)
{
QWriteLocker locker(&m_lock);
m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), m_currentState);
return true;
}
bool ClipModel::importEffects(std::shared_ptr stackModel)
{
QWriteLocker locker(&m_lock);
m_effectStack->importEffects(std::move(stackModel), m_currentState);
return true;
}
bool ClipModel::importEffects(std::weak_ptr service)
{
QWriteLocker locker(&m_lock);
m_effectStack->importEffects(std::move(service), m_currentState);
return true;
}
bool ClipModel::removeFade(bool fromStart)
{
QWriteLocker locker(&m_lock);
m_effectStack->removeFade(fromStart);
return true;
}
bool ClipModel::adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, int offset, Fun &undo, Fun &redo, bool logUndo)
{
QWriteLocker locker(&m_lock);
return m_effectStack->adjustStackLength(adjustFromEnd, oldIn, oldDuration, newIn, duration, offset, undo, redo, logUndo);
}
bool ClipModel::adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
qDebug() << ".... ADJUSTING FADE LENGTH: " << duration << " / " << effectName;
Fun operation = [this, duration, effectName, originalDuration]() {
return m_effectStack->adjustFadeLength(duration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(),
!isAudioOnly(), originalDuration > 0);
};
if (operation() && originalDuration > 0) {
Fun reverse = [this, originalDuration, effectName]() {
return m_effectStack->adjustFadeLength(originalDuration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"),
audioEnabled(), !isAudioOnly(), true);
};
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
}
return true;
}
bool ClipModel::audioEnabled() const
{
READ_LOCK();
return stateToBool(m_currentState).second;
}
bool ClipModel::isAudioOnly() const
{
READ_LOCK();
return m_currentState == PlaylistState::AudioOnly;
}
void ClipModel::refreshProducerFromBin(PlaylistState::ClipState state, double speed)
{
// We require that the producer is not in the track when we refresh the producer, because otherwise the modification will not be propagated. Remove the clip
// first, refresh, and then replant.
QWriteLocker locker(&m_lock);
int in = getIn();
int out = getOut();
qDebug() << "refresh " << speed << m_speed << in << out;
if (!qFuzzyCompare(speed, m_speed) && !qFuzzyCompare(speed, 0.)) {
in = in * std::abs(m_speed / speed);
out = in + getPlaytime() - 1;
// prevent going out of the clip's range
out = std::min(out, int(double(m_producer->get_length()) * std::abs(m_speed / speed)) - 1);
m_speed = speed;
qDebug() << "changing speed" << in << out << m_speed;
}
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
std::shared_ptr binProducer = binClip->getTimelineProducer(m_currentTrackId, m_id, state, m_speed);
m_producer = std::move(binProducer);
m_producer->set_in_and_out(in, out);
// replant effect stack in updated service
m_effectStack->resetService(m_producer);
m_producer->set("kdenlive:id", binClip->clipId().toUtf8().constData());
m_producer->set("_kdenlive_cid", m_id);
m_endlessResize = !binClip->hasLimitedDuration();
}
void ClipModel::refreshProducerFromBin()
{
refreshProducerFromBin(m_currentState);
}
bool ClipModel::useTimewarpProducer(double speed, Fun &undo, Fun &redo)
{
if (m_endlessResize) {
// no timewarp for endless producers
return false;
}
if (qFuzzyCompare(speed, m_speed)) {
// nothing to do
return true;
}
std::function local_undo = []() { return true; };
std::function local_redo = []() { return true; };
double previousSpeed = getSpeed();
int oldDuration = getPlaytime();
int newDuration = int(double(oldDuration) * std::abs(previousSpeed / speed));
int oldOut = getOut();
int oldIn = getIn();
auto operation = useTimewarpProducer_lambda(speed);
auto reverse = useTimewarpProducer_lambda(previousSpeed);
if (oldOut >= newDuration) {
// in that case, we are going to shrink the clip when changing the producer. We must undo that when reloading the old producer
reverse = [reverse, oldIn, oldOut, this]() {
bool res = reverse();
if (res) {
setInOut(oldIn, oldOut);
}
return res;
};
}
if (operation()) {
UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
bool res = requestResize(newDuration, true, local_undo, local_redo, true);
if (!res) {
local_undo();
return false;
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
qDebug() << "tw: operation fail";
return false;
}
Fun ClipModel::useTimewarpProducer_lambda(double speed)
{
QWriteLocker locker(&m_lock);
return [speed, this]() {
qDebug() << "timeWarp producer" << speed;
refreshProducerFromBin(m_currentState, speed);
if (auto ptr = m_parent.lock()) {
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
ptr->notifyChange(ix, ix, TimelineModel::SpeedRole);
}
return true;
};
}
QVariant ClipModel::getAudioWaveform()
{
READ_LOCK();
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
if (binClip) {
return QVariant::fromValue(binClip->audioFrameCache);
}
return QVariant();
}
const QString &ClipModel::binId() const
{
return m_binClipId;
}
std::shared_ptr ClipModel::getMarkerModel() const
{
READ_LOCK();
return pCore->projectItemModel()->getClipByBinID(m_binClipId)->getMarkerModel();
}
int ClipModel::audioChannels() const
{
READ_LOCK();
return pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioChannels();
}
int ClipModel::fadeIn() const
{
return m_effectStack->getFadePosition(true);
}
int ClipModel::fadeOut() const
{
return m_effectStack->getFadePosition(false);
}
double ClipModel::getSpeed() const
{
return m_speed;
}
KeyframeModel *ClipModel::getKeyframeModel()
{
return m_effectStack->getEffectKeyframeModel();
}
bool ClipModel::showKeyframes() const
{
READ_LOCK();
return !service()->get_int("kdenlive:hide_keyframes");
}
void ClipModel::setShowKeyframes(bool show)
{
QWriteLocker locker(&m_lock);
service()->set("kdenlive:hide_keyframes", (int)!show);
}
void ClipModel::setPosition(int pos)
{
MoveableItem::setPosition(pos);
m_clipMarkerModel->updateSnapModelPos(pos);
}
void ClipModel::setInOut(int in, int out)
{
MoveableItem::setInOut(in, out);
m_clipMarkerModel->updateSnapModelInOut(std::pair(in, out));
}
void ClipModel::setCurrentTrackId(int tid, bool finalMove)
{
if (tid == m_currentTrackId) {
return;
}
bool registerSnap = m_currentTrackId == -1 && tid > -1;
if (m_currentTrackId > -1 && tid == -1) {
// Removing clip
m_clipMarkerModel->deregisterSnapModel();
}
MoveableItem::setCurrentTrackId(tid, finalMove);
if (registerSnap) {
if (auto ptr = m_parent.lock()) {
m_clipMarkerModel->registerSnapModel(ptr->m_snaps, getPosition(), getIn(), getOut());
}
}
if (finalMove && tid != -1) {
refreshProducerFromBin(m_currentState);
}
}
Fun ClipModel::setClipState_lambda(PlaylistState::ClipState state)
{
QWriteLocker locker(&m_lock);
return [this, state]() {
if (auto ptr = m_parent.lock()) {
switch (state) {
case PlaylistState::Disabled:
m_producer->set("set.test_audio", 1);
m_producer->set("set.test_image", 1);
break;
case PlaylistState::VideoOnly:
m_producer->set("set.test_image", 0);
break;
case PlaylistState::AudioOnly:
m_producer->set("set.test_audio", 0);
break;
default:
// error
break;
}
m_currentState = state;
if (m_currentTrackId != -1 && ptr->isClip(m_id)) { // if this is false, the clip is being created. Don't update model in that case
QModelIndex ix = ptr->makeClipIndexFromID(m_id);
ptr->dataChanged(ix, ix, {TimelineModel::StatusRole});
}
return true;
}
return false;
};
}
bool ClipModel::setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo)
{
if (state == PlaylistState::VideoOnly && !canBeVideo()) {
return false;
}
if (state == PlaylistState::AudioOnly && !canBeAudio()) {
return false;
}
if (state == m_currentState) {
return true;
}
auto old_state = m_currentState;
auto operation = setClipState_lambda(state);
if (operation()) {
auto reverse = setClipState_lambda(old_state);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return true;
}
return false;
}
PlaylistState::ClipState ClipModel::clipState() const
{
READ_LOCK();
return m_currentState;
}
ClipType::ProducerType ClipModel::clipType() const
{
READ_LOCK();
return m_clipType;
}
void ClipModel::passTimelineProperties(const std::shared_ptr &other)
{
READ_LOCK();
Mlt::Properties source(m_producer->get_properties());
Mlt::Properties dest(other->service()->get_properties());
dest.pass_list(source, "kdenlive:hide_keyframes,kdenlive:activeeffect");
}
bool ClipModel::canBeVideo() const
{
return m_canBeVideo;
}
bool ClipModel::canBeAudio() const
{
return m_canBeAudio;
}
const QString ClipModel::effectNames() const
{
READ_LOCK();
return m_effectStack->effectNames();
}
int ClipModel::getFakeTrackId() const
{
return m_fakeTrack;
}
void ClipModel::setFakeTrackId(int fid)
{
m_fakeTrack = fid;
}
int ClipModel::getFakePosition() const
{
return m_fakePosition;
}
void ClipModel::setFakePosition(int fid)
{
m_fakePosition = fid;
}
QDomElement ClipModel::toXml(QDomDocument &document)
{
QDomElement container = document.createElement(QStringLiteral("clip"));
container.setAttribute(QStringLiteral("binid"), m_binClipId);
container.setAttribute(QStringLiteral("id"), m_id);
container.setAttribute(QStringLiteral("in"), getIn());
container.setAttribute(QStringLiteral("out"), getOut());
container.setAttribute(QStringLiteral("position"), getPosition());
container.setAttribute(QStringLiteral("state"), (int)m_currentState);
if (auto ptr = m_parent.lock()) {
int trackId = ptr->getTrackPosition(m_currentTrackId);
container.setAttribute(QStringLiteral("track"), trackId);
if (ptr->isAudioTrack(getCurrentTrackId())) {
container.setAttribute(QStringLiteral("audioTrack"), 1);
int mirrorId = ptr->getMirrorVideoTrackId(m_currentTrackId);
if (mirrorId > -1) {
mirrorId = ptr->getTrackPosition(mirrorId);
}
container.setAttribute(QStringLiteral("mirrorTrack"), mirrorId);
}
}
container.setAttribute(QStringLiteral("speed"), m_speed);
container.appendChild(m_effectStack->toXml(document));
return container;
}
bool ClipModel::checkConsistency()
{
if (!m_effectStack->checkConsistency()) {
qDebug() << "Consistency check failed for effecstack";
return false;
}
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId);
auto instances = binClip->timelineInstances();
bool found = false;
for (const auto &i : instances) {
if (i == m_id) {
found = true;
break;
}
}
if (!found) {
qDebug() << "ERROR: binClip doesn't acknowledge timeline clip existence";
return false;
}
if (m_currentState == PlaylistState::VideoOnly && !m_canBeVideo) {
qDebug() << "ERROR: clip is in video state but doesn't have video";
return false;
}
if (m_currentState == PlaylistState::AudioOnly && !m_canBeAudio) {
qDebug() << "ERROR: clip is in video state but doesn't have video";
return false;
}
// TODO: check speed
return true;
}
int ClipModel::getSubPlaylistIndex() const
{
return m_subPlaylistIndex;
}
void ClipModel::setSubPlaylistIndex(int index)
{
m_subPlaylistIndex = index;
}
+void ClipModel::setOffset(int offset)
+{
+ m_positionOffset = offset;
+ if (auto ptr = m_parent.lock()) {
+ QModelIndex ix = ptr->makeClipIndexFromID(m_id);
+ ptr->dataChanged(ix, ix, {TimelineModel::PositionOffsetRole});
+ }
+}
+
+void ClipModel::clearOffset()
+{
+ if (m_positionOffset != 0) {
+ setOffset(0);
+ }
+}
+
+int ClipModel::getOffset() const
+{
+ return m_positionOffset;
+}
diff --git a/src/timeline2/model/clipmodel.hpp b/src/timeline2/model/clipmodel.hpp
index f705f13c7..cdbfb783e 100644
--- a/src/timeline2/model/clipmodel.hpp
+++ b/src/timeline2/model/clipmodel.hpp
@@ -1,227 +1,235 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#ifndef CLIPMODEL_H
#define CLIPMODEL_H
#include "moveableItem.hpp"
#include "undohelper.hpp"
#include
#include
namespace Mlt {
class Producer;
}
class EffectStackModel;
class MarkerListModel;
class TimelineModel;
class TrackModel;
class KeyframeModel;
class ClipSnapModel;
/* @brief This class represents a Clip object, as viewed by the backend.
In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the
validity of the modifications
*/
class ClipModel : public MoveableItem
{
ClipModel() = delete;
protected:
/* This constructor is not meant to be called, call the static construct instead */
ClipModel(const std::shared_ptr &parent, std::shared_ptr prod, const QString &binClipId, int id,
PlaylistState::ClipState state, double speed = 1.);
public:
~ClipModel() override;
/* @brief Creates a clip, which references itself to the parent timeline
Returns the (unique) id of the created clip
@param parent is a pointer to the timeline
@param binClip is the id of the bin clip associated
@param id Requested id of the clip. Automatic if -1
*/
static int construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState::ClipState state, double speed = 1.);
/* @brief Creates a clip, which references itself to the parent timeline
Returns the (unique) id of the created clip
This variants assumes a producer is already known, which should typically happen only at loading time.
Note that there is no guarantee that this producer is actually going to be used. It might be discarded.
*/
static int construct(const std::shared_ptr &parent, const QString &binClipId, const std::shared_ptr &producer,
PlaylistState::ClipState state);
/* @brief returns a property of the clip, or from it's parent if it's a cut
*/
const QString getProperty(const QString &name) const override;
int getIntProperty(const QString &name) const;
double getDoubleProperty(const QString &name) const;
QSize getFrameSize() const;
Q_INVOKABLE bool showKeyframes() const;
Q_INVOKABLE void setShowKeyframes(bool show);
/* @brief Returns true if the clip can be converted to a video clip */
bool canBeVideo() const;
/* @brief Returns true if the clip can be converted to an audio clip */
bool canBeAudio() const;
/* @brief Returns a comma separated list of effect names */
const QString effectNames() const;
/** @brief Returns the timeline clip status (video / audio only) */
PlaylistState::ClipState clipState() const;
/** @brief Returns the bin clip type (image, color, AV, ...) */
ClipType::ProducerType clipType() const;
/** @brief Sets the timeline clip status (video / audio only) */
bool setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo);
/** @brief The fake track is used in insrt/overwrote mode.
* in this case, dragging a clip is always accepted, but the change is not applied to the model.
* so we use a 'fake' track id to pass to the qml view
*/
int getFakeTrackId() const;
void setFakeTrackId(int fid);
int getFakePosition() const;
void setFakePosition(int fid);
/* @brief Returns an XML representation of the clip with its effects */
QDomElement toXml(QDomDocument &document);
protected:
// helper functions that creates the lambda
Fun setClipState_lambda(PlaylistState::ClipState state);
public:
/* @brief returns the length of the item on the timeline
*/
int getPlaytime() const override;
/** @brief Returns audio cache data from bin clip to display audio thumbs */
QVariant getAudioWaveform();
/** @brief Returns the bin clip's id */
const QString &binId() const;
void registerClipToBin(std::shared_ptr service, bool registerProducer);
void deregisterClipToBin();
bool addEffect(const QString &effectId);
bool copyEffect(const std::shared_ptr &stackModel, int rowId);
/* @brief Import effects from a different stackModel */
bool importEffects(std::shared_ptr stackModel);
/* @brief Import effects from a service that contains some (another clip?) */
bool importEffects(std::weak_ptr service);
bool removeFade(bool fromStart);
/** @brief Adjust effects duration. Should be called after each resize / cut operation */
bool adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, int offset, Fun &undo, Fun &redo, bool logUndo);
bool adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo);
void passTimelineProperties(const std::shared_ptr &other);
KeyframeModel *getKeyframeModel();
int fadeIn() const;
int fadeOut() const;
/**@brief Tracks have two sub playlists to enable same track transitions. This returns the index of the sub-playlist containing this clip */
int getSubPlaylistIndex() const;
void setSubPlaylistIndex(int index);
friend class TrackModel;
friend class TimelineModel;
friend class TimelineItemModel;
friend class TimelineController;
friend struct TimelineFunctions;
protected:
Mlt::Producer *service() const override;
/* @brief Performs a resize of the given clip.
Returns true if the operation succeeded, and otherwise nothing is modified
This method is protected because it shouldn't be called directly. Call the function in the timeline instead.
If a snap point is within reach, the operation will be coerced to use it.
@param size is the new size of the clip
@param right is true if we change the right side of the clip, false otherwise
@param undo Lambda function containing the current undo stack. Will be updated with current operation
@param redo Lambda function containing the current redo queue. Will be updated with current operation
*/
bool requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo = true) override;
void setCurrentTrackId(int tid, bool finalMove = true) override;
void setPosition(int pos) override;
void setInOut(int in, int out) override;
/* @brief This function change the global (timeline-wise) enabled state of the effects
*/
void setTimelineEffectsEnabled(bool enabled);
/* @brief This functions should be called when the producer of the binClip changes, to allow refresh
* @param state corresponds to the state of the clip we want (audio or video)
* @param speed corresponds to the speed we need. Leave to 0 to keep current speed. Warning: this function doesn't notify the model. Unless you know what
* you are doing, better use useTimewarProducer to change the speed
*/
void refreshProducerFromBin(PlaylistState::ClipState state, double speed = 0);
void refreshProducerFromBin();
/* @brief This functions replaces the current producer with a slowmotion one
It also resizes the producer so that set of frames contained in the clip is the same
*/
bool useTimewarpProducer(double speed, Fun &undo, Fun &redo);
// @brief Lambda that merely changes the speed (in and out are untouched)
Fun useTimewarpProducer_lambda(double speed);
/** @brief Returns the marker model associated with this clip */
std::shared_ptr getMarkerModel() const;
/** @brief Returns the number of audio channels for this clip */
int audioChannels() const;
bool audioEnabled() const;
bool isAudioOnly() const;
double getSpeed() const;
+ /** @brief Returns the clip offset (calculated in the model between 2 clips from same bin clip */
+ void setOffset(int offset);
+ /** @brief Clears the clip offset (calculated in the model between 2 clips from same bin clip */
+ void clearOffset();
+ int getOffset() const;
+
/*@brief This is a debug function to ensure the clip is in a valid state */
bool checkConsistency();
protected:
std::shared_ptr m_producer;
std::shared_ptr getProducer();
std::shared_ptr m_effectStack;
std::shared_ptr m_clipMarkerModel;
QString m_binClipId; // This is the Id of the bin clip this clip corresponds to.
bool m_endlessResize; // Whether this clip can be freely resized
bool forceThumbReload; // Used to trigger a forced thumb reload, when producer changes
PlaylistState::ClipState m_currentState;
ClipType::ProducerType m_clipType;
double m_speed = -1; // Speed of the clip
bool m_canBeVideo, m_canBeAudio;
// Fake track id, used when dragging in insert/overwrite mode
int m_fakeTrack;
int m_fakePosition;
+ // Temporary val to store offset between two clips with same bin id.
+ int m_positionOffset;
int m_subPlaylistIndex; // Tracks have two sub playlists to enable same track transitions, we store in which one this clip is.
};
#endif
diff --git a/src/timeline2/model/timelineitemmodel.cpp b/src/timeline2/model/timelineitemmodel.cpp
index 1d521efa1..d66a74f71 100644
--- a/src/timeline2/model/timelineitemmodel.cpp
+++ b/src/timeline2/model/timelineitemmodel.cpp
@@ -1,617 +1,620 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "timelineitemmodel.hpp"
#include "assets/keyframes/model/keyframemodel.hpp"
#include "bin/model/markerlistmodel.hpp"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "groupsmodel.hpp"
#include "kdenlivesettings.h"
#include "macros.hpp"
#include "trackmodel.hpp"
#include "snapmodel.hpp"
#include "transitions/transitionsrepository.hpp"
#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_("TimelineItemModel");
}
TimelineItemModel::TimelineItemModel(Mlt::Profile *profile, std::weak_ptr undo_stack)
: TimelineModel(profile, std::move(undo_stack))
{
}
void TimelineItemModel::finishConstruct(const std::shared_ptr &ptr, const std::shared_ptr &guideModel)
{
ptr->weak_this_ = ptr;
ptr->m_groups = std::make_unique(ptr);
guideModel->registerSnapModel(std::static_pointer_cast(ptr->m_snaps));
}
std::shared_ptr TimelineItemModel::construct(Mlt::Profile *profile, std::shared_ptr guideModel,
std::weak_ptr undo_stack)
{
std::shared_ptr ptr(new TimelineItemModel(profile, std::move(undo_stack)));
finishConstruct(ptr, std::move(guideModel));
return ptr;
}
TimelineItemModel::~TimelineItemModel() = default;
QModelIndex TimelineItemModel::index(int row, int column, const QModelIndex &parent) const
{
READ_LOCK();
QModelIndex result;
if (parent.isValid()) {
auto trackId = int(parent.internalId());
Q_ASSERT(isTrack(trackId));
int clipId = getTrackById_const(trackId)->getClipByRow(row);
if (clipId != -1) {
result = createIndex(row, 0, quintptr(clipId));
} else if (row < getTrackClipsCount(trackId) + getTrackCompositionsCount(trackId)) {
int compoId = getTrackById_const(trackId)->getCompositionByRow(row);
if (compoId != -1) {
result = createIndex(row, 0, quintptr(compoId));
}
} else {
// Invalid index requested
Q_ASSERT(false);
}
} else if (row < getTracksCount() && row >= 0) {
// Get sort order
// row = getTracksCount() - 1 - row;
auto it = m_allTracks.cbegin();
std::advance(it, row);
int trackId = (*it)->getId();
result = createIndex(row, column, quintptr(trackId));
}
return result;
}
/*QModelIndex TimelineItemModel::makeIndex(int trackIndex, int clipIndex) const
{
return index(clipIndex, 0, index(trackIndex));
}*/
QModelIndex TimelineItemModel::makeClipIndexFromID(int clipId) const
{
Q_ASSERT(m_allClips.count(clipId) > 0);
int trackId = m_allClips.at(clipId)->getCurrentTrackId();
if (trackId == -1) {
// Clip is not inserted in a track
qDebug() << "/// WARNING; INVALID CLIP INDEX REQUESTED: "<getRowfromClip(clipId);
return index(row, 0, makeTrackIndexFromID(trackId));
}
QModelIndex TimelineItemModel::makeCompositionIndexFromID(int compoId) const
{
Q_ASSERT(m_allCompositions.count(compoId) > 0);
int trackId = m_allCompositions.at(compoId)->getCurrentTrackId();
return index(getTrackById_const(trackId)->getRowfromComposition(compoId), 0, makeTrackIndexFromID(trackId));
}
QModelIndex TimelineItemModel::makeTrackIndexFromID(int trackId) const
{
// we retrieve iterator
Q_ASSERT(m_iteratorTable.count(trackId) > 0);
auto it = m_iteratorTable.at(trackId);
int ind = (int)std::distance(m_allTracks.begin(), it);
// Get sort order
// ind = getTracksCount() - 1 - ind;
return index(ind);
}
QModelIndex TimelineItemModel::parent(const QModelIndex &index) const
{
READ_LOCK();
// qDebug() << "TimelineItemModel::parent"<< index;
if (index == QModelIndex()) {
return index;
}
const int id = static_cast(index.internalId());
if (!index.isValid() || isTrack(id)) {
return QModelIndex();
}
if (isClip(id)) {
const int trackId = getClipTrackId(id);
return makeTrackIndexFromID(trackId);
}
if (isComposition(id)) {
const int trackId = getCompositionTrackId(id);
return makeTrackIndexFromID(trackId);
}
return {};
}
int TimelineItemModel::rowCount(const QModelIndex &parent) const
{
READ_LOCK();
if (parent.isValid()) {
const int id = (int)parent.internalId();
if (!isTrack(id)) {
// clips don't have children
// if it is not a track, it is something invalid
return 0;
}
return getTrackClipsCount(id) + getTrackCompositionsCount(id);
}
return getTracksCount();
}
int TimelineItemModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
QHash TimelineItemModel::roleNames() const
{
QHash roles;
roles[NameRole] = "name";
roles[ResourceRole] = "resource";
roles[ServiceRole] = "mlt_service";
roles[BinIdRole] = "binId";
roles[TrackIdRole] = "trackId";
roles[FakeTrackIdRole] = "fakeTrackId";
roles[FakePositionRole] = "fakePosition";
roles[StartRole] = "start";
roles[DurationRole] = "duration";
roles[MarkersRole] = "markers";
roles[KeyframesRole] = "keyframeModel";
roles[ShowKeyframesRole] = "showKeyframes";
roles[StatusRole] = "clipStatus";
roles[TypeRole] = "clipType";
roles[InPointRole] = "in";
roles[OutPointRole] = "out";
roles[FramerateRole] = "fps";
roles[GroupedRole] = "grouped";
roles[IsDisabledRole] = "disabled";
roles[IsAudioRole] = "audio";
roles[AudioLevelsRole] = "audioLevels";
roles[AudioChannelsRole] = "audioChannels";
roles[IsCompositeRole] = "composite";
roles[IsLockedRole] = "locked";
roles[FadeInRole] = "fadeIn";
roles[FadeOutRole] = "fadeOut";
roles[FileHashRole] = "hash";
roles[SpeedRole] = "speed";
roles[HeightRole] = "trackHeight";
roles[TrackTagRole] = "trackTag";
roles[ItemIdRole] = "item";
roles[ItemATrack] = "a_track";
roles[HasAudio] = "hasAudio";
roles[CanBeAudioRole] = "canBeAudio";
roles[CanBeVideoRole] = "canBeVideo";
roles[ReloadThumbRole] = "reloadThumb";
+ roles[PositionOffsetRole] = "positionOffset";
roles[ThumbsFormatRole] = "thumbsFormat";
roles[AudioRecordRole] = "audioRecord";
roles[TrackActiveRole] = "trackActive";
roles[EffectNamesRole] = "effectNames";
roles[EffectsEnabledRole] = "isStackEnabled";
roles[GrabbedRole] = "isGrabbed";
return roles;
}
QVariant TimelineItemModel::data(const QModelIndex &index, int role) const
{
READ_LOCK();
if (!m_tractor || !index.isValid()) {
// qDebug() << "DATA abort. Index validity="< clip = m_allClips.at(id);
// Get data for a clip
switch (role) {
// TODO
case NameRole:
case Qt::DisplayRole: {
QString result = clip->getProperty("kdenlive:clipname");
if (result.isEmpty()) {
result = clip->getProperty("kdenlive:originalurl");
if (result.isEmpty()) {
result = clip->getProperty("resource");
}
if (!result.isEmpty()) {
result = QFileInfo(result).fileName();
} else {
result = clip->getProperty("mlt_service");
}
}
return result;
}
case ResourceRole: {
QString result = clip->getProperty("resource");
if (result == QLatin1String("")) {
result = clip->getProperty("mlt_service");
}
return result;
}
case FakeTrackIdRole:
return clip->getFakeTrackId();
case FakePositionRole:
return clip->getFakePosition();
case BinIdRole:
return clip->binId();
case TrackIdRole:
return clip->getCurrentTrackId();
case ServiceRole:
return clip->getProperty("mlt_service");
break;
case AudioLevelsRole:
// Dumb property to trigger audio thumbs reload
return true;
case AudioChannelsRole:
return clip->audioChannels();
case HasAudio:
return clip->audioEnabled();
case IsAudioRole:
return clip->isAudioOnly();
case CanBeAudioRole:
return clip->canBeAudio();
case CanBeVideoRole:
return clip->canBeVideo();
case MarkersRole: {
return QVariant::fromValue(clip->getMarkerModel().get());
}
case KeyframesRole: {
return QVariant::fromValue(clip->getKeyframeModel());
}
case StatusRole:
return QVariant::fromValue(clip->clipState());
case TypeRole:
return QVariant::fromValue(clip->clipType());
case StartRole:
return clip->getPosition();
case DurationRole:
return clip->getPlaytime();
case GroupedRole:
return m_groups->isInGroup(id);
case EffectNamesRole:
return clip->effectNames();
case InPointRole:
return clip->getIn();
case OutPointRole:
return clip->getOut();
case ShowKeyframesRole:
return clip->showKeyframes();
case FadeInRole:
return clip->fadeIn();
case FadeOutRole:
return clip->fadeOut();
case ReloadThumbRole:
return clip->forceThumbReload;
+ case PositionOffsetRole:
+ return clip->getOffset();
case SpeedRole:
return clip->getSpeed();
case GrabbedRole:
return clip->isGrabbed();
default:
break;
}
} else if (isTrack(id)) {
// qDebug() << "DATA REQUESTED FOR TRACK "<< id;
switch (role) {
case NameRole:
case Qt::DisplayRole: {
return getTrackById_const(id)->getProperty("kdenlive:track_name").toString();
}
case TypeRole:
return QVariant::fromValue(ClipType::ProducerType::Track);
case DurationRole:
// qDebug() << "DATA yielding duration" << m_tractor->get_playtime();
return getTrackById_const(id)->trackDuration();
case IsDisabledRole:
// qDebug() << "DATA yielding mute" << 0;
return getTrackById_const(id)->isAudioTrack() ? getTrackById_const(id)->isMute() : getTrackById_const(id)->isHidden();
case IsAudioRole:
return getTrackById_const(id)->isAudioTrack();
case TrackTagRole:
return getTrackTagById(id);
case IsLockedRole:
return getTrackById_const(id)->getProperty("kdenlive:locked_track").toInt() == 1;
case HeightRole: {
int collapsed = getTrackById_const(id)->getProperty("kdenlive:collapsed").toInt();
if (collapsed > 0) {
return collapsed;
}
int height = getTrackById_const(id)->getProperty("kdenlive:trackheight").toInt();
// qDebug() << "DATA yielding height" << height;
return (height > 0 ? height : 60);
}
case ThumbsFormatRole:
return getTrackById_const(id)->getProperty("kdenlive:thumbs_format").toInt();
case IsCompositeRole: {
case AudioRecordRole:
return getTrackById_const(id)->getProperty("kdenlive:audio_rec").toInt();
}
case TrackActiveRole: {
return getTrackById_const(id)->isTimelineActive();
}
case EffectNamesRole: {
return getTrackById_const(id)->effectNames();
}
case EffectsEnabledRole: {
return getTrackById_const(id)->stackEnabled();
}
default:
break;
}
} else if (isComposition(id)) {
std::shared_ptr compo = m_allCompositions.at(id);
switch (role) {
case NameRole:
case Qt::DisplayRole:
case ResourceRole:
case ServiceRole:
return compo->displayName();
break;
case TypeRole:
return QVariant::fromValue(ClipType::ProducerType::Composition);
case StartRole:
return compo->getPosition();
case TrackIdRole:
return compo->getCurrentTrackId();
case DurationRole:
return compo->getPlaytime();
case GroupedRole:
return m_groups->isInGroup(id);
case InPointRole:
return 0;
case OutPointRole:
return 100;
case BinIdRole:
return 5;
case KeyframesRole: {
return QVariant::fromValue(compo->getEffectKeyframeModel());
}
case ShowKeyframesRole:
return compo->showKeyframes();
case ItemATrack:
return compo->getForcedTrack();
case MarkersRole: {
QVariantList markersList;
return markersList;
}
case GrabbedRole:
return compo->isGrabbed();
default:
break;
}
} else {
qDebug() << "UNKNOWN DATA requested " << index << roleNames()[role];
}
return QVariant();
}
void TimelineItemModel::setTrackProperty(int trackId, const QString &name, const QString &value)
{
std::shared_ptr track = getTrackById(trackId);
track->setProperty(name, value);
QVector roles;
if (name == QLatin1String("kdenlive:track_name")) {
roles.push_back(NameRole);
} else if (name == QLatin1String("kdenlive:locked_track")) {
roles.push_back(IsLockedRole);
} else if (name == QLatin1String("hide")) {
roles.push_back(IsDisabledRole);
if (!track->isAudioTrack()) {
pCore->requestMonitorRefresh();
}
} else if (name == QLatin1String("kdenlive:timeline_active")) {
roles.push_back(TrackActiveRole);
} else if (name == QLatin1String("kdenlive:thumbs_format")) {
roles.push_back(ThumbsFormatRole);
} else if (name == QLatin1String("kdenlive:collapsed")) {
roles.push_back(HeightRole);
} else if (name == QLatin1String("kdenlive:audio_rec")) {
roles.push_back(AudioRecordRole);
}
if (!roles.isEmpty()) {
QModelIndex ix = makeTrackIndexFromID(trackId);
emit dataChanged(ix, ix, roles);
}
}
void TimelineItemModel::setTrackStackEnabled(int tid, bool enable)
{
std::shared_ptr track = getTrackById(tid);
track->setEffectStackEnabled(enable);
QModelIndex ix = makeTrackIndexFromID(tid);
emit dataChanged(ix, ix, {TimelineModel::EffectsEnabledRole});
}
void TimelineItemModel::importTrackEffects(int tid, std::weak_ptr service)
{
std::shared_ptr track = getTrackById(tid);
track->importEffects(std::move(service));
}
QVariant TimelineItemModel::getTrackProperty(int tid, const QString &name) const
{
return getTrackById_const(tid)->getProperty(name);
}
int TimelineItemModel::getFirstVideoTrackIndex() const
{
int trackId = -1;
auto it = m_allTracks.cbegin();
while (it != m_allTracks.cend()) {
trackId = (*it)->getId();
if (!(*it)->isAudioTrack()) {
break;
}
++it;
}
return trackId;
}
const QString TimelineItemModel::getTrackFullName(int tid) const
{
QString tag = getTrackTagById(tid);
QString trackName = getTrackById_const(tid)->getProperty(QStringLiteral("kdenlive:track_name")).toString();
return trackName.isEmpty() ? tag : tag + QStringLiteral(" - ") + trackName;
}
const QString TimelineItemModel::groupsData()
{
return m_groups->toJson();
}
bool TimelineItemModel::loadGroups(const QString &groupsData)
{
return m_groups->fromJson(groupsData);
}
void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb)
{
QVector roles;
if (start) {
roles.push_back(TimelineModel::StartRole);
if (updateThumb) {
roles.push_back(TimelineModel::InPointRole);
}
}
if (duration) {
roles.push_back(TimelineModel::DurationRole);
if (updateThumb) {
roles.push_back(TimelineModel::OutPointRole);
}
}
emit dataChanged(topleft, bottomright, roles);
}
void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles)
{
emit dataChanged(topleft, bottomright, roles);
}
void TimelineItemModel::buildTrackCompositing(bool rebuild)
{
auto it = m_allTracks.cbegin();
QScopedPointer field(m_tractor->field());
field->lock();
// Make sure all previous track compositing is removed
if (rebuild) {
QScopedPointer service(new Mlt::Service(field->get_service()));
while ((service != nullptr) && service->is_valid()) {
if (service->type() == transition_type) {
Mlt::Transition t((mlt_transition)service->get_service());
QString serviceName = t.get("mlt_service");
if (t.get_int("internal_added") == 237) {
// remove all compositing transitions
field->disconnect_service(t);
}
}
service.reset(service->producer());
}
}
QString composite = TransitionsRepository::get()->getCompositingTransition();
while (it != m_allTracks.cend()) {
int trackId = getTrackMltIndex((*it)->getId());
if (!composite.isEmpty() && !(*it)->isAudioTrack()) {
// video track, add composition
std::unique_ptr transition = TransitionsRepository::get()->getTransition(composite);
transition->set("internal_added", 237);
transition->set("always_active", 1);
field->plant_transition(*transition, 0, trackId);
transition->set_tracks(0, trackId);
} else if ((*it)->isAudioTrack()) {
// audio mix
std::unique_ptr transition = TransitionsRepository::get()->getTransition(QStringLiteral("mix"));
transition->set("internal_added", 237);
transition->set("always_active", 1);
transition->set("sum", 1);
field->plant_transition(*transition, 0, trackId);
transition->set_tracks(0, trackId);
}
++it;
}
field->unlock();
if (composite.isEmpty()) {
pCore->displayMessage(i18n("Could not setup track compositing, check your install"), MessageType::ErrorMessage);
}
}
void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role)
{
emit dataChanged(topleft, bottomright, {role});
}
void TimelineItemModel::_beginRemoveRows(const QModelIndex &i, int j, int k)
{
// qDebug()<<"FORWARDING beginRemoveRows"<. *
***************************************************************************/
#include "timelinemodel.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "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)
{
// 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);
}
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, Fun &undo, Fun &redo)
{
// qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView;
if (trackId == -1) {
return false;
}
Q_ASSERT(isClip(clipId));
if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) {
if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) {
return false;
}
if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) {
return false;
}
} else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) {
// Move not allowed (audio / video mismatch)
qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState();
return false;
}
std::function local_undo = []() { return true; };
std::function local_redo = []() { return true; };
bool ok = true;
int old_trackId = getClipTrackId(clipId);
bool notifyViewOnly = false;
// qDebug()<<"MOVING CLIP FROM: "<requestClipDeletion(clipId, updateView, invalidateTimeline, local_undo, local_redo);
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
ok = ok & getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, local_undo, local_redo);
if (!ok) {
qDebug() << "-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------";
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
update_model();
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_redo);
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestFakeClipMove(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();
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, 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, 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, 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, false, audio_undo, audio_redo);
}
// use lazy evaluation to group only if move was successful
res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit);
if (!res || !move) {
pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage);
bool undone = audio_undo();
Q_ASSERT(undone);
} else {
UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo);
}
} else {
pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage);
bool undone = audio_undo();
Q_ASSERT(undone);
}
}
}
} else {
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid);
if (dropType == PlaylistState::Disabled) {
dropType = getTrackById_const(trackId)->trackType();
} else if (dropType != getTrackById_const(trackId)->trackType()) {
qDebug() << "// INCORRECT DRAG, ABORTING";
return false;
}
QString normalisedBinId = binClipId;
if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) {
normalisedBinId.remove(0, 1);
}
res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo);
res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo);
}
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
id = -1;
return false;
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestItemDeletion(int 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; };
// Sort clips. We need to delete from right to left to avoid confusing the view, and compositions from top to bottom
std::vector sorted_clips(all_items.begin(), all_items.end());
std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_track](int clipId1, int clipId2) {
int p1 = isClip(clipId1) ? m_allClips[clipId1]->getPosition()
: delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId())
: m_allCompositions[clipId1]->getPosition();
int p2 = isClip(clipId2) ? m_allClips[clipId2]->getPosition()
: delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId())
: m_allCompositions[clipId2]->getPosition();
return p2 <= p1;
});
// Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
// This way, we ensure that no conflict will arise with clips inside the group being moved
Fun update_model = []() { return true; };
// Check if there is a track move
bool updatePositionOnly = false;
if (delta_track == 0 && updateView) {
updateView = false;
allowViewRefresh = false;
updatePositionOnly = true;
update_model = [sorted_clips, this]() {
QModelIndex modelIndex;
QVector roles{StartRole};
for (int item : sorted_clips) {
if (isClip(item)) {
modelIndex = makeClipIndexFromID(item);
} else {
modelIndex = makeCompositionIndexFromID(item);
}
notifyChange(modelIndex, modelIndex, roles);
}
return true;
};
}
// First, remove clips
std::unordered_map old_track_ids, old_position, old_forced_track;
for (int item : sorted_clips) {
int old_trackId = getItemTrackId(item);
old_track_ids[item] = old_trackId;
if (old_trackId != -1) {
bool updateThisView = allowViewRefresh;
if (isClip(item)) {
ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo);
old_position[item] = m_allClips[item]->getPosition();
} else {
// ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, 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;
}
// Reverse sort. We need to insert from left to right to avoid confusing the view
std::reverse(std::begin(sorted_clips), std::end(sorted_clips));
for (int item : sorted_clips) {
int current_track_id = old_track_ids[item];
int current_track_position = getTrackPosition(current_track_id);
int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
int target_track_position = current_track_position + d;
bool updateThisView = allowViewRefresh;
if (target_track_position >= 0 && target_track_position < getTracksCount()) {
auto it = m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
int target_position = old_position[item] + delta_pos;
if (isClip(item)) {
ok = ok && requestClipMove(item, target_track, target_position, updateThisView, finalMove, local_undo, local_redo);
} else {
ok = ok &&
requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, finalMove, local_undo, local_redo);
}
} else {
qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n..";
ok = false;
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
if (updatePositionOnly) {
update_model();
PUSH_LAMBDA(update_model, local_redo);
PUSH_LAMBDA(update_model, local_undo);
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo)
{
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);
if (result) {
PUSH_UNDO(undo, redo, i18n("Insert Track"));
}
TRACE_RES(result);
return result;
}
bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView)
{
// TODO: make sure we disable overlayTrack before inserting a track
if (position == -1) {
position = (int)(m_allTracks.size());
}
if (position < 0 || position > (int)m_allTracks.size()) {
return false;
}
int trackId = TimelineModel::getNextId();
id = trackId;
Fun local_undo = deregisterTrack_lambda(trackId, true);
TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack);
auto track = getTrackById(trackId);
Fun local_redo = [track, position, updateView, this]() {
// We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
// sufficient to register it.
registerTrack(track, position, updateView);
return true;
};
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestTrackDeletion(int trackId)
{
// TODO: make sure we disable overlayTrack before deleting a track
QWriteLocker locker(&m_lock);
TRACE(trackId);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = requestTrackDeletion(trackId, undo, redo);
if (result) {
if (m_videoTarget == trackId) {
m_videoTarget = -1;
}
if (m_audioTarget == trackId) {
m_audioTarget = -1;
}
PUSH_UNDO(undo, redo, i18n("Delete Track"));
}
TRACE_RES(result);
return result;
}
bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo)
{
Q_ASSERT(isTrack(trackId));
std::vector clips_to_delete;
for (const auto &it : getTrackById(trackId)->m_allClips) {
clips_to_delete.push_back(it.first);
}
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
for (int clip : clips_to_delete) {
bool res = true;
while (res && m_groups->isInGroup(clip)) {
res = requestClipUngroup(clip, local_undo, local_redo);
}
if (res) {
res = requestClipDeletion(clip, local_undo, local_redo);
}
if (!res) {
bool u = local_undo();
Q_ASSERT(u);
return false;
}
}
int old_position = getTrackPosition(trackId);
auto operation = deregisterTrack_lambda(trackId, true);
std::shared_ptr track = getTrackById(trackId);
Fun reverse = [this, track, old_position]() {
// We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
// sufficient to register it.
registerTrack(track, old_position);
return true;
};
if (operation()) {
UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
local_undo();
return false;
}
void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert, bool reloadView)
{
// qDebug() << "REGISTER TRACK" << track->getId() << pos;
int id = track->getId();
if (pos == -1) {
pos = static_cast(m_allTracks.size());
}
Q_ASSERT(pos >= 0);
Q_ASSERT(pos <= static_cast(m_allTracks.size()));
// effective insertion (MLT operation), add 1 to account for black background track
if (doInsert) {
int error = m_tractor->insert_track(*track, pos + 1);
Q_ASSERT(error == 0); // we might need better error handling...
}
// we now insert in the list
auto posIt = m_allTracks.begin();
std::advance(posIt, pos);
auto it = m_allTracks.insert(posIt, std::move(track));
// it now contains the iterator to the inserted element, we store it
Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen)
m_iteratorTable[id] = it;
if (reloadView) {
// don't reload view on each track load on project opening
_resetView();
}
}
void TimelineModel::registerClip(const std::shared_ptr &clip, bool registerProducer)
{
int id = clip->getId();
qDebug() << " // /REQUEST TL CLP REGSTR: " << id << "\n--------\nCLIPS COUNT: " << m_allClips.size();
Q_ASSERT(m_allClips.count(id) == 0);
m_allClips[id] = clip;
clip->registerClipToBin(clip->getProducer(), registerProducer);
m_groups->createGroupItem(id);
clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled);
}
void TimelineModel::registerGroup(int groupId)
{
Q_ASSERT(m_allGroups.count(groupId) == 0);
m_allGroups.insert(groupId);
}
Fun TimelineModel::deregisterTrack_lambda(int id, bool updateView)
{
return [this, id, updateView]() {
// qDebug() << "DEREGISTER TRACK" << id;
auto it = m_iteratorTable[id]; // iterator to the element
int index = getTrackPosition(id); // compute index in list
m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track
// send update to the model
m_allTracks.erase(it); // actual deletion of object
m_iteratorTable.erase(id); // clean table
if (updateView) {
_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()
{
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 - pos1 - getClipIn(pairIds.at(1)) - 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 - pos2 - getClipIn(pairIds.at(0)) - 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 0c25ab917..8eea22365 100644
--- a/src/timeline2/model/timelinemodel.hpp
+++ b/src/timeline2/model/timelinemodel.hpp
@@ -1,790 +1,791 @@
/***************************************************************************
* 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, 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
*/
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