diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp index 6d297fc88..03f6e4ba0 100644 --- a/src/timeline2/model/timelinefunctions.cpp +++ b/src/timeline2/model/timelinefunctions.cpp @@ -1,512 +1,536 @@ /* Copyright (C) 2017 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 "timelinefunctions.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "timelineitemmodel.hpp" #include "trackmodel.hpp" #include #include bool TimelineFunctions::copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo) { // Special case: slowmotion clips double clipSpeed = timeline->m_allClips[clipId]->getSpeed(); bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, undo, redo); timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize; // Apply speed effect if necessary if (!qFuzzyCompare(clipSpeed, 1.0)) { timeline->m_allClips[newId]->useTimewarpProducer(clipSpeed, -1, undo, redo); } // copy useful timeline properties timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]); int duration = timeline->getClipPlaytime(clipId); int init_duration = timeline->getClipPlaytime(newId); if (duration != init_duration) { int in = timeline->m_allClips[clipId]->getIn(); res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo); res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo); } if (!res) { return false; } std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->importEffects(sourceStack); return res; } +bool TimelineFunctions::requestMultipleClipsInsertion(std::shared_ptr timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView) +{ + std::function undo = []() { return true; }; + std::function redo = []() { return true; }; + + for (const QString &binId : binIds) { + int clipId; + if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, undo, redo)) { + clipIds.append(clipId); + position += timeline->getItemPlaytime(clipId); + } else { + undo(); + clipIds.clear(); + return false; + } + } + + if (logUndo) { + pCore->pushUndo(undo, redo, i18n("Insert Clips")); + } + + return true; +} + bool TimelineFunctions::processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo) { int trackId = timeline->getClipTrackId(clipId); int trackDuration = timeline->getTrackById_const(trackId)->trackDuration(); int start = timeline->getClipPosition(clipId); int duration = timeline->getClipPlaytime(clipId); if (start > position || (start + duration) < position) { return false; } PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState(); bool res = copyClip(timeline, clipId, newId, state, undo, redo); res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo); int newDuration = timeline->getClipPlaytime(clipId); res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo); // parse effects std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); sourceStack->cleanFadeEffects(true, undo, redo); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->cleanFadeEffects(false, undo, redo); // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration(); res = res && timeline->requestClipMove(newId, trackId, position, true, false, undo, redo); if (durationChanged) { // Track length changed, check project duration Fun updateDuration = [timeline]() { timeline->updateDuration(); return true; }; updateDuration(); PUSH_LAMBDA(updateDuration, redo); } return res; } bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Cut clip")); } return result; } bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo) { const std::unordered_set clips = timeline->getGroupElements(clipId); int count = 0; for (int cid : clips) { int start = timeline->getClipPosition(cid); int duration = timeline->getClipPlaytime(cid); if (start < position && (start + duration) > position) { count++; int newId; bool res = processClipCut(timeline, cid, position, newId, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } // splitted elements go temporarily in the same group as original ones. timeline->m_groups->setInGroupOf(newId, cid, undo, redo); } } if (count > 0 && timeline->m_groups->isInGroup(clipId)) { // we now split the group hiearchy. // As a splitting criterion, we compare start point with split position auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; }; int root = timeline->m_groups->getRootId(clipId); bool res = timeline->m_groups->split(root, criterion, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } return count > 0; } int TimelineFunctions::requestSpacerStartOperation(std::shared_ptr timeline, int trackId, int position) { std::unordered_set clips = timeline->getItemsAfterPosition(trackId, position, -1); if (clips.size() > 0) { timeline->requestClipsGroup(clips, false); return (*clips.cbegin()); } return -1; } bool TimelineFunctions::requestSpacerEndOperation(std::shared_ptr timeline, int clipId, int startPosition, int endPosition) { // Move group back to original position int track = timeline->getItemTrackId(clipId); timeline->requestClipMove(clipId, track, startPosition, false, false); std::unordered_set clips = timeline->getGroupElements(clipId); // break group timeline->requestClipUngroup(clipId, false); // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; int res = timeline->requestClipsGroup(clips, undo, redo); bool final = false; if (res > -1) { if (clips.size() > 1) { final = timeline->requestGroupMove(clipId, res, 0, endPosition - startPosition, true, true, undo, redo); } else { // only 1 clip to be moved final = timeline->requestClipMove(clipId, track, endPosition, true, true, undo, redo); } } if (final && clips.size() > 1) { final = timeline->requestClipUngroup(clipId, undo, redo); } if (final) { pCore->pushUndo(undo, redo, i18n("Insert space")); return true; } return false; } bool TimelineFunctions::extractZone(std::shared_ptr timeline, QVector tracks, QPoint zone, bool liftOnly) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = false; for (int trackId : tracks) { result = TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo); if (result && !liftOnly) { result = TimelineFunctions::removeSpace(timeline, trackId, zone, undo, redo); } } pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone")); return result; } bool TimelineFunctions::insertZone(std::shared_ptr timeline, int trackId, const QString &binId, int insertFrame, QPoint zone, bool overwrite) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = false; if (overwrite) { result = TimelineFunctions::liftZone(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); } else { // Cut all tracks auto it = timeline->m_allTracks.cbegin(); while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (timeline->getTrackById_const(target_track)->isLocked()) { ++it; continue; } int startClipId = timeline->getClipByPosition(target_track, insertFrame); if (startClipId > -1) { // There is a clip, cut it TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo); } ++it; } result = TimelineFunctions::insertSpace(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); } int newId = -1; QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1); timeline->requestClipInsertion(binClipId, trackId, insertFrame, newId, true, true, undo, redo); pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); return result; } bool TimelineFunctions::liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { // Check if there is a clip at start point int startClipId = timeline->getClipByPosition(trackId, zone.x()); if (startClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(startClipId) < zone.x()) { TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo); } } int endClipId = timeline->getClipByPosition(trackId, zone.y()); if (endClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) { TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo); } } std::unordered_set clips = timeline->getItemsAfterPosition(trackId, zone.x(), zone.y() - 1); for (const auto &clipId : clips) { timeline->requestItemDeletion(clipId, undo, redo); } return true; } bool TimelineFunctions::removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsAfterPosition(-1, zone.y() - 1, -1, true); bool result = false; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { int res = timeline->requestClipsGroup(clips, undo, redo); if (res > -1) { result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo); if (result) { result = timeline->requestClipUngroup(clipId, undo, redo); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, undo, redo); } } return result; } bool TimelineFunctions::insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsAfterPosition(-1, zone.x(), -1, true); bool result = false; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { int res = timeline->requestClipsGroup(clips, undo, redo); if (res > -1) { result = timeline->requestGroupMove(clipId, res, 0, zone.y() - zone.x(), true, true, undo, redo); if (result) { result = timeline->requestClipUngroup(clipId, undo, redo); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart + (zone.y() - zone.x()), true, true, undo, redo); } } return result; } bool TimelineFunctions::requestItemCopy(std::shared_ptr timeline, int clipId, int trackId, int position) { Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId)); int deltaPos = position - timeline->getItemPosition(clipId); std::unordered_set allIds = timeline->getGroupElements(clipId); std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips bool res = true; for (int id : allIds) { int newId = -1; if (timeline->isClip(id)) { PlaylistState::ClipState state = timeline->m_allClips[id]->clipState(); res = copyClip(timeline, id, newId, state, undo, redo); res = res && (newId != -1); } int target_position = timeline->getItemPosition(id) + deltaPos; int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack; if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) { auto it = timeline->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); if (timeline->isClip(id)) { res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo); } else { const QString &transitionId = timeline->m_allCompositions[id]->getAssetId(); QScopedPointer transProps(timeline->m_allCompositions[id]->properties()); res = res & timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position, timeline->m_allCompositions[id]->getPlaytime(), transProps.data(), newId, undo, redo); } } else { res = false; } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } mapping[id] = newId; } qDebug() << "Sucessful copy, coping groups..."; res = timeline->m_groups->copyGroups(mapping, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } return true; } void TimelineFunctions::showClipKeyframes(std::shared_ptr timeline, int clipId, bool value) { timeline->m_allClips[clipId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::KeyframesRole}); } void TimelineFunctions::showCompositionKeyframes(std::shared_ptr timeline, int compoId, bool value) { timeline->m_allCompositions[compoId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::KeyframesRole}); } bool TimelineFunctions::changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status) { PlaylistState::ClipState oldState = timeline->m_allClips[clipId]->clipState(); if (oldState == status) { return false; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = changeClipState(timeline, clipId, status, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Change clip state")); } return result; } bool TimelineFunctions::changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo) { PlaylistState::ClipState oldState = timeline->m_allClips[clipId]->clipState(); if (oldState == status) { return false; } Fun local_redo = [timeline, clipId, status]() { int trackId = timeline->getClipTrackId(clipId); bool res = timeline->m_allClips[clipId]->setClipState(status); // in order to make the producer change effective, we need to unplant / replant the clip in int track if (trackId != -1) { timeline->getTrackById(trackId)->replugClip(clipId); QModelIndex ix = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(ix, ix, {TimelineModel::StatusRole}); int start = timeline->getItemPosition(clipId); int end = start + timeline->getItemPlaytime(clipId); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); } return res; }; Fun local_undo = [timeline, clipId, oldState]() { bool res = timeline->m_allClips[clipId]->setClipState(oldState); // in order to make the producer change effective, we need to unplant / replant the clip in int track int trackId = timeline->getClipTrackId(clipId); if (trackId != -1) { int start = timeline->getItemPosition(clipId); int end = start + timeline->getItemPlaytime(clipId); timeline->getTrackById(trackId)->replugClip(clipId); QModelIndex ix = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(ix, ix, {TimelineModel::StatusRole}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); } return res; }; bool result = local_redo(); if (result) { PUSH_LAMBDA(local_undo, undo); PUSH_LAMBDA(local_redo, redo); } return result; } bool TimelineFunctions::requestSplitAudio(std::shared_ptr timeline, int clipId, int audioTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); for (int cid : clips) { int position = timeline->getClipPosition(cid); int track = timeline->getClipTrackId(cid); QList possibleTracks = audioTarget >= 0 ? QList() <getLowerTracksId(track, TrackType::AudioTrack); if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage); return false; } int newId; bool res = copyClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo); bool move = false; while (!move && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); move = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo); } std::unordered_set groupClips; groupClips.insert(cid); groupClips.insert(newId); if (!res || !move) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo); timeline->requestClipsGroup(groupClips, undo, redo, GroupType::AVSplit); } pCore->pushUndo(undo, redo, i18n("Split Audio")); return true; } void TimelineFunctions::setCompositionATrack(std::shared_ptr timeline, int cid, int aTrack) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; std::shared_ptrcompo = timeline->getCompositionPtr(cid); int previousATrack = compo->getATrack(); int previousAutoTrack = compo->getForcedTrack() == -1; bool autoTrack = aTrack < 0; if (autoTrack) { // Automatic track compositing, find lower video track aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId()); } int start = timeline->getItemPosition(cid); int end = start + timeline->getItemPlaytime(cid); Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack); timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!previousAutoTrack); timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack<= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; if (local_redo()) { PUSH_LAMBDA(local_undo, undo); PUSH_LAMBDA(local_redo, redo); } pCore->pushUndo(undo, redo, i18n("Change Composition Track")); } diff --git a/src/timeline2/model/timelinefunctions.hpp b/src/timeline2/model/timelinefunctions.hpp index 750085ee4..a8a49b5af 100644 --- a/src/timeline2/model/timelinefunctions.hpp +++ b/src/timeline2/model/timelinefunctions.hpp @@ -1,72 +1,82 @@ /* Copyright (C) 2017 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMELINEFUNCTIONS_H #define TIMELINEFUNCTIONS_H #include "definitions.h" #include "undohelper.hpp" #include #include /** * @namespace TimelineFunction * @brief This namespace contains a list of static methods for advanced timeline editing features * based on timelinemodel methods */ class TimelineItemModel; struct TimelineFunctions { /* @brief Cuts a clip at given position If the clip is part of the group, all clips of the groups are cut at the same position. The group structure is then preserved for clips on both sides Returns true on success @param timeline : ptr to the timeline model @param clipId: Id of the clip to split @param position: position (in frames) where to cut */ static bool requestClipCut(std::shared_ptr timeline, int clipId, int position); /* This is the same function, except that it accumulates undo/redo */ static bool requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo); /* This is the same function, except that it accumulates undo/redo and do not deal with groups. Do not call directly */ static bool processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo); /* @brief Makes a perfect copy of a given clip, but do not insert it */ static bool copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo); + /* @brief Request the addition of multiple clips to the timeline + * If the addition of any of the clips fails, the entire operation is undone. + * @returns true on success, false otherwise. + * @param binIds the list of bin ids to be inserted + * @param trackId the track where the insertion should happen + * @param position the position at which the clips should be inserted + * @param clipIds a return parameter with the ids assigned to the clips if success, empty otherwise + */ + static bool requestMultipleClipsInsertion(std::shared_ptr timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView); + static int requestSpacerStartOperation(std::shared_ptr timeline, int trackId, int position); static bool requestSpacerEndOperation(std::shared_ptr timeline, int clipId, int startPosition, int endPosition); static bool extractZone(std::shared_ptr timeline, QVector tracks, QPoint zone, bool liftOnly); static bool liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); static bool removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); static bool insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); static bool insertZone(std::shared_ptr timeline, int trackId, const QString &binId, int insertFrame, QPoint zone, bool overwrite); static bool requestItemCopy(std::shared_ptr timeline, int clipId, int trackId, int position); static void showClipKeyframes(std::shared_ptr timeline, int clipId, bool value); static void showCompositionKeyframes(std::shared_ptr timeline, int compoId, bool value); static bool changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status); static bool changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo); static bool requestSplitAudio(std::shared_ptr timeline, int clipId, int audioTarget); static void setCompositionATrack(std::shared_ptr timeline, int cid, int aTrack); }; #endif diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp index 8ede16863..77db8bdf2 100644 --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -1,2137 +1,2138 @@ /*************************************************************************** * 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 "groupsmodel.hpp" #include "kdenlivesettings.h" #include "snapmodel.hpp" #include "timelinefunctions.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include #include #ifdef LOGGING #include #include #endif #include "macros.hpp" int TimelineModel::next_id = 0; 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(undo_stack) , m_profile(profile) , m_blackClip(new Mlt::Producer(*profile, "color:black")) , m_lock(QReadWriteLock::Recursive) , m_timelineEffectsEnabled(true) , m_id(getNextId()) , m_temporarySelectionGroup(-1) , m_overlayTrackCount(-1) , m_audioTarget(-1) , m_videoTarget(-1) { // 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("length", INT_MAX); m_blackClip->set_in_and_out(0, 10); m_tractor->insert_track(*m_blackClip, 0); #ifdef LOGGING m_logFile = std::ofstream("log.txt"); m_logFile << "TEST_CASE(\"Regression\") {" << std::endl; m_logFile << "Mlt::Profile profile;" << std::endl; m_logFile << "std::shared_ptr undoStack = std::make_shared(nullptr);" << std::endl; m_logFile << "std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), undoStack);" << std::endl; m_logFile << "TimelineModel::next_id = 0;" << std::endl; m_logFile << "int dummy_id;" << std::endl; #endif } TimelineModel::~TimelineModel() { std::vector all_ids; for (auto tracks : m_iteratorTable) { all_ids.push_back(tracks.first); } for (auto tracks : all_ids) { deregisterTrack_lambda(tracks, false)(); } for (const auto &clip : m_allClips) { clip.second->deregisterClipToBin(); } } int TimelineModel::getTracksCount() const { READ_LOCK(); int count = m_tractor->count(); if (m_overlayTrackCount > -1) { count -= m_overlayTrackCount; } Q_ASSERT(count >= 0); // don't count the black background track Q_ASSERT(count - 1 == static_cast(m_allTracks.size())); return count - 1; } int TimelineModel::getTrackIndexFromPosition(int pos) const { Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size()); READ_LOCK(); auto it = m_allTracks.begin(); while (pos > 0) { it++; pos--; } return (*it)->getId(); } int TimelineModel::getClipsCount() const { READ_LOCK(); int size = int(m_allClips.size()); return size; } int TimelineModel::getCompositionsCount() const { READ_LOCK(); int size = int(m_allCompositions.size()); return size; } int TimelineModel::getClipTrackId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getCurrentTrackId(); } int TimelineModel::getCompositionTrackId(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getCurrentTrackId(); } int TimelineModel::getItemTrackId(int itemId) const { READ_LOCK(); Q_ASSERT(isClip(itemId) || isComposition(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::getClipIn(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getIn(); return pos; } const QString TimelineModel::getClipBinId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); QString id = clip->binId(); return id; } int TimelineModel::getClipPlaytime(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); int playtime = clip->getPlaytime(); return playtime; } QSize TimelineModel::getClipFrameSize(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); return clip->getFrameSize(); } int TimelineModel::getTrackClipsCount(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); int count = getTrackById_const(trackId)->getClipsCount(); return count; } int TimelineModel::getClipByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getClipByPosition(position); } int TimelineModel::getCompositionByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionByPosition(position); } int TimelineModel::getTrackPosition(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_allTracks.begin(); int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId)); return pos; } int TimelineModel::getTrackMltIndex(int trackId) const { READ_LOCK(); // Because of the black track that we insert in first position, the mlt index is the position + 1 return getTrackPosition(trackId) + 1; } QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); QList results; auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (type == TrackType::AnyTrack) { results << (*it)->getId(); continue; } int audioTrack = (*it)->getProperty("kdenlive:audio_track").toInt(); if (type == TrackType::AudioTrack && audioTrack == 1) { results << (*it)->getId(); } else if (type == TrackType::VideoTrack && audioTrack == 0) { results << (*it)->getId(); } } return results; } int TimelineModel::getPreviousVideoTrackPos(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && (*it)->getProperty("kdenlive:audio_track").toInt() == 0) { break; } } return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId()); } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView; if (trackId == -1) { return false; } Q_ASSERT(isClip(clipId)); if (getTrackById_const(trackId)->isLocked()) { return false; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool ok = true; int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { if (getTrackById_const(old_trackId)->isLocked()) { return false; } ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, invalidateTimeline, local_undo, local_redo); if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } ok = getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, local_undo, local_redo); if (!ok) { // qDebug()<<"-------------\n\nINSERTION FAILED, REVERTING\n\n-------------------"; bool undone = local_undo(); Q_ASSERT(undone); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { #ifdef LOGGING m_logFile << "timeline->requestClipMove(" << clipId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", " << (logUndo ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } return res; } int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int snapDistance) { #ifdef LOGGING m_logFile << "timeline->suggestClipMove(" << clipId << "," << trackId << " ," << position << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isClip(clipId)); Q_ASSERT(isTrack(trackId)); int currentPos = getClipPosition(clipId); if (currentPos == 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_clips = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_clips = m_groups->getLeaves(groupId); } for (int current_clipId : all_clips) { 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 = requestBestSnapPos(position, m_allClips[clipId]->getPlaytime(), ignored_pts, snapDistance); // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = requestClipMove(clipId, trackId, position, false, false, false); // bool possible = requestClipMove(clipId, trackId, position, false, false, undo, redo); if (possible) { return position; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Easy 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 { return false; } possible = requestClipMove(clipId, trackId, position, false, false, false); return possible ? position : currentPos; } // find best pos for groups int groupId = m_groups->getRootId(clipId); std::unordered_set all_clips = 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_clips) { int clipTrack = getClipTrackId(current_clipId); if (clipTrack == -1) { continue; } int in = getItemPosition(current_clipId); if (trackPosition.contains(clipTrack)) { if (after) { // keep only last clip position for track int out = in + getItemPlaytime(current_clipId); if (trackPosition.value(clipTrack) < out) { trackPosition.insert(clipTrack, out); } } else { // keep only first clip position for track if (trackPosition.value(clipTrack) > in) { trackPosition.insert(clipTrack, in); } } } else { trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) : in); } } // Now check space on each track QMapIterator i(trackPosition); int blank_length = -1; while (i.hasNext()) { i.next(); int track_space; if (!after) { // Check space before the position track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1); if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } else { // Check after before the position track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value(); 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, false, false, false); if (possible) { return updatedPos; } } return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int snapDistance) { #ifdef LOGGING m_logFile << "timeline->suggestCompositionMove(" << compoId << "," << trackId << " ," << position << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); int currentPos = getCompositionPosition(compoId); int currentTrack = getCompositionTrackId(compoId); if (currentPos == position || currentTrack != trackId) { 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_clips = m_groups->getLeaves(groupId); for (int current_compoId : all_clips) { // 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 = requestBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, snapDistance); qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool possible = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, false, undo, redo); qDebug() << "Original move success" << possible; if (possible) { bool undone = undo(); Q_ASSERT(undone); 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; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView) { #ifdef LOGGING m_logFile << "timeline->requestClipInsertion(" << binClipId.toStdString() << "," << trackId << " ," << position << ", dummy_id );" << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Clip")); } return result; } bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, Fun &undo, Fun &redo) { int clipId = TimelineModel::getNextId(); id = clipId; Fun local_undo = deregisterClip_lambda(clipId); QString bid = binClipId; if (binClipId.contains(QLatin1Char('/'))) { bid = binClipId.section(QLatin1Char('/'), 0, 0); } if (!pCore->projectItemModel()->hasClip(bid)) { return false; } ClipModel::construct(shared_from_this(), bid, clipId, state); 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); 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 = 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, Fun &undo, Fun &redo) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool res = false; if (getTrackById_const(trackId)->isLocked()) { return false; } ClipType type = ClipType::Unknown; if (KdenliveSettings::splitaudio()) { QString bid = binClipId.section(QLatin1Char('/'), 0, 0); if (!pCore->projectItemModel()->hasClip(bid)) { return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); type = master->clipType(); } if (type == ClipType::AV) { bool audioDrop = getTrackById_const(trackId)->isAudioTrack(); res = requestClipCreation(binClipId, id, audioDrop ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); if (res && logUndo && !audioDrop) { QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack); if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort pCore->displayMessage(i18n("No available audio 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, PlaylistState::AudioOnly, audio_undo, audio_redo); if (res) { bool move = false; while (!move && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); move = requestClipMove(newId, newTrack, position, true, false, audio_undo, audio_redo); } // use lazy evaluation to group only if move was successful res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit); if (!res || !move) { pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } else { UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo); } } else { pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } } } } else { res = requestClipCreation(binClipId, id, PlaylistState::Original, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestItemDeletion(int clipId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (m_groups->isInGroup(clipId)) { return requestGroupDeletion(clipId, undo, redo); } return requestClipDeletion(clipId, undo, redo); + } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestItemDeletion(" << itemId << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (m_groups->isInGroup(itemId)) { return requestGroupDeletion(itemId, logUndo); } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = false; QString actionLabel; if (isClip(itemId)) { actionLabel = i18n("Delete Clip"); res = requestClipDeletion(itemId, undo, redo); } else { actionLabel = i18n("Delete Composition"); res = requestCompositionDeletion(itemId, undo, redo); } if (res && logUndo) { PUSH_UNDO(undo, redo, actionLabel); } return res; } bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo) { int trackId = getClipTrackId(clipId); if (trackId != -1) { bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo); if (!res) { undo(); return false; } } auto operation = deregisterClip_lambda(clipId); auto clip = m_allClips[clipId]; Fun reverse = [this, clip]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip); 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, undo, redo); if (!res) { undo(); return false; } else { unplantComposition(compositionId); } } Fun operation = deregisterComposition_lambda(compositionId); auto composition = m_allCompositions[compositionId]; Fun reverse = [this, composition]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } std::unordered_set TimelineModel::getItemsAfterPosition(int trackId, int position, int end, bool listCompositions) { Q_UNUSED(listCompositions) std::unordered_set allClips; auto it = m_allTracks.cbegin(); if (trackId == -1) { while (it != m_allTracks.cend()) { std::unordered_set clipTracks = (*it)->getClipsAfterPosition(position, end); allClips.insert(clipTracks.begin(), clipTracks.end()); ++it; } } else { int target_track_position = getTrackPosition(trackId); std::advance(it, target_track_position); std::unordered_set clipTracks = (*it)->getClipsAfterPosition(position, end); allClips.insert(clipTracks.begin(), clipTracks.end()); } return allClips; } bool TimelineModel::requestGroupMove(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 = requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } return res; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo) { Q_UNUSED(clipId) #ifdef LOGGING m_logFile << "timeline->requestGroupMove(" << clipId << "," << groupId << " ," << delta_track << ", " << delta_pos << ", " << (updateView ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_clips = m_groups->getLeaves(groupId); std::vector sorted_clips(all_clips.begin(), all_clips.end()); // we have to sort clip in an order that allows to do the move without self conflicts // If we move up, we move first the clips on the upper tracks (and conversely). // If we move left, we move first the leftmost clips (and conversely). std::sort(sorted_clips.begin(), sorted_clips.end(), [delta_track, delta_pos, this](int clipId1, int clipId2) { int trackId1 = getItemTrackId(clipId1); int trackId2 = getItemTrackId(clipId2); int track_pos1 = getTrackPosition(trackId1); int track_pos2 = getTrackPosition(trackId2); if (trackId1 == trackId2) { int p1 = isClip(clipId1) ? m_allClips[clipId1]->getPosition() : m_allCompositions[clipId1]->getPosition(); int p2 = isClip(clipId2) ? m_allClips[clipId2]->getPosition() : m_allCompositions[clipId2]->getPosition(); return !(p1 <= p2) == !(delta_pos <= 0); } return !(track_pos1 <= track_pos2) == !(delta_track <= 0); }); for (int clip : sorted_clips) { int current_track_id = getItemTrackId(clip); int current_track_position = getTrackPosition(current_track_id); int target_track_position = current_track_position + delta_track; 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(); if (isClip(clip)) { int target_position = m_allClips[clip]->getPosition() + delta_pos; ok = requestClipMove(clip, target_track, target_position, updateView, finalMove, undo, redo); } else { int target_position = m_allCompositions[clip]->getPosition() + delta_pos; ok = requestCompositionMove(clip, target_track, m_allCompositions[clip]->getForcedTrack(), target_position, updateView, undo, redo); } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestGroupDeletion(" << clipId << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestGroupDeletion(clipId, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Remove group")); } 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_clips; while (!group_queue.empty()) { int current_group = group_queue.front(); if (m_temporarySelectionGroup == current_group) { m_temporarySelectionGroup = -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_clips.insert(c); one_child = c; } else { Q_ASSERT(isGroup(c)); one_child = c; group_queue.push(c); } } if (one_child != -1) { bool res = m_groups->ungroupItem(one_child, undo, redo); if (!res) { undo(); return false; } } } for (int clip : all_clips) { bool res = requestClipDeletion(clip, undo, redo); if (!res) { undo(); return false; } } return true; } int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) { #ifdef LOGGING m_logFile << "timeline->requestItemResize(" << itemId << "," << size << " ," << (right ? "true" : "false") << ", " << (logUndo ? "true" : "false") << ", " << (snapDistance > 0 ? "true" : "false") << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (size <= 0) return -1; int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); if (snapDistance > 0) { Fun temp_undo = []() { return true; }; Fun temp_redo = []() { return true; }; int proposed_size = m_snaps->proposeSize(in, out, size, right, snapDistance); if (proposed_size < 0) { proposed_size = size; } bool success = false; if (isClip(itemId)) { success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo); } else { success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo); } if (success) { temp_undo(); // undo temp move size = proposed_size; } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::unordered_set all_items; if (!allowSingleResize && m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); auto items = m_groups->getLeaves(groupId); 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) { result = result && requestItemResize(id, size, right, logUndo, undo, redo); } if (!result) { bool undone = undo(); Q_ASSERT(undone); return -1; } if (result && logUndo) { if (isClip(itemId)) { PUSH_UNDO(undo, redo, i18n("Resize clip")); } else { PUSH_UNDO(undo, redo, i18n("Resize composition")); } } return result ? size : -1; } bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; Fun update_model = [itemId, right, logUndo, this]() { Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (getItemTrackId(itemId) != -1) { QModelIndex modelIndex = isClip(itemId) ? makeClipIndexFromID(itemId) : makeCompositionIndexFromID(itemId); notifyChange(modelIndex, modelIndex, !right, true, logUndo); } return true; }; bool result = false; if (isClip(itemId)) { result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } else { Q_ASSERT(isComposition(itemId)); result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo); } if (result) { if (!blockUndo) { PUSH_LAMBDA(update_model, local_undo); } PUSH_LAMBDA(update_model, local_redo); update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, bool logUndo, GroupType type) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (m_temporarySelectionGroup > -1) { int firstChild = *m_groups->getDirectChildren(m_temporarySelectionGroup).begin(); requestClipUngroup(firstChild, undo, redo); m_temporarySelectionGroup = -1; } int result = requestClipsGroup(ids, undo, redo, type); if (type == GroupType::Selection) { m_temporarySelectionGroup = result; } if (result > -1 && logUndo && type != GroupType::Selection) { PUSH_UNDO(undo, redo, i18n("Group clips")); } return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type) { #ifdef LOGGING std::stringstream group; m_logFile << "{" << std::endl; m_logFile << "auto group = {"; bool deb = true; for (int clipId : ids) { if (deb) deb = false; else group << ", "; group << clipId; } m_logFile << group.str() << "};" << std::endl; m_logFile << "timeline->requestClipsGroup(group);" << std::endl; m_logFile << std::endl << "}" << std::endl; #endif QWriteLocker locker(&m_lock); for (int id : ids) { if (isClip(id)) { if (getClipTrackId(id) == -1) { return -1; } } else if (isComposition(id)) { if (getCompositionTrackId(id) == -1) { return -1; } } else if (!isGroup(id)) { return -1; } } int groupId = m_groups->groupItems(ids, undo, redo, type); if (type == GroupType::Selection && *(ids.begin()) == groupId) { // only one element selected, no group created return -1; } return groupId; } bool TimelineModel::requestClipUngroup(int id, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestClipUngroup(" << id << " ); " << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipUngroup(id, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } if (id == m_temporarySelectionGroup) { m_temporarySelectionGroup = -1; } return result; } bool TimelineModel::requestClipUngroup(int id, Fun &undo, Fun &redo) { return m_groups->ungroupItem(id, undo, redo); } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack) { #ifdef LOGGING m_logFile << "timeline->requestTrackInsertion(" << position << ", dummy_id ); " << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo); if (result) { PUSH_UNDO(undo, redo, i18n("Insert Track")); } return result; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView) { // TODO: make sure we disable overlayTrack before inserting a track if (position == -1) { position = (int)(m_allTracks.size()); } if (position < 0 || position > (int)m_allTracks.size()) { return false; } int trackId = TimelineModel::getNextId(); id = trackId; Fun local_undo = deregisterTrack_lambda(trackId, true); TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack); auto track = getTrackById(trackId); Fun local_redo = [track, position, updateView, this]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, position, updateView); return true; }; UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestTrackDeletion(int trackId) { // TODO: make sure we disable overlayTrack before deleting a track #ifdef LOGGING m_logFile << "timeline->requestTrackDeletion(" << trackId << "); " << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackDeletion(trackId, undo, redo); if (result) { PUSH_UNDO(undo, redo, i18n("Delete Track")); } return result; } bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo) { Q_ASSERT(isTrack(trackId)); std::vector clips_to_delete; for (const auto &it : getTrackById(trackId)->m_allClips) { clips_to_delete.push_back(it.first); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (int clip : clips_to_delete) { bool res = true; while (res && m_groups->isInGroup(clip)) { res = requestClipUngroup(clip, local_undo, local_redo); } if (res) { res = requestClipDeletion(clip, local_undo, local_redo); } if (!res) { bool u = local_undo(); Q_ASSERT(u); return false; } } int old_position = getTrackPosition(trackId); auto operation = deregisterTrack_lambda(trackId, true); std::shared_ptr track = getTrackById(trackId); Fun reverse = [this, track, old_position]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, old_position); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } local_undo(); return false; } void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert, bool reloadView) { qDebug() << "REGISTER TRACK" << track->getId() << pos; int id = track->getId(); if (pos == -1) { pos = static_cast(m_allTracks.size()); } Q_ASSERT(pos >= 0); Q_ASSERT(pos <= static_cast(m_allTracks.size())); // effective insertion (MLT operation), add 1 to account for black background track if (doInsert) { int error = m_tractor->insert_track(*track, pos + 1); Q_ASSERT(error == 0); // we might need better error handling... } // we now insert in the list auto posIt = m_allTracks.begin(); std::advance(posIt, pos); auto it = m_allTracks.insert(posIt, std::move(track)); // it now contains the iterator to the inserted element, we store it Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen) m_iteratorTable[id] = it; if (reloadView) { // don't reload view on each track load on project opening _resetView(); } } void TimelineModel::registerClip(const std::shared_ptr &clip) { 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(); 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) { QModelIndex root; _resetView(); } return true; }; } Fun TimelineModel::deregisterClip_lambda(int clipId) { return [this, clipId]() { // qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size(); 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); return (*m_iteratorTable.at(trackId))->addEffect(effectId); } 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) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->addEffect(effectId); } 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::isTrack(int id) const { return m_iteratorTable.count(id) > 0; } bool TimelineModel::isGroup(int id) const { return m_allGroups.count(id) > 0; } void TimelineModel::updateDuration() { int current = m_blackClip->get_playtime(); int duration = 10; for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); duration = qMax(duration, track->trackDuration()); } if (duration != current) { // update black track length m_blackClip->set_in_and_out(0, duration); emit durationUpdated(); } } int TimelineModel::duration() const { return m_tractor->get_playtime(); } 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::requestBestSnapPos(int pos, int length, const std::vector &pts, int snapDistance) { if (!pts.empty()) { m_snaps->ignore(pts); } int snapped_start = m_snaps->getClosestPoint(pos); int snapped_end = m_snaps->getClosestPoint(pos + length); m_snaps->unIgnore(); 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::requestNextSnapPos(int pos) { return m_snaps->getNextPoint(pos); } int TimelineModel::requestPreviousSnapPos(int pos) { return m_snaps->getPreviousPoint(pos); } void TimelineModel::addSnap(int pos) { return m_snaps->addPoint(pos); } void TimelineModel::removeSnap(int 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, Mlt::Properties *transProps, int &id, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestCompositionInsertion(\"composite\"," << trackId << " ," << position << "," << length << ", dummy_id );" << std::endl; #endif QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, transProps, id, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Composition")); } return result; } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, Mlt::Properties *transProps, int &id, Fun &undo, Fun &redo) { 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, 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, local_undo, local_redo); qDebug() << "trying to move" << trackId << "pos" << position << "succes " << res; if (res) { res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); qDebug() << "trying to resize" << compositionId << "length" << length << "succes " << 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 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 compoId) const { return getTrackById_const(compoId)->getCompositionsCount(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo) { #ifdef LOGGING m_logFile << "timeline->requestCompositionMove(" << compoId << "," << trackId << " ," << position << ", " << (updateView ? "true" : "false") << ", " << (logUndo ? "true" : "false") << " ); " << std::endl; #endif 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, 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::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, 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); 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) { UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, local_undo, local_redo); } if (!ok) { qDebug() << "Move failed because of first deletion request"; bool undone = local_undo(); Q_ASSERT(undone); return false; } } ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, 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, trackId, 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) { UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::replantCompositions(int currentCompo, bool updateView) { // We ensure that the compositions are planted in a decreasing order of b_track. // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order. std::vector> compos; for (const auto &compo : m_allCompositions) { int trackId = compo.second->getCurrentTrackId(); if (trackId == -1 || compo.second->getATrack() == -1) { continue; } // Note: we need to retrieve the position of the track, that is its melt index. int trackPos = getTrackMltIndex(trackId); compos.push_back({trackPos, compo.first}); if (compo.first != currentCompo) { unplantComposition(compo.first); } } // sort by decreasing b_track std::sort(compos.begin(), compos.end(), [](const std::pair &a, const std::pair &b) { return a.first > b.first; }); // replant QScopedPointer field(m_tractor->field()); field->lock(); // Unplant track compositing mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString resource = mlt_properties_get(properties, "mlt_service"); mlt_service_type mlt_type = mlt_service_identify(nextservice); QList trackCompositions; while (mlt_type == transition_type) { Mlt::Transition transition((mlt_transition)nextservice); nextservice = mlt_service_producer(nextservice); int internal = transition.get_int("internal_added"); if (internal > 0 && resource != QLatin1String("mix")) { trackCompositions << new Mlt::Transition(transition); field->disconnect_service(transition); transition.disconnect_all_producers(); } if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); properties = MLT_SERVICE_PROPERTIES(nextservice); resource = mlt_properties_get(properties, "mlt_service"); } // Sort track compositing std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); for (const auto &compo : compos) { int aTrack = m_allCompositions[compo.second]->getATrack(); Q_ASSERT(aTrack != -1); int ret = field->plant_transition(*m_allCompositions[compo.second].get(), aTrack, compo.first); qDebug() << "Planting composition " << compo.second << "in " << aTrack << "/" << compo.first << "IN = " << m_allCompositions[compo.second]->getIn() << "OUT = " << m_allCompositions[compo.second]->getOut() << "ret=" << ret; Mlt::Transition &transition = *m_allCompositions[compo.second].get(); transition.set_tracks(aTrack, compo.first); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); if (ret != 0) { field->unlock(); return false; } } // Replant last tracks compositing while (!trackCompositions.isEmpty()) { Mlt::Transition *firstTr = trackCompositions.takeFirst(); field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track()); } field->unlock(); if (updateView) { QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo); QVector roles; roles.push_back(ItemATrack); notifyChange(modelIndex, modelIndex, roles); } 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() << "Constistency 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; } } // Check snaps auto stored_snaps = m_snaps->_snaps(); if (snaps.size() != stored_snaps.size()) { qDebug() << "Wrong number of snaps"; 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) { mlt_transition 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 && getTrackMltIndex(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; } return true; } bool TimelineModel::requestItemResizeToPos(int itemId, int position, bool right) { QWriteLocker locker(&m_lock); Q_ASSERT(isClip(itemId) || isComposition(itemId)); int in, out; if (isClip(itemId)) { in = getClipPosition(itemId); out = in + getClipPlaytime(itemId) - 1; } else { in = getCompositionPosition(itemId); out = in + getCompositionPlaytime(itemId) - 1; } int size = 0; if (!right) { if (position < out) { size = out - position; } } else { if (position > in) { size = position - in; } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestItemResize(itemId, size, right, true, undo, redo); if (result) { if (isClip(itemId)) { PUSH_UNDO(undo, redo, i18n("Resize clip")); } else { PUSH_UNDO(undo, redo, i18n("Resize composition")); } } return result; } 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 } Mlt::Producer *TimelineModel::producer() { auto *prod = new Mlt::Producer(tractor()); return prod; } void TimelineModel::checkRefresh(int start, int end) { 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) { pCore->adjustAssetRange(clipId, in, out); } void TimelineModel::requestClipReload(int clipId) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; m_allClips[clipId]->refreshProducerFromBin(); // 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); if (old_trackId != -1) { getTrackById(old_trackId)->requestClipDeletion(clipId, false, true, local_undo, local_redo); getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, true, 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, int trackId, int blankSpace, double speed, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); 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; success = getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo); if (success) { success = m_allClips[clipId]->useTimewarpProducer(speed, blankSpace, local_undo, local_redo); } if (success) { success = getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); } PUSH_LAMBDA(local_undo, undo); PUSH_LAMBDA(local_redo, redo); return success; } bool TimelineModel::changeItemSpeed(int clipId, int speed) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Get main clip info int trackId = getClipTrackId(clipId); int splitId = -1; // Check if clip has a split partner bool result = true; if (trackId != -1) { int blankSpace = getTrackById(trackId)->getBlankSizeNearClip(clipId, true); splitId = m_groups->getSplitPartner(clipId); bool success = true; if (splitId > -1) { int split_trackId = getClipTrackId(splitId); blankSpace = qMin(blankSpace, getTrackById(split_trackId)->getBlankSizeNearClip(splitId, true)); result = requestClipTimeWarp(splitId, split_trackId, blankSpace, speed / 100.0, undo, redo); } if (result) { result = requestClipTimeWarp(clipId, trackId, blankSpace, speed / 100.0, undo, redo); } } else { m_allClips[clipId]->useTimewarpProducer(speed, -1, undo, redo); } if (result) { QVector roles; roles.push_back(SpeedRole); Fun update_model = [clipId, roles, this]() { QModelIndex modelIndex = makeClipIndexFromID(clipId); notifyChange(modelIndex, modelIndex, roles); return true; }; PUSH_LAMBDA(update_model, undo); PUSH_LAMBDA(update_model, redo); if (splitId > -1) { Fun update_model2 = [splitId, roles, this]() { QModelIndex modelIndex = makeClipIndexFromID(splitId); notifyChange(modelIndex, modelIndex, roles); return true; }; PUSH_LAMBDA(update_model2, undo); PUSH_LAMBDA(update_model2, redo); } PUSH_UNDO(undo, redo, i18n("Change clip speed")); return true; } return false; } diff --git a/src/timeline2/model/timelinemodel.hpp b/src/timeline2/model/timelinemodel.hpp index 6f7417936..026447e8d 100644 --- a/src/timeline2/model/timelinemodel.hpp +++ b/src/timeline2/model/timelinemodel.hpp @@ -1,659 +1,662 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef TIMELINEMODEL_H #define TIMELINEMODEL_H #include "definitions.h" #include "undohelper.hpp" #include #include #include #include #include #include #include #include //#define LOGGING 1 // If set to 1, we log the actions requested to the timeline as a reproducer script #ifdef LOGGING #include #endif class AssetParameterModel; class EffectStackModel; class ClipModel; class CompositionModel; class DocUndoStack; class GroupsModel; class SnapModel; class TimelineItemModel; class TrackModel; /* @brief This class represents a Timeline object, as viewed by the backend. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications. This class also serves to keep track of all objects. It holds pointers to all tracks and clips, and gives them unique IDs on creation. These Ids are used in any interactions with the objects and have nothing to do with Melt IDs. This is the entry point for any modifications that has to be made on an element. The dataflow beyond this entry point may vary, for example when the user request a clip resize, the call is deferred to the clip itself, that check if there is enough data to extend by the requested amount, compute the new in and out, and then asks the track if there is enough room for extension. To avoid any confusion on which function to call first, rembember to always call the version in timeline. This is also required to generate the Undo/Redo operators Generally speaking, we don't check ahead of time if an action is going to succeed or not before applying it. We just apply it naively, and if it fails at some point, we use the undo operator that we are constructing on the fly to revert what we have done so far. For example, when we move a group of clips, we apply the move operation to all the clips inside this group (in the right order). If none fails, we are good, otherwise we revert what we've already done. This kind of behaviour frees us from the burden of simulating the actions before actually applying theme. This is a good thing because this simulation step would be very sensitive to corruptions and small discrepancies, which we try to avoid at all cost. It derives from AbstractItemModel (indirectly through TimelineItemModel) to provide the model to the QML interface. An itemModel is organized with row and columns that contain the data. It can be hierarchical, meaning that a given index (row,column) can contain another level of rows and column. Our organization is as follows: at the top level, each row contains a track. These rows are in the same order as in the actual timeline. Then each of this row contains itself sub-rows that correspond to the clips. Here the order of these sub-rows is unrelated to the chronological order of the clips, but correspond to their Id order. For example, if you have three clips, with ids 12, 45 and 150, they will receive row index 0,1 and 2. This is because the order actually doesn't matter since the clips are rendered based on their positions rather than their row order. The id order has been choosed because it is consistant with a valid ordering of the clips. The columns are never used, so the data is always in column 0 An ModelIndex in the ItemModel consists of a row number, a column number, and a parent index. In our case, tracks have always an empty parent, and the clip have a track index as parent. A ModelIndex can also store one additional integer, and we exploit this feature to store the unique ID of the object it corresponds to. */ class TimelineModel : public QAbstractItemModel_shared_from_this { Q_OBJECT protected: /* @brief this constructor should not be called. Call the static construct instead */ TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack); public: friend class TrackModel; template friend class MoveableItem; friend class ClipModel; friend class CompositionModel; friend class GroupsModel; friend class TimelineController; friend struct TimelineFunctions; /// Two level model: tracks and clips on track enum { NameRole = Qt::UserRole + 1, ResourceRole, /// clip only ServiceRole, /// clip only IsBlankRole, /// clip only StartRole, /// clip only BinIdRole, /// clip only MarkersRole, /// clip only StatusRole, /// clip only GroupDragRole, /// indicates if the clip is in current timeline selection, needed for group drag KeyframesRole, DurationRole, InPointRole, /// clip only OutPointRole, /// clip only FramerateRole, /// clip only GroupedRole, /// clip only HasAudio, /// clip only IsMuteRole, /// track only IsHiddenRole, /// track only IsAudioRole, SortRole, ShowKeyframesRole, AudioLevelsRole, /// clip only IsCompositeRole, /// track only IsLockedRole, /// track only HeightRole, /// track only FadeInRole, /// clip only FadeOutRole, /// clip only IsCompositionRole, /// clip only FileHashRole, /// clip only SpeedRole, /// clip only ReloadThumbRole, /// clip only ItemATrack, /// composition only ItemIdRole }; virtual ~TimelineModel(); Mlt::Tractor *tractor() const { return m_tractor.get(); } /* @brief Load tracks from the current tractor, used on project opening */ void loadTractor(); /* @brief Returns the current tractor's producer, useful fo control seeking, playing, etc */ Mlt::Producer *producer(); Mlt::Profile *getProfile(); /* @brief returns the number of tracks */ int getTracksCount() const; /* @brief returns the track index (id) from its position */ int getTrackIndexFromPosition(int pos) const; /* @brief returns the number of clips */ int getClipsCount() const; /* @brief returns the number of compositions */ int getCompositionsCount() const; /* @brief Returns the id of the track containing clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipTrackId(int clipId) const; /* @brief Returns the id of the track containing composition (-1 if it is not inserted) @param clipId Id of the composition to test */ Q_INVOKABLE int getCompositionTrackId(int compoId) const; /* @brief Convenience function that calls either of the previous ones based on item type*/ int getItemTrackId(int itemId) const; /* @brief Helper function that returns true if the given ID corresponds to a clip */ bool isClip(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a composition */ bool isComposition(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a track */ bool isTrack(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a track */ bool isGroup(int id) const; /* @brief Given a composition Id, returns its underlying parameter model */ std::shared_ptr getCompositionParameterModel(int compoId) const; /* @brief Given a clip Id, returns its underlying effect stack model */ std::shared_ptr getClipEffectStackModel(int clipId) const; /* @brief Returns the position of clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipPosition(int clipId) const; Q_INVOKABLE bool addClipEffect(int clipId, const QString &effectId); Q_INVOKABLE bool addTrackEffect(int trackId, const QString &effectId); double getClipSpeed(int clipId) const; bool removeFade(int clipId, bool fromStart); Q_INVOKABLE bool copyClipEffect(int clipId, const QString &sourceId); bool adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration); /* @brief Returns the closest snap point within snapDistance */ Q_INVOKABLE int suggestSnapPoint(int pos, int snapDistance); /* @brief Returns the in cut position of a clip @param clipId Id of the clip to test */ int getClipIn(int clipId) const; /* @brief Returns the bin id of the clip master @param clipId Id of the clip to test */ const QString getClipBinId(int clipId) const; /* @brief Returns the duration of a clip @param clipId Id of the clip to test */ int getClipPlaytime(int clipId) const; /* @brief Returns the duration of a clip @param clipId Id of the clip to test */ QSize getClipFrameSize(int clipId) const; /* @brief Returns the number of clips in a given track @param trackId Id of the track to test */ int getTrackClipsCount(int trackId) const; /* @brief Returns the number of compositions in a given track @param trackId Id of the track to test */ int getTrackCompositionsCount(int trackId) const; /* @brief Returns the position of the track in the order of the tracks @param trackId Id of the track to test */ int getTrackPosition(int trackId) const; /* @brief Returns the track's index in terms of mlt's internal representation */ int getTrackMltIndex(int trackId) const; /* @brief Returns the ids of the tracks below the given track in the order of the tracks Returns an empty list if no track available @param trackId Id of the track to test */ QList getLowerTracksId(int trackId, TrackType type = TrackType::AnyTrack) const; /* @brief Returns the MLT track index of the video track just below the given trackC @param trackId Id of the track to test */ int getPreviousVideoTrackPos(int trackId) const; /* @brief Move a clip to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param clipId is the ID of the clip @param trackId is the ID of the target track @param position is the position where we want to move @param updateView if set to false, no signal is sent to qml @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestClipMove(int clipId, int trackId, int position, bool updateView = true, bool logUndo = true, bool invalidateTimeline = false); /* @brief Move a composition to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param transid is the ID of the composition @param trackId is the ID of the track */ Q_INVOKABLE bool requestCompositionMove(int compoId, int trackId, int position, bool updateView = true, bool logUndo = true); /* Same function, but accumulates undo and redo, and doesn't check for group*/ bool requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo); bool requestCompositionMove(int transid, int trackId, int compositionTrack, int position, bool updateView, Fun &undo, Fun &redo); Q_INVOKABLE int getCompositionPosition(int compoId) const; int getCompositionPlaytime(int compoId) const; /* Returns an item position, item can be clip or composition */ int getItemPosition(int itemId) const; /* Returns an item duration, item can be clip or composition */ int getItemPlaytime(int itemId) const; /* @brief Given an intended move, try to suggest a more valid one (accounting for snaps and missing UI calls) @param clipId id of the clip to move @param trackId id of the target track @param position target position @param snapDistance the maximum distance for a snap result, -1 for no snapping of the clip */ Q_INVOKABLE int suggestClipMove(int clipId, int trackId, int position, int snapDistance = -1); Q_INVOKABLE int suggestCompositionMove(int compoId, int trackId, int position, int snapDistance = -1); /* @brief Request clip insertion at given position. This action is undoable - Returns true on success. If it fails, nothing is modified. @param - binClipId id of the clip in the bin @param track Id of the track where to - insert @param Requested position @param ID return parameter of the id of - the inserted clip @param logUndo if set to false, no undo object is - stored */ + Returns true on success. If it fails, nothing is modified. + @param binClipId id of the clip in the bin + @param track Id of the track where to insert + @param position Requested position + @param ID return parameter of the id of the inserted clip + @param logUndo if set to false, no undo object is stored + @param refreshView whether the view should be refreshed + */ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo = true, bool refreshView = false); /* Same function, but accumulates undo and redo*/ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, Fun &undo, Fun &redo); /* @brief Creates a new clip instance without inserting it. This action is undoable, returns true on success @param binClipId: Bin id of the clip to insert @param id: return parameter for the id of the newly created clip. @param state: The desired clip state (original, audio/video only). */ bool requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, Fun &undo, Fun &redo); /* @brief Deletes the given clip or composition from the timeline This action is undoable Returns true on success. If it fails, nothing is modified. If the clip/composition is in a group, the call is deferred to requestGroupDeletion @param clipId is the ID of the clip/composition @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestItemDeletion(int clipId, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestItemDeletion(int clipId, Fun &undo, Fun &redo); /* @brief Move a group to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clips in the group are not in inserted in a track yet, they get inserted for the first time. @param clipId is the id of the clip that triggers the group move @param groupId is the id of the group @param delta_track is the delta applied to the track index @param delta_pos is the requested position change @param updateView if set to false, no signal is sent to qml for the clip clipId @param logUndo if set to true, an undo object is created */ bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true); bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo); /* @brief Deletes all clips inside the group that contains the given clip. This action is undoable Note that if their is a hierarchy of groups, all of them will be deleted. Returns true on success. If it fails, nothing is modified. @param clipId is the id of the clip that triggers the group deletion */ Q_INVOKABLE bool requestGroupDeletion(int clipId, bool logUndo = true); bool requestGroupDeletion(int clipId, Fun &undo, Fun &redo); /* @brief Change the duration of an item (clip or composition) This action is undoable Returns the real size reached (can be different, if snapping occurs). If it fails, nothing is modified, and -1 is returned @param itemId is the ID of the item @param size is the new size of the item @param right is true if we change the right side of the item, false otherwise @param logUndo if set to true, an undo object is created @param snap if set to true, the resize order will be coerced to use the snapping grid */ Q_INVOKABLE int requestItemResize(int itemId, int size, bool right, bool logUndo = true, int snapDistance = -1, bool allowSingleResize = false); /* @brief Change the duration of an item (clip or composition) This action is undoable Returns true on success. If it fails, nothing is modified. @param itemId is the ID of the item @param position is the requested start or end position @param resizeStart is true if we want to resize clip start, false to resize clip end */ bool requestItemResizeToPos(int itemId, int position, bool right); /* Same function, but accumulates undo and redo and doesn't deal with snapping*/ bool requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo = false); /* @brief Group together a set of ids The ids are either a group ids or clip ids. The involved clip must already be inserted in a track This action is undoable Returns the group id on success, -1 if it fails and nothing is modified. Typically, ids would be ids of clips, but for convenience, some of them can be ids of groups as well. @param ids Set of ids to group */ int requestClipsGroup(const std::unordered_set &ids, bool logUndo = true, GroupType type = GroupType::Normal); int requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type = GroupType::Normal); /* @brief Destruct the topmost group containing clip This action is undoable Returns true on success. If it fails, nothing is modified. @param id of the clip to degroup (all clips belonging to the same group will be ungrouped as well) */ bool requestClipUngroup(int id, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestClipUngroup(int id, Fun &undo, Fun &redo); /* @brief Create a track at given position This action is undoable Returns true on success. If it fails, nothing is modified. @param Requested position (order). If set to -1, the track is inserted last. @param id is a return parameter that holds the id of the resulting track (-1 on failure) */ bool requestTrackInsertion(int pos, int &id, const QString &trackName = QString(), bool audioTrack = false); /* Same function, but accumulates undo and redo*/ bool requestTrackInsertion(int pos, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView = true); /* @brief Delete track with given id This also deletes all the clips contained in the track. This action is undoable Returns true on success. If it fails, nothing is modified. @param trackId id of the track to delete */ bool requestTrackDeletion(int trackId); /* Same function, but accumulates undo and redo*/ bool requestTrackDeletion(int trackId, Fun &undo, Fun &redo); /* @brief Get project duration Returns the duration in frames */ int duration() const; /* @brief Get all the elements of the same group as the given clip. If there is a group hierarchy, only the topmost group is considered. @param clipId id of the clip to test */ std::unordered_set getGroupElements(int clipId); /* @brief Removes all the elements on the timeline (tracks and clips) */ bool requestReset(Fun &undo, Fun &redo); /* @brief Updates the current the pointer to the current undo_stack Must be called for example when the doc change */ void setUndoStack(std::weak_ptr undo_stack); /* @brief Requests the best snapped position for a clip @param pos is the clip's requested position @param length is the clip's duration @param pts snap points to ignore (for example currently moved clip) @param snapDistance the maximum distance for a snap result, -1 for no snapping @returns best snap position or -1 if no snap point is near */ int requestBestSnapPos(int pos, int length, const std::vector &pts = std::vector(), int snapDistance = -1); /* @brief Requests the next snapped point @param pos is the current position */ int requestNextSnapPos(int pos); /* @brief Requests the previous snapped point @param pos is the current position */ int requestPreviousSnapPos(int pos); /* @brief Add a new snap point @param pos is the current position */ void addSnap(int pos); /* @brief Remove snap point @param pos is the current position */ void removeSnap(int pos); /* @brief Request composition insertion at given position. This action is undoable Returns true on success. If it fails, nothing is modified. @param transitionId Identifier of the Mlt transition to insert (as given by repository) @param track Id of the track where to insert @param position Requested position @param length Requested initial length. @param id return parameter of the id of the inserted composition @param logUndo if set to false, no undo object is stored */ bool requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, Mlt::Properties *transProps, int &id, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, Mlt::Properties *transProps, int &id, Fun &undo, Fun &redo); /* @brief This function change the global (timeline-wise) enabled state of the effects It disables/enables track and clip effects (recursively) */ void setTimelineEffectsEnabled(bool enabled); /* @brief Get a timeline clip id by its position or -1 if not found */ int getClipByPosition(int trackId, int position) const; /* @brief Get a timeline composition id by its starting position or -1 if not found */ int getCompositionByPosition(int trackId, int position) const; /* @brief Returns a list of all items that are at or after a given position. * @param trackId is the id of the track for concerned items. Setting trackId to -1 returns items on all tracks * @param position is the position where we the items should start * @param end is the position after which items will not be selected, set to -1 to get all clips on track * @param listCompositions if enabled, the list will also contains composition ids */ std::unordered_set getItemsAfterPosition(int trackId, int position, int end = -1, bool listCompositions = true); /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Inform asset view of duration change */ virtual void adjustAssetRange(int clipId, int in, int out); void requestClipReload(int clipId); void requestClipUpdate(int clipId, const QVector &roles); /** @brief Returns the effectstack of a given clip. */ std::shared_ptr getClipEffectStack(int itemId); std::shared_ptr getTrackEffectStackModel(int trackId); /** @brief Add slowmotion effect to clip in timeline. */ bool requestClipTimeWarp(int clipId, int trackId, int blankSpace, double speed, Fun &undo, Fun &redo); bool changeItemSpeed(int clipId, int speed); void replugClip(int clipId); protected: /* @brief Register a new track. This is a call-back meant to be called from TrackModel @param pos indicates the number of the track we are adding. If this is -1, then we add at the end. */ void registerTrack(std::shared_ptr track, int pos = -1, bool doInsert = true, bool reloadView = true); /* @brief Register a new clip. This is a call-back meant to be called from ClipModel */ void registerClip(const std::shared_ptr &clip); /* @brief Register a new composition. This is a call-back meant to be called from CompositionModel */ void registerComposition(const std::shared_ptr &composition); /* @brief Register a new group. This is a call-back meant to be called from GroupsModel */ void registerGroup(int groupId); /* @brief Deregister and destruct the track with given id. @parame updateView Whether to send updates to the model. Must be false when called from a constructor/destructor */ Fun deregisterTrack_lambda(int id, bool updateView = false); /* @brief Return a lambda that deregisters and destructs the clip with given id. Note that the clip must already be deleted from its track and groups. */ Fun deregisterClip_lambda(int id); /* @brief Return a lambda that deregisters and destructs the composition with given id. */ Fun deregisterComposition_lambda(int compoId); /* @brief Deregister a group with given id */ void deregisterGroup(int id); /* @brief Helper function to get a pointer to the track, given its id */ std::shared_ptr getTrackById(int trackId); const std::shared_ptr getTrackById_const(int trackId) const; /*@brief Helper function to get a pointer to a clip, given its id*/ std::shared_ptr getClipPtr(int clipId) const; /*@brief Helper function to get a pointer to a composition, given its id*/ std::shared_ptr getCompositionPtr(int compoId) const; /* @brief Returns next valid unique id to create an object */ static int getNextId(); /* @brief unplant and the replant all the compositions in the correct order @param currentCompo is the id of a compo that have not yet been planted, if any. Otherwise send -1 */ bool replantCompositions(int currentCompo, bool updateView); /* @brief Unplant the composition with given Id */ bool unplantComposition(int compoId); /* Same function, but accumulates undo and redo, and doesn't check for group*/ bool requestClipDeletion(int clipId, Fun &undo, Fun &redo); bool requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo); /** @brief Check tracks duration and update black track accordingly */ void updateDuration(); public: /* @brief Debugging function that checks consistency with Mlt objects */ bool checkConsistency(); protected: /* @brief Refresh project monitor if cursor was inside range */ void checkRefresh(int start, int end); /* @brief Send signal to require clearing effet/composition view */ void clearAssetView(int itemId); signals: /* @brief signal triggered by clearAssetView */ void requestClearAssetView(int); void requestMonitorRefresh(); /* @brief signal triggered by track operations */ void invalidateZone(int in, int out); /* @brief signal triggered when a track duration changed (insertion/deletion) */ void durationUpdated(); protected: std::unique_ptr m_tractor; std::list> m_allTracks; std::unordered_map>::iterator> m_iteratorTable; // this logs the iterator associated which each track id. This allows easy access of a track based on its id. std::unordered_map> m_allClips; // the keys are the clip id, and the values are the corresponding pointers std::unordered_map> m_allCompositions; // the keys are the composition id, and the values are the corresponding pointers static int next_id; // next valid id to assign std::unique_ptr m_groups; std::shared_ptr m_snaps; std::unordered_set m_allGroups; // ids of all the groups std::weak_ptr m_undoStack; Mlt::Profile *m_profile; // The black track producer. Its length / out should always be adjusted to the projects's length std::unique_ptr m_blackClip; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access #ifdef LOGGING std::ofstream m_logFile; // this is a temporary debug member to help reproduce issues #endif bool m_timelineEffectsEnabled; bool m_id; // id of the timeline itself // id of the currently selected group in timeline, should be destroyed on each new selection int m_temporarySelectionGroup; // The index of the temporary overlay track in tractor, or -1 if not connected int m_overlayTrackCount; // The preferred audio target for clip insertion or -1 if not defined int m_audioTarget; // The preferred video target for clip insertion or -1 if not defined int m_videoTarget; // what follows are some virtual function that corresponds to the QML. They are implemented in TimelineItemModel protected: virtual void _beginRemoveRows(const QModelIndex &, int, int) = 0; virtual void _beginInsertRows(const QModelIndex &, int, int) = 0; virtual void _endRemoveRows() = 0; virtual void _endInsertRows() = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) = 0; virtual QModelIndex makeClipIndexFromID(int) const = 0; virtual QModelIndex makeCompositionIndexFromID(int) const = 0; virtual QModelIndex makeTrackIndexFromID(int) const = 0; virtual void _resetView() = 0; }; #endif diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml index 0b773210d..edb279e87 100644 --- a/src/timeline2/view/qml/timeline.qml +++ b/src/timeline2/view/qml/timeline.qml @@ -1,1128 +1,1154 @@ 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 QtGraphicalEffects 1.0 import QtQuick.Window 2.2 import 'Timeline.js' as Logic Rectangle { id: root objectName: "timelineview" SystemPalette { id: activePalette } color: activePalette.window signal clipClicked() signal mousePosChanged(int position) FontMetrics { id: fontMetrics font.family: "Arial" } ClipMenu { id: clipMenu } CompositionMenu { id: compositionMenu } 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).trackId } function zoomByWheel(wheel) { if (wheel.modifiers & Qt.ControlModifier) { timeline.setScaleFactor(timeline.scaleFactor + 0.2 * wheel.angleDelta.y / 120); } else { var newScroll = Math.min(scrollView.flickableItem.contentX + wheel.angleDelta.y, timeline.duration * root.timeScale - (scrollView.width - scrollView.__verticalScrollBar.width) + root.projectMargin) scrollView.flickableItem.contentX = Math.max(newScroll, 0) } } 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 getMouseTrack() { return Logic.getTrackIdFromPos(tracksArea.mouseY - ruler.height) } function getTrackColor(audio, header) { var col = activePalette.alternateBase if (audio) { col = Qt.tint(col, "#1000cc00") } if (header) { col = Qt.darker(col, 1.05) } return col } + function clearDropData() { + clipBeingDroppedId = -1 + droppedPosition = -1 + droppedTrack = -1 + scrollTimer.running = false + } + property int headerWidth: timeline.headerWidth() property int activeTool: 0 property int projectMargin: 200 property real baseUnit: fontMetrics.font.pointSize property color selectedTrackColor: Qt.rgba(activePalette.highlight.r, activePalette.highlight.g, activePalette.highlight.b, 0.4) property bool stopScrolling: false property int duration: timeline.duration property color shotcutBlue: Qt.rgba(23/255, 92/255, 118/255, 1.0) 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 ? 10 / Math.sqrt(timeScale) : -1 property var timelineSelection: timeline.selection property int trackHeight property int copiedClip: -1 property var dragList: [] property int zoomOnMouse: -1 //onCurrentTrackChanged: timeline.selection = [] onTimeScaleChanged: { if (root.zoomOnMouse >= 0) { var mouseFraction = 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() } onTimelineSelectionChanged: { if (root.timelineSelection.length == 0) { root.dragList = [] } } 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) var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) droppedPosition = frame if (track >= 0) { 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) if (track !=-1) { var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) frame = controller.suggestSnapPoint(frame, root.snapping) if (clipBeingDroppedId >= 0){ controller.requestCompositionMove(clipBeingDroppedId, track, frame, true, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { 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) } - clipBeingDroppedId = -1 - droppedPosition = -1 - droppedTrack = -1 - scrollTimer.running = 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.insertComposition(track, frame, clipBeingDroppedData, true) } - clipBeingDroppedId = -1 - droppedPosition = -1 - droppedTrack = -1 - scrollTimer.running = false + 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) + } else { + var ids = timeline.insertClips(timeline.activeTrack, frame, binIds, false, true) + + // if the clip insertion succeeded, request the clips to be grouped + if (ids.length > 0) { + timeline.groupClips(ids) + id = ids[0] + } + } + return id + } + 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) if (track >= 0 && track < tracksRepeater.count) { var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) droppedPosition = frame timeline.activeTrack = tracksRepeater.itemAt(track).trackId //drag.acceptProposedAction() clipBeingDroppedData = drag.getDataAsString('kdenlive/producerslist') console.log('dropped data: ', clipBeingDroppedData) - clipBeingDroppedId = timeline.insertClip(timeline.activeTrack, frame, clipBeingDroppedData, false, true) + clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, clipBeingDroppedData) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { drag.accepted = false } } } onExited:{ if (clipBeingDroppedId != -1) { controller.requestItemDeletion(clipBeingDroppedId, false) } - clipBeingDroppedId = -1 - droppedPosition = -1 - droppedTrack = -1 - scrollTimer.running = false + clearDropData() } onPositionChanged: { if (clipBeingMovedId == -1) { var track = Logic.getTrackIndexFromPos(drag.y) if (track >= 0 && track < tracksRepeater.count) { timeline.activeTrack = tracksRepeater.itemAt(track).trackId var frame = Math.round((drag.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) frame = controller.suggestSnapPoint(frame, root.snapping) if (clipBeingDroppedId >= 0){ controller.requestClipMove(clipBeingDroppedId, timeline.activeTrack, frame, true, false, false) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } else { - clipBeingDroppedId = timeline.insertClip(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true) + clipBeingDroppedId = insertAndMaybeGroup(timeline.activeTrack, frame, drag.getDataAsString('kdenlive/producerslist'), false, true) continuousScrolling(drag.x + scrollView.flickableItem.contentX) } } } } onDropped: { if (clipBeingDroppedId != -1) { var frame = controller.getClipPosition(clipBeingDroppedId) var track = controller.getClipTrackId(clipBeingDroppedId) - // we simulate insertion at the final position so that stored undo has correct value + /* 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) - timeline.insertClip(track, frame, clipBeingDroppedData, true, true) + + var binIds = clipBeingDroppedData.split(";") + if (binIds.length == 1) { + timeline.insertClip(track, frame, clipBeingDroppedData, true, true) + } else { + timeline.insertClips(track, frame, binIds, true, true) + } } - clipBeingDroppedId = -1 - droppedPosition = -1 - droppedTrack = -1 - scrollTimer.running = false + clearDropData() } } OLD.Menu { id: menu property int clickedX property int clickedY OLD.MenuItem { text: i18n('Paste') visible: copiedClip != -1 onTriggered: { var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height) var frame = Math.round((menu.clickedX + scrollView.flickableItem.contentX) / timeline.scaleFactor) timeline.pasteItem(copiedClip, track, frame) } } OLD.MenuItem { text: i18n('Insert Space') onTriggered: { var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height) var frame = Math.round((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) var frame = Math.round((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) var frame = Math.round((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); } } OLD.MenuItem { id: editGuideMenu text: i18n('Edit Guide') visible: false onTriggered: { timeline.editGuide(timeline.position); } } AssetMenu { title: i18n('Insert a composition...') menuModel: transitionModel onAssetSelected: { var track = Logic.getTrackIdFromPos(menu.clickedY - ruler.height) 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') editGuideMenu.visible = false } console.log("pop menu") } } OLD.Menu { id: rulermenu property int clickedX property int clickedY OLD.MenuItem { id: addGuideMenu2 text: i18n('Add Guide') onTriggered: { timeline.switchGuide(timeline.position); } } OLD.MenuItem { id: editGuideMenu2 text: i18n('Edit Guide') visible: false onTriggered: { timeline.editGuide(timeline.position); } } onAboutToShow: { if (guidesModel.hasMarker(timeline.position)) { // marker at timeline position addGuideMenu2.text = i18n('Remove Guide') editGuideMenu2.visible = true } else { addGuideMenu2.text = i18n('Add Guide') editGuideMenu2.visible = false } 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 OLD.MenuItem { text: i18n('Add Track') onTriggered: timeline.addTrack(timeline.activeTrack); } OLD.MenuItem { text: i18n('Delete Track') onTriggered: timeline.deleteTrack(timeline.activeTrack); } } Row { Column { 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 MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton onClicked: { //timeline.selectMultitrack() } } } Flickable { // Non-slider scroll area for the track headers. contentY: scrollView.flickableItem.contentY width: headerWidth height: 100 interactive: false Column { id: trackHeaders spacing: 0 Repeater { id: trackHeaderRepeater model: multitrack TrackHead { trackName: model.name isMute: model.mute isHidden: model.hidden isComposite: model.composite isLocked: model.locked isAudio: model.audio width: headerWidth height: model.trackHeight selected: false current: item === timeline.activeTrack trackId: item onIsLockedChanged: tracksRepeater.itemAt(index).isLocked = isLocked onMyTrackHeightChanged: { trackBaseRepeater.itemAt(index).height = myTrackHeight tracksRepeater.itemAt(index).height = myTrackHeight height = myTrackHeight collapsed = height <= collapsedHeight if (!collapsed) { controller.setTrackProperty(trackId, "kdenlive:trackheight", myTrackHeight) } // hack: change property to trigger transition adjustment root.trackHeight = root.trackHeight === 1 ? 0 : 1 } onClicked: { timeline.activeTrack = tracksRepeater.itemAt(index).trackId console.log('track name: ',index, ' = ', model.name,'/',tracksRepeater.itemAt(index).trackId) //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 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 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 cursorShape: tracksArea.mouseY < ruler.height || root.activeTool === 0 ? Qt.ArrowCursor : root.activeTool === 1 ? Qt.IBeamCursor : Qt.SplitHCursor onWheel: { timeline.seekPosition = Math.min(timeline.position + (wheel.angleDelta.y > 0 ? 1 : -1), timeline.duration - 1) timeline.position = timeline.seekPosition } onPressed: { focus = true if (mouse.modifiers & Qt.ShiftModifier) { // 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 === 2 && mouse.y > ruler.height) { // spacer tool var y = mouse.y - ruler.height var frame = (scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor var track = (mouse.modifiers & Qt.ControlModifier) ? tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackId : -1 spacerGroup = timeline.requestSpacerStartOperation(track, frame) if (spacerGroup > -1) { drag.axis = Drag.XAxis Drag.active = true Drag.proposedAction = Qt.MoveAction spacerClickFrame = frame spacerFrame = controller.getClipPosition(spacerGroup) } } else if (root.activeTool === 0 || mouse.y <= ruler.height) { if (mouse.y > ruler.height) { timeline.selection = [] } timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.duration - 1) timeline.position = timeline.seekPosition } else if (root.activeTool === 1) { // razor tool var y = mouse.y - ruler.height timeline.cutClipUnderCursor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, tracksRepeater.itemAt(Logic.getTrackIndexFromPos(y)).trackId) } } } onClicked: { 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)).trackId menu.popup() } else { // ruler menu rulermenu.popup() } } } property bool scim: false onExited: { scim = false } onPositionChanged: { root.mousePosChanged(Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor)) if ((mouse.modifiers & Qt.ShiftModifier) && mouse.buttons === Qt.LeftButton && !rubberSelect.visible) { // 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.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.duration - 1) timeline.position = timeline.seekPosition } else if (root.activeTool === 2 && spacerGroup > -1) { // Move group var track = controller.getClipTrackId(spacerGroup) var frame = Math.round((mouse.x + scrollView.flickableItem.contentX) / timeline.scaleFactor) + spacerFrame - spacerClickFrame frame = controller.suggestClipMove(spacerGroup, track, frame, root.snapping); controller.requestClipMove(spacerGroup, track, frame, true, false, false) continuousScrolling(mouse.x + scrollView.flickableItem.contentX) } scim = true } else { scim = false if (root.activeTool === 1) { cutLine.x = Math.floor((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor) * timeline.scaleFactor - scrollView.flickableItem.contentX } } } onReleased: { if (rubberSelect.visible) { rubberSelect.visible = false var topTrack = Logic.getTrackIndexFromPos(Math.max(0, rubberSelect.y - ruler.height)) var bottomTrack = Logic.getTrackIndexFromPos(rubberSelect.y - ruler.height + rubberSelect.height) if (bottomTrack >= topTrack) { var t = [] for (var i = topTrack; i <= bottomTrack; i++) { t.push(tracksRepeater.itemAt(i).trackId) } 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); } } else if (mouse.modifiers & Qt.ShiftModifier) { // Shift click, process seek timeline.seekPosition = Math.min((scrollView.flickableItem.contentX + mouse.x) / timeline.scaleFactor, timeline.duration - 1) timeline.position = timeline.seekPosition } if (spacerGroup > -1) { var frame = controller.getClipPosition(spacerGroup) timeline.requestSpacerEndOperation(spacerGroup, spacerFrame, frame); spacerClickFrame = -1 spacerFrame = -1 spacerGroup = -1 } scim = false } Timer { id: scrubTimer interval: 25 repeat: true running: parent.scim && parent.containsMouse && (parent.mouseX < 50 || parent.mouseX > parent.width - 50) && (timeline.position * timeline.scaleFactor >= 50) onTriggered: { if (parent.mouseX < 50) timeline.seekPosition = Math.max(0, timeline.position - 10) else timeline.seekPosition = Math.min(timeline.position + 10, timeline.duration - 1) } } Column { Flickable { // Non-slider scroll area for the Ruler. contentX: scrollView.flickableItem.contentX width: root.width - headerWidth height: ruler.height interactive: false clip: true Ruler { id: ruler width: Math.max(root.width - headerWidth, timeline.duration * timeScale + root.projectMargin) 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 anchors.bottom: parent.bottom x: timeline.position * timeline.scaleFactor - (width / 2) } } } 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 { width: Math.max(root.width - headerWidth - scrollView.__verticalScrollBar.width, timeline.duration * timeScale + root.projectMargin) height: trackHeaders.height color: activePalette.window id: tracksContainerArea MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: zoomByWheel(wheel) cursorShape: 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: Qt.rgba(activePalette.windowText.r, activePalette.windowText.g, activePalette.windowText.b, 0.1) height: model.trackHeight color: tracksRepeater.itemAt(index) ? ((tracksRepeater.itemAt(index).trackId === 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 } Repeater { id: guidesRepeater; model: guidesDelegateModel } } Rectangle { id: cursor visible: timeline.position > -1 color: activePalette.text 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: activePalette.window //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 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 Track { model: multitrack rootIndex: trackDelegateModel.modelIndex(index) height: trackHeight timeScale: timeline.scaleFactor width: tracksContainerArea.width isAudio: audio isMute: mute isHidden: hidden isCurrentTrack: item === timeline.activeTrack trackId: item onClipClicked: { timeline.activeTrack = track.trackId if (shiftClick === 1) { timeline.addSelection(clip.clipId) } else { timeline.selection = [ clip.clipId ] } timeline.showAsset(clip.clipId) } onClipDragged: { continuousScrolling(x) // Show distance moved as time in a "bubble" help. var delta = Math.round((clip.x / timeline.scaleFactor) - Math.round(clip.originalX) / timeline.scaleFactor) if (delta != 0) { var track = tracksRepeater.itemAt(clip.trackIndex) var s = timeline.timecode(Math.abs(delta)) // remove leading zeroes if (s.substring(0, 3) === '00:') s = s.substring(3) s = ((delta < 0)? '-' : (delta > 0)? '+' : '') + s bubbleHelp.show(x, track.y + height/2, s) clipBeingMovedId = clip.clipId } else bubbleHelp.hide() } onClipDropped: { console.log(" + + + ++ + DROPPED + + + + + + +"); scrollTimer.running = false bubbleHelp.hide() clipBeingMovedId = -1 } onCompositionDropped: { console.log(" + + + ++ + COMPOSITION DROPPED + + + + + + +"); scrollTimer.running = false bubbleHelp.hide() clipBeingMovedId = -1 } onClipDraggedToTrack: { var y = Math.max(0, pos - ruler.height) var activeTrack = Logic.getTrackIndexFromPos(y) var frame = Math.max(0, Math.round(clip.x / timeScale)) if (activeTrack >= 0 && activeTrack < tracksRepeater.count) { var track = tracksRepeater.itemAt(activeTrack) //console.log('Dragging clip ',clip.clipId,' to track: ', activeTrack, ' - ', y) if (controller.requestClipMove(clip.clipId, track.trackId, frame, false, false, false)) { timeline.activeTrack = track.trackId //clip.reparent(track) clip.height = track.height clip.y = track.y - Logic.getTrackById(clip.originalTrackId).y clip.trackId = track.trackId clip.currentFrame = frame if (root.dragList.length > 1) { var currentMasterY = track.y var sourceTrack = Logic.getTrackIndexFromId(clip.originalTrackId) var destTrack = Logic.getTrackIndexFromId(clip.trackId) var trackOffset = destTrack - sourceTrack for (var i = 0; i < root.dragList.length; i++) { if (root.dragList[i] != clip) { if (root.dragList[i].originalTrackId == clip.originalTrackId) { if (root.dragList[i].isComposition == false) { root.dragList[i].height = clip.height } else { root.dragList[i].displayHeight = clip.height / 2 } } else { var newTrackIndex = Logic.getTrackIndexFromId(root.dragList[i].originalTrackId) + trackOffset var newTrack = tracksRepeater.itemAt(newTrackIndex) if (root.dragList[i].isComposition == false) { root.dragList[i].height = newTrack.height root.dragList[i].y = newTrack.y - currentMasterY } else { root.dragList[i].displayHeight = newTrack.height / 2 root.dragList[i].y = newTrack.y - currentMasterY } } } } } } else { if (track.trackId != clip.trackId) { // check if we can move on existing track if (!controller.requestClipMove(clip.clipId, clip.trackId, frame, false, false, false)) { // Abort move clip.x = clip.currentFrame * timeScale } else { clip.x = frame * timeScale clip.currentFrame = frame var delta = Math.round((clip.x - clip.originalX) / timeline.scaleFactor) var s = timeline.timecode(Math.abs(delta)) // remove leading zeroes if (s.substring(0, 3) === '00:') s = s.substring(3) s = ((delta < 0)? '-' : (delta > 0)? '+' : '') + s bubbleHelp.show(xpos, activeTrack.y + height/2, s) } } else { // Abort move clip.x = clip.draggedX } } } else { console.log('+ + + + +\nWARNING CLIP DRAGGED TO INVALID TRACK: ',activeTrack,'\n+ + + +'); } } onCompositionDraggedToTrack: { var y = pos - ruler.height var tk = Logic.getTrackIndexFromPos(y) var frame = Math.round(composition.x / timeScale) if (tk >= 0 && tk < tracksRepeater.count) { var track = tracksRepeater.itemAt(tk) timeline.activeTrack = track.trackId if (controller.requestCompositionMove(composition.clipId, track.trackId, frame, false, false)) { composition.reparent(track) composition.trackIndex = track.DelegateModel.itemsIndex composition.trackId = track.trackId if (root.dragList.length > 1) { var currentMasterY = track.y var sourceTrack = Logic.getTrackIndexFromId(composition.originalTrackId) var destTrack = Logic.getTrackIndexFromId(composition.trackId) var trackOffset = destTrack - sourceTrack for (var i = 0; i < root.dragList.length; i++) { if (root.dragList[i] != composition) { if (root.dragList[i].originalTrackId == composition.originalTrackId) { if (root.dragList[i].isComposition == false) { root.dragList[i].height = track.height } else { root.dragList[i].displayHeight = composition.displayHeight } } else { var newTrackIndex = Logic.getTrackIndexFromId(root.dragList[i].originalTrackId) + trackOffset var newTrack = tracksRepeater.itemAt(newTrackIndex) if (root.dragList[i].isComposition == false) { root.dragList[i].height = newTrack.height root.dragList[i].y = newTrack.y - currentMasterY } else { root.dragList[i].displayHeight = newTrack.height / 2 root.dragList[i].y = newTrack.y - currentMasterY } } } } } } } } Rectangle { anchors.right: parent.right anchors.left: parent.left height: parent.height //source: "qrc:///pics/kdenlive-lock.svgz" //fillMode: Image.Tile opacity: parent.isLocked color: '#33ff3300' visible: opacity Behavior on opacity { NumberAnimation {} } MouseArea { anchors.fill: parent onPressed: { mouse.accepted = true; trackHeaderRepeater.itemAt(index).pulseLockButton() } } } } } DelegateModel { id: guidesDelegateModel model: guidesModel Item { id: guideRoot 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 + 20 color: 'white' } } } Connections { target: timeline onPositionChanged: if (!stopScrolling) Logic.scrollIfNeeded() onFrameFormatChanged: ruler.adjustFormat() /*onDragging: Logic.dragging(pos, duration) onDropped: Logic.dropped() onDropAccepted: Logic.acceptDrop(xml)*/ onSelectionChanged: { //cornerstone.selected = timeline.isMultitrackSelected() var selectedTrack = timeline.selectedTrack() for (var i = 0; i < trackHeaderRepeater.count; i++) { trackHeaderRepeater.itemAt(i).selected = (i === selectedTrack) } } } // 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) stop() } } } diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index 3697aee84..76cdad1d4 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,1502 +1,1527 @@ /*************************************************************************** * 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 "bin/bin.h" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/spacerdialog.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.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/trackdialog.h" #include "timelinewidget.h" #include "transitions/transitionsrepository.hpp" #include "utils/KoIconUtils.h" #include #include #include #include int TimelineController::m_duration = 0; TimelineController::TimelineController(KActionCollection *actionCollection, QObject *parent) : QObject(parent) , m_root(nullptr) , m_actionCollection(actionCollection) , m_usePreview(false) , m_position(0) , m_seekPosition(-1) , m_activeTrack(0) , m_scale(3.0) , m_timelinePreview(nullptr) { m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); m_disablePreview->setEnabled(false); } TimelineController::~TimelineController() { delete m_timelinePreview; m_timelinePreview = nullptr; } void TimelineController::setModel(std::shared_ptr model) { delete m_timelinePreview; 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); } 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(); } void TimelineController::addSelection(int newSelection) { if (m_selection.selectedItems.contains(newSelection)) { return; } std::unordered_set previousSelection = getCurrentSelectionIds(); m_selection.selectedItems << newSelection; std::unordered_set ids; ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); std::unordered_set newIds; QVector roles; roles.push_back(TimelineModel::GroupDragRole); if (m_model->m_temporarySelectionGroup >= 0) { // new items were selected, inform model to prepare for group drag newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); for (int i : newIds) { if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } // Make sure to remove items from previous selection for (int i : previousSelection) { if (newIds.find(i) == newIds.end()) { // item not in selection anymore if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } emit selectionChanged(); if (!m_selection.selectedItems.isEmpty()) emitSelectedFromSelection(); else emit selected(nullptr); } int TimelineController::getCurrentItem() { // TODO: if selection is empty, return topmost clip under timeline cursor if (m_selection.selectedItems.isEmpty()) { return -1; } // TODO: if selection contains more than 1 clip, return topmost clip under timeline cursor in selection return m_selection.selectedItems.constFirst(); } 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->getTrackById(m_model->getTrackIndexFromPosition(trackPos - 1))->getProperty(QStringLiteral("kdenlive:track_name")).toString(); } const QString TimelineController::getTrackNameFromIndex(int trackIndex) { return m_model->getTrackById(trackIndex)->getProperty(QStringLiteral("kdenlive:track_name")).toString(); } QMap TimelineController::getTrackNames(bool videoOnly) { QMap names; for (const auto &track : m_model->m_iteratorTable) { if (videoOnly && m_model->getTrackById(track.first)->getProperty(QStringLiteral("kdenlive:audio_track")).toInt() == 1) { continue; } names[m_model->getTrackMltIndex(track.first)] = m_model->getTrackById(track.first)->getProperty("kdenlive:track_name").toString(); } return names; } void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) { /*if (m_duration * scale < width() - 160) { // Don't allow scaling less than full project's width scale = (width() - 160.0) / m_duration; }*/ m_root->setProperty("zoomOnMouse", zoomOnMouse ? getMousePos() : -1); m_scale = scale; emit scaleFactorChanged(); } void TimelineController::setScaleFactor(double scale) { /*if (m_duration * scale < width() - 160) { // Don't allow scaling less than full project's width scale = (width() - 160.0) / m_duration; }*/ m_scale = scale; emit scaleFactorChanged(); } int TimelineController::duration() const { return m_duration; } void TimelineController::checkDuration() { int currentLength = m_model->duration(); if (currentLength != m_duration) { m_duration = currentLength; emit durationChanged(); } } std::unordered_set TimelineController::getCurrentSelectionIds() const { std::unordered_set selection; if (m_model->m_temporarySelectionGroup >= 0 || (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { selection = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } else { for (int i : m_selection.selectedItems) { selection.insert(i); } } return selection; } void TimelineController::setSelection(const QList &newSelection, int trackIndex, bool isMultitrack) { if (newSelection != selection() || trackIndex != m_selection.selectedTrack || isMultitrack != m_selection.isMultitrackSelected) { qDebug() << "Changing selection to" << newSelection << " trackIndex" << trackIndex << "isMultitrack" << isMultitrack; std::unordered_set previousSelection = getCurrentSelectionIds(); m_selection.selectedItems = newSelection; m_selection.selectedTrack = trackIndex; m_selection.isMultitrackSelected = isMultitrack; if (m_model->m_temporarySelectionGroup > -1) { // CLear current selection m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } std::unordered_set newIds; QVector roles; roles.push_back(TimelineModel::GroupDragRole); if (m_selection.selectedItems.size() > 0) { std::unordered_set ids; ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); if (m_model->m_temporarySelectionGroup >= 0 || (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); for (int i : newIds) { if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } else { qDebug() << "// NON GROUPED SELCTUIIN: " << m_selection.selectedItems << " !!!!!!"; } emitSelectedFromSelection(); } else { // Empty selection emit selected(nullptr); emit showItemEffectStack(QString(), nullptr, QPair(), QSize(), false); } for (int i : previousSelection) { // Clear previously selcted items if (newIds.find(i) == newIds.end()) { // item not in selection anymore if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } emit selectionChanged(); } } void TimelineController::emitSelectedFromSelection() { /*if (!m_model.trackList().count()) { if (m_model.tractor()) selectMultitrack(); else emit selected(0); return; } int trackIndex = currentTrack(); int clipIndex = selection().isEmpty()? 0 : selection().first(); Mlt::ClipInfo* info = getClipInfo(trackIndex, clipIndex); if (info && info->producer && info->producer->is_valid()) { delete m_updateCommand; m_updateCommand = new Timeline::UpdateCommand(*this, trackIndex, clipIndex, info->start); // We need to set these special properties so time-based filters // can get information about the cut while still applying filters // to the cut parent. info->producer->set(kFilterInProperty, info->frame_in); info->producer->set(kFilterOutProperty, info->frame_out); if (MLT.isImageProducer(info->producer)) info->producer->set("out", info->cut->get_int("out")); info->producer->set(kMultitrackItemProperty, 1); m_ignoreNextPositionChange = true; emit selected(info->producer); delete info; }*/ } QList TimelineController::selection() const { if (!m_root) return QList(); return m_selection.selectedItems; } void TimelineController::selectMultitrack() { setSelection(QList(), -1, true); QMetaObject::invokeMethod(m_root, "selectMultitrack"); // emit selected(m_model.tractor()); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } void TimelineController::snapChanged(bool snap) { m_root->setProperty("snapping", snap ? 10 / std::sqrt(m_scale) : -1); } 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) { int id; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView)) { 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::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) { int id; if (!m_model->requestCompositionInsertion(transitionId, tid, position, 100, nullptr, id, logUndo)) { id = -1; } return id; } void TimelineController::deleteSelectedClips() { if (m_selection.selectedItems.isEmpty()) { return; } if (m_model->m_temporarySelectionGroup != -1) { // selection is grouped, delete group only m_model->requestGroupDeletion(m_model->m_temporarySelectionGroup); } else { for (int cid : m_selection.selectedItems) { m_model->requestItemDeletion(cid); } } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::copyItem() { int clipId = -1; if (!m_selection.selectedItems.isEmpty()) { clipId = m_selection.selectedItems.first(); } else { return; } m_root->setProperty("copiedClip", clipId); } bool TimelineController::pasteItem(int clipId, int tid, int position) { // TODO: copy multiple clips / groups if (clipId == -1) { clipId = m_root->property("copiedClip").toInt(); if (clipId == -1) { return -1; } } if (tid == -1 && position == -1) { tid = getMouseTrack(); position = getMousePos(); } if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } qDebug() << "PASTING CLIP: " << clipId << ", " << tid << ", " << position; return TimelineFunctions::requestItemCopy(m_model, clipId, tid, position); return false; } void TimelineController::triggerAction(const QString &name) { QAction *action = m_actionCollection->action(name); if (action) { action->trigger(); } } QString TimelineController::timecode(int frames) { return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); } 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, m_model->getTrackMltIndex(tid), qApp->activeWindow()); if (d->exec() == QDialog::Accepted) { int mltIndex = d->selectedTrack(); int newTid; m_model->requestTrackInsertion(mltIndex, newTid, d->trackName(), d->addAudioTrack()); } } void TimelineController::deleteTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, m_model->getTrackMltIndex(tid), qApp->activeWindow(), true); if (d->exec() == QDialog::Accepted) { int mltIndex = d->selectedTrack(); m_model->requestTrackDeletion(mltIndex); } } void TimelineController::gotoNextSnap() { setPosition(m_model->requestNextSnapPos(timelinePosition())); } void TimelineController::gotoPreviousSnap() { setPosition(m_model->requestPreviousSnapPos(timelinePosition())); } void TimelineController::groupSelection() { if (m_selection.selectedItems.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } std::unordered_set clips; for (int id : m_selection.selectedItems) { clips.insert(id); } m_model->requestClipsGroup(clips); } void TimelineController::unGroupSelection(int cid) { if (cid == -1 && m_selection.selectedItems.isEmpty()) { pCore->displayMessage(i18n("Select at least 1 item to ungroup"), InformationMessage, 500); return; } if (cid == -1) { for (int id : m_selection.selectedItems) { if (m_model->m_groups->isInGroup(id) && !m_model->isInSelection(id)) { cid = id; break; } } } if (cid > -1) { m_model->requestClipUngroup(cid); } if (m_model->m_temporarySelectionGroup >= 0) { m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::setInPoint() { int cursorPos = timelinePosition(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { m_model->requestItemResizeToPos(id, cursorPos, false); } } } int TimelineController::timelinePosition() const { return m_seekPosition >= 0 ? m_seekPosition : m_position; } void TimelineController::setOutPoint() { int cursorPos = timelinePosition(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { m_model->requestItemResizeToPos(id, cursorPos, true); } } } void TimelineController::editMarker(const QString &cid, int frame) { std::shared_ptr clip = pCore->bin()->getBinClip(cid); GenTime pos(frame, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } 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(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { if (m_model->isClip(id)) { m_model->addClipEffect(id, effect); } } } 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), QPair(m_model->getClipPosition(id), m_model->getClipPosition(id) + m_model->getClipPlaytime(id)), m_model->getClipFrameSize(id), showKeyframes); } } void TimelineController::showTrackAsset(int trackId) { emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), QPair(), pCore->getCurrentFrameSize(), false); } void TimelineController::setPosition(int position) { setSeekPosition(position); emit seeked(position); } void TimelineController::setAudioTarget(int track) { m_model->m_audioTarget = track; emit audioTargetChanged(); } void TimelineController::setVideoTarget(int track) { m_model->m_videoTarget = track; emit videoTargetChanged(); } void TimelineController::setActiveTrack(int track) { 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()); } if (zone.x() > 0) { m_model->addSnap(zone.x()); } if (zone.y() > 0) { m_model->addSnap(zone.y()); } 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()); } if (outPoint > 0) { m_model->addSnap(outPoint); } m_zone.setY(outPoint); emit zoneMoved(m_zone); } void TimelineController::selectItems(QVariantList arg, int startFrame, int endFrame, bool addToSelect) { std::unordered_set previousSelection = getCurrentSelectionIds(); std::unordered_set itemsToSelect; if (addToSelect) { for (int cid : m_selection.selectedItems) { itemsToSelect.insert(cid); } } m_selection.selectedItems.clear(); for (int i = 0; i < arg.count(); i++) { std::unordered_set trackClips = m_model->getTrackById(arg.at(i).toInt())->getClipsAfterPosition(startFrame, endFrame); itemsToSelect.insert(trackClips.begin(), trackClips.end()); std::unordered_set trackCompos = m_model->getTrackById(arg.at(i).toInt())->getCompositionsAfterPosition(startFrame, endFrame); itemsToSelect.insert(trackCompos.begin(), trackCompos.end()); } if (itemsToSelect.size() > 0) { for (int x : itemsToSelect) { m_selection.selectedItems << x; } qDebug() << "// GROUPING ITEMS: " << m_selection.selectedItems; m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(itemsToSelect, true, GroupType::Selection); } else if (m_model->m_temporarySelectionGroup > -1) { m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } std::unordered_set newIds; QVector roles; roles.push_back(TimelineModel::GroupDragRole); if (m_model->m_temporarySelectionGroup >= 0) { newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); for (int i : newIds) { if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = true; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } for (int i : previousSelection) { if (newIds.find(i) == newIds.end()) { // item not in selection anymore if (m_model->isClip(i)) { m_model->m_allClips[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeClipIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } else if (m_model->isComposition(i)) { m_model->m_allCompositions[i]->isInGroupDrag = false; QModelIndex modelIndex = m_model->makeCompositionIndexFromID(i); m_model->notifyChange(modelIndex, modelIndex, roles); } } } emit selectionChanged(); } 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(); } bool foundClip = false; for (int cid : m_selection.selectedItems) { if (m_model->isClip(cid)) { if (TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } 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) { // TODO: display warning, no clip found } } 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) { for (int cid : m_selection.selectedItems) { 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) { 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 Mlt::Producer *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); 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 Mlt::Playlist *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_timelinePreview && !m_zone.isNull()) { m_timelinePreview->addPreviewRange(m_zone, add); } } void TimelineController::clearPreviewRange() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); } } 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 KdenliveSettings::useTimelineZoneToEdit(); } void TimelineController::resetPreview() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); initializePreview(); } } void TimelineController::loadPreview(QString chunks, 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(); // TODO // props.insert(QStringLiteral("audiotargettrack"), QString::number(audioTarget)); // props.insert(QStringLiteral("videotargettrack"), QString::number(videoTarget)); 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; } // find blank duration int spaceDuration = m_model->getTrackById(trackId)->getBlankSizeAtPos(frame); int cid = requestSpacerStartOperation(affectAllTracks ? -1 : trackId, frame); 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::invalidateClip(int cid) { if (!m_timelinePreview) { return; } int start = m_model->getItemPosition(cid); int end = start + m_model->getItemPlaytime(cid); m_timelinePreview->invalidatePreview(start, end); } void TimelineController::invalidateZone(int in, int out) { if (!m_timelinePreview) { return; } m_timelinePreview->invalidatePreview(in, out); } void TimelineController::changeItemSpeed(int clipId, int speed) { if (speed == -1) { speed = 100 * m_model->m_allClips[clipId]->getSpeed(); bool ok; speed = QInputDialog::getInt(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), speed, -100000, 100000, 1, &ok); if (!ok) { return; } } m_model->changeItemSpeed(clipId, speed); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); qDebug() << "//// SWITCH COMPO: " << 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 = 1; 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("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) { QVector tracks; if (audioTarget() >= 0) { tracks << audioTarget(); } if (videoTarget() >= 0) { tracks << videoTarget(); } if (tracks.isEmpty()) { tracks << m_activeTrack; } 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, false); } 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, QVector() << track, zone, false); } void TimelineController::liftZone(QPoint zone) { QVector tracks; if (audioTarget() >= 0) { tracks << audioTarget(); } if (videoTarget() >= 0) { tracks << videoTarget(); } if (tracks.isEmpty()) { tracks << m_activeTrack; } 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, true); } bool TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); int targetTrack = -1; if (clip->clipType() == ClipType::Audio) { targetTrack = audioTarget(); } else { targetTrack = videoTarget(); } if (targetTrack == -1) { targetTrack = m_activeTrack; } int insertPoint; QPoint sourceZone; if (KdenliveSettings::useTimelineZoneToEdit() && 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 curent timeline pos and clip zone for in/out insertPoint = timelinePosition(); sourceZone = zone; } return TimelineFunctions::insertZone(m_model, targetTrack, binId, insertPoint, sourceZone, overwrite); } void TimelineController::updateClip(int clipId, 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::setClipStatus(int clipId, int status) { TimelineFunctions::changeClipState(m_model, clipId, (PlaylistState::ClipState)status); } void TimelineController::splitAudio(int clipId) { TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget()); } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:locked_track").toInt() == 1; m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:locked_track"), locked ? QStringLiteral("0") : QStringLiteral("1")); } else { // Invert track lock // Get track states first QMap trackLockState; int unlockedTracksCount = 0; int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); bool isLocked = m_model->getTrackById_const(trackIx)->getProperty("kdenlive:locked_track").toInt() == 1; if (!isLocked) { unlockedTracksCount++; } trackLockState.insert(trackIx, isLocked); } if (unlockedTracksCount == tracksCount) { // do not lock all tracks, leave active track unlocked trackLockState.insert(m_activeTrack, true); } QMapIterator i(trackLockState); while (i.hasNext()) { i.next(); m_model->setTrackProperty(i.key(), QStringLiteral("kdenlive:locked_track"), i.value() ? QStringLiteral("0") : QStringLiteral("1")); } } } 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; } 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}); } + +int TimelineController::groupClips(const QList &clipIds) +{ + std::unordered_set theSet(clipIds.begin(), clipIds.end()); + return m_model->requestClipsGroup(theSet, false, GroupType::Selection); +} + +bool TimelineController::ungroupClips(int clipId) +{ + return m_model->requestClipUngroup(clipId); +} diff --git a/src/timeline2/view/timelinecontroller.h b/src/timeline2/view/timelinecontroller.h index 9857cfa7a..5824dbd7c 100644 --- a/src/timeline2/view/timelinecontroller.h +++ b/src/timeline2/view/timelinecontroller.h @@ -1,410 +1,428 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef TIMELINECONTROLLER_H #define TIMELINECONTROLLER_H #include "definitions.h" #include "timeline2/model/timelineitemmodel.hpp" #include "timelinewidget.h" class PreviewManager; class QAction; // see https://bugreports.qt.io/browse/QTBUG-57714, don't expose a QWidget as a context property class TimelineController : public QObject { Q_OBJECT /* @brief holds a list of currently selected clips (list of clipId's) */ Q_PROPERTY(QList selection READ selection WRITE setSelection NOTIFY selectionChanged) /* @brief holds the timeline zoom factor */ Q_PROPERTY(double scaleFactor READ scaleFactor WRITE setScaleFactor NOTIFY scaleFactorChanged) /* @brief holds the current project duration */ Q_PROPERTY(int duration READ duration NOTIFY durationChanged) Q_PROPERTY(bool audioThumbFormat READ audioThumbFormat NOTIFY audioThumbFormatChanged) /* @brief holds the current timeline position */ Q_PROPERTY(int position READ position WRITE setPosition NOTIFY positionChanged) Q_PROPERTY(int zoneIn READ zoneIn WRITE setZoneIn NOTIFY zoneChanged) Q_PROPERTY(int zoneOut READ zoneOut WRITE setZoneOut NOTIFY zoneChanged) Q_PROPERTY(int seekPosition READ seekPosition WRITE setSeekPosition NOTIFY seekPositionChanged) Q_PROPERTY(bool ripple READ ripple NOTIFY rippleChanged) Q_PROPERTY(bool scrub READ scrub NOTIFY scrubChanged) Q_PROPERTY(bool showThumbnails READ showThumbnails NOTIFY showThumbnailsChanged) Q_PROPERTY(bool showMarkers READ showMarkers NOTIFY showMarkersChanged) Q_PROPERTY(bool showAudioThumbnails READ showAudioThumbnails NOTIFY showAudioThumbnailsChanged) Q_PROPERTY(QVariantList dirtyChunks READ dirtyChunks NOTIFY dirtyChunksChanged) Q_PROPERTY(QVariantList renderedChunks READ renderedChunks NOTIFY renderedChunksChanged) Q_PROPERTY(int workingPreview READ workingPreview NOTIFY workingPreviewChanged) Q_PROPERTY(bool useRuler READ useRuler NOTIFY useRulerChanged) Q_PROPERTY(int activeTrack READ activeTrack WRITE setActiveTrack NOTIFY activeTrackChanged) Q_PROPERTY(int audioTarget READ audioTarget WRITE setAudioTarget NOTIFY audioTargetChanged) Q_PROPERTY(int videoTarget READ videoTarget WRITE setVideoTarget NOTIFY videoTargetChanged) public: TimelineController(KActionCollection *actionCollection, QObject *parent); virtual ~TimelineController(); /* @brief Sets the model that this widgets displays */ void setModel(std::shared_ptr model); std::shared_ptr getModel() const; void setRoot(QQuickItem *root); Q_INVOKABLE bool isMultitrackSelected() const { return m_selection.isMultitrackSelected; } Q_INVOKABLE int selectedTrack() const { return m_selection.selectedTrack; } /* @brief Add a clip id to current selection */ Q_INVOKABLE void addSelection(int newSelection); /* @brief returns current timeline's zoom factor */ Q_INVOKABLE double scaleFactor() const; /* @brief set current timeline's zoom factor */ void setScaleFactorOnMouse(double scale, bool zoomOnMouse); Q_INVOKABLE void setScaleFactor(double scale); /* @brief Returns the project's duration (tractor) */ Q_INVOKABLE int duration() const; /* @brief Returns the current cursor position (frame currently displayed by MLT) */ Q_INVOKABLE int position() const { return m_position; } /* @brief Returns the seek request position (-1 = no seek pending) */ Q_INVOKABLE int seekPosition() const { return m_seekPosition; } Q_INVOKABLE int audioTarget() const; Q_INVOKABLE int videoTarget() const; Q_INVOKABLE int activeTrack() const { return m_activeTrack; } /* @brief Request a seek operation @param position is the desired new timeline position */ Q_INVOKABLE int zoneIn() const { return m_zone.x(); } Q_INVOKABLE int zoneOut() const { return m_zone.y(); } Q_INVOKABLE void setZoneIn(int inPoint); Q_INVOKABLE void setZoneOut(int outPoint); void setZone(const QPoint &zone); /* @brief Request a seek operation @param position is the desired new timeline position */ Q_INVOKABLE void setPosition(int position); Q_INVOKABLE bool snap(); Q_INVOKABLE bool ripple(); Q_INVOKABLE bool scrub(); Q_INVOKABLE QString timecode(int frames); /* @brief Request inserting a new clip in timeline (dragged from bin or monitor) @param tid is the destination track @param position is the timeline position @param xml is the data describing the dropped clip @param logUndo if set to false, no undo object is stored @return the id of the inserted clip */ Q_INVOKABLE int insertClip(int tid, int position, const QString &xml, bool logUndo, bool refreshView); + /* @brief Request inserting multiple clips into the timeline (dragged from bin or monitor) + * @param tid is the destination track + * @param position is the timeline position + * @param binIds the IDs of the bins being dropped + * @param logUndo if set to false, no undo object is stored + * @return the ids of the inserted clips + */ + Q_INVOKABLE QList insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView); + /* @brief Request the grouping of the given clips + * @param clipIds the ids to be grouped + * @return the group id or -1 in case of faiure + */ + Q_INVOKABLE int groupClips(const QList &clipIds); + /* @brief Request the ungrouping of clips + * @param clipId the id of a clip belonging to the group + * @return true in case of success, false otherwise + */ + Q_INVOKABLE bool ungroupClips(int clipId); Q_INVOKABLE void copyItem(); Q_INVOKABLE bool pasteItem(int clipId = -1, int tid = -1, int position = -1); /* @brief Request inserting a new composition in timeline (dragged from compositions list) @param tid is the destination track @param position is the timeline position @param transitionId is the data describing the dropped composition @param logUndo if set to false, no undo object is stored @return the id of the inserted composition */ Q_INVOKABLE int insertComposition(int tid, int position, const QString &transitionId, bool logUndo); /* @brief Request deletion of the currently selected clips */ Q_INVOKABLE void deleteSelectedClips(); Q_INVOKABLE void triggerAction(const QString &name); /* @brief Do we want to display video thumbnails */ bool showThumbnails() const; bool showAudioThumbnails() const; bool showMarkers() const; bool audioThumbFormat() const; /* @brief Do we want to display audio thumbnails */ Q_INVOKABLE bool showWaveforms() const; /* @brief Insert a timeline track */ Q_INVOKABLE void addTrack(int tid); /* @brief Remove a timeline track */ Q_INVOKABLE void deleteTrack(int tid); /* @brief Group selected items in timeline */ Q_INVOKABLE void groupSelection(); /* @brief Ungroup selected items in timeline */ Q_INVOKABLE void unGroupSelection(int cid = -1); /* @brief Ask for edit marker dialog */ Q_INVOKABLE void editMarker(const QString &cid, int frame); /* @brief Ask for edit timeline guide dialog */ Q_INVOKABLE void editGuide(int frame = -1); Q_INVOKABLE void moveGuide(int frame, int newFrame); /* @brief Add a timeline guide */ Q_INVOKABLE void switchGuide(int frame = -1, bool deleteOnly = false); /* @brief Request monitor refresh */ Q_INVOKABLE void requestRefresh(); /* @brief Show the asset of the given item in the AssetPanel If the id corresponds to a clip, we show the corresponding effect stack If the id corresponds to a composition, we show its properties */ Q_INVOKABLE void showAsset(int id); Q_INVOKABLE void showTrackAsset(int trackId); Q_INVOKABLE void selectItems(QVariantList arg, int startFrame, int endFrame, bool addToSelect); Q_INVOKABLE int headerWidth() const; Q_INVOKABLE void setHeaderWidth(int width); /* @brief Seek to next snap point */ void gotoNextSnap(); /* @brief Seek to previous snap point */ void gotoPreviousSnap(); /* @brief Set current item's start point to cursor position */ void setInPoint(); /* @brief Set current item's end point to cursor position */ void setOutPoint(); /* @brief Return the project's tractor */ Mlt::Tractor *tractor(); /* @brief Sets the list of currently selected clips @param selection is the list of id's @param trackIndex is current clip's track @param isMultitrack is true if we want to select the whole tractor (currently unused) */ void setSelection(const QList &selection = QList(), int trackIndex = -1, bool isMultitrack = false); /* @brief Get the list of currenly selected clip id's */ QList selection() const; /* @brief Add an asset (effect, composition) */ void addAsset(const QVariantMap data); /* @brief Cuts the clip on current track at timeline position */ Q_INVOKABLE void cutClipUnderCursor(int position = -1, int track = -1); /* @brief Request a spacer operation */ Q_INVOKABLE int requestSpacerStartOperation(int trackId, int position); /* @brief Request a spacer operation */ Q_INVOKABLE bool requestSpacerEndOperation(int clipId, int startPosition, int endPosition); /* @brief Request a Fade in effect for clip */ Q_INVOKABLE void adjustFade(int cid, const QString &effectId, int duration, int initialDuration); Q_INVOKABLE const QString getTrackNameFromMltIndex(int trackPos); /* @brief Request inserting space in a track */ Q_INVOKABLE void insertSpace(int trackId = -1, int frame = -1); Q_INVOKABLE void removeSpace(int trackId = -1, int frame = -1, bool affectAllTracks = false); /* @brief Change a clip status (normal / audio only / video only) */ Q_INVOKABLE void setClipStatus(int clipId, int status); Q_INVOKABLE void requestClipCut(int clipId, int position); Q_INVOKABLE void extract(int clipId); Q_INVOKABLE void splitAudio(int clipId); void switchTrackLock(bool applyToAll = false); void switchTargetTrack(); const QString getTrackNameFromIndex(int trackIndex); /* @brief Seeks to selected clip start / end */ void seekCurrentClip(bool seekToEnd = false); /* @brief Seeks to a clip start (or end) based on it's clip id */ void seekToClip(int cid, bool seekToEnd); /* @brief Returns the number of tracks (audioTrakcs, videoTracks) */ QPoint getTracksCount() const; /* @brief Request monitor refresh if item (clip or composition) is under timeline cursor */ void refreshItem(int id); /* @brief Seek timeline to mouse position */ void seekToMouse(); /* @brief User enabled / disabled snapping, update timeline behavior */ void snapChanged(bool snap); /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Get the frame where mouse is positionned */ int getMousePos(); /* @brief Get the frame where mouse is positionned */ int getMouseTrack(); /* @brief Returns a map of track ids/track names */ QMap getTrackNames(bool videoOnly); /* @brief Returns the transition a track index for a composition (MLT index / Track id) */ QPair getCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); /* @brief Return true if composition's a_track is automatic (no forced track) */ bool compositionAutoTrack(int cid) const; const QString getClipBinId(int clipId) const; void focusItem(int itemId); /* @brief Create and display a split clip view to compare effect */ bool createSplitOverlay(Mlt::Filter *filter); /* @brief Delete the split clip view to compare effect */ void removeSplitOverlay(); /* @brief Add current timeline zone to preview rendering */ void addPreviewRange(bool add); /* @brief Clear current timeline zone from preview rendering */ void clearPreviewRange(); void startPreviewRender(); void stopPreviewRender(); QVariantList dirtyChunks() const; QVariantList renderedChunks() const; /* @brief returns the frame currently processed by timeline preview, -1 if none */ int workingPreview() const; /** @brief Return true if we want to use timeline ruler zone for editing */ bool useRuler() const; /* @brief Load timeline preview from saved doc */ void loadPreview(QString chunks, QString dirty, const QDateTime &documentDate, int enable); /* @brief Return document properties with added settings from timeline */ QMap documentProperties(); /** @brief Change track compsiting mode */ void switchCompositing(int mode); /** @brief Change a clip item's speed in timeline */ Q_INVOKABLE void changeItemSpeed(int clipId, int speed); /** @brief Delete selected zone and fill gap by moving following clips*/ void extractZone(QPoint zone); /** @brief Delete selected zone */ void liftZone(QPoint zone); bool insertZone(const QString &binId, QPoint zone, bool overwrite); void updateClip(int clipId, QVector roles); void showClipKeyframes(int clipId, bool value); void showCompositionKeyframes(int clipId, bool value); /** @brief Returns last usable timeline position (seek request or current pos) */ int timelinePosition() const; /** @brief Adjust all timeline tracks height */ void resetTrackHeight(); /** @brief timeline preview params changed, reset */ void resetPreview(); public slots: void selectMultitrack(); Q_INVOKABLE void setSeekPosition(int position); Q_INVOKABLE void setAudioTarget(int track); Q_INVOKABLE void setVideoTarget(int track); Q_INVOKABLE void setActiveTrack(int track); void onSeeked(int position); void addEffectToCurrentClip(const QStringList &effectData); /** @brief Dis / enable timeline preview. */ void disablePreview(bool disable); void invalidateClip(int cid); void invalidateZone(int in, int out); void checkDuration(); private: QQuickItem *m_root; KActionCollection *m_actionCollection; std::shared_ptr m_model; bool m_usePreview; struct Selection { QList selectedItems; int selectedTrack; bool isMultitrackSelected; }; int m_position; int m_seekPosition; int m_audioTarget; int m_videoTarget; int m_activeTrack; QPoint m_zone; double m_scale; static int m_duration; Selection m_selection; Selection m_savedSelection; PreviewManager *m_timelinePreview; QAction *m_disablePreview; void emitSelectedFromSelection(); int getCurrentItem(); void initializePreview(); // Get a list of currently selected items, including clips grouped with selection std::unordered_set getCurrentSelectionIds() const; signals: void selected(Mlt::Producer *producer); void selectionChanged(); void frameFormatChanged(); void trackHeightChanged(); void scaleFactorChanged(); void audioThumbFormatChanged(); void durationChanged(); void positionChanged(); void seekPositionChanged(); void audioTargetChanged(); void videoTargetChanged(); void activeTrackChanged(); void showThumbnailsChanged(); void showAudioThumbnailsChanged(); void showMarkersChanged(); void rippleChanged(); void scrubChanged(); void seeked(int position); void zoneChanged(); void zoneMoved(const QPoint &zone); /* @brief Requests that a given parameter model is displayed in the asset panel */ void showTransitionModel(int tid, std::shared_ptr); void showItemEffectStack(const QString &clipName, std::shared_ptr, QPair range, QSize frameSize, bool showKeyframes); /* @brief notify of chunks change */ void dirtyChunksChanged(); void renderedChunksChanged(); void workingPreviewChanged(); void useRulerChanged(); }; #endif