Commit ba564eda authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Initial commit for same track transitions

parent e83f8dc0
......@@ -1131,6 +1131,12 @@ void MainWindow::setupActions()
addAction(QStringLiteral("collapse_expand"), collapseItem, Qt::Key_Less);
connect(collapseItem, &QAction::triggered, this, &MainWindow::slotCollapse);
QAction *sameTrack = new QAction(QIcon::fromTheme(QStringLiteral("collapse-all")), i18n("Same Track"), this);
addAction(QStringLiteral("same_track"), sameTrack, Qt::Key_U);
connect(sameTrack, &QAction::triggered, [this]() {
getCurrentTimeline()->controller()->sameTrack();
});
// toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
/*QWidget * actionWidget;
......
......@@ -45,6 +45,8 @@ ClipModel::ClipModel(const std::shared_ptr<TimelineModel> &parent, std::shared_p
, m_speed(speed)
, m_fakeTrack(-1)
, m_positionOffset(0)
, m_subPlaylistIndex(0)
, m_mixDuration(0)
{
m_producer->set("kdenlive:id", binClipId.toUtf8().constData());
m_producer->set("_kdenlive_cid", m_id);
......@@ -648,6 +650,16 @@ void ClipModel::setPosition(int pos)
m_clipMarkerModel->updateSnapModelPos(pos);
}
void ClipModel::setMixDuration(int mix)
{
m_mixDuration = mix;
}
int ClipModel::getMixDuration() const
{
return m_mixDuration;
}
void ClipModel::setInOut(int in, int out)
{
MoveableItem::setInOut(in, out);
......
......@@ -101,6 +101,8 @@ public:
void setFakeTrackId(int fid);
int getFakePosition() const;
void setFakePosition(int fid);
void setMixDuration(int mix);
int getMixDuration() const;
void setGrab(bool grab) override;
void setSelected(bool sel) override;
......@@ -248,6 +250,9 @@ protected:
// Remember last set track, so that we don't unnecessarily refresh the producer when deleting and re-adding a clip on same track
int m_lastTrackId = -1;
// Duration of a same track mix.
int m_mixDuration;
};
#endif
......@@ -197,6 +197,7 @@ QHash<int, QByteArray> TimelineItemModel::roleNames() const
roles[FakeTrackIdRole] = "fakeTrackId";
roles[FakePositionRole] = "fakePosition";
roles[StartRole] = "start";
roles[MixRole] = "mixDuration";
roles[DurationRole] = "duration";
roles[MaxDurationRole] = "maxDuration";
roles[MarkersRole] = "markers";
......@@ -333,6 +334,8 @@ QVariant TimelineItemModel::data(const QModelIndex &index, int role) const
return clip->fadeIn();
case FadeOutRole:
return clip->fadeOut();
case MixRole:
return clip->getMixDuration();
case ReloadThumbRole:
return clip->forceThumbReload;
case PositionOffsetRole:
......
......@@ -609,6 +609,40 @@ bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool
return true;
};
}
Fun move_mix = []() { return true; };
Fun restore_mix = []() { return true; };
if (m_allClips[clipId]->getMixDuration() > 0) {
std::pair<int, int> mixData = getTrackById_const(old_trackId)->getMixInfo(clipId);
int offset = position - mixData.first;
qDebug()<<"==== MIX UPDATED: "<<mixData.second<<", OFFSET: "<<offset;
if (finalMove && (old_trackId != trackId || position >= mixData.first + mixData.second)) {;
// Clip moved to another track, or outside of mix duration, delete mix
move_mix = [this, old_trackId, clipId]() {
qDebug()<<"======\nRESETTING SUB PLAYLIST\n====";
m_allClips[clipId]->setSubPlaylistIndex(0);
return getTrackById_const(old_trackId)->deleteMix(clipId);
};
restore_mix = [this, old_trackId, clipId, mixData]() {
m_allClips[clipId]->setSubPlaylistIndex(1);
return getTrackById_const(old_trackId)->createMix(clipId, mixData);
};
qDebug()<<"========\n\n\nDELETED MIX\n\n================";
move_mix();
UPDATE_UNDO_REDO(move_mix, restore_mix, local_undo, local_redo);
} else if (old_trackId == trackId) {
// Clip moved on same track, resize mix
move_mix = [this, old_trackId, clipId, position]() {
return getTrackById_const(old_trackId)->resizeMix(clipId, position);
};
restore_mix = [this, old_trackId, clipId, position]() {
return getTrackById_const(old_trackId)->resizeMix(clipId, position);
};
move_mix();
UPDATE_UNDO_REDO(move_mix, restore_mix, local_undo, local_redo);
}
}
if (old_trackId != -1) {
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_undo);
......@@ -635,6 +669,52 @@ bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool
return true;
}
bool TimelineModel::requestClipMixMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
{
// qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView<<", FINAL: "<<finalMove;
if (trackId == -1) {
return false;
}
Q_ASSERT(isClip(clipId));
std::function<bool(void)> local_undo = []() { return true; };
std::function<bool(void)> local_redo = []() { return true; };
bool ok = true;
bool notifyViewOnly = false;
Fun update_model = []() { return true; };
// Move on same track, simply inform the view
updateView = false;
notifyViewOnly = true;
update_model = [clipId, this, trackId, invalidateTimeline]() {
qDebug()<<"==== PROCESSING UPDATE MODEL";
QModelIndex modelIndex = makeClipIndexFromID(clipId);
notifyChange(modelIndex, modelIndex, StartRole);
if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
int in = getClipPosition(clipId);
emit invalidateZone(in, in + getClipPlaytime(clipId));
}
return true;
};
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_undo);
}
ok = getTrackById(trackId)->requestClipMix(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove);
if (!ok) {
qDebug() << "-------------\nMIX FAILED, REVERTING\n\n-------------------";
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
update_model();
if (notifyViewOnly) {
PUSH_LAMBDA(update_model, local_redo);
}
qDebug()<<"======== FINISHED MIX CREATION FOR CLIP: "<<clipId;
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return ok;
}
bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
{
QWriteLocker locker(&m_lock);
......
......@@ -117,6 +117,7 @@ public:
IsProxyRole, /// clip only
ServiceRole, /// clip only
StartRole, /// clip only
MixRole, /// clip only
BinIdRole, /// clip only
TrackIdRole,
FakeTrackIdRole,
......@@ -354,6 +355,8 @@ public:
@param logUndo if set to false, no undo object is stored
*/
Q_INVOKABLE bool requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks = true, bool updateView = true, bool logUndo = true, bool invalidateTimeline = false);
bool requestClipMixMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove);
/* @brief Move a composition to a specific position This action is undoable
Returns true on success. If it fails, nothing is modified. If the clip is
......
......@@ -23,6 +23,7 @@
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "transitions/transitionsrepository.hpp"
#include "kdenlivesettings.h"
#include "logger.hpp"
#include "snapmodel.hpp"
......@@ -53,7 +54,7 @@ TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, int id, const
}
}
// For now we never use the second playlist, only planned for same track transitions
m_playlists[1].set("hide", 3);
//m_playlists[1].set("hide", 3);
m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight());
m_effectStack = EffectStackModel::construct(m_mainPlaylist, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
// TODO
......@@ -129,19 +130,23 @@ int TrackModel::getClipsCount()
Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove, bool groupMove)
{
QWriteLocker locker(&m_lock);
qDebug()<<"== PROCESSING INSRET OF_: "<<clipId;
// By default, insertion occurs in topmost track
// Find out the clip id at position
int target_clip = m_playlists[0].get_clip_index_at(position);
int count = m_playlists[0].count();
int target_playlist = 0;
if (auto ptr = m_parent.lock()) {
Q_ASSERT(ptr->getClipPtr(clipId)->getCurrentTrackId() == -1);
target_playlist = ptr->getClipPtr(clipId)->getSubPlaylistIndex();
qDebug()<<"==== GOT TRARGET PLAYLIST: "<<target_playlist;
} else {
qDebug() << "impossible to get parent timeline";
Q_ASSERT(false);
}
// Find out the clip id at position
int target_clip = m_playlists[target_playlist].get_clip_index_at(position);
int count = m_playlists[target_playlist].count();
// we create the function that has to be executed after the melt order. This is essentially book-keeping
auto end_function = [clipId, this, position, updateView, finalMove](int subPlaylist) {
auto end_function = [clipId, this, position, updateView, finalMove, target_playlist](int subPlaylist) {
if (auto ptr = m_parent.lock()) {
std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
m_allClips[clip->getId()] = clip; // store clip
......@@ -170,46 +175,46 @@ Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updat
qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
return false;
};
if (target_clip >= count && isBlankAt(position)) {
if (target_clip >= count && m_playlists[target_playlist].is_blank_at(position)) {
// In that case, we append after, in the first playlist
return [this, position, clipId, end_function, finalMove, groupMove]() {
return [this, position, clipId, end_function, finalMove, groupMove, target_playlist]() {
if (isLocked()) return false;
if (auto ptr = m_parent.lock()) {
// Lock MLT playlist so that we don't end up with an invalid frame being displayed
m_playlists[0].lock();
m_playlists[target_playlist].lock();
std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
clip->setCurrentTrackId(m_id, finalMove);
int index = m_playlists[0].insert_at(position, *clip, 1);
m_playlists[0].consolidate_blanks();
m_playlists[0].unlock();
int index = m_playlists[target_playlist].insert_at(position, *clip, 1);
m_playlists[target_playlist].consolidate_blanks();
m_playlists[target_playlist].unlock();
if (finalMove && !groupMove) {
ptr->updateDuration();
}
return index != -1 && end_function(0);
return index != -1 && end_function(target_playlist);
}
qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
return false;
};
}
if (isBlankAt(position)) {
int blank_end = getBlankEnd(position);
if (m_playlists[target_playlist].is_blank_at(position)) {
int blank_end = getBlankEnd(position, target_playlist);
int length = -1;
if (auto ptr = m_parent.lock()) {
std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
length = clip->getPlaytime();
}
if (blank_end >= position + length) {
return [this, position, clipId, end_function]() {
return [this, position, clipId, end_function, target_playlist]() {
if (isLocked()) return false;
if (auto ptr = m_parent.lock()) {
// Lock MLT playlist so that we don't end up with an invalid frame being displayed
m_playlists[0].lock();
m_playlists[target_playlist].lock();
std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
clip->setCurrentTrackId(m_id);
int index = m_playlists[0].insert_at(position, *clip, 1);
m_playlists[0].consolidate_blanks();
m_playlists[0].unlock();
return index != -1 && end_function(0);
int index = m_playlists[target_playlist].insert_at(position, *clip, 1);
m_playlists[target_playlist].consolidate_blanks();
m_playlists[target_playlist].unlock();
return index != -1 && end_function(target_playlist);
}
qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
return false;
......@@ -326,7 +331,7 @@ Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool fin
if (prod != nullptr) {
m_playlists[target_track].consolidate_blanks();
m_allClips[clipId]->setCurrentTrackId(-1);
m_allClips[clipId]->setSubPlaylistIndex(-1);
//m_allClips[clipId]->setSubPlaylistIndex(-1);
m_allClips.erase(clipId);
delete prod;
m_playlists[target_track].unlock();
......@@ -1352,3 +1357,179 @@ bool TrackModel::isAvailable(int position, int duration)
}
return m_playlists[0].is_blank(start_clip);
}
bool TrackModel::requestClipMix(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
{
QWriteLocker locker(&m_lock);
// By default, insertion occurs in topmost track
// Find out the clip id at position
int clipInitialPos;
int source_track;
MixInfo mixInfo;
if (auto ptr = m_parent.lock()) {
// The clip that will be moved to playlist 1
std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipId));
source_track = movedClip->getSubPlaylistIndex();
clipInitialPos = movedClip->getPosition();
mixInfo.mixDuration = clipInitialPos - position;
mixInfo.mixPosition = position;
} else {
// Error, timeline unavailable
return false;
}
int dest_track = 1;
if (source_track == 1) {
dest_track = 0;
}
// Create mix compositing
Fun build_mix = [clipId, mixInfo, this]() {
if (auto ptr = m_parent.lock()) {
std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipId));
movedClip->setMixDuration(mixInfo.mixDuration);
QModelIndex ix = ptr->makeClipIndexFromID(clipId);
emit ptr->dataChanged(ix, ix, {TimelineModel::StartRole,TimelineModel::MixRole});
// Insert mix transition
if (isAudioTrack()) {
std::shared_ptr<Mlt::Transition> t(new Mlt::Transition(*ptr->getProfile(), "mix"));
t->set_in_and_out(mixInfo.mixPosition, mixInfo.mixPosition + mixInfo.mixDuration);
m_track->plant_transition(*t.get(), 0, 1);
m_sameCompositions[clipId] = t;
} else {
std::shared_ptr<Mlt::Transition> t(new Mlt::Transition(*ptr->getProfile(), "luma"));
t->set_in_and_out(mixInfo.mixPosition, mixInfo.mixPosition + mixInfo.mixDuration);
m_track->plant_transition(*t.get(), 0, 1);
m_sameCompositions[clipId] = t;
}
}
return true;
};
Fun destroy_mix = [clipId, mixInfo, this]() {
if (auto ptr = m_parent.lock()) {
Mlt::Transition &transition = *m_sameCompositions[clipId].get();
std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipId));
movedClip->setMixDuration(0);
QModelIndex ix = ptr->makeClipIndexFromID(clipId);
emit ptr->dataChanged(ix, ix, {TimelineModel::StartRole,TimelineModel::MixRole});
QScopedPointer<Mlt::Field> field(m_track->field());
field->lock();
field->disconnect_service(transition);
field->unlock();
m_sameCompositions.erase(clipId);
}
return true;
};
// lock MLT playlist so that we don't end up with invalid frames in monitor
auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove, finalMove);
bool res = operation();
if (res) {
qDebug()<<"=== CLIP DELETED; OK";
auto reverse = requestClipInsertion_lambda(clipId, clipInitialPos, updateView, finalMove, groupMove);
if (auto ptr = m_parent.lock()) {
ptr->getClipPtr(clipId)->setSubPlaylistIndex(dest_track);
}
auto operation2 = requestClipInsertion_lambda(clipId, position, updateView, finalMove, groupMove);
res = res && operation2();
if (res) {
auto reverse2 = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove, finalMove);
// Create mix composition
build_mix();
qDebug()<<"=============\nSECOND INSERT SUCCESS\n\n=================";
PUSH_LAMBDA(operation2, operation);
PUSH_LAMBDA(build_mix, operation);
PUSH_LAMBDA(reverse, reverse2);
PUSH_LAMBDA(reverse2, destroy_mix);
UPDATE_UNDO_REDO(operation, destroy_mix, undo, redo);
} else {
qDebug()<<"=============\nSECOND INSERT FAILED\n\n=================";
reverse();
}
} else {
qDebug()<<"=== CLIP DELETION FAILED";
}
return res;
}
std::pair<int, int> TrackModel::getMixInfo(int clipId) const
{
std::pair<int, int> result = {0,0};
if (m_sameCompositions.count(clipId) > 0) {
result.first = m_sameCompositions.at(clipId)->get_in();
result.second = m_sameCompositions.at(clipId)->get_out() - result.first;
}
return result;
}
bool TrackModel::deleteMix(int clipId)
{
qDebug()<<"=== DELETING MIX FROM CLIP: "<<clipId;
if (m_sameCompositions.count(clipId) <= 0) {
return false;
}
if (auto ptr = m_parent.lock()) {
Mlt::Transition &transition = *m_sameCompositions[clipId].get();
std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipId));
movedClip->setMixDuration(0);
QModelIndex ix = ptr->makeClipIndexFromID(clipId);
emit ptr->dataChanged(ix, ix, {TimelineModel::StartRole,TimelineModel::MixRole});
QScopedPointer<Mlt::Field> field(m_track->field());
field->lock();
field->disconnect_service(transition);
field->unlock();
m_sameCompositions.erase(clipId);
return true;
}
return false;
}
bool TrackModel::createMix(int clipId, std::pair<int, int> mixData)
{
if (m_sameCompositions.count(clipId) > 0) {
return false;
}
if (auto ptr = m_parent.lock()) {
std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipId));
movedClip->setMixDuration(mixData.second);
QModelIndex ix = ptr->makeClipIndexFromID(clipId);
emit ptr->dataChanged(ix, ix, {TimelineModel::StartRole,TimelineModel::MixRole});
// Insert mix transition
if (isAudioTrack()) {
std::shared_ptr<Mlt::Transition> t(new Mlt::Transition(*ptr->getProfile(), "mix"));
t->set_in_and_out(mixData.first, mixData.first + mixData.second);
m_track->plant_transition(*t.get(), 0, 1);
m_sameCompositions[clipId] = t;
} else {
std::shared_ptr<Mlt::Transition> t(new Mlt::Transition(*ptr->getProfile(), "luma"));
t->set_in_and_out(mixData.first, mixData.first + mixData.second);
m_track->plant_transition(*t.get(), 0, 1);
m_sameCompositions[clipId] = t;
}
return true;
}
return false;
}
bool TrackModel::resizeMix(int clipId, int position)
{
if (m_sameCompositions.count(clipId) <= 0) {
return false;
}
if (auto ptr = m_parent.lock()) {
Mlt::Transition &transition = *m_sameCompositions[clipId].get();
std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipId));
int in = position;
int out = transition.get_out();
transition.set_in_and_out(in, out);
int updatedDuration = out - in;
movedClip->setMixDuration(qMax(1, updatedDuration));
QModelIndex ix = ptr->makeClipIndexFromID(clipId);
emit ptr->dataChanged(ix, ix, {TimelineModel::StartRole,TimelineModel::MixRole});
return true;
}
return false;
}
......@@ -37,6 +37,19 @@ class ClipModel;
class CompositionModel;
class EffectStackModel;
class MixInfo
{
public:
int firstClipId;
int secondClipId;
int firstClipOut;
int secondClipIn;
int secondClipDuration;
int mixPosition;
int mixDuration;
};
/* @brief This class represents a Track object, as viewed by the backend.
To allow same track transitions, a Track object corresponds to two Mlt::Playlist, between which we can switch when required by the transitions.
In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the
......@@ -109,6 +122,16 @@ public:
// TODO make protected
QVariant getProperty(const QString &name) const;
void setProperty(const QString &name, const QString &value);
/** @brief Create a composition between 2 same track clips */
bool requestClipMix(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove);
/** @brief Get in/out position for mix composition */
std::pair<int, int> getMixInfo(int position) const;
/** @brief Delete a mix composition */
bool deleteMix(int clipId);
/** @brief Create a mix composition */
bool createMix(int clipId, std::pair<int, int> mixData);
/** @brief Resize a mix composition */
bool resizeMix(int clipId, int offset);
protected:
/* @brief This will lock the track: it will no longer allow insertion/deletion/resize of items
......@@ -281,6 +304,7 @@ private:
std::shared_ptr<Mlt::Tractor> m_track;
std::shared_ptr<Mlt::Producer> m_mainPlaylist;
Mlt::Playlist m_playlists[2];
QList <MixInfo> m_mixList;
std::map<int, std::shared_ptr<ClipModel>> m_allClips; /*this is important to keep an
ordered structure to store the clips, since we use their ids order as row order*/
......@@ -295,6 +319,7 @@ private:
protected:
std::shared_ptr<EffectStackModel> m_effectStack;
std::unordered_map<int, std::shared_ptr<Mlt::Transition>> m_sameCompositions;
};
#endif
......@@ -33,6 +33,7 @@ Rectangle {
property string effectNames
property bool isProxy: false
property int modelStart
property int mixDuration: 0
property real scrollX: 0
property int inPoint: 0
property int outPoint: 0
......@@ -329,6 +330,16 @@ Rectangle {
anchors.margins: clipRoot.border.width
//clip: true
property bool showDetails: (!clipRoot.selected || !effectRow.visible) && container.height > 2.2 * labelRect.height
Rectangle {
// Mix indicator
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: clipRoot.mixDuration * timeScale
color: 'red'
opacity: 0.5
}
Repeater {
// Clip markers
......
......@@ -68,6 +68,12 @@ Item{
value: model.fakePosition
when: loader.status == Loader.Ready && loader.item && isClip(model.clipType)
}
Binding {
target: loader.item
property: "mixDuration"
value: model.mixDuration
when: loader.status == Loader.Ready && loader.item && isClip(model.clipType)
}
Binding {
target: loader.item
property: "selected"
......
......@@ -3664,3 +3664,46 @@ void TimelineController::addTracks(int videoTracks, int audioTracks)
undo();
}
}
void TimelineController::sameTrack()
{
qDebug()<<"=== PROCESS SAME TRACK COMPO";
auto sel = m_model->getCurrentSelection();
if (sel.empty()) {
return;
}
QList <int> selectedItems;
int selectedTrack = -1;