diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp
index d4bc5e932..96ef32562 100644
--- a/src/timeline2/model/timelinemodel.cpp
+++ b/src/timeline2/model/timelinemodel.cpp
@@ -1,3621 +1,3621 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "timelinemodel.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "effects/effectsrepository.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "groupsmodel.hpp"
#include "kdenlivesettings.h"
#include "logger.hpp"
#include "snapmodel.hpp"
#include "timelinefunctions.hpp"
#include "trackmodel.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock"))
.method("requestClipMove", select_overload(&TimelineModel::requestClipMove))(
parameter_names("clipId", "trackId", "position", "moveMirrorTracks", "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", "moveMirrorTracks", "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"))
.method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance", "moveMirrorTracks"))
.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","changeDuration"));
}
int TimelineModel::next_id = 0;
int TimelineModel::seekDuration = 30000;
TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack)
: QAbstractItemModel_shared_from_this()
, m_blockRefresh(false)
, m_tractor(new Mlt::Tractor(*profile))
, m_masterStack(nullptr)
, m_snaps(new SnapModel())
, m_undoStack(std::move(undo_stack))
, m_profile(profile)
, m_blackClip(new Mlt::Producer(*profile, "color:0"))
, m_lock(QReadWriteLock::Recursive)
, m_timelineEffectsEnabled(true)
, m_id(getNextId())
, m_overlayTrackCount(-1)
, m_audioTarget(-1)
, m_videoTarget(-1)
, m_editMode(TimelineMode::NormalEdit)
, m_closing(false)
{
// Create black background track
m_blackClip->set("id", "black_track");
m_blackClip->set("mlt_type", "producer");
m_blackClip->set("aspect_ratio", 1);
m_blackClip->set("length", INT_MAX);
m_blackClip->set("mlt_image_format", "rgb24a");
m_blackClip->set("set.test_audio", 0);
m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
m_tractor->insert_track(*m_blackClip, 0);
TRACE_CONSTR(this);
}
void TimelineModel::prepareClose()
{
requestClearSelection(true);
QWriteLocker locker(&m_lock);
// Unlock all tracks to allow delting clip from tracks
m_closing = true;
auto it = m_allTracks.begin();
while (it != m_allTracks.end()) {
(*it)->unlock();
++it;
}
}
TimelineModel::~TimelineModel()
{
std::vector all_ids;
for (auto tracks : m_iteratorTable) {
all_ids.push_back(tracks.first);
}
for (auto tracks : all_ids) {
deregisterTrack_lambda(tracks, false)();
}
for (const auto &clip : m_allClips) {
clip.second->deregisterClipToBin();
}
}
int TimelineModel::getTracksCount() const
{
READ_LOCK();
int count = m_tractor->count();
if (m_overlayTrackCount > -1) {
count -= m_overlayTrackCount;
}
Q_ASSERT(count >= 0);
// don't count the black background track
Q_ASSERT(count - 1 == static_cast(m_allTracks.size()));
return count - 1;
}
int TimelineModel::getTrackIndexFromPosition(int pos) const
{
Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size());
READ_LOCK();
auto it = m_allTracks.cbegin();
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.cbegin();
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, int separated) const
{
if (separated == 1) {
// This will be A2, A1, V1, V2
return getTrackPosition(trackId) + 1;
}
if (separated == 2) {
// This will be A1, A2, V1, V2
// Count audio/video tracks
auto it = m_allTracks.cbegin();
int aCount = 0;
int vCount = 0;
int refPos = 0;
bool isVideo = true;
while (it != m_allTracks.cend()) {
if ((*it)->isAudioTrack()) {
if ((*it)->getId() == trackId) {
refPos = aCount;
isVideo = false;
}
aCount++;
} else {
// video track
if ((*it)->getId() == trackId) {
refPos = vCount;
}
vCount++;
}
++it;
}
return isVideo ? aCount + refPos + 1 : aCount - refPos;
}
// This will be A1, V1, A2, V2
auto it = m_allTracks.cend();
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 = qMax(0, aCount - vCount);
if (trackDiff > 0) {
// more audio tracks, keep them below
if (isAudio && trackPos > vCount) {
return -trackPos;
}
}
return isAudio ? 2 * trackPos : 2 * (vCount + 1 - trackPos) + 1;
}
QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
QList results;
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.cbegin()) {
--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.cbegin()) {
--it;
if (!(*it)->isAudioTrack()) {
return (*it)->getId();
}
}
return 0;
}
int TimelineModel::getPreviousVideoTrackPos(int trackId) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.cbegin()) {
--it;
if (!(*it)->isAudioTrack()) {
return getTrackMltIndex((*it)->getId());
}
}
return 0;
}
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;
while (it != m_allTracks.cend()) {
if ((*it)->isAudioTrack()) {
count++;
} else {
count--;
if (count == 0) {
return (*it)->getId();
}
}
++it;
}
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...
qDebug()<<"++++++++\n+++++++ ERROR RQSTNG AUDIO MIRROR FOR AUDIO";
return -1;
}
int count = 0;
while (it != m_allTracks.cbegin()) {
if (!(*it)->isAudioTrack()) {
count++;
} else {
count--;
if (count == 0) {
return (*it)->getId();
}
}
--it;
}
if ((*it)->isAudioTrack() && count == 1) {
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 moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
{
// qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<clipState() == PlaylistState::Disabled) {
if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) {
return false;
}
if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) {
return false;
}
} else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) {
// Move not allowed (audio / video mismatch)
qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState();
return false;
}
std::function local_undo = []() { return true; };
std::function local_redo = []() { return true; };
bool ok = true;
int old_trackId = getClipTrackId(clipId);
bool notifyViewOnly = false;
// qDebug()<<"MOVING CLIP FROM: "<isAudioTrack()) {
int in = getClipPosition(clipId);
emit invalidateZone(in, in + getClipPlaytime(clipId));
}
return true;
};
}
if (old_trackId != -1) {
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_undo);
}
ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove, false);
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
ok = ok & getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove);
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 && m_allClips[clipId]->getFakeTrackId() == trackId) {
TRACE_RES(true);
qDebug()<<"........\nABORTING MOVE; SAME POS/TRACK\n..........";
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 moveMirrorTracks, 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, moveMirrorTracks, updateView, logUndo);
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = requestClipMove(clipId, trackId, position, moveMirrorTracks, updateView, invalidateTimeline, logUndo, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, i18n("Move clip"));
}
TRACE_RES(res);
return res;
}
bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allClips.count(clipId) > 0);
if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
return true;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool res = true;
if (m_groups->isInGroup(clipId)) {
// element is in a group.
int groupId = m_groups->getRootId(clipId);
int current_trackId = getClipTrackId(clipId);
int track_pos1 = getTrackPosition(trackId);
int track_pos2 = getTrackPosition(current_trackId);
int delta_track = track_pos1 - track_pos2;
int delta_pos = position - m_allClips[clipId]->getPosition();
res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false);
} else {
res = requestClipMove(clipId, trackId, position, true, false, false, false, undo, redo);
}
if (res) {
undo();
}
return res;
}
int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
{
if (isClip(itemId)) {
return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
}
return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
}
int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool moveMirrorTracks)
{
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 && m_editMode == TimelineMode::NormalEdit && 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, moveMirrorTracks, 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, moveMirrorTracks, 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_const(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, moveMirrorTracks, true, false, false);
TRACE_RES(possible ? position : currentPos);
return possible ? position : currentPos;
}
if (trackId != sourceTrackId) {
// Try same track move
possible = requestClipMove(clipId, sourceTrackId, position, moveMirrorTracks, true, false, false);
return possible ? position : currentPos;
}
// find best pos for groups
int groupId = m_groups->getRootId(clipId);
std::unordered_set all_items = m_groups->getLeaves(groupId);
QMap trackPosition;
// First pass, sort clips by track and keep only the first / last depending on move direction
for (int current_clipId : all_items) {
int clipTrack = getItemTrackId(current_clipId);
if (clipTrack == -1) {
continue;
}
int in = getItemPosition(current_clipId);
if (trackPosition.contains(clipTrack)) {
if (after) {
// keep only last clip position for track
int out = in + getItemPlaytime(current_clipId);
if (trackPosition.value(clipTrack) < out) {
trackPosition.insert(clipTrack, out);
}
} else {
// keep only first clip position for track
if (trackPosition.value(clipTrack) > in) {
trackPosition.insert(clipTrack, in);
}
}
} else {
trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in);
}
}
// Now check space on each track
QMapIterator i(trackPosition);
int blank_length = 0;
while (i.hasNext()) {
i.next();
int track_space;
if (!after) {
// Check space before the position
track_space = i.value() - getTrackById_const(i.key())->getBlankStart(i.value() - 1);
if (blank_length == 0 || 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 == 0 || blank_length > track_space) {
blank_length = track_space;
}
}
}
if (snapDistance > 0) {
if (blank_length > 10 * snapDistance) {
blank_length = 0;
}
} else if (blank_length / m_profile->fps() > 5) {
blank_length = 0;
}
if (blank_length != 0) {
int updatedPos = currentPos + (after ? blank_length : -blank_length);
possible = requestClipMove(clipId, trackId, updatedPos, moveMirrorTracks, 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, QVector allowedTracks)
{
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() || !allowedTracks.contains(trackId))) {
trackId = -1;
}
} else if (useTargets && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) {
// Video target set but locked
trackId = m_audioTarget;
if (trackId > -1 && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) {
trackId = -1;
}
}
if (trackId == -1) {
if (!allowedTracks.isEmpty()) {
// No active tracks, aborting
return true;
}
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, true, refreshView, logUndo, logUndo, local_undo, local_redo);
int target_track;
if (audioDrop) {
target_track = m_videoTarget == -1 ? -1 : getTrackById_const(m_videoTarget)->isLocked() ? -1 : m_videoTarget;
} else {
target_track = m_audioTarget == -1 ? -1 : getTrackById_const(m_audioTarget)->isLocked() ? -1 : m_audioTarget;
}
if (useTargets && !allowedTracks.contains(target_track)) {
target_track = -1;
}
qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo();
int mirror = getMirrorTrackId(trackId);
if (mirror > -1 && getTrackById_const(mirror)->isLocked()) {
mirror = -1;
}
bool canMirrorDrop = !useTargets && mirror > -1;
if (res && (canMirrorDrop || target_track > -1) && master->hasAudioAndVideo()) {
if (!useTargets) {
target_track = mirror;
}
// QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack);
QList possibleTracks;
qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets;
if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) {
possibleTracks << target_track;
}
if (possibleTracks.isEmpty()) {
// No available audio track for splitting, abort
pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage);
res = false;
} else {
std::function audio_undo = []() { return true; };
std::function audio_redo = []() { return true; };
int newId;
res = requestClipCreation(binClipId, newId, audioDrop ? PlaylistState::VideoOnly : PlaylistState::AudioOnly, 1.0, audio_undo, audio_redo);
if (res) {
bool move = false;
while (!move && !possibleTracks.isEmpty()) {
int newTrack = possibleTracks.takeFirst();
move = requestClipMove(newId, newTrack, position, true, true, true, true, audio_undo, audio_redo);
}
// use lazy evaluation to group only if move was successful
res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit);
if (!res || !move) {
pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage);
bool undone = audio_undo();
Q_ASSERT(undone);
} else {
UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo);
}
} else {
pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage);
bool undone = audio_undo();
Q_ASSERT(undone);
}
}
}
} else {
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid);
if (dropType == PlaylistState::Disabled) {
dropType = getTrackById_const(trackId)->trackType();
} else if (dropType != getTrackById_const(trackId)->trackType()) {
qDebug() << "// INCORRECT DRAG, ABORTING";
return false;
}
QString normalisedBinId = binClipId;
if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) {
normalisedBinId.remove(0, 1);
}
res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo);
res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo);
}
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
id = -1;
return false;
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestItemDeletion(int itemId, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
if (m_groups->isInGroup(itemId)) {
return requestGroupDeletion(itemId, undo, redo);
}
if (isClip(itemId)) {
return requestClipDeletion(itemId, undo, redo);
}
if (isComposition(itemId)) {
return requestCompositionDeletion(itemId, undo, redo);
}
Q_ASSERT(false);
return false;
}
bool TimelineModel::requestItemDeletion(int itemId, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(itemId, logUndo);
Q_ASSERT(isItem(itemId));
QString actionLabel;
if (m_groups->isInGroup(itemId)) {
actionLabel = i18n("Remove group");
} else {
if (isClip(itemId)) {
actionLabel = i18n("Delete Clip");
} else {
actionLabel = i18n("Delete Composition");
}
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = requestItemDeletion(itemId, undo, redo);
if (res && logUndo) {
PUSH_UNDO(undo, redo, actionLabel);
}
TRACE_RES(res);
requestClearSelection(true);
return res;
}
bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo)
{
int trackId = getClipTrackId(clipId);
if (trackId != -1) {
bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo, false, true);
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, true);
if (!res) {
undo();
return false;
} else {
Fun unplant_op = [this, compositionId]() {
unplantComposition(compositionId);
return true;
};
unplant_op();
PUSH_LAMBDA(unplant_op, redo);
}
}
Fun operation = deregisterComposition_lambda(compositionId);
auto composition = m_allCompositions[compositionId];
int new_in = composition->getPosition();
int new_out = new_in + composition->getPlaytime();
Fun reverse = [this, composition, compositionId, trackId, new_in, new_out]() {
// 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);
composition->setCurrentTrackId(trackId, true);
replantCompositions(compositionId, false);
checkRefresh(new_in, new_out);
return true;
};
if (operation()) {
Fun update_monitor = [this, new_in, new_out]() {
checkRefresh(new_in, new_out);
return true;
};
update_monitor();
PUSH_LAMBDA(update_monitor, 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
// 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 moveMirrorTracks, 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, moveMirrorTracks);
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 moveMirrorTracks,
bool allowViewRefresh, QVector allowedTracks)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_allGroups.count(groupId) > 0);
Q_ASSERT(isItem(itemId));
if (getGroupElements(groupId).count(itemId) == 0) {
// this group doesn't contain the clip, abort
return false;
}
bool ok = true;
auto all_items = m_groups->getLeaves(groupId);
Q_ASSERT(all_items.size() > 1);
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
std::unordered_set all_clips;
std::unordered_set all_compositions;
// Separate clips from compositions to sort
for (int affectedItemId : all_items) {
if (isClip(affectedItemId)) {
all_clips.insert(affectedItemId);
} else {
all_compositions.insert(affectedItemId);
}
}
// Sort clips first
std::vector sorted_clips(all_clips.begin(), all_clips.end());
- std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_pos](int clipId1, int clipId2) {
+ std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_pos](const int clipId1, const int clipId2) {
int p1 = m_allClips[clipId1]->getPosition();
int p2 = m_allClips[clipId2]->getPosition();
return delta_pos > 0 ? p2 <= p1 : p1 <= p2;
});
// Sort compositions. We need to delete in the move direction from top to bottom
std::vector sorted_compositions(all_compositions.begin(), all_compositions.end());
- std::sort(sorted_compositions.begin(), sorted_compositions.end(), [this, delta_track, delta_pos](int clipId1, int clipId2) {
+ std::sort(sorted_compositions.begin(), sorted_compositions.end(), [this, delta_track, delta_pos](const int clipId1, const int clipId2) {
int p1 = delta_track < 0
? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition();
int p2 = delta_track < 0
? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition();
return delta_track == 0 ? (delta_pos > 0 ? p2 <= p1 : p1 <= p2) : p1 <= p2;
});
sorted_clips.insert(sorted_clips.end(), sorted_compositions.begin(), sorted_compositions.end());
// Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
// This way, we ensure that no conflict will arise with clips inside the group being moved
Fun update_model = [this, finalMove]() {
if (finalMove) {
updateDuration();
}
return true;
};
// Check if there is a track move
bool updatePositionOnly = false;
// Second step, reinsert clips at correct positions
int audio_delta, video_delta;
audio_delta = video_delta = delta_track;
if (delta_track == 0 && updateView) {
updateView = false;
allowViewRefresh = false;
updatePositionOnly = true;
update_model = [sorted_clips, finalMove, 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);
}
if (finalMove) {
updateDuration();
}
return true;
};
}
std::unordered_map old_track_ids, old_position, old_forced_track;
// First, remove clips
if (delta_track != 0) {
// We delete our clips only if changing 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, true, false);
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;
}
}
}
if (!moveMirrorTracks) {
if (getTrackById(old_track_ids[itemId])->isAudioTrack()) {
// Master clip is audio, so reverse delta for video clips
video_delta = 0;
} else {
audio_delta = 0;
}
} else {
if (getTrackById(old_track_ids[itemId])->isAudioTrack()) {
// Master clip is audio, so reverse delta for video clips
video_delta = -delta_track;
} else {
audio_delta = -delta_track;
}
}
}
// We need to insert depending on the move direction to avoid confusing the view
// std::reverse(std::begin(sorted_clips), std::end(sorted_clips));
bool updateThisView = allowViewRefresh;
if (delta_track == 0) {
// Special case, we are moving on same track, avoid too many calculations
for (int item : sorted_clips) {
int current_track_id = getItemTrackId(item);
if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) {
continue;
}
int target_position = getItemPosition(item) + delta_pos;
if (isClip(item)) {
ok = ok && requestClipMove(item, current_track_id, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true);
} else {
ok = ok &&
requestCompositionMove(item, current_track_id, m_allCompositions[item]->getForcedTrack(), target_position, updateThisView, finalMove, local_undo, local_redo);
}
}
if (!ok) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
} else {
// Track changed
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;
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, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, true);
} 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 is processed in requestClipDeletion
return false;
}
}
for (int compo : all_compositions) {
bool res = requestCompositionDeletion(compo, undo, redo);
if (!res) {
undo();
return false;
}
}
return true;
}
const QVariantList TimelineModel::getGroupData(int itemId)
{
QWriteLocker locker(&m_lock);
if (!m_groups->isInGroup(itemId)) {
return {itemId, getItemPosition(itemId), getItemPlaytime(itemId)};
}
int groupId = m_groups->getRootId(itemId);
QVariantList result;
std::unordered_set items = m_groups->getLeaves(groupId);
for (int id : items) {
result << id << getItemPosition(id) << getItemPlaytime(id);
}
return result;
}
void TimelineModel::processGroupResize(QVariantList startPos, QVariantList endPos, bool right)
{
Q_ASSERT(startPos.size() == endPos.size());
QMap> startData;
QMap> endData;
while (!startPos.isEmpty()) {
int id = startPos.takeFirst().toInt();
int in = startPos.takeFirst().toInt();
int duration = startPos.takeFirst().toInt();
startData.insert(id, {in, duration});
id = endPos.takeFirst().toInt();
in = endPos.takeFirst().toInt();
duration = endPos.takeFirst().toInt();
endData.insert(id, {in, duration});
}
QMapIterator> i(startData);
QList changedItems;
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = true;
while (i.hasNext()) {
i.next();
QPair startItemPos = i.value();
QPair endItemPos = endData.value(i.key());
if (startItemPos.first != endItemPos.first || startItemPos.second != endItemPos.second) {
// Revert individual items to original position
requestItemResize(i.key(), startItemPos.second, right, false, 0, true);
changedItems << i.key();
}
}
for (int id : changedItems) {
QPair endItemPos = endData.value(id);
result = result & requestItemResize(id, endItemPos.second, right, true, undo, redo, false);
if (!result) {
break;
}
}
if (result) {
PUSH_UNDO(undo, redo, i18n("Resize group"));
} else {
undo();
}
}
const std::vector TimelineModel::getBoundaries(int itemId)
{
std::vector boundaries;
std::unordered_set items;
if (m_groups->isInGroup(itemId)) {
int groupId = m_groups->getRootId(itemId);
items = m_groups->getLeaves(groupId);
} else {
items.insert(itemId);
}
for (int id : items) {
if (isClip(id) || isComposition(id)) {
int in = getItemPosition(id);
int out = in + getItemPlaytime(id);
boundaries.push_back(in);
boundaries.push_back(out);
}
}
return boundaries;
}
int TimelineModel::requestClipResizeAndTimeWarp(int itemId, int size, bool right, int snapDistance, bool allowSingleResize, double speed)
{
QWriteLocker locker(&m_lock);
TRACE(itemId, size, right, true, snapDistance, allowSingleResize);
Q_ASSERT(isClip(itemId));
if (size <= 0) {
TRACE_RES(-1);
return -1;
}
int in = getItemPosition(itemId);
int out = in + getItemPlaytime(itemId);
//size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance);
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;
}
// First delete clip, then timewarp, resize and reinsert
int pos = getItemPosition(id);
if (!right) {
pos += getItemPlaytime(id) - size;
}
result = getTrackById(tid)->requestClipDeletion(id, true, true, undo, redo, false, true);
result = result && requestClipTimeWarp(id, speed, false, undo, redo);
result = result && requestItemResize(id, size, true, true, undo, redo);
result = result && getTrackById(tid)->requestClipInsertion(id, pos, true, true, undo, redo);
if (!result) {
break;
}
}
if (!result) {
bool undone = undo();
Q_ASSERT(undone);
TRACE_RES(-1);
return -1;
}
if (result) {
PUSH_UNDO(undo, redo, i18n("Resize clip speed"));
}
int res = result ? size : -1;
TRACE_RES(res);
return res;
}
int TimelineModel::requestItemResizeInfo(int itemId, int in, int out, int size, bool right, int snapDistance)
{
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, getBoundaries(itemId), 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;
}
}
}
return size;
}
int TimelineModel::requestItemSpeedChange(int itemId, int size, bool right, int snapDistance)
{
Q_ASSERT(isClip(itemId));
QWriteLocker locker(&m_lock);
TRACE(itemId, size, right, snapDistance);
Q_ASSERT(isItem(itemId));
if (size <= 0) {
TRACE_RES(-1);
return -1;
}
int in = getItemPosition(itemId);
int out = in + getItemPlaytime(itemId);
if (right && size > out - in) {
int targetPos = in + size - 1;
int trackId = getItemTrackId(itemId);
if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, out + 1, targetPos, false).empty()) {
size = getTrackById_const(trackId)->getBlankEnd(out + 1) - in;
}
} else if (!right && size > (out - in)) {
int targetPos = out - size;
int trackId = getItemTrackId(itemId);
if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, targetPos, in - 1, false).empty()) {
size = out - getTrackById_const(trackId)->getBlankStart(in - 1);
}
}
int timelinePos = pCore->getTimelinePosition();
m_snaps->addPoint(timelinePos);
int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance);
m_snaps->removePoint(timelinePos);
qDebug()<<"==== RESIZE REQUEST: "< 0 ? proposed_size : size;
}
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);
size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance);
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);
}
all_items.insert(itemId);
for (int id : items) {
if (id == itemId) {
continue;
}
int start = getItemPosition(id);
int end = start + 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;
int finalPos = right ? in + size : out - size;
int finalSize;
int resizedCount = 0;
for (int id : all_items) {
int tid = getItemTrackId(id);
if (tid > -1 && getTrackById_const(tid)->isLocked()) {
continue;
}
if (right) {
finalSize = finalPos - getItemPosition(id);
} else {
finalSize = getItemPosition(id) + getItemPlaytime(id) - finalPos;
}
result = result && requestItemResize(id, finalSize, right, logUndo, undo, redo);
resizedCount++;
}
if (!result || resizedCount == 0) {
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; };
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) {
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();
}
int clipsCount = 0;
QList tracks;
for (int id : ids) {
if (isClip(id)) {
int trackId = getClipTrackId(id);
if (trackId == -1) {
return -1;
}
tracks << trackId;
clipsCount++;
} 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;
}
if (ids.size() == 2 && clipsCount == 2 && type == GroupType::Normal) {
// Check if we are grouping an AVSplit
std::unordered_set::const_iterator it = ids.begin();
int firstId = *it;
std::advance(it, 1);
int secondId = *it;
bool isAVGroup = false;
if (getClipBinId(firstId) == getClipBinId(secondId)) {
if (getClipState(firstId) == PlaylistState::AudioOnly) {
if (getClipState(secondId) == PlaylistState::VideoOnly) {
isAVGroup = true;
}
} else if (getClipState(secondId) == PlaylistState::AudioOnly) {
isAVGroup = true;
}
}
if (isAVGroup) {
type = GroupType::AVSplit;
}
}
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) {
if (isGroup(root)) {
result = result && requestClipUngroup(root, undo, redo);
}
}
if (!result) {
bool undone = undo();
Q_ASSERT(undone);
}
if (result && logUndo) {
PUSH_UNDO(undo, redo, i18n("Ungroup clips"));
}
TRACE_RES(result);
return result;
}
bool TimelineModel::requestClipUngroup(int itemId, bool logUndo)
{
QWriteLocker locker(&m_lock);
TRACE(itemId, logUndo);
requestClearSelection();
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = true;
result = requestClipUngroup(itemId, undo, redo);
if (result && logUndo) {
PUSH_UNDO(undo, redo, i18n("Ungroup clips"));
}
TRACE_RES(result);
return result;
}
bool TimelineModel::requestClipUngroup(int itemId, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
bool isSelection = m_groups->getType(m_groups->getRootId(itemId)) == GroupType::Selection;
if (!isSelection) {
requestClearSelection();
}
bool res = m_groups->ungroupItem(itemId, undo, redo);
if (res && !isSelection) {
// we make sure that the undo and the redo are going to unselect before doing anything else
Fun unselect = [this]() { return requestClearSelection(); };
PUSH_FRONT_LAMBDA(unselect, undo);
PUSH_FRONT_LAMBDA(unselect, redo);
}
return res;
}
bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack)
{
QWriteLocker locker(&m_lock);
TRACE(position, id, trackName, audioTrack);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo, true);
if (result) {
PUSH_UNDO(undo, redo, i18n("Insert Track"));
}
TRACE_RES(result);
return result;
}
bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView)
{
// TODO: make sure we disable overlayTrack before inserting a track
if (position == -1) {
position = (int)(m_allTracks.size());
}
if (position < 0 || position > (int)m_allTracks.size()) {
return false;
}
int previousId = -1;
if (position < (int)m_allTracks.size()) {
previousId = getTrackIndexFromPosition(position);
}
int trackId = TimelineModel::getNextId();
id = trackId;
Fun local_undo = deregisterTrack_lambda(trackId, true);
TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack);
// Adjust compositions that were affecting track at previous pos
Fun local_update = [previousId, position, this]() {
if (previousId > -1) {
for (auto &compo : m_allCompositions) {
if (compo.second->getATrack() == position && compo.second->getForcedTrack() == -1) {
compo.second->setATrack(position + 1, previousId);
}
}
}
return true;
};
local_update();
auto track = getTrackById(trackId);
Fun local_redo = [track, position, updateView, local_update, this]() {
// We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
// sufficient to register it.
registerTrack(track, position, true);
local_update();
if (updateView) {
_resetView();
}
return true;
};
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
if (updateView) {
_resetView();
}
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));
if (m_allTracks.size() < 2) {
pCore->displayMessage(i18n("Cannot delete last track in timeline"), InformationMessage, 500);
return false;
}
std::vector clips_to_delete;
for (const auto &it : getTrackById(trackId)->m_allClips) {
clips_to_delete.push_back(it.first);
}
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
for (int clip : clips_to_delete) {
bool res = true;
while (res && m_groups->isInGroup(clip)) {
res = requestClipUngroup(clip, local_undo, local_redo);
}
if (res) {
res = requestClipDeletion(clip, local_undo, local_redo);
}
if (!res) {
bool u = local_undo();
Q_ASSERT(u);
return false;
}
}
int old_position = getTrackPosition(trackId);
auto operation = deregisterTrack_lambda(trackId, true);
std::shared_ptr track = getTrackById(trackId);
Fun reverse = [this, track, old_position]() {
// We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
// sufficient to register it.
registerTrack(track, old_position);
_resetView();
return true;
};
if (operation()) {
UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
local_undo();
return false;
}
void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert)
{
// qDebug() << "REGISTER TRACK" << track->getId() << pos;
int id = track->getId();
if (pos == -1) {
pos = static_cast(m_allTracks.size());
}
Q_ASSERT(pos >= 0);
Q_ASSERT(pos <= static_cast(m_allTracks.size()));
// effective insertion (MLT operation), add 1 to account for black background track
if (doInsert) {
int error = m_tractor->insert_track(*track, pos + 1);
Q_ASSERT(error == 0); // we might need better error handling...
}
// we now insert in the list
auto posIt = m_allTracks.begin();
std::advance(posIt, pos);
auto it = m_allTracks.insert(posIt, std::move(track));
// it now contains the iterator to the inserted element, we store it
Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen)
m_iteratorTable[id] = it;
beginInsertRows(QModelIndex(), pos, pos);
endInsertRows();
int cache = (int)QThread::idealThreadCount() + ((int)m_allTracks.size() + 1) * 2;
mlt_service_cache_set_size(NULL, "producer_avformat", qMax(4, cache));
}
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;
emit checkTrackDeletion(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
beginRemoveRows(QModelIndex(), index, index);
endInsertRows();
if (updateView) {
_resetView();
}
int cache = (int)QThread::idealThreadCount() + ((int)m_allTracks.size() + 1) * 2;
mlt_service_cache_set_size(NULL, "producer_avformat", qMax(4, cache));
return true;
};
}
Fun TimelineModel::deregisterClip_lambda(int clipId)
{
return [this, clipId]() {
// qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size();
requestClearSelection(true);
clearAssetView(clipId);
Q_ASSERT(m_allClips.count(clipId) > 0);
Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point
Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point
auto clip = m_allClips[clipId];
m_allClips.erase(clipId);
clip->deregisterClipToBin();
m_groups->destructGroupItem(clipId);
return true;
};
}
void TimelineModel::deregisterGroup(int id)
{
Q_ASSERT(m_allGroups.count(id) > 0);
m_allGroups.erase(id);
}
std::shared_ptr TimelineModel::getTrackById(int trackId)
{
Q_ASSERT(m_iteratorTable.count(trackId) > 0);
return *m_iteratorTable[trackId];
}
const std::shared_ptr TimelineModel::getTrackById_const(int trackId) const
{
Q_ASSERT(m_iteratorTable.count(trackId) > 0);
return *m_iteratorTable.at(trackId);
}
bool TimelineModel::addTrackEffect(int trackId, const QString &effectId)
{
Q_ASSERT(m_iteratorTable.count(trackId) > 0);
if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) {
QString effectName = EffectsRepository::get()->getName(effectId);
pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500);
return false;
}
return true;
}
bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId)
{
QStringList source = sourceId.split(QLatin1Char('-'));
Q_ASSERT(m_iteratorTable.count(trackId) > 0 && source.count() == 3);
int itemType = source.at(0).toInt();
int itemId = source.at(1).toInt();
int itemRow = source.at(2).toInt();
std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId);
if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) {
pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500);
return false;
}
return true;
}
std::shared_ptr TimelineModel::getClipPtr(int clipId) const
{
Q_ASSERT(m_allClips.count(clipId) > 0);
return m_allClips.at(clipId);
}
bool TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify)
{
Q_ASSERT(m_allClips.count(clipId) > 0);
bool result = m_allClips.at(clipId)->addEffect(effectId);
if (!result && notify) {
QString effectName = EffectsRepository::get()->getName(effectId);
pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500);
}
return result;
}
bool TimelineModel::removeFade(int clipId, bool fromStart)
{
Q_ASSERT(m_allClips.count(clipId) > 0);
return m_allClips.at(clipId)->removeFade(fromStart);
}
std::shared_ptr TimelineModel::getClipEffectStack(int itemId)
{
Q_ASSERT(m_allClips.count(itemId));
return m_allClips.at(itemId)->m_effectStack;
}
bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId)
{
QStringList source = sourceId.split(QLatin1Char('-'));
Q_ASSERT(m_allClips.count(clipId) && source.count() == 3);
int itemType = source.at(0).toInt();
int itemId = source.at(1).toInt();
int itemRow = source.at(2).toInt();
std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId);
return m_allClips.at(clipId)->copyEffect(effectStack, itemRow);
}
bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration)
{
Q_ASSERT(m_allClips.count(clipId));
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo);
if (res && initialDuration > 0) {
PUSH_UNDO(undo, redo, i18n("Adjust Fade"));
}
return res;
}
std::shared_ptr TimelineModel::getCompositionPtr(int compoId) const
{
Q_ASSERT(m_allCompositions.count(compoId) > 0);
return m_allCompositions.at(compoId);
}
int TimelineModel::getNextId()
{
return TimelineModel::next_id++;
}
bool TimelineModel::isClip(int id) const
{
return m_allClips.count(id) > 0;
}
bool TimelineModel::isComposition(int id) const
{
return m_allCompositions.count(id) > 0;
}
bool TimelineModel::isItem(int id) const
{
return isClip(id) || isComposition(id);
}
bool TimelineModel::isTrack(int id) const
{
return m_iteratorTable.count(id) > 0;
}
bool TimelineModel::isGroup(int id) const
{
return m_allGroups.count(id) > 0;
}
void TimelineModel::updateDuration()
{
if (m_closing) {
return;
}
int current = m_blackClip->get_playtime() - TimelineModel::seekDuration;
int duration = 0;
for (const auto &tck : m_iteratorTable) {
auto track = (*tck.second);
duration = qMax(duration, track->trackDuration());
}
if (duration != current) {
// update black track length
m_blackClip->set("out", 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, false);
}
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 a_track, and increasing 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) {
if (m_allCompositions[a.second]->getATrack() == m_allCompositions[b.second]->getATrack()) {
return a.first < b.first;
}
return m_allCompositions[a.second]->getATrack() > m_allCompositions[b.second]->getATrack();
});
// 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());
Mlt::Transition &transition = *m_allCompositions[compo.second].get();
int ret = field->plant_transition(transition, 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;
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;
}
std::shared_ptr TimelineModel::getMasterEffectStackModel()
{
READ_LOCK();
if (m_masterStack == nullptr) {
m_masterService.reset(new Mlt::Service(*m_tractor.get()));
m_masterStack = EffectStackModel::construct(m_masterService, {ObjectType::Master, 0}, m_undoStack);
}
return m_masterStack;
}
void TimelineModel::importMasterEffects(std::weak_ptr service)
{
READ_LOCK();
if (m_masterStack == nullptr) {
getMasterEffectStackModel();
}
m_masterStack->importEffects(std::move(service), PlaylistState::Disabled);
}
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, false, false);
}
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, bool changeDuration, 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, false, false);
}
if (success) {
success = m_allClips[clipId]->useTimewarpProducer(speed, changeDuration, 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, bool changeDuration)
{
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, changeDuration, undo, redo);
}
if (result) {
result = requestClipTimeWarp(clipId, speed / 100.0, changeDuration, undo, redo);
}
if (!result) {
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, changeDuration, 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.cbegin();
bool found = false;
while ((isAudio || !found) && it != m_allTracks.cend()) {
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.cbegin()) {
--it;
if ((*it)->isAudioTrack() == audioWanted) {
return (*it)->getId();
}
}
return trackId;
}
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.cend()) {
++it;
if (it != m_allTracks.cend() && (*it)->isAudioTrack() == audioWanted) {
break;
}
}
return it == m_allTracks.cend() ? 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)) {
// Reset offset display on clips
std::unordered_set items = m_groups->getLeaves(m_currentSelection);
for (auto &id : items) {
if (isGroup(id)) {
std::unordered_set children = m_groups->getLeaves(id);
items.insert(children.begin(), children.end());
} else if (isClip(id)) {
m_allClips[id]->clearOffset();
m_allClips[id]->setGrab(false);
m_allClips[id]->setSelected(false);
} else if (isComposition(id)) {
m_allCompositions[id]->setGrab(false);
m_allCompositions[id]->setSelected(false);
}
if (m_groups->getType(m_currentSelection) == GroupType::Selection) {
m_groups->destructGroupItem(m_currentSelection);
}
}
} else {
if (isClip(m_currentSelection)) {
m_allClips[m_currentSelection]->setGrab(false);
m_allClips[m_currentSelection]->setSelected(false);
} else if (isComposition(m_currentSelection)) {
m_allCompositions[m_currentSelection]->setGrab(false);
m_allCompositions[m_currentSelection]->setSelected(false);
}
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());
setSelected(m_currentSelection, true);
} else {
Fun undo = []() { return true; };
Fun redo = []() { return true; };
if (ids.size() == 2) {
// Check if we selected 2 clips from the same master
QList pairIds;
for (auto &id : roots) {
if (isClip(id)) {
pairIds << id;
}
}
if (pairIds.size() == 2 && getClipBinId(pairIds.at(0)) == getClipBinId(pairIds.at(1))) {
// Check if they have same bin id
// Both clips have same bin ID, display offset
int pos1 = getClipPosition(pairIds.at(0));
int pos2 = getClipPosition(pairIds.at(1));
if (pos2 > pos1) {
int offset = pos2 - getClipIn(pairIds.at(1)) - (pos1 - getClipIn(pairIds.at(0)));
if (offset != 0) {
m_allClips[pairIds.at(1)]->setOffset(offset);
m_allClips[pairIds.at(0)]->setOffset(-offset);
}
} else {
int offset = pos1 - getClipIn(pairIds.at(0)) - (pos2 - getClipIn(pairIds.at(1)));
if (offset != 0) {
m_allClips[pairIds.at(0)]->setOffset(offset);
m_allClips[pairIds.at(1)]->setOffset(-offset);
}
}
}
}
result = (m_currentSelection = m_groups->groupItems(ids, undo, redo, GroupType::Selection)) >= 0;
Q_ASSERT(m_currentSelection >= 0);
}
emit selectionChanged();
return result;
}
void TimelineModel::setSelected(int itemId, bool sel)
{
if (isClip(itemId)) {
m_allClips[itemId]->setSelected(sel);
} else if (isComposition(itemId)) {
m_allCompositions[itemId]->setSelected(sel);
} else if (isGroup(itemId)) {
auto leaves = m_groups->getLeaves(itemId);
for (auto &id : leaves) {
setSelected(id, true);
}
}
}
bool TimelineModel::requestSetSelection(const std::unordered_set &ids, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
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;
}
void TimelineModel::setTrackLockedState(int trackId, bool lock)
{
QWriteLocker locker(&m_lock);
TRACE(trackId, lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
Fun lock_lambda = [this, trackId]() {
getTrackById(trackId)->lock();
return true;
};
Fun unlock_lambda = [this, trackId]() {
getTrackById(trackId)->unlock();
return true;
};
if (lock) {
if (lock_lambda()) {
UPDATE_UNDO_REDO(lock_lambda, unlock_lambda, undo, redo);
PUSH_UNDO(undo, redo, i18n("Lock track"));
}
} else {
if (unlock_lambda()) {
UPDATE_UNDO_REDO(unlock_lambda, lock_lambda, undo, redo);
PUSH_UNDO(undo, redo, i18n("Unlock track"));
}
}
}
std::unordered_set TimelineModel::getAllTracksIds() const
{
READ_LOCK();
std::unordered_set result;
std::transform(m_iteratorTable.begin(), m_iteratorTable.end(), std::inserter(result, result.begin()), [&](const auto &track) { return track.first; });
return result;
}
void TimelineModel::switchComposition(int cid, const QString &compoId)
{
Q_ASSERT(isComposition(cid));
std::shared_ptr compo = m_allCompositions.at(cid);
int currentPos = compo->getPosition();
int duration = compo->getPlaytime();
int currentTrack = compo->getCurrentTrackId();
int a_track = compo->getATrack();
int forcedTrack = compo->getForcedTrack();
Fun undo = []() { return true; };
Fun redo = []() { return true; };
// Clear selection
requestClearSelection(true);
if (m_groups->isInGroup(cid)) {
pCore->displayMessage(i18n("Cannot operate on grouped composition, please ungroup"), ErrorMessage);
return;
}
bool res = requestCompositionDeletion(cid, undo, redo);
int newId;
res = res && requestCompositionInsertion(compoId, currentTrack, a_track, currentPos, duration, nullptr, newId, undo, redo);
if (res) {
if (forcedTrack > -1 && isComposition(newId)) {
m_allCompositions[newId]->setForceTrack(true);
}
Fun local_redo = [newId, this]() {
requestSetSelection({newId});
return true;
};
Fun local_undo = [cid, this]() {
requestSetSelection({cid});
return true;
};
local_redo();
PUSH_LAMBDA(local_redo, redo);
PUSH_LAMBDA(local_undo, undo);
PUSH_UNDO(undo, redo, i18n("Change composition"));
} else {
undo();
}
}
diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp
index 3c34feb5d..580ff06e7 100644
--- a/src/timeline2/view/timelinecontroller.cpp
+++ b/src/timeline2/view/timelinecontroller.cpp
@@ -1,2697 +1,2701 @@
/***************************************************************************
* Copyright (C) 2017 by Jean-Baptiste Mardelle *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "timelinecontroller.h"
#include "../model/timelinefunctions.hpp"
#include "assets/keyframes/model/keyframemodellist.hpp"
#include "bin/bin.h"
#include "bin/clipcreator.hpp"
#include "bin/model/markerlistmodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectfolder.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "dialogs/spacerdialog.h"
#include "dialogs/speeddialog.h"
#include "doc/kdenlivedoc.h"
#include "effects/effectsrepository.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "kdenlivesettings.h"
#include "lib/audio/audioEnvelope.h"
#include "mainwindow.h"
#include "monitor/monitormanager.h"
#include "previewmanager.h"
#include "project/projectmanager.h"
#include "timeline2/model/clipmodel.hpp"
#include "timeline2/model/compositionmodel.hpp"
#include "timeline2/model/groupsmodel.hpp"
#include "timeline2/model/timelineitemmodel.hpp"
#include "timeline2/model/trackmodel.hpp"
#include "timeline2/view/dialogs/clipdurationdialog.h"
#include "timeline2/view/dialogs/trackdialog.h"
#include "transitions/transitionsrepository.hpp"
#include "audiomixer/mixermanager.hpp"
#include
#include
#include
#include
#include
#include
int TimelineController::m_duration = 0;
TimelineController::TimelineController(QObject *parent)
: QObject(parent)
, m_root(nullptr)
, m_usePreview(false)
, m_position(0)
, m_seekPosition(-1)
, m_activeTrack(0)
, m_audioRef(-1)
, m_zone(-1, -1)
, m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250)
, m_timelinePreview(nullptr)
{
m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview"));
connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview);
connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
m_disablePreview->setEnabled(false);
connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording);
connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged);
connect(pCore->mixer(), &MixerManager::recordAudio, this, &TimelineController::switchRecording);
}
TimelineController::~TimelineController()
{
prepareClose();
}
void TimelineController::prepareClose()
{
// Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate
delete m_timelinePreview;
m_timelinePreview = nullptr;
}
void TimelineController::setModel(std::shared_ptr model)
{
delete m_timelinePreview;
m_zone = QPoint(-1, -1);
m_timelinePreview = nullptr;
m_model = std::move(model);
connect(m_model.get(), &TimelineItemModel::requestClearAssetView, [&](int id) { pCore->clearAssetPanel(id); });
connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); });
connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection);
connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration);
connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection);
}
void TimelineController::setTargetTracks(bool hasVideo, QList audioTargets)
{
int videoTrack = -1;
QList audioTracks;
m_hasVideoTarget = hasVideo;
m_hasAudioTarget = !audioTargets.isEmpty();
if (m_hasVideoTarget) {
videoTrack = m_model->getFirstVideoTrackIndex();
}
if (m_hasAudioTarget) {
QList tracks;
auto it = m_model->m_allTracks.cbegin();
while (it != m_model->m_allTracks.cend()) {
if ((*it)->isAudioTrack()) {
tracks << (*it)->getId();
}
++it;
}
int i = 0;
while (i < audioTargets.size() && !tracks.isEmpty()) {
audioTracks << tracks.takeLast();
i++;
}
}
emit hasAudioTargetChanged();
emit hasVideoTargetChanged();
if (m_videoTargetActive) {
setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack);
}
if (m_audioTargetActive) {
setIntAudioTarget((m_hasAudioTarget && (m_lastAudioTarget.size() == audioTargets.size())) ? m_lastAudioTarget : audioTracks);
}
}
std::shared_ptr TimelineController::getModel() const
{
return m_model;
}
void TimelineController::setRoot(QQuickItem *root)
{
m_root = root;
}
Mlt::Tractor *TimelineController::tractor()
{
return m_model->tractor();
}
double TimelineController::scaleFactor() const
{
return m_scale;
}
const QString TimelineController::getTrackNameFromMltIndex(int trackPos)
{
if (trackPos == -1) {
return i18n("unknown");
}
if (trackPos == 0) {
return i18n("Black");
}
return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1));
}
const QString TimelineController::getTrackNameFromIndex(int trackIndex)
{
QString trackName = m_model->getTrackFullName(trackIndex);
return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName;
}
QMap TimelineController::getTrackNames(bool videoOnly)
{
QMap names;
for (const auto &track : m_model->m_iteratorTable) {
if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) {
continue;
}
QString trackName = m_model->getTrackFullName(track.first);
names[m_model->getTrackMltIndex(track.first)] = trackName;
}
return names;
}
void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse)
{
if (m_root) {
m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMin(getMousePos(), duration()) : -1);
m_scale = scale;
emit scaleFactorChanged();
} else {
qWarning("Timeline root not created, impossible to zoom in");
}
}
void TimelineController::setScaleFactor(double scale)
{
m_scale = scale;
// Update mainwindow's zoom slider
emit updateZoom(scale);
// inform qml
emit scaleFactorChanged();
}
int TimelineController::duration() const
{
return m_duration;
}
int TimelineController::fullDuration() const
{
return m_duration + TimelineModel::seekDuration;
}
void TimelineController::checkDuration()
{
int currentLength = m_model->duration();
if (currentLength != m_duration) {
m_duration = currentLength;
emit durationChanged();
}
}
int TimelineController::selectedTrack() const
{
std::unordered_set sel = m_model->getCurrentSelection();
if (sel.empty()) return -1;
std::vector> selected_tracks; // contains pairs of (track position, track id) for each selected item
for (int s : sel) {
int tid = m_model->getItemTrackId(s);
selected_tracks.push_back({m_model->getTrackPosition(tid), tid});
}
// sort by track position
std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; });
return selected_tracks.front().second;
}
void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent)
{
QList toSelect;
int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, timelinePosition())
: m_model->getCompositionByPosition(m_activeTrack, timelinePosition());
if (currentClip == -1) {
pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500);
return;
}
if (!select) {
m_model->requestRemoveFromSelection(currentClip);
} else {
m_model->requestAddToSelection(currentClip, !addToCurrent);
}
}
QList TimelineController::selection() const
{
if (!m_root) return QList();
std::unordered_set sel = m_model->getCurrentSelection();
QList items;
for (int id : sel) {
items << id;
}
return items;
}
void TimelineController::selectItems(const QList &ids)
{
std::unordered_set ids_s(ids.begin(), ids.end());
m_model->requestSetSelection(ids_s);
}
void TimelineController::setScrollPos(int pos)
{
if (pos > 0 && m_root) {
QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos));
}
}
void TimelineController::resetView()
{
m_model->_resetView();
if (m_root) {
QMetaObject::invokeMethod(m_root, "updatePalette");
}
emit colorsChanged();
}
bool TimelineController::snap()
{
return KdenliveSettings::snaptopoints();
}
bool TimelineController::ripple()
{
return false;
}
bool TimelineController::scrub()
{
return false;
}
int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets)
{
int id;
if (tid == -1) {
tid = m_activeTrack;
}
if (position == -1) {
position = timelinePosition();
}
if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) {
id = -1;
}
return id;
}
QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView)
{
QList clipIds;
if (tid == -1) {
tid = m_activeTrack;
}
if (position == -1) {
position = timelinePosition();
}
TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView);
// we don't need to check the return value of the above function, in case of failure it will return an empty list of ids.
return clipIds;
}
int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo)
{
int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
if (clipId > 0) {
int minimum = m_model->getClipPosition(clipId);
return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo);
}
return insertComposition(tid, position, transitionId, logUndo);
}
int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo)
{
int id;
int minimumPos = m_model->getClipPosition(clipId);
int clip_duration = m_model->getClipPlaytime(clipId);
int endPos = minimumPos + clip_duration;
int position = minimumPos;
int duration = qMin(pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()), m_model->getTrackById_const(tid)->suggestCompositionLength(position));
int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid);
bool revert = offset > clip_duration / 2;
if (lowerVideoTrackId > 0) {
int bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position + offset);
if (bottomId > 0) {
QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime());
if (bottom.first > minimumPos) {
// Lower clip is after top clip
if (position + offset > bottom.first) {
int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first);
if (test_duration > 0) {
offset -= (bottom.first - position);
position = bottom.first;
duration = test_duration;
revert = position > minimumPos;
}
}
} else if (position >= bottom.first) {
// Lower clip is before or at same pos as top clip
int test_duration = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position);
if (test_duration > 0) {
duration = qMin(test_duration, clip_duration);
}
}
} else {
qDebug()<<"///// NO CLIP FOUND BELOW!!!";
}
} else {
qDebug()<<"///// NO TRACK FOUND BELOW!!!";
}
if (duration < 0) {
duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration());
} else if (duration <= 1) {
// if suggested composition duration is lower than 4 frames, use default
duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration());
if (minimumPos + clip_duration - position < 3) {
position = minimumPos + clip_duration - duration;
}
}
QPair finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos);
position = finalPos.first;
duration = finalPos.second;
std::unique_ptr props(nullptr);
if (revert) {
props = std::make_unique();
if (transitionId == QLatin1String("dissolve")) {
props->set("reverse", 1);
} else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) {
props->set("invert", 1);
} else if (transitionId == QLatin1String("wipe")) {
props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0");
}
}
if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
id = -1;
pCore->displayMessage(i18n("Could not add composition at selected position"), InformationMessage, 500);
}
return id;
}
int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo)
{
int id;
int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration());
if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) {
id = -1;
}
return id;
}
void TimelineController::deleteSelectedClips()
{
auto sel = m_model->getCurrentSelection();
if (sel.empty()) {
return;
}
// only need to delete the first item, the others will be deleted in cascade
m_model->requestItemDeletion(*sel.begin());
}
int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition)
{
auto sel = m_model->getCurrentSelection();
if (sel.empty() || sel.size() > 2) {
return -1;
}
int itemId = *(sel.begin());
if (sel.size() == 2) {
int parentGroup = m_model->m_groups->getRootId(itemId);
if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) {
return -1;
}
}
if (!restrictToCurrentPos) {
if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) {
return itemId;
}
}
if (m_model->isClip(itemId)) {
int position = timelinePosition();
int start = m_model->getClipPosition(itemId);
int end = start + m_model->getClipPlaytime(itemId);
if (position >= start && position <= end) {
return itemId;
}
}
return -1;
}
void TimelineController::copyItem()
{
std::unordered_set selectedIds = m_model->getCurrentSelection();
if (selectedIds.empty()) {
return;
}
int clipId = *(selectedIds.begin());
QString copyString = TimelineFunctions::copyClips(m_model, selectedIds);
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(copyString);
m_root->setProperty("copiedClip", clipId);
m_model->requestSetSelection(selectedIds);
}
bool TimelineController::pasteItem(int position, int tid)
{
QClipboard *clipboard = QApplication::clipboard();
QString txt = clipboard->text();
if (tid == -1) {
tid = getMouseTrack();
}
if (position == -1) {
position = getMousePos();
}
if (tid == -1) {
tid = m_activeTrack;
}
if (position == -1) {
position = timelinePosition();
}
return TimelineFunctions::pasteClips(m_model, txt, tid, position);
}
void TimelineController::triggerAction(const QString &name)
{
pCore->triggerAction(name);
}
QString TimelineController::timecode(int frames) const
{
return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
}
QString TimelineController::framesToClock(int frames) const
{
return m_model->tractor()->frames_to_time(frames, mlt_time_clock);
}
QString TimelineController::simplifiedTC(int frames) const
{
if (KdenliveSettings::frametimecode()) {
return QString::number(frames);
}
QString s = m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
return s.startsWith(QLatin1String("00:")) ? s.remove(0, 3) : s;
}
bool TimelineController::showThumbnails() const
{
return KdenliveSettings::videothumbnails();
}
bool TimelineController::showAudioThumbnails() const
{
return KdenliveSettings::audiothumbnails();
}
bool TimelineController::showMarkers() const
{
return KdenliveSettings::showmarkers();
}
bool TimelineController::audioThumbFormat() const
{
return KdenliveSettings::displayallchannels();
}
bool TimelineController::showWaveforms() const
{
return KdenliveSettings::audiothumbnails();
}
void TimelineController::addTrack(int tid)
{
if (tid == -1) {
tid = m_activeTrack;
}
QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow());
if (d->exec() == QDialog::Accepted) {
int newTid;
bool audioRecTrack = d->addRecTrack();
bool addAVTrack = d->addAVTrack();
m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack());
if (addAVTrack) {
int newTid2;
int mirrorPos = 0;
int mirrorId = m_model->getMirrorAudioTrackId(newTid);
if (mirrorId > -1) {
mirrorPos = m_model->getTrackMltIndex(mirrorId);
}
m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true);
}
m_model->buildTrackCompositing(true);
if (audioRecTrack) {
m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1"));
}
}
}
void TimelineController::deleteTrack(int tid)
{
if (tid == -1) {
tid = m_activeTrack;
}
QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true);
if (d->exec() == QDialog::Accepted) {
int selectedTrackIx = d->selectedTrackId();
m_model->requestTrackDeletion(selectedTrackIx);
m_model->buildTrackCompositing(true);
if (m_activeTrack == -1) {
setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1));
}
}
}
void TimelineController::checkTrackDeletion(int selectedTrackIx)
{
if (m_activeTrack == selectedTrackIx) {
// Make sure we don't keep an index on a deleted track
m_activeTrack = -1;
}
if (m_model->m_audioTarget == selectedTrackIx) {
setAudioTarget(-1);
}
if (m_model->m_videoTarget == selectedTrackIx) {
setVideoTarget(-1);
}
if (m_lastAudioTarget.contains(selectedTrackIx)) {
m_lastAudioTarget.removeAll(selectedTrackIx);
emit lastAudioTargetChanged();
}
if (m_lastVideoTarget == selectedTrackIx) {
m_lastVideoTarget = -1;
emit lastVideoTargetChanged();
}
}
void TimelineController::showConfig(int page, int tab)
{
pCore->showConfigDialog(page, tab);
}
void TimelineController::gotoNextSnap()
{
int nextSnap = m_model->getNextSnapPos(timelinePosition());
if (nextSnap > timelinePosition()) {
setPosition(nextSnap);
}
}
void TimelineController::gotoPreviousSnap()
{
if (timelinePosition() > 0) {
setPosition(m_model->getPreviousSnapPos(timelinePosition()));
}
}
void TimelineController::groupSelection()
{
const auto selection = m_model->getCurrentSelection();
if (selection.size() < 2) {
pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500);
return;
}
m_model->requestClearSelection();
m_model->requestClipsGroup(selection);
m_model->requestSetSelection(selection);
}
void TimelineController::unGroupSelection(int cid)
{
auto ids = m_model->getCurrentSelection();
// ask to unselect if needed
m_model->requestClearSelection();
if (cid > -1) {
ids.insert(cid);
}
if (!ids.empty()) {
m_model->requestClipsUngroup(ids);
}
}
bool TimelineController::dragOperationRunning()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue));
return returnedValue.toBool();
}
void TimelineController::setInPoint()
{
if (dragOperationRunning()) {
// Don't allow timeline operation while drag in progress
qDebug() << "Cannot operate while dragging";
return;
}
int cursorPos = timelinePosition();
const auto selection = m_model->getCurrentSelection();
bool selectionFound = false;
if (!selection.empty()) {
for (int id : selection) {
int start = m_model->getItemPosition(id);
if (start == cursorPos) {
continue;
}
int size = start + m_model->getItemPlaytime(id) - cursorPos;
m_model->requestItemResize(id, size, false, true, 0, false);
selectionFound = true;
}
}
if (!selectionFound) {
if (m_activeTrack >= 0) {
int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
if (cid < 0) {
// Check first item after timeline position
int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos);
if (maximumSpace < INT_MAX) {
cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1);
}
}
if (cid >= 0) {
int start = m_model->getItemPosition(cid);
if (start != cursorPos) {
int size = start + m_model->getItemPlaytime(cid) - cursorPos;
m_model->requestItemResize(cid, size, false, true, 0, false);
}
}
}
}
}
int TimelineController::timelinePosition() const
{
return m_seekPosition >= 0 ? m_seekPosition : m_position;
}
void TimelineController::setOutPoint()
{
if (dragOperationRunning()) {
// Don't allow timeline operation while drag in progress
qDebug() << "Cannot operate while dragging";
return;
}
int cursorPos = timelinePosition();
const auto selection = m_model->getCurrentSelection();
bool selectionFound = false;
if (!selection.empty()) {
for (int id : selection) {
int start = m_model->getItemPosition(id);
if (start + m_model->getItemPlaytime(id) == cursorPos) {
continue;
}
int size = cursorPos - start;
m_model->requestItemResize(id, size, true, true, 0, false);
selectionFound = true;
}
}
if (!selectionFound) {
if (m_activeTrack >= 0) {
int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
if (cid < 0) {
// Check first item after timeline position
int minimumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos);
cid = m_model->getClipByPosition(m_activeTrack, qMax(0, minimumSpace - 1));
}
if (cid >= 0) {
int start = m_model->getItemPosition(cid);
if (start + m_model->getItemPlaytime(cid) != cursorPos) {
int size = cursorPos - start;
m_model->requestItemResize(cid, size, true, true, 0, false);
}
}
}
}
}
void TimelineController::editMarker(int cid, int position)
{
Q_ASSERT(m_model->isClip(cid));
double speed = m_model->getClipSpeed(cid);
if (position == -1) {
// Calculate marker position relative to timeline cursor
position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
position = position * speed;
}
if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500);
return;
}
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
if (clip->getMarkerModel()->hasMarker(position)) {
GenTime pos(position, pCore->getCurrentFps());
clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get());
} else {
pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500);
}
}
void TimelineController::addMarker(int cid, int position)
{
Q_ASSERT(m_model->isClip(cid));
double speed = m_model->getClipSpeed(cid);
if (position == -1) {
// Calculate marker position relative to timeline cursor
position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
position = position * speed;
}
if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500);
return;
}
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
GenTime pos(position, pCore->getCurrentFps());
clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get());
}
void TimelineController::addQuickMarker(int cid, int position)
{
Q_ASSERT(m_model->isClip(cid));
double speed = m_model->getClipSpeed(cid);
if (position == -1) {
// Calculate marker position relative to timeline cursor
position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
position = position * speed;
}
if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500);
return;
}
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
GenTime pos(position, pCore->getCurrentFps());
CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type());
clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType());
}
void TimelineController::deleteMarker(int cid, int position)
{
Q_ASSERT(m_model->isClip(cid));
double speed = m_model->getClipSpeed(cid);
if (position == -1) {
// Calculate marker position relative to timeline cursor
position = timelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
position = position * speed;
}
if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500);
return;
}
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
GenTime pos(position, pCore->getCurrentFps());
clip->getMarkerModel()->removeMarker(pos);
}
void TimelineController::deleteAllMarkers(int cid)
{
Q_ASSERT(m_model->isClip(cid));
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid));
clip->getMarkerModel()->removeAllMarkers();
}
void TimelineController::editGuide(int frame)
{
if (frame == -1) {
frame = timelinePosition();
}
auto guideModel = pCore->projectManager()->current()->getGuideModel();
GenTime pos(frame, pCore->getCurrentFps());
guideModel->editMarkerGui(pos, qApp->activeWindow(), false);
}
void TimelineController::moveGuide(int frame, int newFrame)
{
auto guideModel = pCore->projectManager()->current()->getGuideModel();
GenTime pos(frame, pCore->getCurrentFps());
GenTime newPos(newFrame, pCore->getCurrentFps());
guideModel->editMarker(pos, newPos);
}
void TimelineController::switchGuide(int frame, bool deleteOnly)
{
bool markerFound = false;
if (frame == -1) {
frame = timelinePosition();
}
CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound);
if (!markerFound) {
if (deleteOnly) {
pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500);
return;
}
GenTime pos(frame, pCore->getCurrentFps());
pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide"));
} else {
pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time());
}
}
void TimelineController::addAsset(const QVariantMap &data)
{
QString effect = data.value(QStringLiteral("kdenlive/effect")).toString();
const auto selection = m_model->getCurrentSelection();
if (!selection.empty()) {
QList effectSelection;
for (int id : selection) {
if (m_model->isClip(id)) {
effectSelection << id;
}
}
bool foundMatch = false;
for (int id : effectSelection) {
if (m_model->addClipEffect(id, effect, false)) {
foundMatch = true;
}
}
if (!foundMatch) {
QString effectName = EffectsRepository::get()->getName(effect);
pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500);
}
} else {
pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500);
}
}
void TimelineController::requestRefresh()
{
pCore->requestMonitorRefresh();
}
void TimelineController::showAsset(int id)
{
if (m_model->isComposition(id)) {
emit showTransitionModel(id, m_model->getCompositionParameterModel(id));
} else if (m_model->isClip(id)) {
QModelIndex clipIx = m_model->makeClipIndexFromID(id);
QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString();
bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt();
qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes;
emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes);
}
}
void TimelineController::showTrackAsset(int trackId)
{
emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false);
}
void TimelineController::adjustAllTrackHeight(int trackId, int height)
{
bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack();
auto it = m_model->m_allTracks.cbegin();
while (it != m_model->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) {
m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height));
}
++it;
}
int tracksCount = m_model->getTracksCount();
QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
}
void TimelineController::setPosition(int position)
{
setSeekPosition(position);
emit seeked(position);
}
void TimelineController::setAudioTarget(int track)
{
if ((track > -1 && !m_model->isTrack(track)) || !m_hasAudioTarget) {
return;
}
m_model->m_audioTarget = track;
emit audioTargetChanged();
}
void TimelineController::setIntAudioTarget(QList tracks)
{
if ((!tracks.isEmpty() && !m_model->isTrack(tracks.first())) || !m_hasAudioTarget) {
return;
}
qDebug()<<"/// GOT AUDIO TRACKS: "<m_audioTarget = tracks.isEmpty() ? -1 : tracks.first();
emit audioTargetChanged();
}
void TimelineController::setVideoTarget(int track)
{
if ((track > -1 && !m_model->isTrack(track)) || !m_hasVideoTarget) {
return;
}
m_model->m_videoTarget = track;
emit videoTargetChanged();
}
void TimelineController::setActiveTrack(int track)
{
if (track > -1 && !m_model->isTrack(track)) {
return;
}
m_activeTrack = track;
emit activeTrackChanged();
}
void TimelineController::setSeekPosition(int position)
{
m_seekPosition = position;
emit seekPositionChanged();
}
void TimelineController::onSeeked(int position)
{
m_position = position;
emit positionChanged();
if (m_seekPosition > -1 && position == m_seekPosition) {
m_seekPosition = -1;
emit seekPositionChanged();
}
}
void TimelineController::setZone(const QPoint &zone)
{
if (m_zone.x() > 0) {
m_model->removeSnap(m_zone.x());
}
if (m_zone.y() > 0) {
m_model->removeSnap(m_zone.y() - 1);
}
if (zone.x() > 0) {
m_model->addSnap(zone.x());
}
if (zone.y() > 0) {
m_model->addSnap(zone.y() - 1);
}
m_zone = zone;
emit zoneChanged();
}
void TimelineController::setZoneIn(int inPoint)
{
if (m_zone.x() > 0) {
m_model->removeSnap(m_zone.x());
}
if (inPoint > 0) {
m_model->addSnap(inPoint);
}
m_zone.setX(inPoint);
emit zoneMoved(m_zone);
}
void TimelineController::setZoneOut(int outPoint)
{
if (m_zone.y() > 0) {
m_model->removeSnap(m_zone.y() - 1);
}
if (outPoint > 0) {
m_model->addSnap(outPoint - 1);
}
m_zone.setY(outPoint);
emit zoneMoved(m_zone);
}
void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect)
{
std::unordered_set itemsToSelect;
if (addToSelect) {
itemsToSelect = m_model->getCurrentSelection();
}
for (int i = 0; i < tracks.count(); i++) {
if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) {
continue;
}
auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, true);
itemsToSelect.insert(currentClips.begin(), currentClips.end());
}
m_model->requestSetSelection(itemsToSelect);
}
void TimelineController::requestClipCut(int clipId, int position)
{
if (position == -1) {
position = timelinePosition();
}
TimelineFunctions::requestClipCut(m_model, clipId, position);
}
void TimelineController::cutClipUnderCursor(int position, int track)
{
if (position == -1) {
position = timelinePosition();
}
QMutexLocker lk(&m_metaMutex);
bool foundClip = false;
const auto selection = m_model->getCurrentSelection();
if (track == -1) {
for (int cid : selection) {
if (m_model->isClip(cid)) {
if (TimelineFunctions::requestClipCut(m_model, cid, position)) {
foundClip = true;
// Cutting clips in the selection group is handled in TimelineFunctions
break;
}
} else {
qDebug() << "//// TODO: COMPOSITION CUT!!!";
}
}
}
if (!foundClip) {
if (track == -1) {
track = m_activeTrack;
}
if (track >= 0) {
int cid = m_model->getClipByPosition(track, position);
if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) {
foundClip = true;
}
}
}
if (!foundClip) {
pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500);
}
}
int TimelineController::requestSpacerStartOperation(int trackId, int position)
{
- return TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position);
+ QMutexLocker lk(&m_metaMutex);
+ int itemId = TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position);
+ return itemId;
}
bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition)
{
- return TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition);
+ QMutexLocker lk(&m_metaMutex);
+ bool result = TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition);
+ return result;
}
void TimelineController::seekCurrentClip(bool seekToEnd)
{
const auto selection = m_model->getCurrentSelection();
for (int cid : selection) {
int start = m_model->getItemPosition(cid);
if (seekToEnd) {
start += m_model->getItemPlaytime(cid);
}
setPosition(start);
break;
}
}
void TimelineController::seekToClip(int cid, bool seekToEnd)
{
int start = m_model->getItemPosition(cid);
if (seekToEnd) {
start += m_model->getItemPlaytime(cid);
}
setPosition(start);
}
void TimelineController::seekToMouse()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue));
int mousePos = returnedValue.toInt();
setPosition(mousePos);
}
int TimelineController::getMousePos()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue));
return returnedValue.toInt();
}
int TimelineController::getMouseTrack()
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue));
return returnedValue.toInt();
}
void TimelineController::refreshItem(int id)
{
int in = m_model->getItemPosition(id);
if (in > m_position || (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly())) {
return;
}
if (m_position <= in + m_model->getItemPlaytime(id)) {
pCore->requestMonitorRefresh();
}
}
QPoint TimelineController::getTracksCount() const
{
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue));
QVariantList tracks = returnedValue.toList();
QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt());
return p;
}
QStringList TimelineController::extractCompositionLumas() const
{
return m_model->extractCompositionLumas();
}
void TimelineController::addEffectToCurrentClip(const QStringList &effectData)
{
QList activeClips;
for (int track = m_model->getTracksCount() - 1; track >= 0; track--) {
int trackIx = m_model->getTrackIndexFromPosition(track);
int cid = m_model->getClipByPosition(trackIx, timelinePosition());
if (cid > -1) {
activeClips << cid;
}
}
if (!activeClips.isEmpty()) {
if (effectData.count() == 4) {
QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3);
m_model->copyClipEffect(activeClips.first(), effectString);
} else {
m_model->addClipEffect(activeClips.first(), effectData.constFirst());
}
}
}
void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration)
{
if (duration <= 0) {
// remove fade
m_model->removeFade(cid, effectId == QLatin1String("fadein"));
} else {
m_model->adjustEffectLength(cid, effectId, duration, initialDuration);
}
}
QPair TimelineController::getCompositionATrack(int cid) const
{
QPair result;
std::shared_ptr compo = m_model->getCompositionPtr(cid);
if (compo) {
result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId()));
}
return result;
}
void TimelineController::setCompositionATrack(int cid, int aTrack)
{
TimelineFunctions::setCompositionATrack(m_model, cid, aTrack);
}
bool TimelineController::compositionAutoTrack(int cid) const
{
std::shared_ptr compo = m_model->getCompositionPtr(cid);
return compo && compo->getForcedTrack() == -1;
}
const QString TimelineController::getClipBinId(int clipId) const
{
return m_model->getClipBinId(clipId);
}
void TimelineController::focusItem(int itemId)
{
int start = m_model->getItemPosition(itemId);
setPosition(start);
}
int TimelineController::headerWidth() const
{
return qMax(10, KdenliveSettings::headerwidth());
}
void TimelineController::setHeaderWidth(int width)
{
KdenliveSettings::setHeaderwidth(width);
}
bool TimelineController::createSplitOverlay(int clipId, std::shared_ptr filter)
{
if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) {
return true;
}
if (clipId == -1) {
pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500);
return false;
}
std::shared_ptr clip = m_model->getClipPtr(clipId);
const QString binId = clip->binId();
// Get clean bin copy of the clip
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId);
std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut()));
// Get copy of timeline producer
std::shared_ptr clipProducer(new Mlt::Producer(*clip));
// Built tractor and compositing
Mlt::Tractor trac(*m_model->m_tractor->profile());
Mlt::Playlist play(*m_model->m_tractor->profile());
Mlt::Playlist play2(*m_model->m_tractor->profile());
play.append(*clipProducer.get());
play2.append(*binProd);
trac.set_track(play, 0);
trac.set_track(play2, 1);
play2.attach(*filter.get());
QString splitTransition = TransitionsRepository::get()->getCompositingTransition();
Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData());
t.set("always_active", 1);
trac.plant_transition(t, 0, 1);
int startPos = m_model->getClipPosition(clipId);
// plug in overlay playlist
auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile());
overlay->insert_blank(0, startPos);
Mlt::Producer split(trac.get_producer());
overlay->insert_at(startPos, &split, 1);
// insert in tractor
if (!m_timelinePreview) {
initializePreview();
}
m_timelinePreview->setOverlayTrack(overlay);
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
return true;
}
void TimelineController::removeSplitOverlay()
{
if (!m_timelinePreview || !m_timelinePreview->hasOverlayTrack()) {
return;
}
// disconnect
m_timelinePreview->removeOverlayTrack();
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
void TimelineController::addPreviewRange(bool add)
{
if (m_zone.isNull()) {
return;
}
if (!m_timelinePreview) {
initializePreview();
}
if (m_timelinePreview) {
m_timelinePreview->addPreviewRange(m_zone, add);
}
}
void TimelineController::clearPreviewRange(bool resetZones)
{
if (m_timelinePreview) {
m_timelinePreview->clearPreviewRange(resetZones);
}
}
void TimelineController::startPreviewRender()
{
// Timeline preview stuff
if (!m_timelinePreview) {
initializePreview();
} else if (m_disablePreview->isChecked()) {
m_disablePreview->setChecked(false);
disablePreview(false);
}
if (m_timelinePreview) {
if (!m_usePreview) {
m_timelinePreview->buildPreviewTrack();
m_usePreview = true;
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
m_timelinePreview->startPreviewRender();
}
}
void TimelineController::stopPreviewRender()
{
if (m_timelinePreview) {
m_timelinePreview->abortRendering();
}
}
void TimelineController::initializePreview()
{
if (m_timelinePreview) {
// Update parameters
if (!m_timelinePreview->loadParams()) {
if (m_usePreview) {
// Disconnect preview track
m_timelinePreview->disconnectTrack();
m_usePreview = false;
}
delete m_timelinePreview;
m_timelinePreview = nullptr;
}
} else {
m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get());
if (!m_timelinePreview->initialize()) {
// TODO warn user
delete m_timelinePreview;
m_timelinePreview = nullptr;
} else {
}
}
QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone"));
if (previewRender) {
previewRender->setEnabled(m_timelinePreview != nullptr);
}
m_disablePreview->setEnabled(m_timelinePreview != nullptr);
m_disablePreview->blockSignals(true);
m_disablePreview->setChecked(false);
m_disablePreview->blockSignals(false);
}
bool TimelineController::hasPreviewTrack() const
{
return (m_timelinePreview && m_timelinePreview->hasOverlayTrack());
}
void TimelineController::updatePreviewConnection(bool enable)
{
if (m_timelinePreview) {
if (enable) {
m_timelinePreview->reconnectTrack();
} else {
m_timelinePreview->disconnectTrack();
}
}
}
void TimelineController::disablePreview(bool disable)
{
if (disable) {
m_timelinePreview->deletePreviewTrack();
m_usePreview = false;
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
} else {
if (!m_usePreview) {
if (!m_timelinePreview->buildPreviewTrack()) {
// preview track already exists, reconnect
m_model->m_tractor->lock();
m_timelinePreview->reconnectTrack();
m_model->m_tractor->unlock();
}
m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime());
m_usePreview = true;
}
}
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
QVariantList TimelineController::dirtyChunks() const
{
return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList();
}
QVariantList TimelineController::renderedChunks() const
{
return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList();
}
int TimelineController::workingPreview() const
{
return m_timelinePreview ? m_timelinePreview->workingPreview : -1;
}
bool TimelineController::useRuler() const
{
return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1;
}
void TimelineController::resetPreview()
{
if (m_timelinePreview) {
m_timelinePreview->clearPreviewRange(true);
initializePreview();
}
}
void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable)
{
if (chunks.isEmpty() && dirty.isEmpty()) {
return;
}
if (!m_timelinePreview) {
initializePreview();
}
QVariantList renderedChunks;
QVariantList dirtyChunks;
QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts);
QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts);
for (const QString &frame : chunksList) {
renderedChunks << frame.toInt();
}
for (const QString &frame : dirtyList) {
dirtyChunks << frame.toInt();
}
m_disablePreview->blockSignals(true);
m_disablePreview->setChecked(enable);
m_disablePreview->blockSignals(false);
if (!enable) {
m_timelinePreview->buildPreviewTrack();
m_usePreview = true;
m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
}
m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate);
}
QMap TimelineController::documentProperties()
{
QMap props = pCore->currentDoc()->documentProperties();
int audioTarget = m_model->m_audioTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_audioTarget);
int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget);
int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack);
props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget));
props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget));
props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack));
props.insert(QStringLiteral("position"), QString::number(timelinePosition()));
QVariant returnedValue;
QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue));
int scrollPos = returnedValue.toInt();
props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos));
props.insert(QStringLiteral("zonein"), QString::number(m_zone.x()));
props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y()));
if (m_timelinePreview) {
QPair chunks = m_timelinePreview->previewChunks();
props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(',')));
props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(',')));
}
props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked()));
return props;
}
void TimelineController::insertSpace(int trackId, int frame)
{
if (frame == -1) {
frame = timelinePosition();
}
if (trackId == -1) {
trackId = m_activeTrack;
}
QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow());
if (d->exec() != QDialog::Accepted) {
delete d;
return;
}
int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame);
int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps());
delete d;
if (cid == -1) {
pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500);
return;
}
int start = m_model->getItemPosition(cid);
requestSpacerEndOperation(cid, start, start + spaceDuration);
}
void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks)
{
if (frame == -1) {
frame = timelinePosition();
}
if (trackId == -1) {
trackId = m_activeTrack;
}
bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks);
if (!res) {
pCore->displayMessage(i18n("Cannot remove space at given position"), InformationMessage, 500);
}
}
void TimelineController::invalidateItem(int cid)
{
if (!m_timelinePreview || !m_model->isItem(cid) || m_model->getItemTrackId(cid) == -1) {
return;
}
int start = m_model->getItemPosition(cid);
int end = start + m_model->getItemPlaytime(cid);
m_timelinePreview->invalidatePreview(start, end);
}
void TimelineController::invalidateTrack(int tid)
{
if (!m_timelinePreview || !m_model->isTrack(tid)) {
return;
}
for (auto clp : m_model->getTrackById_const(tid)->m_allClips) {
invalidateItem(clp.first);
}
}
void TimelineController::invalidateZone(int in, int out)
{
if (!m_timelinePreview) {
return;
}
m_timelinePreview->invalidatePreview(in, out == -1 ? m_duration : out);
}
void TimelineController::changeItemSpeed(int clipId, double speed)
{
if (clipId == -1) {
clipId = getMainSelectedItem(false, true);
}
if (clipId == -1) {
pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500);
return;
}
if (qFuzzyCompare(speed, -1)) {
speed = 100 * m_model->getClipSpeed(clipId);
double duration = m_model->getItemPlaytime(clipId);
// this is the max speed so that the clip is at least one frame long
double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId));
// this is the min speed so that the clip doesn't bump into the next one on track
double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true)));
// if there is a split partner, we must also take it into account
int partner = m_model->getClipSplitPartner(clipId);
if (partner != -1) {
double duration2 = m_model->getItemPlaytime(partner);
double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner));
double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true)));
minSpeed = std::max(minSpeed, minSpeed2);
maxSpeed = std::min(maxSpeed, maxSpeed2);
}
QScopedPointer d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), minSpeed, maxSpeed, speed < 0));
if (d->exec() != QDialog::Accepted) {
return;
}
speed = d->getValue();
qDebug() << "requesting speed " << speed;
}
m_model->requestClipTimeWarp(clipId, speed, true);
}
void TimelineController::switchCompositing(int mode)
{
// m_model->m_tractor->lock();
pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(mode));
QScopedPointer service(m_model->m_tractor->field());
Mlt::Field *field = m_model->m_tractor->field();
field->lock();
while ((service != nullptr) && service->is_valid()) {
if (service->type() == transition_type) {
Mlt::Transition t((mlt_transition)service->get_service());
QString serviceName = t.get("mlt_service");
if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) {
// remove all compositing transitions
field->disconnect_service(t);
}
}
service.reset(service->producer());
}
if (mode > 0) {
const QString compositeGeometry =
QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height());
// Loop through tracks
for (int track = 0; track < m_model->getTracksCount(); track++) {
if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) {
// This is a video track
Mlt::Transition t(*m_model->m_tractor->profile(),
mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData());
t.set("always_active", 1);
t.set("a_track", 0);
t.set("b_track", track + 1);
if (mode == 1) {
t.set("valign", "middle");
t.set("halign", "centre");
t.set("fill", 1);
t.set("aligned", 0);
t.set("geometry", compositeGeometry.toUtf8().constData());
}
t.set("internal_added", 237);
field->plant_transition(t, 0, track + 1);
}
}
}
field->unlock();
delete field;
pCore->requestMonitorRefresh();
}
void TimelineController::extractZone(QPoint zone, bool liftOnly)
{
QVector tracks;
auto it = m_model->m_allTracks.cbegin();
while (it != m_model->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
tracks << target_track;
}
++it;
}
if (tracks.isEmpty()) {
pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), InformationMessage);
}
if (m_zone == QPoint()) {
// Use current timeline position and clip zone length
zone.setY(timelinePosition() + zone.y() - zone.x());
zone.setX(timelinePosition());
}
TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly);
}
void TimelineController::extract(int clipId)
{
// TODO: grouped clips?
int in = m_model->getClipPosition(clipId);
QPoint zone(in, in + m_model->getClipPlaytime(clipId));
int track = m_model->getClipTrackId(clipId);
TimelineFunctions::extractZone(m_model, {track}, zone, false);
}
bool TimelineController::insertClipZone(const QString &binId, int tid, int position)
{
QStringList binIdData = binId.split(QLatin1Char('/'));
int in = 0;
int out = -1;
if (binIdData.size() >= 3) {
in = binIdData.at(1).toInt();
out = binIdData.at(2).toInt();
}
QString bid = binIdData.first();
// 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);
}
int aTrack = -1;
int vTrack = -1;
std::shared_ptr clip = pCore->bin()->getBinClip(bid);
if (out <= in) {
out = (int)clip->frameDuration() - 1;
}
if (dropType == PlaylistState::VideoOnly) {
vTrack = tid;
} else if (dropType == PlaylistState::AudioOnly) {
aTrack = tid;
} else {
if (m_model->getTrackById_const(tid)->isAudioTrack()) {
aTrack = tid;
vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(aTrack) : -1;
} else {
vTrack = tid;
aTrack = clip->hasAudioAndVideo() ? m_model->getMirrorAudioTrackId(vTrack) : -1;
}
}
QList target_tracks;
if (vTrack > -1) {
target_tracks << vTrack;
}
if (aTrack > -1) {
target_tracks << aTrack;
}
return TimelineFunctions::insertZone(m_model, target_tracks, binId, position, QPoint(in, out + 1), m_model->m_editMode == TimelineMode::OverwriteEdit, false);
}
int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite)
{
std::shared_ptr clip = pCore->bin()->getBinClip(binId);
int aTrack = -1;
int vTrack = -1;
if (clip->hasAudio()) {
aTrack = audioTarget();
}
if (clip->hasVideo()) {
vTrack = videoTarget();
}
/*if (aTrack == -1 && vTrack == -1) {
// No target tracks defined, use active track
if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) {
aTrack = m_activeTrack;
vTrack = m_model->getMirrorVideoTrackId(aTrack);
} else {
vTrack = m_activeTrack;
aTrack = m_model->getMirrorAudioTrackId(vTrack);
}
}*/
int insertPoint;
QPoint sourceZone;
if (useRuler() && m_zone != QPoint()) {
// We want to use timeline zone for in/out insert points
insertPoint = m_zone.x();
sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x());
} else {
// Use current timeline pos and clip zone for in/out
insertPoint = timelinePosition();
sourceZone = zone;
}
QList target_tracks;
if (vTrack > -1) {
target_tracks << vTrack;
}
if (aTrack > -1) {
target_tracks << aTrack;
}
if (target_tracks.isEmpty()) {
pCore->displayMessage(i18n("Please select a target track by clicking on a track's target zone"), InformationMessage);
return -1;
}
return TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite) ? insertPoint + (sourceZone.y() - sourceZone.x())
: -1;
}
void TimelineController::updateClip(int clipId, const QVector &roles)
{
QModelIndex ix = m_model->makeClipIndexFromID(clipId);
if (ix.isValid()) {
m_model->dataChanged(ix, ix, roles);
}
}
void TimelineController::showClipKeyframes(int clipId, bool value)
{
TimelineFunctions::showClipKeyframes(m_model, clipId, value);
}
void TimelineController::showCompositionKeyframes(int clipId, bool value)
{
TimelineFunctions::showCompositionKeyframes(m_model, clipId, value);
}
void TimelineController::switchEnableState(std::unordered_set selection)
{
if (selection.empty()) {
selection = m_model->getCurrentSelection();
//clipId = getMainSelectedItem(false, false);
}
TimelineFunctions::switchEnableState(m_model, selection);
}
void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset)
{
int track = m_model->getClipTrackId(clipId);
int compoId = -1;
if (assetId.isEmpty()) {
QStringList compositions = KdenliveSettings::favorite_transitions();
if (compositions.isEmpty()) {
pCore->displayMessage(i18n("Select a favorite composition"), InformationMessage, 500);
return;
}
compoId = insertNewComposition(track, clipId, offset, compositions.first(), true);
} else {
compoId = insertNewComposition(track, clipId, offset, assetId, true);
}
if (compoId > 0) {
m_model->requestSetSelection({compoId});
}
}
void TimelineController::addEffectToClip(const QString &assetId, int clipId)
{
qDebug() << "/// ADDING ASSET: " << assetId;
m_model->addClipEffect(clipId, assetId);
}
bool TimelineController::splitAV()
{
int cid = *m_model->getCurrentSelection().begin();
if (m_model->isClip(cid)) {
std::shared_ptr clip = m_model->getClipPtr(cid);
if (clip->clipState() == PlaylistState::AudioOnly) {
return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget());
} else {
return TimelineFunctions::requestSplitAudio(m_model, cid, audioTarget());
}
}
pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500);
return false;
}
void TimelineController::splitAudio(int clipId)
{
TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget());
}
void TimelineController::splitVideo(int clipId)
{
TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget());
}
void TimelineController::setAudioRef(int clipId)
{
m_audioRef = clipId;
std::unique_ptr envelope(new AudioEnvelope(getClipBinId(clipId), clipId));
m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope)));
connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, [&](int cid, int shift) {
int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef);
bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true, true);
if (!result) {
pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), InformationMessage, 500);
}
});
connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage);
}
void TimelineController::alignAudio(int clipId)
{
// find other clip
if (m_audioRef == -1 || m_audioRef == clipId) {
pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500);
return;
}
const QString masterBinClipId = getClipBinId(m_audioRef);
if (m_model->m_groups->isInGroup(clipId)) {
std::unordered_set groupIds = m_model->getGroupElements(clipId);
// Check that no item is grouped with our audioRef item
// TODO
m_model->requestClearSelection();
}
const QString otherBinId = getClipBinId(clipId);
if (otherBinId == masterBinClipId) {
// easy, same clip.
int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(clipId);
if (newPos) {
bool result = m_model->requestClipMove(clipId, m_model->getClipTrackId(clipId), newPos, true, true, true);
if (!result) {
pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500);
}
return;
}
}
// Perform audio calculation
AudioEnvelope *envelope = new AudioEnvelope(otherBinId, clipId, (size_t)m_model->getClipIn(clipId), (size_t)m_model->getClipPlaytime(clipId),
(size_t)m_model->getClipPosition(clipId));
m_audioCorrelator->addChild(envelope);
}
void TimelineController::switchTrackActive(int trackId)
{
if (trackId == -1) {
trackId = m_activeTrack;
}
bool active = m_model->getTrackById_const(trackId)->isTimelineActive();
m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1"));
}
void TimelineController::switchTrackLock(bool applyToAll)
{
if (!applyToAll) {
// apply to active track only
bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked();
m_model->setTrackLockedState(m_activeTrack, !locked);
} else {
// Invert track lock
const auto ids = m_model->getAllTracksIds();
// count the number of tracks to be locked
int toBeLockedCount =
std::accumulate(ids.begin(), ids.end(), 0, [this](int s, int id) { return s + (m_model->getTrackById_const(id)->isLocked() ? 0 : 1); });
bool leaveOneUnlocked = toBeLockedCount == m_model->getTracksCount();
for (const int id : ids) {
// leave active track unlocked
if (leaveOneUnlocked && id == m_activeTrack) {
continue;
}
bool isLocked = m_model->getTrackById_const(id)->isLocked();
m_model->setTrackLockedState(id, !isLocked);
}
}
}
void TimelineController::switchTargetTrack()
{
bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1;
if (isAudio) {
setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack);
} else {
setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack);
}
}
int TimelineController::audioTarget() const
{
return m_model->m_audioTarget;
}
int TimelineController::videoTarget() const
{
return m_model->m_videoTarget;
}
bool TimelineController::hasAudioTarget() const
{
return m_hasAudioTarget;
}
bool TimelineController::hasVideoTarget() const
{
return m_hasVideoTarget;
}
bool TimelineController::autoScroll() const
{
return KdenliveSettings::autoscroll();
}
void TimelineController::resetTrackHeight()
{
int tracksCount = m_model->getTracksCount();
for (int track = tracksCount - 1; track >= 0; track--) {
int trackIx = m_model->getTrackIndexFromPosition(track);
m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
}
QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
}
void TimelineController::selectAll()
{
std::unordered_set ids;
for (auto clp : m_model->m_allClips) {
ids.insert(clp.first);
}
for (auto clp : m_model->m_allCompositions) {
ids.insert(clp.first);
}
m_model->requestSetSelection(ids);
}
void TimelineController::selectCurrentTrack()
{
std::unordered_set ids;
for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) {
ids.insert(clp.first);
}
for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) {
ids.insert(clp.first);
}
m_model->requestSetSelection(ids);
}
void TimelineController::pasteEffects(int targetId)
{
std::unordered_set targetIds;
if (targetId == -1) {
std::unordered_set sel = m_model->getCurrentSelection();
if (sel.empty()) {
pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500);
}
for (int s : sel) {
if (m_model->isGroup(s)) {
std::unordered_set sub = m_model->m_groups->getLeaves(s);
for (int current_id : sub) {
if (m_model->isClip(current_id)) {
targetIds.insert(current_id);
}
}
} else if (m_model->isClip(s)) {
targetIds.insert(s);
}
}
} else {
if (m_model->m_groups->isInGroup(targetId)) {
targetId = m_model->m_groups->getRootId(targetId);
}
if (m_model->isGroup(targetId)) {
std::unordered_set sub = m_model->m_groups->getLeaves(targetId);
for (int current_id : sub) {
if (m_model->isClip(current_id)) {
targetIds.insert(current_id);
}
}
} else if (m_model->isClip(targetId)) {
targetIds.insert(targetId);
}
}
if (targetIds.empty()) {
pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500);
}
QClipboard *clipboard = QApplication::clipboard();
QString txt = clipboard->text();
if (txt.isEmpty()) {
pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500);
return;
}
QDomDocument copiedItems;
copiedItems.setContent(txt);
if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) {
pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500);
return;
}
QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
if (clips.isEmpty()) {
pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500);
return;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects"));
effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in")));
for (int i = 1; i < clips.size(); i++) {
QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects"));
QDomNodeList subs = subeffects.childNodes();
while (!subs.isEmpty()) {
subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in")));
effects.appendChild(subs.at(0));
}
}
bool result = true;
for (int target : targetIds) {
std::shared_ptr destStack = m_model->getClipEffectStackModel(target);
result = result && destStack->fromXml(effects, undo, redo);
if (!result) {
break;
}
}
if (result) {
pCore->pushUndo(undo, redo, i18n("Paste effects"));
} else {
pCore->displayMessage(i18n("Cannot paste effect on selected clip"), InformationMessage, 500);
undo();
}
}
double TimelineController::fps() const
{
return pCore->getCurrentFps();
}
void TimelineController::editItemDuration(int id)
{
if (id == -1) {
id = getMainSelectedItem(false, true);
}
if (id == -1) {
pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500);
return;
}
int start = m_model->getItemPosition(id);
int in = 0;
int duration = m_model->getItemPlaytime(id);
int maxLength = -1;
bool isComposition = false;
if (m_model->isClip(id)) {
in = m_model->getClipIn(id);
std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id));
if (clip && clip->hasLimitedDuration()) {
maxLength = clip->getProducerDuration();
}
} else if (m_model->isComposition(id)) {
// nothing to do
isComposition = true;
} else {
pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500);
return;
}
int trackId = m_model->getItemTrackId(id);
int maxFrame = qMax(0, start + duration +
(isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true)
: m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true)));
int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false)
: m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false)));
int partner = isComposition ? -1 : m_model->getClipSplitPartner(id);
QPointer dialog =
new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow());
if (dialog->exec() == QDialog::Accepted) {
std::function undo = []() { return true; };
std::function redo = []() { return true; };
int newPos = dialog->startPos().frames(pCore->getCurrentFps());
int newIn = dialog->cropStart().frames(pCore->getCurrentFps());
int newDuration = dialog->duration().frames(pCore->getCurrentFps());
bool result = true;
if (newPos < start) {
if (!isComposition) {
result = m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo);
}
} else {
result = m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo);
}
if (result && newIn != in) {
m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo);
}
}
if (newDuration != duration + (in - newIn)) {
result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo);
}
}
} else {
// perform resize first
if (newIn != in) {
result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo);
}
}
if (newDuration != duration + (in - newIn)) {
result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo);
}
}
if (start != newPos || newIn != in) {
if (!isComposition) {
result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo);
if (result && partner > -1) {
result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo);
}
} else {
result = result &&
m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo);
}
}
}
if (result) {
pCore->pushUndo(undo, redo, i18n("Edit item"));
} else {
undo();
}
}
}
void TimelineController::updateClipActions()
{
if (m_model->getCurrentSelection().empty()) {
for (QAction *act : clipActions) {
act->setEnabled(false);
}
emit timelineClipSelected(false);
// nothing selected
emit showItemEffectStack(QString(), nullptr, QSize(), false);
return;
}
std::shared_ptr clip(nullptr);
int item = *m_model->getCurrentSelection().begin();
if (m_model->getCurrentSelection().size() == 1 && (m_model->isClip(item) || m_model->isComposition(item))) {
showAsset(item);
}
if (m_model->isClip(item)) {
clip = m_model->getClipPtr(item);
}
for (QAction *act : clipActions) {
bool enableAction = true;
const QChar actionData = act->data().toChar();
if (actionData == QLatin1Char('G')) {
enableAction = isInSelection(item);
} else if (actionData == QLatin1Char('U')) {
enableAction = isInSelection(item) || (m_model->m_groups->isInGroup(item) && !isInSelection(item));
} else if (actionData == QLatin1Char('A')) {
enableAction = clip && clip->clipState() == PlaylistState::AudioOnly;
} else if (actionData == QLatin1Char('V')) {
enableAction = clip && clip->clipState() == PlaylistState::VideoOnly;
} else if (actionData == QLatin1Char('D')) {
enableAction = clip && clip->clipState() == PlaylistState::Disabled;
} else if (actionData == QLatin1Char('E')) {
enableAction = clip && clip->clipState() != PlaylistState::Disabled;
} else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) {
enableAction = clip && clip->canBeVideo() && clip->canBeAudio();
if (enableAction && actionData == QLatin1Char('S')) {
act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio"));
}
} else if (actionData == QLatin1Char('W')) {
enableAction = clip != nullptr;
if (enableAction) {
act->setText(clip->clipState() == PlaylistState::Disabled ? i18n("Enable clip") : i18n("Disable clip"));
}
} else if (actionData == QLatin1Char('C') && clip == nullptr) {
enableAction = false;
}
act->setEnabled(enableAction);
}
emit timelineClipSelected(clip != nullptr);
}
const QString TimelineController::getAssetName(const QString &assetId, bool isTransition)
{
return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId);
}
void TimelineController::grabCurrent()
{
std::unordered_set ids = m_model->getCurrentSelection();
std::unordered_set items_list;
int mainId = -1;
for (int i : ids) {
if (m_model->isGroup(i)) {
std::unordered_set children = m_model->m_groups->getLeaves(i);
items_list.insert(children.begin(), children.end());
} else {
items_list.insert(i);
}
}
for (int id : items_list) {
if (mainId == -1 && m_model->getItemTrackId(id) == m_activeTrack) {
mainId = id;
continue;
}
if (m_model->isClip(id)) {
std::shared_ptr clip = m_model->getClipPtr(id);
clip->setGrab(!clip->isGrabbed());
} else if (m_model->isComposition(id)) {
std::shared_ptr clip = m_model->getCompositionPtr(id);
clip->setGrab(!clip->isGrabbed());
}
}
if (mainId > -1) {
if (m_model->isClip(mainId)) {
std::shared_ptr clip = m_model->getClipPtr(mainId);
clip->setGrab(!clip->isGrabbed());
} else if (m_model->isComposition(mainId)) {
std::shared_ptr clip = m_model->getCompositionPtr(mainId);
clip->setGrab(!clip->isGrabbed());
}
}
}
int TimelineController::getItemMovingTrack(int itemId) const
{
if (m_model->isClip(itemId)) {
int trackId = -1;
if (m_model->m_editMode != TimelineMode::NormalEdit) {
trackId = m_model->m_allClips[itemId]->getFakeTrackId();
}
return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId;
}
return m_model->m_allCompositions[itemId]->getCurrentTrackId();
}
bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
Q_ASSERT(m_model->m_allClips.count(clipId) > 0);
int trackId = m_model->m_allClips[clipId]->getFakeTrackId();
if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) {
qDebug() << "* * ** END FAKE; NO MOVE RQSTED";
return true;
}
if (m_model->m_groups->isInGroup(clipId)) {
// element is in a group.
int groupId = m_model->m_groups->getRootId(clipId);
int current_trackId = m_model->getClipTrackId(clipId);
int track_pos1 = m_model->getTrackPosition(trackId);
int track_pos2 = m_model->getTrackPosition(current_trackId);
int delta_track = track_pos1 - track_pos2;
int delta_pos = position - m_model->m_allClips[clipId]->getPosition();
return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
}
qDebug() << "//////\n//////\nENDING FAKE MNOVE: " << trackId << ", POS: " << position;
std::function undo = []() { return true; };
std::function redo = []() { return true; };
int duration = m_model->getClipPlaytime(clipId);
int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId();
bool res = true;
if (currentTrack > -1) {
res = res && m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo, false, false);
}
if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo);
} else if (m_model->m_editMode == TimelineMode::InsertEdit) {
int startClipId = m_model->getClipByPosition(trackId, position);
if (startClipId > -1) {
// There is a clip, cut
res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo);
}
res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo);
}
res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo);
if (res) {
// Terminate fake move
if (m_model->isClip(clipId)) {
m_model->m_allClips[clipId]->setFakeTrackId(-1);
}
if (logUndo) {
pCore->pushUndo(undo, redo, i18n("Move item"));
}
} else {
qDebug() << "//// FAKE FAILED";
undo();
}
return res;
}
bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
{
std::function