diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp
index 72daf8caa..37fbc0a7b 100644
--- a/src/timeline2/model/timelinemodel.cpp
+++ b/src/timeline2/model/timelinemodel.cpp
@@ -1,3521 +1,3522 @@
/***************************************************************************
* 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_tractor(new Mlt::Tractor(*profile))
, m_snaps(new SnapModel())
, m_undoStack(std::move(undo_stack))
, m_profile(profile)
, m_blackClip(new Mlt::Producer(*profile, "color:black"))
, m_lock(QReadWriteLock::Recursive)
, m_timelineEffectsEnabled(true)
, m_id(getNextId())
, m_overlayTrackCount(-1)
, m_audioTarget(-1)
, m_videoTarget(-1)
, m_editMode(TimelineMode::NormalEdit)
, m_blockRefresh(false)
, m_closing(false)
{
// Create black background track
m_blackClip->set("id", "black_track");
m_blackClip->set("mlt_type", "producer");
m_blackClip->set("aspect_ratio", 1);
m_blackClip->set("length", INT_MAX);
m_blackClip->set("set.test_audio", 0);
m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
m_tractor->insert_track(*m_blackClip, 0);
TRACE_CONSTR(this);
}
void TimelineModel::prepareClose()
{
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) {
return getTrackPosition(trackId) + 1;
}
if (separated == 2) {
// 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;
}
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 = aCount - vCount;
if (trackDiff > 0) {
// more audio tracks
if (!isAudio) {
trackPos -= trackDiff;
} else if (trackPos > vCount) {
return -trackPos;
}
}
return isAudio ? ((aCount * trackPos) - 1) : (vCount + 1 - trackPos) * 2;
}
QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
QList results;
auto it = m_iteratorTable.at(trackId);
while (it != m_allTracks.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 && getClipTrackId(clipId) == trackId) {
+ 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 && sourceTrackId == trackId) {
+ 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)
+ 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(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;
}
// 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 = -1;
while (i.hasNext()) {
i.next();
int track_space;
if (!after) {
// Check space before the position
track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1);
if (blank_length == -1 || blank_length > track_space) {
blank_length = track_space;
}
} else {
// Check space after the position
track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1;
if (blank_length == -1 || blank_length > track_space) {
blank_length = track_space;
}
}
}
if (blank_length != 0) {
int updatedPos = currentPos + (after ? blank_length : -blank_length);
possible = requestClipMove(clipId, trackId, updatedPos, 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) {
int p1 = m_allClips[clipId1]->getPosition();
int p2 = m_allClips[clipId2]->getPosition();
return delta_pos > 0 ? p2 <= p1 : p1 <= p2;
});
// Sort compositions. We need to delete in the move direction from top to bottom
std::vector sorted_compositions(all_compositions.begin(), all_compositions.end());
std::sort(sorted_compositions.begin(), sorted_compositions.end(), [this, delta_track, delta_pos](int clipId1, int clipId2) {
int p1 = delta_track < 0
? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition();
int p2 = delta_track < 0
? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId())
: delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition();
return delta_track == 0 ? (delta_pos > 0 ? p2 <= p1 : p1 <= p2) : p1 <= p2;
});
sorted_clips.insert(sorted_clips.end(), sorted_compositions.begin(), sorted_compositions.end());
// Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
// This way, we ensure that no conflict will arise with clips inside the group being moved
Fun update_model = [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 trackId = TimelineModel::getNextId();
id = trackId;
Fun local_undo = deregisterTrack_lambda(trackId, true);
TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack);
if (updateView) {
_resetView();
}
auto track = getTrackById(trackId);
Fun local_redo = [track, position, updateView, this]() {
// We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
// sufficient to register it.
registerTrack(track, position, true);
if (updateView) {
_resetView();
}
return true;
};
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
bool TimelineModel::requestTrackDeletion(int trackId)
{
// TODO: make sure we disable overlayTrack before deleting a track
QWriteLocker locker(&m_lock);
TRACE(trackId);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = requestTrackDeletion(trackId, undo, redo);
if (result) {
if (m_videoTarget == trackId) {
m_videoTarget = -1;
}
if (m_audioTarget == trackId) {
m_audioTarget = -1;
}
PUSH_UNDO(undo, redo, i18n("Delete Track"));
}
TRACE_RES(result);
return result;
}
bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo)
{
Q_ASSERT(isTrack(trackId));
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;
int cache = QThread::idealThreadCount() + (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;
auto it = m_iteratorTable[id]; // iterator to the element
int index = getTrackPosition(id); // compute index in list
m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track
// send update to the model
m_allTracks.erase(it); // actual deletion of object
m_iteratorTable.erase(id); // clean table
if (updateView) {
_resetView();
}
int cache = QThread::idealThreadCount() + (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;
}
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;
}
diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml
index 5cdb8e95a..308fbb701 100644
--- a/src/timeline2/view/qml/timeline.qml
+++ b/src/timeline2/view/qml/timeline.qml
@@ -1,1517 +1,1517 @@
import QtQuick 2.6
import QtQml.Models 2.2
import QtQuick.Controls 1.4 as OLD
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.2
import Kdenlive.Controls 1.0
import QtQuick.Window 2.2
import 'Timeline.js' as Logic
Rectangle {
id: root
objectName: "timelineview"
SystemPalette { id: activePalette }
color: activePalette.window
property bool validMenu: false
property color textColor: activePalette.text
property bool dragInProgress: dragProxyArea.pressed || dragProxyArea.drag.active
signal clipClicked()
signal mousePosChanged(int position)
signal zoomIn(bool onMouse)
signal zoomOut(bool onMouse)
signal processingDrag(bool dragging)
FontMetrics {
id: fontMetrics
font.family: "Arial"
}
ClipMenu {
id: clipMenu
}
CompositionMenu {
id: compositionMenu
}
onDragInProgressChanged: {
processingDrag(!root.dragInProgress)
}
function fitZoom() {
return scrollView.width / (timeline.duration * 1.1)
}
function scrollPos() {
return scrollView.flickableItem.contentX
}
function goToStart(pos) {
scrollView.flickableItem.contentX = pos
}
function updatePalette() {
root.color = activePalette.window
root.textColor = activePalette.text
playhead.fillColor = activePalette.windowText
ruler.repaintRuler()
}
function moveSelectedTrack(offset) {
var cTrack = Logic.getTrackIndexFromId(timeline.activeTrack)
var newTrack = cTrack + offset
var max = tracksRepeater.count;
if (newTrack < 0) {
newTrack = max - 1;
} else if (newTrack >= max) {
newTrack = 0;
}
console.log('Setting curr tk: ', newTrack, 'MAX: ',max)
timeline.activeTrack = tracksRepeater.itemAt(newTrack).trackInternalId
}
function zoomByWheel(wheel) {
if (wheel.modifiers & Qt.AltModifier) {
// Seek to next snap
if (wheel.angleDelta.x > 0) {
timeline.triggerAction('monitor_seek_snap_backward')
} else {
timeline.triggerAction('monitor_seek_snap_forward')
}
} else if (wheel.modifiers & Qt.ControlModifier) {
root.wheelAccumulatedDelta += wheel.angleDelta.y;
// Zoom
if (root.wheelAccumulatedDelta >= defaultDeltasPerStep) {
root.zoomIn(true);
root.wheelAccumulatedDelta = 0;
} else if (root.wheelAccumulatedDelta <= -defaultDeltasPerStep) {
root.zoomOut(true);
root.wheelAccumulatedDelta = 0;
}
} else if (wheel.modifiers & Qt.ShiftModifier) {
// Vertical scroll
var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, trackHeaders.height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height)
scrollView.flickableItem.contentY = Math.max(newScroll, 0)
} else {
// Horizontal scroll
var newScroll = Math.min(scrollView.flickableItem.contentX - wheel.angleDelta.y, timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
scrollView.flickableItem.contentX = Math.max(newScroll, 0)
}
wheel.accepted = true
}
function continuousScrolling(x) {
// This provides continuous scrolling at the left/right edges.
if (x > scrollView.flickableItem.contentX + scrollView.width - 50) {
scrollTimer.item = clip
scrollTimer.backwards = false
scrollTimer.start()
} else if (x < 50) {
scrollView.flickableItem.contentX = 0;
scrollTimer.stop()
} else if (x < scrollView.flickableItem.contentX + 50) {
scrollTimer.item = clip
scrollTimer.backwards = true
scrollTimer.start()
} else {
scrollTimer.stop()
}
}
function getTrackYFromId(a_track) {
return Logic.getTrackYFromId(a_track)
}
function getTrackYFromMltIndex(a_track) {
return Logic.getTrackYFromMltIndex(a_track)
}
function getTracksCount() {
return Logic.getTracksList()
}
function getMousePos() {
return (scrollView.flickableItem.contentX + tracksArea.mouseX) / timeline.scaleFactor
}
function getScrollPos() {
return scrollView.flickableItem.contentX
}
function setScrollPos(pos) {
return scrollView.flickableItem.contentX = pos
}
function getCopiedItemId() {
return copiedClip
}
function getMouseTrack() {
return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height + scrollView.flickableItem.contentY)
}
function getTrackColor(audio, header) {
var col = activePalette.alternateBase
if (audio) {
col = Qt.tint(col, "#06FF00CC")
}
if (header) {
col = Qt.darker(col, 1.05)
}
return col
}
function clearDropData() {
clipBeingDroppedId = -1
droppedPosition = -1
droppedTrack = -1
scrollTimer.running = false
scrollTimer.stop()
}
function isDragging() {
return dragInProgress
}
function initDrag(itemObject, itemCoord, itemId, itemPos, itemTrack, isComposition) {
dragProxy.x = itemObject.modelStart * timeScale
dragProxy.y = itemCoord.y
dragProxy.width = itemObject.clipDuration * timeScale
dragProxy.height = itemCoord.height
dragProxy.masterObject = itemObject
dragProxy.draggedItem = itemId
dragProxy.sourceTrack = itemTrack
dragProxy.sourceFrame = itemPos
dragProxy.isComposition = isComposition
dragProxy.verticalOffset = isComposition ? itemObject.displayHeight : 0
}
function endDrag() {
dragProxy.draggedItem = -1
dragProxy.x = 0
dragProxy.y = 0
dragProxy.width = 0
dragProxy.height = 0
dragProxy.verticalOffset = 0
}
function getItemAtPos(tk, posx, isComposition) {
var track = Logic.getTrackById(tk)
var container = track.children[0]
var tentativeClip = undefined
//console.log('TESTING ITMES OK TK: ', tk, ', POS: ', posx, ', CHILREN: ', container.children.length, ', COMPO: ', isComposition)
for (var i = 0 ; i < container.children.length; i++) {
if (container.children[i].children.length == 0 || container.children[i].children[0].children.length == 0) {
continue
}
tentativeClip = container.children[i].children[0].childAt(posx, 1)
if (tentativeClip && tentativeClip.clipId && (tentativeClip.isComposition == isComposition)) {
//console.log('found item with id: ', tentativeClip.clipId, ' IS COMPO: ', tentativeClip.isComposition)
break
}
}
return tentativeClip
}
property int headerWidth: timeline.headerWidth()
property int activeTool: 0
property real baseUnit: fontMetrics.font.pointSize
property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.2)
property color frameColor: Qt.rgba(activePalette.shadow.r, activePalette.shadow.g, activePalette.shadow.b, 0.3)
property bool stopScrolling: false
property int duration: timeline.duration
property color audioColor: timeline.audioColor
property color videoColor: timeline.videoColor
property color lockedColor: timeline.lockedColor
property color selectionColor: timeline.selectionColor
property color groupColor: timeline.groupColor
property int clipBeingDroppedId: -1
property string clipBeingDroppedData
property int droppedPosition: -1
property int droppedTrack: -1
property int clipBeingMovedId: -1
property int spacerGroup: -1
property int spacerFrame: -1
property int spacerClickFrame: -1
property real timeScale: timeline.scaleFactor
property real snapping: (timeline.snap && (timeScale < 2 * baseUnit)) ? 10 / Math.sqrt(timeScale) - 0.5 : -1
property var timelineSelection: timeline.selection
property int trackHeight
property int copiedClip: -1
property int zoomOnMouse: -1
property int viewActiveTrack: timeline.activeTrack
property int wheelAccumulatedDelta: 0
readonly property int defaultDeltasPerStep: 120
//onCurrentTrackChanged: timeline.selection = []
onTimeScaleChanged: {
if (root.zoomOnMouse >= 0) {
scrollView.flickableItem.contentX = Math.max(0, root.zoomOnMouse * timeline.scaleFactor - tracksArea.mouseX)
root.zoomOnMouse = -1
} else {
scrollView.flickableItem.contentX = Math.max(0, (timeline.seekPosition > -1 ? timeline.seekPosition : timeline.position) * timeline.scaleFactor - (scrollView.width / 2))
}
//root.snapping = timeline.snap ? 10 / Math.sqrt(root.timeScale) : -1
ruler.adjustStepSize()
if (dragProxy.draggedItem > -1 && dragProxy.masterObject) {
// update dragged item pos
dragProxy.masterObject.updateDrag()
}
}
onViewActiveTrackChanged: {
var tk = Logic.getTrackById(timeline.activeTrack)
if (tk.y < scrollView.flickableItem.contentY) {
scrollView.flickableItem.contentY = Math.max(0, tk.y - scrollView.height / 3)
} else if (tk.y + tk.height > scrollView.flickableItem.contentY + scrollView.viewport.height) {
scrollView.flickableItem.contentY = Math.min(trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height, tk.y - scrollView.height / 3)
}
}
onActiveToolChanged: {
if (root.activeTool == 2) {
// Spacer activated
endDrag()
} else if (root.activeTool == 0) {
var tk = getMouseTrack()
if (tk < 0) {
console.log('........ MOUSE OUTSIDE TRAKS\n\n.........')
return
}
var pos = getMousePos() * timeline.scaleFactor
var sourceTrack = Logic.getTrackById(tk)
var allowComposition = tracksArea.mouseY- sourceTrack.y > sourceTrack.height / 2
var tentativeItem = undefined
if (allowComposition) {
tentativeItem = getItemAtPos(tk, pos, true)
}
if (!tentativeItem) {
tentativeItem = getItemAtPos(tk, pos, false)
}
if (tentativeItem) {
tentativeItem.updateDrag()
}
}
}
DropArea { //Drop area for compositions
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
x: headerWidth
keys: 'kdenlive/composition'
onEntered: {
console.log("Trying to drop composition")
if (clipBeingMovedId == -1) {
console.log("No clip being moved")
var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
droppedPosition = frame
if (track >= 0 && !controller.isAudioTrack(track)) {
clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
console.log("Trying to insert",track, frame, clipBeingDroppedData)
clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData, false)
console.log("id",clipBeingDroppedId)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
drag.acceptProposedAction()
} else {
drag.accepted = false
}
}
}
onPositionChanged: {
if (clipBeingMovedId == -1) {
var track = Logic.getTrackIdFromPos(drag.y + scrollView.flickableItem.contentY)
if (track !=-1) {
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
if (clipBeingDroppedId >= 0){
if (controller.isAudioTrack(track)) {
// Don't allow moving composition to an audio track
track = controller.getCompositionTrackId(clipBeingDroppedId)
}
controller.requestCompositionMove(clipBeingDroppedId, track, frame, true, false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else if (!controller.isAudioTrack(track)) {
clipBeingDroppedData = drag.getDataAsString('kdenlive/composition')
clipBeingDroppedId = timeline.insertComposition(track, frame, clipBeingDroppedData , false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
}
}
}
}
onExited:{
if (clipBeingDroppedId != -1) {
controller.requestItemDeletion(clipBeingDroppedId, false)
}
clearDropData()
}
onDropped: {
if (clipBeingDroppedId != -1) {
var frame = controller.getCompositionPosition(clipBeingDroppedId)
var track = controller.getCompositionTrackId(clipBeingDroppedId)
// we simulate insertion at the final position so that stored undo has correct value
controller.requestItemDeletion(clipBeingDroppedId, false)
timeline.insertNewComposition(track, frame, clipBeingDroppedData, true)
}
clearDropData()
}
}
DropArea { //Drop area for bin/clips
/** @brief local helper function to handle the insertion of multiple dragged items */
function insertAndMaybeGroup(track, frame, droppedData) {
var binIds = droppedData.split(";")
if (binIds.length == 0) {
return -1
}
var id = -1
if (binIds.length == 1) {
id = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true, false)
} else {
var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true, false)
// if the clip insertion succeeded, request the clips to be grouped
if (ids.length > 0) {
timeline.selectItems(ids)
id = ids[0]
}
}
return id
}
property int fakeFrame: -1
property int fakeTrack: -1
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
x: headerWidth
keys: 'kdenlive/producerslist'
onEntered: {
if (clipBeingMovedId == -1) {
//var track = Logic.getTrackIdFromPos(drag.y)
var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
if (track >= 0 && track < tracksRepeater.count) {
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
droppedPosition = frame
timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
//drag.acceptProposedAction()
clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist')
console.log('dropped data: ', clipBeingDroppedData)
if (controller.normalEdit()) {
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData)
} else {
// we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
if (clipBeingDroppedId > -1) {
fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping))
fakeTrack = timeline.activeTrack
} else {
drag.accepted = false
}
}
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else {
drag.accepted = false
}
}
}
onExited:{
if (clipBeingDroppedId != -1) {
controller.requestItemDeletion(clipBeingDroppedId, false)
}
clearDropData()
}
onPositionChanged: {
if (clipBeingMovedId == -1) {
var track = Logic.getTrackIndexFromPos(drag.y + scrollView.flickableItem.contentY)
if (track >= 0 && track < tracksRepeater.count) {
timeline.activeTrack = tracksRepeater.itemAt(track).trackInternalId
var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)
if (clipBeingDroppedId >= 0){
fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping))
fakeTrack = timeline.activeTrack
//controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false)
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
} else {
frame = controller.suggestSnapPoint(frame, Math.floor(root.snapping))
if (controller.normalEdit()) {
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true)
} else {
// we want insert/overwrite mode, make a fake insert at end of timeline, then move to position
clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, timeline.fullDuration, clipBeingDroppedData)
fakeFrame = controller.suggestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, timeline.position, Math.floor(root.snapping))
fakeTrack = timeline.activeTrack
}
continuousScrolling(drag.x + scrollView.flickableItem.contentX)
}
}
}
}
onDropped: {
if (clipBeingDroppedId != -1) {
var frame = controller.getClipPosition(clipBeingDroppedId)
var track = controller.getClipTrackId(clipBeingDroppedId)
if (!controller.normalEdit()) {
frame = fakeFrame
track = fakeTrack
}
/* We simulate insertion at the final position so that stored undo has correct value
* NOTE: even if dropping multiple clips, requesting the deletion of the first one is
* enough as internally it will request the group deletion
*/
controller.requestItemDeletion(clipBeingDroppedId, false)
var binIds = clipBeingDroppedData.split(";")
if (binIds.length == 1) {
if (controller.normalEdit()) {
timeline.insertClip(track, frame, clipBeingDroppedData, true, true, false)
} else {
timeline.insertClipZone(clipBeingDroppedData, track, frame)
}
} else {
if (controller.normalEdit()) {
timeline.insertClips(track, frame, binIds, true, true)
} else {
// TODO
console.log('multiple clips insert/overwrite not supported yet')
}
}
fakeTrack = -1
fakeFrame = -1
}
clearDropData()
}
}
OLD.Menu {
id: menu
property int clickedX
property int clickedY
onAboutToHide: {
timeline.ungrabHack()
editGuideMenu.visible = false
}
OLD.MenuItem {
text: i18n("Paste")
iconName: 'edit-paste'
visible: copiedClip != -1
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.pasteItem(frame, track)
}
}
OLD.MenuItem {
text: i18n("Insert Space")
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.insertSpace(track, frame);
}
}
OLD.MenuItem {
text: i18n("Remove Space On Active Track")
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.removeSpace(track, frame);
}
}
OLD.MenuItem {
text: i18n("Remove Space")
onTriggered: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.floor((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
timeline.removeSpace(track, frame, true);
}
}
OLD.MenuItem {
id: addGuideMenu
text: i18n("Add Guide")
onTriggered: {
timeline.switchGuide(timeline.position);
}
}
GuidesMenu {
title: i18n("Go to guide...")
menuModel: guidesModel
enabled: guidesDelegateModel.count > 0
onGuideSelected: {
timeline.seekPosition = assetFrame
timeline.position = timeline.seekPosition
}
}
OLD.MenuItem {
id: editGuideMenu
text: i18n("Edit Guide")
visible: false
onTriggered: {
timeline.editGuide(timeline.position);
}
}
AssetMenu {
title: i18n("Insert a composition...")
menuModel: transitionModel
isTransition: true
onAssetSelected: {
var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height + scrollView.flickableItem.contentY)
var frame = Math.round((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor)
var id = timeline.insertComposition(track, frame, assetId, true)
if (id == -1) {
compositionFail.open()
}
}
}
onAboutToShow: {
if (guidesModel.hasMarker(timeline.position)) {
// marker at timeline position
addGuideMenu.text = i18n("Remove Guide")
editGuideMenu.visible = true
} else {
addGuideMenu.text = i18n("Add Guide")
}
console.log("pop menu")
}
}
OLD.Menu {
id: rulermenu
property int clickedX
property int clickedY
onAboutToHide: {
timeline.ungrabHack()
editGuideMenu2.visible = false
}
OLD.MenuItem {
id: addGuideMenu2
text: i18n("Add Guide")
onTriggered: {
timeline.switchGuide(timeline.position);
}
}
GuidesMenu {
title: i18n("Go to guide...")
menuModel: guidesModel
enabled: guidesDelegateModel.count > 0
onGuideSelected: {
timeline.seekPosition = assetFrame
timeline.position = timeline.seekPosition
}
}
OLD.MenuItem {
id: editGuideMenu2
text: i18n("Edit Guide")
visible: false
onTriggered: {
timeline.editGuide(timeline.position);
}
}
OLD.MenuItem {
id: addProjectNote
text: i18n("Add Project Note")
onTriggered: {
timeline.triggerAction('add_project_note')
}
}
onAboutToShow: {
if (guidesModel.hasMarker(timeline.position)) {
// marker at timeline position
addGuideMenu2.text = i18n("Remove Guide")
editGuideMenu2.visible = true
} else {
addGuideMenu2.text = i18n("Add Guide")
}
console.log("pop menu")
}
}
MessageDialog {
id: compositionFail
title: i18n("Timeline error")
icon: StandardIcon.Warning
text: i18n("Impossible to add a composition at that position. There might not be enough space")
standardButtons: StandardButton.Ok
}
OLD.Menu {
id: headerMenu
property int trackId: -1
property int thumbsFormat: 0
property bool audioTrack: false
property bool recEnabled: false
onAboutToHide: {
timeline.ungrabHack()
}
OLD.MenuItem {
text: i18n("Add Track")
onTriggered: {
timeline.addTrack(timeline.activeTrack)
}
}
OLD.MenuItem {
text: i18n("Delete Track")
onTriggered: {
timeline.deleteTrack(timeline.activeTrack)
}
}
OLD.MenuItem {
visible: headerMenu.audioTrack
id: showRec
text: i18n("Show Record Controls")
onTriggered: {
controller.setTrackProperty(headerMenu.trackId, "kdenlive:audio_rec", showRec.checked ? '1' : '0')
}
checkable: true
checked: headerMenu.recEnabled
}
OLD.MenuItem {
visible: headerMenu.audioTrack
id: configRec
text: i18n("Configure Recording")
onTriggered: {
timeline.showConfig(4,2)
}
}
OLD.Menu {
title: i18n("Track thumbnails")
visible: !headerMenu.audioTrack
OLD.ExclusiveGroup { id: thumbStyle }
OLD.MenuItem {
text: i18n("In frame")
id: inFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 2)
checkable: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: i18n("In / out frames")
id: inOutFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 0)
checkable: true
checked: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: i18n("All frames")
id: allFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 1)
checkable: true
exclusiveGroup: thumbStyle
}
OLD.MenuItem {
text: i18n("No thumbnails")
id: noFrame
onTriggered:controller.setTrackProperty(headerMenu.trackId, "kdenlive:thumbs_format", 3)
checkable: true
exclusiveGroup: thumbStyle
}
onAboutToShow: {
switch(headerMenu.thumbsFormat) {
case 3:
noFrame.checked = true
break
case 2:
inFrame.checked = true
break
case 1:
allFrame.checked = true
break
default:
inOutFrame.checked = true
break
}
}
}
}
Row {
Column {
id: headerContainer
z: 1
Rectangle {
id: cornerstone
property bool selected: false
// Padding between toolbar and track headers.
width: headerWidth
height: ruler.height
color: 'transparent' //selected? shotcutBlue : activePalette.window
border.color: selected? 'red' : 'transparent'
border.width: selected? 1 : 0
z: 1
}
Flickable {
// Non-slider scroll area for the track headers.
id: headerFlick
contentY: scrollView.flickableItem.contentY
width: headerWidth
height: 100
interactive: false
MouseArea {
width: trackHeaders.width
height: trackHeaders.height
acceptedButtons: Qt.NoButton
onWheel: {
var newScroll = Math.min(scrollView.flickableItem.contentY - wheel.angleDelta.y, height - tracksArea.height + scrollView.__horizontalScrollBar.height + cornerstone.height)
scrollView.flickableItem.contentY = Math.max(newScroll, 0)
}
}
Column {
id: trackHeaders
spacing: 0
Repeater {
id: trackHeaderRepeater
model: multitrack
TrackHead {
trackName: model.name
thumbsFormat: model.thumbsFormat
trackTag: model.trackTag
isDisabled: model.disabled
isComposite: model.composite
isLocked: model.locked
isActive: model.trackActive
isAudio: model.audio
showAudioRecord: model.audioRecord
effectNames: model.effectNames
isStackEnabled: model.isStackEnabled
width: headerWidth
current: item === timeline.activeTrack
trackId: item
height: model.trackHeight
onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked
collapsed: height <= collapsedHeight
onMyTrackHeightChanged: {
collapsed = myTrackHeight <= collapsedHeight
if (!collapsed) {
controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight)
controller.setTrackProperty(trackId, "kdenlive:collapsed", "0")
} else {
controller.setTrackProperty(trackId, "kdenlive:collapsed", collapsedHeight)
}
// hack: change property to trigger transition adjustment
root.trackHeight = root.trackHeight === 1 ? 0 : 1
}
onClicked: {
timeline.activeTrack = tracksRepeater.itemAt(index).trackInternalId
console.log('track name: ',index, ' = ', model.name,'/',tracksRepeater.itemAt(index).trackInternalId)
//timeline.selectTrackHead(currentTrack)
}
}
}
}
Column {
id: trackHeadersResizer
spacing: 0
width: 5
Rectangle {
id: resizer
height: trackHeaders.height
width: 3
x: root.headerWidth - 2
color: 'red'
opacity: 0
Drag.active: headerMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
MouseArea {
id: headerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeHorCursor
drag.target: parent
drag.axis: Drag.XAxis
drag.minimumX: 2 * baseUnit
property double startX
property double originalX
drag.smoothed: false
onPressed: {
root.stopScrolling = true
}
onReleased: {
root.stopScrolling = false
parent.opacity = 0
}
onEntered: parent.opacity = 0.5
onExited: parent.opacity = 0
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
parent.opacity = 0.5
headerWidth = Math.max(10, mapToItem(null, x, y).x + 2)
timeline.setHeaderWidth(headerWidth)
}
}
}
}
}
}
}
MouseArea {
id: tracksArea
property real clickX
property real clickY
width: root.width - headerWidth
height: root.height
Keys.onDownPressed: {
root.moveSelectedTrack(1)
}
Keys.onUpPressed: {
root.moveSelectedTrack(-1)
}
// This provides continuous scrubbing and scimming at the left/right edges.
hoverEnabled: true
acceptedButtons: Qt.RightButton | Qt.LeftButton | Qt.MidButton
cursorShape: tracksArea.mouseY < ruler.height || root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor
onWheel: {
if (wheel.modifiers & Qt.AltModifier) {
// Alt + wheel = seek to next snap point
if (wheel.angleDelta.x > 0) {
timeline.triggerAction('monitor_seek_snap_backward')
} else {
timeline.triggerAction('monitor_seek_snap_forward')
}
} else {
var delta = wheel.modifiers & Qt.ShiftModifier ? timeline.fps() : 1
if (timeline.seekPosition > -1) {
timeline.seekPosition = Math.min(timeline.seekPosition - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
} else {
timeline.seekPosition = Math.min(timeline.position - (wheel.angleDelta.y > 0 ? delta : -delta), timeline.fullDuration - 1)
}
timeline.position = timeline.seekPosition
}
}
onPressed: {
focus = true
if (mouse.buttons === Qt.MidButton || (root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier && !(mouse.modifiers & Qt.ShiftModifier))) {
clickX = mouseX
clickY = mouseY
return
}
if (root.activeTool === 0 && mouse.modifiers & Qt.ShiftModifier && mouse.y > ruler.height) {
console.log('1111111111111\nREAL SHIFT PRESSED\n111111111111\n')
// rubber selection
rubberSelect.x = mouse.x + tracksArea.x
rubberSelect.y = mouse.y
rubberSelect.originX = mouse.x
rubberSelect.originY = rubberSelect.y
rubberSelect.width = 0
rubberSelect.height = 0
} else if (mouse.button & Qt.LeftButton) {
if (root.activeTool === 1) {
// razor tool
var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
timeline.cutClipUnderCursor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId)
}
if (dragProxy.draggedItem > -1) {
mouse.accepted = false
return
}
if (root.activeTool === 2 && mouse.y > ruler.height) {
// spacer tool
var y = mouse.y - ruler.height + scrollView.flickableItem.contentY
var frame = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor
var track = (mouse.modifiers & Qt.ControlModifier) ? tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackInternalId : -1
spacerGroup = timeline.requestSpacerStartOperation(track, frame)
if (spacerGroup > -1) {
drag.axis = Drag.XAxis
Drag.active = true
Drag.proposedAction = Qt.MoveAction
spacerClickFrame = frame
spacerFrame = controller.getItemPosition(spacerGroup)
}
} else if (root.activeTool === 0 || mouse.y <= ruler.height) {
if (mouse.y > ruler.height) {
controller.requestClearSelection();
}
timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
timeline.position = timeline.seekPosition
}
} else if (mouse.button & Qt.RightButton) {
menu.clickedX = mouse.x
menu.clickedY = mouse.y
if (mouse.y > ruler.height) {
timeline.activeTrack = tracksRepeater.itemAt(Logic.getTrackIndexFromPos(mouse.y - ruler.height + scrollView.flickableItem.contentY)).trackInternalId
menu.popup()
} else {
// ruler menu
rulermenu.popup()
}
}
}
property bool scim: false
onExited: {
scim = false
}
onPositionChanged: {
if (pressed && ((mouse.buttons === Qt.MidButton) || (mouse.buttons === Qt.LeftButton && root.activeTool == 0 && mouse.modifiers & Qt.ControlModifier && !(mouse.modifiers & Qt.ShiftModifier)))) {
var newScroll = Math.min(scrollView.flickableItem.contentX - (mouseX - clickX), timeline.fullDuration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width))
var vertScroll = Math.min(scrollView.flickableItem.contentY - (mouseY - clickY), trackHeaders.height - scrollView.height + scrollView.__horizontalScrollBar.height)
scrollView.flickableItem.contentX = Math.max(newScroll, 0)
scrollView.flickableItem.contentY = Math.max(vertScroll, 0)
clickX = mouseX
clickY = mouseY
return
}
if (!pressed && !rubberSelect.visible && root.activeTool === 1) {
cutLine.x = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) * timeline.scaleFactor - scrollView.flickableItem.contentX
if (mouse.modifiers & Qt.ShiftModifier) {
timeline.position = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor)
}
}
var mousePos = Math.max(0, Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor))
root.mousePosChanged(mousePos)
ruler.showZoneLabels = mouse.y < ruler.height
if (mouse.modifiers & Qt.ShiftModifier && mouse.buttons === Qt.LeftButton && root.activeTool === 0 && !rubberSelect.visible && rubberSelect.y > 0) {
// rubber selection
rubberSelect.visible = true
}
if (rubberSelect.visible) {
var newX = mouse.x
var newY = mouse.y
if (newX < rubberSelect.originX) {
rubberSelect.x = newX + tracksArea.x
rubberSelect.width = rubberSelect.originX - newX
} else {
rubberSelect.x = rubberSelect.originX + tracksArea.x
rubberSelect.width = newX - rubberSelect.originX
}
if (newY < rubberSelect.originY) {
rubberSelect.y = newY
rubberSelect.height = rubberSelect.originY - newY
} else {
rubberSelect.y = rubberSelect.originY
rubberSelect.height= newY - rubberSelect.originY
}
} else if (mouse.buttons === Qt.LeftButton) {
if (root.activeTool === 0 || mouse.y < ruler.height) {
timeline.seekPosition = Math.max(0, Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1))
timeline.position = timeline.seekPosition
} else if (root.activeTool === 2 && spacerGroup > -1) {
// Move group
var track = controller.getItemTrackId(spacerGroup)
var frame = Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) + spacerFrame - spacerClickFrame
frame = controller.suggestItemMove(spacerGroup, track, frame, timeline.position, Math.floor(root.snapping))
continuousScrolling(mouse.x + scrollView.flickableItem.contentX)
}
scim = true
} else {
scim = false
}
}
onReleased: {
if (rubberSelect.visible) {
rubberSelect.visible = false
var y = rubberSelect.y - ruler.height + scrollView.flickableItem.contentY
var topTrack = Logic.getTrackIndexFromPos(Math.max(0, y))
var bottomTrack = Logic.getTrackIndexFromPos(y + rubberSelect.height)
if (bottomTrack >= topTrack) {
var t = []
for (var i = topTrack; i <= bottomTrack; i++) {
t.push(tracksRepeater.itemAt(i).trackInternalId)
}
var startFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x) / timeline.scaleFactor
var endFrame = (scrollView.flickableItem.contentX - tracksArea.x + rubberSelect.x + rubberSelect.width) / timeline.scaleFactor
timeline.selectItems(t, startFrame, endFrame, mouse.modifiers & Qt.ControlModifier);
}
rubberSelect.y = -1
} else if (mouse.modifiers & Qt.ShiftModifier) {
if (root.activeTool == 1) {
// Shift click, process seek
timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.fullDuration - 1)
timeline.position = timeline.seekPosition
} else if (dragProxy.draggedItem > -1){
// Select item
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
console.log('ADD SELECTION: ', dragProxy.draggedItem)
controller.requestAddToSelection(dragProxy.draggedItem)
} else {
console.log('REMOVE SELECTION: ', dragProxy.draggedItem)
controller.requestRemoveFromSelection(dragProxy.draggedItem)
}
}
return
}
if (spacerGroup > -1) {
var frame = controller.getItemPosition(spacerGroup)
timeline.requestSpacerEndOperation(spacerGroup, spacerFrame, frame);
spacerClickFrame = -1
spacerFrame = -1
spacerGroup = -1
}
scim = false
}
Column {
Flickable {
// Non-slider scroll area for the Ruler.
id: rulercontainer
width: root.width - headerWidth
height: fontMetrics.font.pixelSize * 2
contentX: scrollView.flickableItem.contentX
contentWidth: Math.max(parent.width, timeline.fullDuration * timeScale)
interactive: false
clip: true
Ruler {
id: ruler
width: rulercontainer.contentWidth
height: parent.height
Rectangle {
id: seekCursor
visible: timeline.seekPosition > -1
color: activePalette.highlight
width: 4
height: ruler.height
opacity: 0.5
x: timeline.seekPosition * timeline.scaleFactor
}
TimelinePlayhead {
id: playhead
visible: timeline.position > -1
height: baseUnit
width: baseUnit * 1.5
fillColor: activePalette.windowText
anchors.bottom: parent.bottom
x: timeline.position * timeline.scaleFactor - (width / 2)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: zoomByWheel(wheel)
cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : tracksArea.cursorShape
}
}
}
OLD.ScrollView {
id: scrollView
width: root.width - headerWidth
height: root.height - ruler.height
y: ruler.height
// Click and drag should seek, not scroll the timeline view
flickableItem.interactive: false
clip: true
Rectangle {
id: tracksContainerArea
width: Math.max(scrollView.width - scrollView.__verticalScrollBar.width, timeline.fullDuration * timeScale)
height: trackHeaders.height
//Math.max(trackHeaders.height, scrollView.contentHeight - scrollView.__horizontalScrollBar.height)
color: root.color
Rectangle {
// Drag proxy, responsible for clip / composition move
id: dragProxy
x: 0
y: 0
width: 0
height: 0
property int draggedItem: -1
property int sourceTrack
property int sourceFrame
property bool isComposition
property int verticalOffset
property var masterObject
color: 'transparent'
//opacity: 0.8
MouseArea {
id: dragProxyArea
anchors.fill: parent
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
drag.minimumX: 0
property int dragFrame
property bool moveMirrorTracks: true
cursorShape: root.activeTool == 0 ? dragProxyArea.drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor : tracksArea.cursorShape
enabled: root.activeTool == 0
onPressed: {
console.log('+++++++++++++++++++ DRAG CLICKED +++++++++++++')
if (mouse.modifiers & Qt.ControlModifier || mouse.modifiers & Qt.ShiftModifier) {
mouse.accepted = false
console.log('+++++++++++++++++++ Shift abort+++++++++++++')
return
}
if (!timeline.exists(dragProxy.draggedItem)) {
endDrag()
mouse.accepted = false
return
}
dragFrame = -1
moveMirrorTracks = !(mouse.modifiers & Qt.MetaModifier)
timeline.activeTrack = dragProxy.sourceTrack
if (timeline.selection.indexOf(dragProxy.draggedItem) == -1) {
controller.requestAddToSelection(dragProxy.draggedItem, /*clear=*/ true)
}
timeline.showAsset(dragProxy.draggedItem)
root.stopScrolling = true
clipBeingMovedId = dragProxy.draggedItem
if (dragProxy.draggedItem > -1) {
var tk = controller.getItemTrackId(dragProxy.draggedItem)
var x = controller.getItemPosition(dragProxy.draggedItem)
var posx = Math.round((parent.x)/ root.timeScale)
var clickAccepted = true
var currentMouseTrack = Logic.getTrackIdFromPos(parent.y)
if (controller.normalEdit() && (tk != currentMouseTrack || x != posx)) {
console.log('INCORRECT DRAG, Trying to recover item: ', parent.y,' XPOS: ',x,'=',posx,'on track: ',tk ,'\n!!!!!!!!!!')
// Try to find correct item
var tentativeClip = getItemAtPos(currentMouseTrack, mouseX + parent.x, dragProxy.isComposition)
if (tentativeClip && tentativeClip.clipId) {
console.log('FOUND MISSING ITEM: ', tentativeClip.clipId)
clickAccepted = true
dragProxy.draggedItem = tentativeClip.clipId
dragProxy.x = tentativeClip.x
dragProxy.y = tentativeClip.y
dragProxy.width = tentativeClip.width
dragProxy.height = tentativeClip.height
dragProxy.masterObject = tentativeClip
dragProxy.sourceTrack = tk
dragProxy.sourceFrame = tentativeClip.modelStart
dragProxy.isComposition = tentativeClip.isComposition
} else {
console.log('COULD NOT FIND ITEM ')
clickAccepted = false
mouse.accepted = false
dragProxy.draggedItem = -1
dragProxy.masterObject = undefined
dragProxy.sourceFrame = -1
parent.x = 0
parent.y = 0
parent.width = 0
parent.height = 0
}
}
if (clickAccepted && dragProxy.draggedItem != -1) {
focus = true;
dragProxy.masterObject.originalX = dragProxy.masterObject.x
dragProxy.masterObject.originalTrackId = dragProxy.masterObject.trackId
dragProxy.masterObject.forceActiveFocus();
}
} else {
mouse.accepted = false
parent.x = 0
parent.y = 0
parent.width = 0
parent.height = 0
}
}
onPositionChanged: {
// we have to check item validity in the controller, because they could have been deleted since the beginning of the drag
if (dragProxy.draggedItem > -1 && !timeline.exists(dragProxy.draggedItem)) {
endDrag()
return
}
if (dragProxy.draggedItem > -1 && mouse.buttons === Qt.LeftButton && (controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) {
continuousScrolling(mouse.x + parent.x)
var mapped = tracksContainerArea.mapFromItem(dragProxy, mouse.x, mouse.y).x
root.mousePosChanged(Math.round(mapped / timeline.scaleFactor))
var posx = Math.round((parent.x)/ root.timeScale)
var posy = Math.min(Math.max(0, mouse.y + parent.y - dragProxy.verticalOffset), tracksContainerArea.height)
var tId = Logic.getTrackIdFromPos(posy)
if (dragProxy.masterObject && tId == dragProxy.masterObject.trackId) {
- if (posx == dragFrame) {
+ if (posx == dragFrame && controller.normalEdit()) {
return
}
}
if (dragProxy.isComposition) {
dragFrame = controller.suggestCompositionMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping))
timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem)
} else {
if (!controller.normalEdit() && dragProxy.masterObject.parent != dragContainer) {
var pos = dragProxy.masterObject.mapToGlobal(dragProxy.masterObject.x, dragProxy.masterObject.y);
dragProxy.masterObject.parent = dragContainer
pos = dragProxy.masterObject.mapFromGlobal(pos.x, pos.y)
dragProxy.masterObject.x = pos.x
dragProxy.masterObject.y = pos.y
//console.log('bringing item to front')
}
dragFrame = controller.suggestClipMove(dragProxy.draggedItem, tId, posx, timeline.position, Math.floor(root.snapping), moveMirrorTracks)
timeline.activeTrack = timeline.getItemMovingTrack(dragProxy.draggedItem)
}
var delta = dragFrame - dragProxy.sourceFrame
if (delta != 0) {
var s = timeline.simplifiedTC(Math.abs(delta))
s = ((delta < 0)? '-' : '+') + s + i18n("\nPosition:%1", timeline.simplifiedTC(dragFrame))
bubbleHelp.show(parent.x + mouseX, Math.max(ruler.height, Logic.getTrackYFromId(timeline.activeTrack)), s)
} else bubbleHelp.hide()
}
}
onReleased: {
clipBeingMovedId = -1
root.stopScrolling = false
if (dragProxy.draggedItem > -1 && dragFrame > -1 && (controller.isClip(dragProxy.draggedItem) || controller.isComposition(dragProxy.draggedItem))) {
var tId = controller.getItemTrackId(dragProxy.draggedItem)
if (dragProxy.isComposition) {
controller.requestCompositionMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, true, false, false)
controller.requestCompositionMove(dragProxy.draggedItem, tId, dragFrame , true, true, true)
} else {
if (controller.normalEdit()) {
controller.requestClipMove(dragProxy.draggedItem, dragProxy.sourceTrack, dragProxy.sourceFrame, moveMirrorTracks, true, false, false)
controller.requestClipMove(dragProxy.draggedItem, tId, dragFrame , moveMirrorTracks, true, true, true)
} else {
// Fake move, only process final move
timeline.endFakeMove(dragProxy.draggedItem, dragFrame, true, true, true)
}
}
if (dragProxy.masterObject && dragProxy.masterObject.isGrabbed) {
dragProxy.masterObject.grabItem()
}
dragProxy.x = controller.getItemPosition(dragProxy.draggedItem) * timeline.scaleFactor
dragProxy.sourceFrame = dragFrame
bubbleHelp.hide()
}
}
onDoubleClicked: {
if (dragProxy.masterObject.keyframeModel) {
var newVal = (dragProxy.height - mouseY) / dragProxy.height
var newPos = Math.round(mouseX / timeScale) + dragProxy.masterObject.inPoint
timeline.addEffectKeyframe(dragProxy.draggedItem, newPos, newVal)
}
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: zoomByWheel(wheel)
cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : tracksArea.cursorShape
}
Column {
// These make the striped background for the tracks.
// It is important that these are not part of the track visual hierarchy;
// otherwise, the clips will be obscured by the Track's background.
Repeater {
model: multitrack
id: trackBaseRepeater
delegate: Rectangle {
width: tracksContainerArea.width
border.width: 1
border.color: root.frameColor
height: model.trackHeight
color: tracksRepeater.itemAt(index) ? ((tracksRepeater.itemAt(index).trackInternalId === timeline.activeTrack) ? Qt.tint(getTrackColor(tracksRepeater.itemAt(index).isAudio, false), selectedTrackColor) : getTrackColor(tracksRepeater.itemAt(index).isAudio, false)) : 'red'
}
}
}
Column {
id: tracksContainer
Repeater { id: tracksRepeater; model: trackDelegateModel }
Item {
id: dragContainer
z: 100
}
Repeater { id: guidesRepeater; model: guidesDelegateModel }
}
Rectangle {
id: cursor
visible: timeline.position > -1
color: root.textColor
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: parent.height
x: timeline.position * timeline.scaleFactor
}
}
}
}
/*CornerSelectionShadow {
y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0
clip: timeline.selection.length ?
tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[0]) : null
opacity: clip && clip.x + clip.width < scrollView.flickableItem.contentX ? 1 : 0
}
CornerSelectionShadow {
y: tracksRepeater.count ? tracksRepeater.itemAt(currentTrack).y + ruler.height - scrollView.flickableItem.contentY : 0
clip: timeline.selection.length ?
tracksRepeater.itemAt(currentTrack).clipAt(timeline.selection[timeline.selection.length - 1]) : null
opacity: clip && clip.x > scrollView.flickableItem.contentX + scrollView.width ? 1 : 0
anchors.right: parent.right
mirrorGradient: true
}*/
Rectangle {
id: cutLine
visible: root.activeTool == 1 && tracksArea.mouseY > ruler.height
color: 'red'
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: root.height - scrollView.__horizontalScrollBar.height - ruler.height
x: 0
//x: timeline.position * timeline.scaleFactor - scrollView.flickableItem.contentX
y: ruler.height
}
}
}
Rectangle {
id: bubbleHelp
property alias text: bubbleHelpLabel.text
color: root.color //application.toolTipBaseColor
width: bubbleHelpLabel.width + 8
height: bubbleHelpLabel.height + 8
radius: 4
states: [
State { name: 'invisible'; PropertyChanges { target: bubbleHelp; opacity: 0} },
State { name: 'visible'; PropertyChanges { target: bubbleHelp; opacity: 0.8} }
]
state: 'invisible'
transitions: [
Transition {
from: 'invisible'
to: 'visible'
OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad }
},
Transition {
from: 'visible'
to: 'invisible'
OpacityAnimator { target: bubbleHelp; duration: 200; easing.type: Easing.InOutQuad }
}
]
Label {
id: bubbleHelpLabel
color: activePalette.text //application.toolTipTextColor
anchors.centerIn: parent
font.pixelSize: root.baseUnit
}
function show(x, y, text) {
bubbleHelp.x = x + tracksArea.x - scrollView.flickableItem.contentX - bubbleHelpLabel.width
bubbleHelp.y = y + tracksArea.y - scrollView.flickableItem.contentY - bubbleHelpLabel.height
bubbleHelp.text = text
if (bubbleHelp.state !== 'visible')
bubbleHelp.state = 'visible'
}
function hide() {
bubbleHelp.state = 'invisible'
bubbleHelp.opacity = 0
}
}
Rectangle {
id: rubberSelect
property int originX
property int originY
y: -1
color: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.4)
border.color: activePalette.highlight
border.width: 1
visible: false
}
/*DropShadow {
source: bubbleHelp
anchors.fill: bubbleHelp
opacity: bubbleHelp.opacity
horizontalOffset: 3
verticalOffset: 3
radius: 8
color: '#80000000'
transparentBorder: true
fast: true
}*/
DelegateModel {
id: trackDelegateModel
model: multitrack
delegate: Track {
trackModel: multitrack
rootIndex: trackDelegateModel.modelIndex(index)
timeScale: timeline.scaleFactor
width: tracksContainerArea.width
height: trackHeight
isAudio: audio
trackThumbsFormat: thumbsFormat
isCurrentTrack: item === timeline.activeTrack
trackInternalId: item
z: tracksRepeater.count - index
}
}
DelegateModel {
id: guidesDelegateModel
model: guidesModel
Item {
id: guideRoot
z: 20
Rectangle {
id: guideBase
width: 1
height: tracksContainer.height
x: model.frame * timeScale;
color: model.color
}
Rectangle {
visible: mlabel.visible
opacity: 0.7
x: guideBase.x
y: mlabel.y
radius: 2
width: mlabel.width + 4
height: mlabel.height
color: model.color
MouseArea {
z: 10
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
property int startX
drag.axis: Drag.XAxis
drag.target: guideRoot
onPressed: {
drag.target = guideRoot
startX = guideRoot.x
}
onReleased: {
if (startX != guideRoot.x) {
timeline.moveGuide(model.frame, model.frame + guideRoot.x / timeline.scaleFactor)
}
drag.target = undefined
}
onPositionChanged: {
if (pressed) {
var frame = Math.round(model.frame + guideRoot.x / timeline.scaleFactor)
frame = controller.suggestSnapPoint(frame, root.snapping)
guideRoot.x = (frame - model.frame) * timeline.scaleFactor
}
}
drag.smoothed: false
onDoubleClicked: {
timeline.editGuide(model.frame)
drag.target = undefined
}
onClicked: timeline.position = guideBase.x / timeline.scaleFactor
}
}
Text {
id: mlabel
visible: timeline.showMarkers
text: model.comment
font.pixelSize: root.baseUnit
x: guideBase.x + 2
y: scrollView.flickableItem.contentY
color: 'white'
}
}
}
Connections {
target: timeline
onPositionChanged: if (!stopScrolling) Logic.scrollIfNeeded()
onFrameFormatChanged: ruler.adjustFormat()
onSelectionChanged: {
if (dragProxy.draggedItem > -1 && !timeline.exists(dragProxy.draggedItem)) {
endDrag()
}
}
}
// This provides continuous scrolling at the left/right edges.
Timer {
id: scrollTimer
interval: 25
repeat: true
triggeredOnStart: true
property var item
property bool backwards
onTriggered: {
var delta = backwards? -10 : 10
if (item) item.x += delta
scrollView.flickableItem.contentX += delta
if (scrollView.flickableItem.contentX <= 0 || clipBeingMovedId == -1)
stop()
}
}
}
diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp
index ab92a6885..98a479e5a 100644
--- a/src/timeline2/view/timelinecontroller.cpp
+++ b/src/timeline2/view/timelinecontroller.cpp
@@ -1,2608 +1,2619 @@
/***************************************************************************
* 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
#include
#include
#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);
}
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);
}
void TimelineController::setTargetTracks(QPair targets)
{
m_hasVideoTarget = targets.first >= 0;
m_hasAudioTarget = targets.second >= 0;
emit hasAudioTargetChanged();
emit hasVideoTargetChanged();
if (m_videoTargetActive) {
setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : targets.first);
}
if (m_audioTargetActive) {
setAudioTarget(m_hasAudioTarget && (m_lastAudioTarget > -1) ? m_lastAudioTarget : targets.second);
}
}
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();
}
int TimelineController::getCurrentItem()
{
// TODO: if selection is empty, return topmost clip under timeline cursor
auto selection = m_model->getCurrentSelection();
if (selection.empty()) {
return -1;
}
// TODO: if selection contains more than 1 clip, return topmost clip under timeline cursor in selection
return *(selection.begin());
}
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 minimum = m_model->getClipPosition(clipId);
int clip_duration = m_model->getClipPlaytime(clipId);
int position = minimum;
if (offset > clip_duration / 2) {
position += offset;
} else {
// Check if we have a composition at beginning
std::unordered_set existing = m_model->getTrackById_const(tid)->getCompositionsInRange(minimum, minimum + offset);
if (existing.size() > 0) {
position += offset;
}
}
position = qMin(minimum + clip_duration - 1, position);
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);
if (bottomId > 0) {
QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime());
if (bottom.first > minimum && position > bottom.first) {
int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first);
if (test_duration > 0) {
position = bottom.first;
duration = test_duration;
revert = position > minimum;
}
}
}
int duration2 = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position);
if (duration2 > 0) {
duration = (duration > 0) ? qMin(duration, duration2) : duration2;
}
}
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 (minimum + clip_duration - position < 3) {
position = minimum + clip_duration - duration;
}
}
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();
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 == selectedTrackIx) {
m_lastAudioTarget = -1;
emit lastAudioTargetChanged();
}
if (m_lastVideoTarget == selectedTrackIx) {
m_lastVideoTarget = -1;
emit lastVideoTargetChanged();
}
m_model->requestTrackDeletion(selectedTrackIx);
m_model->buildTrackCompositing(true);
if (m_activeTrack == -1) {
setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1));
}
}
}
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::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();
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);
}
bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition)
{
return TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition);
}
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(Mlt::Filter *filter)
{
if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) {
return true;
}
int clipId = getCurrentItem();
if (clipId == -1) {
pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500);
return false;
}
std::shared_ptr clip = m_model->getClipPtr(clipId);
const QString binId = clip->binId();
// Get clean bin copy of the clip
std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId);
std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut()));
// Get copy of timeline producer
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);
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);
}
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);
}
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 (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 = 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), 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;
}
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(int clipId)
{
if (clipId == -1) {
clipId = getMainSelectedItem(false, false);
}
TimelineFunctions::switchEnableState(m_model, clipId);
}
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;
}
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 = m_model->m_allClips[itemId]->getFakeTrackId();
+ 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 undo = []() { return true; };
std::function redo = []() { return true; };
bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
if (res && logUndo) {
+ // Terminate fake move
+ if (m_model->isClip(clipId)) {
+ m_model->m_allClips[clipId]->setFakeTrackId(-1);
+ }
pCore->pushUndo(undo, redo, i18n("Move group"));
}
return res;
}
bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo)
{
Q_ASSERT(m_model->m_allGroups.count(groupId) > 0);
bool ok = true;
auto all_items = m_model->m_groups->getLeaves(groupId);
Q_ASSERT(all_items.size() > 1);
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
// Sort clips. We need to delete from right to left to avoid confusing the view
std::vector sorted_clips(all_items.begin(), all_items.end());
std::sort(sorted_clips.begin(), sorted_clips.end(), [this](int clipId1, int clipId2) {
int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition();
int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition();
return p2 <= p1;
});
// Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
// This way, we ensure that no conflict will arise with clips inside the group being moved
// First, remove clips
int audio_delta, video_delta;
audio_delta = video_delta = delta_track;
int master_trackId = m_model->getItemTrackId(clipId);
if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) {
// Master clip is audio, so reverse delta for video clips
video_delta = -delta_track;
} else {
audio_delta = -delta_track;
}
int min = -1;
int max = -1;
std::unordered_map old_track_ids, old_position, old_forced_track, new_track_ids;
for (int item : sorted_clips) {
int old_trackId = m_model->getItemTrackId(item);
old_track_ids[item] = old_trackId;
if (old_trackId != -1) {
bool updateThisView = true;
if (m_model->isClip(item)) {
int current_track_position = m_model->getTrackPosition(old_trackId);
int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta;
int target_track_position = current_track_position + d;
auto it = m_model->m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
new_track_ids[item] = target_track;
old_position[item] = m_model->m_allClips[item]->getPosition();
int duration = m_model->m_allClips[item]->getPlaytime();
min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos);
max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration);
ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo, false, false);
} else {
// ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo);
old_position[item] = m_model->m_allCompositions[item]->getPosition();
old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack();
}
if (!ok) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
}
}
bool res = true;
if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
for (int item : sorted_clips) {
if (m_model->isClip(item) && new_track_ids.count(item) > 0) {
int target_track = new_track_ids[item];
int target_position = old_position[item] + delta_pos;
int duration = m_model->m_allClips[item]->getPlaytime();
res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo);
}
}
} else if (m_model->m_editMode == TimelineMode::InsertEdit) {
QList processedTracks;
for (int item : sorted_clips) {
int target_track = new_track_ids[item];
if (processedTracks.contains(target_track)) {
// already processed
continue;
}
processedTracks << target_track;
int target_position = min;
int startClipId = m_model->getClipByPosition(target_track, target_position);
if (startClipId > -1) {
// There is a clip, cut
res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo);
}
}
res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo);
}
for (int item : sorted_clips) {
if (m_model->isClip(item)) {
int target_track = new_track_ids[item];
int target_position = old_position[item] + delta_pos;
ok = ok && m_model->requestClipMove(item, target_track, target_position, true, updateView, finalMove, finalMove, undo, redo);
} else {
// ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo);
}
if (!ok) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
}
return true;
}
QStringList TimelineController::getThumbKeys()
{
QStringList result;
for (const auto &clp : m_model->m_allClips) {
const QString binId = getClipBinId(clp.first);
std::shared_ptr binClip = pCore->bin()->getBinClip(binId);
result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".png");
result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".png");
}
result.removeDuplicates();
return result;
}
bool TimelineController::isInSelection(int itemId)
{
return m_model->getCurrentSelection().count(itemId) > 0;
}
bool TimelineController::exists(int itemId)
{
return m_model->isClip(itemId) || m_model->isComposition(itemId);
}
void TimelineController::slotMultitrackView(bool enable)
{
TimelineFunctions::enableMultitrackView(m_model, enable);
}
void TimelineController::saveTimelineSelection(const QDir &targetDir)
{
TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir);
}
void TimelineController::addEffectKeyframe(int cid, int frame, double val)
{
if (m_model->isClip(cid)) {
std::shared_ptr