diff --git a/src/timeline2/model/timelinefunctions.hpp b/src/timeline2/model/timelinefunctions.hpp --- a/src/timeline2/model/timelinefunctions.hpp +++ b/src/timeline2/model/timelinefunctions.hpp @@ -52,6 +52,16 @@ /* @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); diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp --- a/src/timeline2/model/timelinefunctions.cpp +++ b/src/timeline2/model/timelinefunctions.cpp @@ -60,6 +60,30 @@ 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); diff --git a/src/timeline2/model/timelinemodel.hpp b/src/timeline2/model/timelinemodel.hpp --- a/src/timeline2/model/timelinemodel.hpp +++ b/src/timeline2/model/timelinemodel.hpp @@ -299,11 +299,14 @@ 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); diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -665,6 +665,7 @@ return requestGroupDeletion(clipId, undo, redo); } return requestClipDeletion(clipId, undo, redo); + } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) diff --git a/src/timeline2/view/qml/timeline.qml b/src/timeline2/view/qml/timeline.qml --- a/src/timeline2/view/qml/timeline.qml +++ b/src/timeline2/view/qml/timeline.qml @@ -101,6 +101,13 @@ 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 @@ -190,10 +197,7 @@ if (clipBeingDroppedId != -1) { controller.requestItemDeletion(clipBeingDroppedId, false) } - clipBeingDroppedId = -1 - droppedPosition = -1 - droppedTrack = -1 - scrollTimer.running = false + clearDropData() } onDropped: { if (clipBeingDroppedId != -1) { @@ -203,13 +207,32 @@ 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 @@ -226,7 +249,7 @@ //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 @@ -237,10 +260,7 @@ if (clipBeingDroppedId != -1) { controller.requestItemDeletion(clipBeingDroppedId, false) } - clipBeingDroppedId = -1 - droppedPosition = -1 - droppedTrack = -1 - scrollTimer.running = false + clearDropData() } onPositionChanged: { if (clipBeingMovedId == -1) { @@ -253,7 +273,7 @@ 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) } } @@ -263,14 +283,20 @@ 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 { diff --git a/src/timeline2/view/timelinecontroller.h b/src/timeline2/view/timelinecontroller.h --- a/src/timeline2/view/timelinecontroller.h +++ b/src/timeline2/view/timelinecontroller.h @@ -118,6 +118,24 @@ @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) diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -377,6 +377,20 @@ 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; @@ -1500,3 +1514,14 @@ 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); +}