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

Add 2 small track functions: remove all spaces after cursor and remove all...

Add 2 small track functions: remove all spaces after cursor and remove all clips after cursor, with test
parent 4b22fb81
......@@ -1375,6 +1375,20 @@ bool SubtitleModel::isBlankAt(int pos) const
;
}
int SubtitleModel::getBlankEnd(int pos) const
{
GenTime matchPos(pos, pCore->getCurrentFps());
bool found = false;
GenTime min;
for (const auto &subtitles : m_subtitleList) {
if (subtitles.first > matchPos && (min == GenTime() || subtitles.first < min)) {
min = subtitles.first;
found = true;
}
}
return found ? min.frames(pCore->getCurrentFps()) : 0;
}
int SubtitleModel::getBlankStart(int pos) const
{
GenTime matchPos(pos, pCore->getCurrentFps());
......@@ -1388,3 +1402,23 @@ int SubtitleModel::getBlankStart(int pos) const
}
return found ? min.frames(pCore->getCurrentFps()) : 0;
}
int SubtitleModel::getNextBlankStart(int pos) const
{
while (!isBlankAt(pos)) {
std::unordered_set<int> matches = getItemsInRange(pos, pos);
if (matches.size() == 0) {
if (isBlankAt(pos)) {
break;
} else {
// We are at the end of the track, abort
return -1;
}
} else {
for (int id : matches) {
pos = qMax(pos, getSubtitleEnd(id));
}
}
}
return getBlankStart(pos);
}
......@@ -134,6 +134,10 @@ public:
QDomElement toXml(int sid, QDomDocument &document);
/** @brief Returns the position of the first blank frame before a position */
int getBlankStart(int pos) const;
/** @brief Returns the position of the first subtitle after the blank at @position */
int getBlankEnd(int pos) const;
/** @brief If pos is blank, returns the position of the blank start. Otherwise returns the position of the next blank frame */
int getNextBlankStart(int pos) const;
/** @brief Returns true is track is empty at pos */
bool isBlankAt(int pos) const;
/** @brief Switch a subtitle's grab state */
......
<!DOCTYPE kpartgui SYSTEM "kpartgui.dtd">
<kpartgui name="kdenlive" version="209" translationDomain="kdenlive">
<kpartgui name="kdenlive" version="211" translationDomain="kdenlive">
<MenuBar>
<Menu name="file" >
<Action name="file_save"/>
......@@ -136,6 +136,10 @@
<Action name="save_to_bin" />
<Action name="expand_timeline_clip" />
</Menu>
<Menu name="current_track" ><text>&amp;Current track</text>
<Action name="delete_all_spaces" />
<Action name="delete_all_clips" />
</Menu>
<Action name="grab_item" />
<Menu name="guide_menu" ><text>&amp;Guides</text>
<Action name="add_guide" />
......@@ -146,8 +150,8 @@
<Action name="lock_guides" />
</Menu>
<Menu name="space_menu" ><text>Space</text>
<Action name="insert_space" />
<Action name="delete_space" />
<Action name="insert_space" />
<Action name="delete_space" />
<Action name="delete_space_all_tracks" />
</Menu>
<Action name="group_clip" />
......
......@@ -1725,7 +1725,9 @@ void MainWindow::setupActions()
addAction(QStringLiteral("insert_space"), i18n("Insert Space…"), this, SLOT(slotInsertSpace()));
addAction(QStringLiteral("delete_space"), i18n("Remove Space"), this, SLOT(slotRemoveSpace()));
addAction(QStringLiteral("delete_space_all_tracks"), i18n("Remove Space in All Tracks"), this, SLOT(slotRemoveAllSpace()));
addAction(QStringLiteral("delete_all_spaces"), i18n("Remove All Spaces After Cursor"), this, SLOT(slotRemoveAllSpacesInTrack()));
addAction(QStringLiteral("delete_all_clips"), i18n("Remove All Clips After Cursor"), this, SLOT(slotRemoveAllClipsInTrack()));
addAction(QStringLiteral("delete_space_all_tracks"), i18n("Remove Space in All Tracks"), this, SLOT(slotRemoveSpaceInAllTracks()));
KActionCategory *timelineActions = new KActionCategory(i18n("Tracks"), actionCollection());
QAction *insertTrack = new QAction(QIcon(), i18nc("@action", "Insert Track…"), this);
......@@ -2710,11 +2712,21 @@ void MainWindow::slotRemoveSpace()
getMainTimeline()->controller()->removeSpace(-1, -1, false);
}
void MainWindow::slotRemoveAllSpace()
void MainWindow::slotRemoveSpaceInAllTracks()
{
getMainTimeline()->controller()->removeSpace(-1, -1, true);
}
void MainWindow::slotRemoveAllSpacesInTrack()
{
getMainTimeline()->controller()->removeTrackSpaces(-1, -1);
}
void MainWindow::slotRemoveAllClipsInTrack()
{
getMainTimeline()->controller()->removeTrackClips(-1, -1);
}
void MainWindow::slotSeparateAudioChannel()
{
KdenliveSettings::setDisplayallchannels(!KdenliveSettings::displayallchannels());
......
......@@ -436,7 +436,9 @@ private slots:
void slotInsertSpace();
void slotRemoveSpace();
void slotRemoveAllSpace();
void slotRemoveSpaceInAllTracks();
void slotRemoveAllSpacesInTrack();
void slotRemoveAllClipsInTrack();
void slotAddGuide();
void slotEditGuide();
void slotExportGuides();
......
......@@ -2202,6 +2202,140 @@ bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr<TimelineItemM
return true;
}
bool TimelineFunctions::requestDeleteAllBlanksFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position)
{
// Abort if track is locked
if (timeline->isSubtitleTrack(trackId) && timeline->getSubtitleModel() && timeline->getSubtitleModel()->isLocked()) {
return false;
}
if (timeline->isTrack(trackId) && timeline->getTrackById_const(trackId)->isLocked()) {
return false;
}
// Start undoable command
std::function<bool(void)> undo = []() { return true; };
std::function<bool(void)> redo = []() { return true; };
if (timeline->isSubtitleTrack(trackId)) {
// Subtitle track
int blankStart = timeline->getSubtitleModel()->getNextBlankStart(position);
if (blankStart == -1) {
return false;
}
while (blankStart != -1) {
int cid = requestSpacerStartOperation(timeline, trackId, blankStart, true);
if (cid == -1) {
break;
}
int start = timeline->getItemPosition(cid);
// Start undoable command
std::function<bool(void)> local_undo = []() { return true; };
std::function<bool(void)> local_redo = []() { return true; };
if (blankStart < start) {
if (!requestSpacerEndOperation(timeline, cid, start, blankStart, trackId, !KdenliveSettings::lockedGuides(), local_undo, local_redo, false)) {
// Failed to remove blank, maybe blocked because of a group. Pass to the next one
blankStart = start;
} else {
UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
}
} else {
if (timeline->getSubtitleModel()->isBlankAt(blankStart)) {
blankStart = timeline->getSubtitleModel()->getBlankEnd(blankStart) + 1;
if (blankStart == 1) {
break;
}
} else {
blankStart = start + timeline->getItemPlaytime(cid) + 1;
}
}
int nextBlank = timeline->getSubtitleModel()->getNextBlankStart(blankStart);
if (nextBlank == blankStart) {
blankStart = timeline->getSubtitleModel()->getBlankEnd(blankStart) + 1;
nextBlank = timeline->getSubtitleModel()->getNextBlankStart(blankStart);
if (nextBlank == blankStart) {
break;
}
}
if (nextBlank < blankStart) {
// Done
break;
}
blankStart = nextBlank;
}
} else {
int blankStart = timeline->getTrackById_const(trackId)->getNextBlankStart(position);
if (blankStart == -1) {
return false;
}
while (blankStart != -1) {
int cid = requestSpacerStartOperation(timeline, trackId, blankStart, true);
if (cid == -1) {
break;
}
int start = timeline->getItemPosition(cid);
// Start undoable command
std::function<bool(void)> local_undo = []() { return true; };
std::function<bool(void)> local_redo = []() { return true; };
if (blankStart < start) {
if (!requestSpacerEndOperation(timeline, cid, start, blankStart, trackId, !KdenliveSettings::lockedGuides(), local_undo, local_redo, false)) {
// Failed to remove blank, maybe blocked because of a group. Pass to the next one
blankStart = start;
} else {
UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
}
} else {
if (timeline->getTrackById_const(trackId)->isBlankAt(blankStart)) {
blankStart = timeline->getTrackById_const(trackId)->getBlankEnd(blankStart) + 1;
} else {
blankStart = start + timeline->getItemPlaytime(cid);
}
}
int nextBlank = timeline->getTrackById_const(trackId)->getNextBlankStart(blankStart);
if (nextBlank == blankStart) {
blankStart = timeline->getTrackById_const(trackId)->getBlankEnd(blankStart) + 1;
nextBlank = timeline->getTrackById_const(trackId)->getNextBlankStart(blankStart);
if (nextBlank == blankStart) {
break;
}
}
if (nextBlank < blankStart) {
// Done
break;
}
blankStart = nextBlank;
}
}
pCore->pushUndo(undo, redo, i18n("Remove space on track"));
return true;
}
bool TimelineFunctions::requestDeleteAllClipsFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position)
{
// Abort if track is locked
if (timeline->isSubtitleTrack(trackId) && timeline->getSubtitleModel() && timeline->getSubtitleModel()->isLocked()) {
return false;
}
if (timeline->isTrack(trackId) && timeline->getTrackById_const(trackId)->isLocked()) {
return false;
}
// Start undoable command
std::function<bool(void)> undo = []() { return true; };
std::function<bool(void)> redo = []() { return true; };
std::unordered_set<int> items;
if (timeline->isSubtitleTrack(trackId)) {
// Subtitle track
items = timeline->getSubtitleModel()->getItemsInRange(position, -1);
} else {
items = timeline->getTrackById_const(trackId)->getClipsInRange(position, -1);
}
if (items.size() == 0) {
return false;
}
for (int id : items) {
timeline->requestItemDeletion(id, undo, redo);
}
pCore->pushUndo(undo, redo, i18n("Delete clips on track"));
return true;
}
QDomDocument TimelineFunctions::extractClip(const std::shared_ptr<TimelineItemModel> &timeline, int cid, const QString &binId)
{
int tid = timeline->getClipTrackId(cid);
......
......@@ -71,6 +71,20 @@ struct TimelineFunctions
*/
static bool requestDeleteBlankAt(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position, bool affectAllTracks);
/** @brief This function will delete all blanks on the given track after the given position
@returns true on success, false otherwise
@param trackId id of the track to search in
@param position of the blank
*/
static bool requestDeleteAllBlanksFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position);
/** @brief This function will delete all clips on the given track after the given position
@returns true on success, false otherwise
@param trackId id of the track to search in
@param position start position for the operation
*/
static bool requestDeleteAllClipsFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position);
/** @brief Starts a spacer operation. Should be used together with requestSpacerEndOperation
@returns clipId of the position-wise first clip in the temporary group
@param timeline TimelineItemModel where the operation should be performed on
......
......@@ -1175,6 +1175,23 @@ bool TrackModel::isBlankAt(int position, int playlist)
return m_playlists[playlist].is_blank_at(position);
}
int TrackModel::getNextBlankStart(int position)
{
while (!isBlankAt(position)) {
int end1 = getClipEnd(position, 0);
int end2 = getClipEnd(position, 1);
if (end1 > position) {
position = end1;
} else if (end2 > position) {
position = end2;
} else {
// We reached playlist end
return -1;
}
}
return getBlankStart(position);
}
int TrackModel::getBlankStart(int position)
{
READ_LOCK();
......
......@@ -221,6 +221,7 @@ protected:
int getBlankSizeNearClip(int clipId, bool after);
int getBlankSizeNearComposition(int compoId, bool after);
int getBlankStart(int position);
int getNextBlankStart(int position);
/** @brief Returns the start of the blank on a specific playlist */
int getBlankStart(int position, int track);
int getBlankSizeAtPos(int frame);
......
......@@ -2645,6 +2645,34 @@ void TimelineController::removeSpace(int trackId, int frame, bool affectAllTrack
}
}
void TimelineController::removeTrackSpaces(int trackId, int frame)
{
if (frame == -1) {
frame = getMenuOrTimelinePos();
}
if (trackId == -1) {
trackId = m_activeTrack;
}
bool res = TimelineFunctions::requestDeleteAllBlanksFrom(m_model, trackId, frame);
if (!res) {
pCore->displayMessage(i18n("Cannot remove all spaces"), ErrorMessage, 500);
}
}
void TimelineController::removeTrackClips(int trackId, int frame)
{
if (frame == -1) {
frame = getMenuOrTimelinePos();
}
if (trackId == -1) {
trackId = m_activeTrack;
}
bool res = TimelineFunctions::requestDeleteAllClipsFrom(m_model, trackId, frame);
if (!res) {
pCore->displayMessage(i18n("Cannot remove all clips"), ErrorMessage, 500);
}
}
void TimelineController::invalidateItem(int cid)
{
if (!m_timelinePreview || !m_model->isItem(cid)) {
......
......@@ -424,6 +424,12 @@ public:
*/
Q_INVOKABLE void insertSpace(int trackId = -1, int frame = -1);
Q_INVOKABLE void removeSpace(int trackId = -1, int frame = -1, bool affectAllTracks = false);
/** @brief Remove all spaces in a @trackId track after @frame position
*/
void removeTrackSpaces(int trackId, int frame);
/** @brief Remove all clips in a @trackId track after @frame position
*/
void removeTrackClips(int trackId, int frame);
/** @brief If clip is enabled, disable, otherwise enable
*/
Q_INVOKABLE void switchEnableState(std::unordered_set<int> selection = {});
......
......@@ -22,6 +22,7 @@ set(KdenliveTest_SOURCES
cachetest.cpp
movetest.cpp
subtitlestest.cpp
spacertest.cpp
)
include(ECMAddTests)
......
/*
SPDX-FileCopyrightText: 2022 Jean-Baptiste Mardelle <jb@kdenlive.org>
SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "catch.hpp"
#include "doc/docundostack.hpp"
#include "test_utils.hpp"
#include "definitions.h"
#define private public
#define protected public
#include "core.h"
using namespace fakeit;
Mlt::Profile profile_spacer;
TEST_CASE("Remove all spaces", "[Spacer]")
{
// Create timeline
auto binModel = pCore->projectItemModel();
binModel->clean();
std::shared_ptr<DocUndoStack> undoStack = std::make_shared<DocUndoStack>(nullptr);
std::shared_ptr<MarkerListModel> guideModel = std::make_shared<MarkerListModel>(undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock<ProjectManager> pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
When(Method(pmMock, cacheDir)).AlwaysReturn(QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(&profile_spacer, undoStack);
Mock<TimelineItemModel> timMock(tim);
auto timeline = std::shared_ptr<TimelineItemModel>(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
// Create a basic timeline
int tid1, tid2, tid3;
REQUIRE(timeline->requestTrackInsertion(-1, tid1));
REQUIRE(timeline->requestTrackInsertion(-1, tid2));
REQUIRE(timeline->requestTrackInsertion(-1, tid3, QString(), true));
// Create clip with audio (40 frames long)
QString binId = createProducer(profile_spacer, "red", binModel, 20);
QString avBinId = createProducerWithSound(profile_spacer, binModel, 100);
// Setup insert stream data
QMap<int, QString> audioInfo;
audioInfo.insert(1, QStringLiteral("stream1"));
timeline->m_binAudioTargets = audioInfo;
// Create clips in timeline
int cid1;
int cid2;
int cid3;
int cid4;
REQUIRE(timeline->requestClipInsertion(binId, tid1, 10, cid1));
REQUIRE(timeline->requestClipInsertion(binId, tid1, 80, cid2));
REQUIRE(timeline->requestClipInsertion(binId, tid1, 101, cid3));
REQUIRE(timeline->requestClipInsertion(binId, tid2, 20, cid4));
auto state1 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getClipsCount() == 4);
REQUIRE(timeline->getClipPosition(cid1) == 10);
REQUIRE(timeline->getClipPosition(cid2) == 80);
REQUIRE(timeline->getClipPosition(cid3) == 101);
REQUIRE(timeline->getClipPosition(cid4) == 20);
};
SECTION("Ensure remove spaces behaves correctly")
{
// We have clips at 10, 80, 101 on track 1 (length 20 frames each)
// One clip at 20 on track 2
REQUIRE(TimelineFunctions::requestDeleteAllBlanksFrom(timeline, tid1, 20));
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getClipsCount() == 4);
REQUIRE(timeline->getClipPosition(cid1) == 10);
REQUIRE(timeline->getClipPosition(cid2) == 30);
REQUIRE(timeline->getClipPosition(cid3) == 50);
REQUIRE(timeline->getClipPosition(cid4) == 20);
undoStack->undo();
state1();
}
SECTION("Ensure remove spaces behaves correctly with a group")
{
// We have clips at 10, 80, 101 on track 1 (length 20 frames each)
// One clip at 20 on track 2
std::unordered_set<int> ids = {cid2, cid3};
int gid = timeline->requestClipsGroup(ids);
REQUIRE(gid > -1);
REQUIRE(TimelineFunctions::requestDeleteAllBlanksFrom(timeline, tid1, 20));
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getClipsCount() == 4);
REQUIRE(timeline->getClipPosition(cid1) == 10);
REQUIRE(timeline->getClipPosition(cid2) == 30);
REQUIRE(timeline->getClipPosition(cid3) == 51);
REQUIRE(timeline->getClipPosition(cid4) == 20);
undoStack->undo();
state1();
undoStack->undo();
}
SECTION("Ensure remove spaces behaves correctly with a group on different tracks")
{
// We have clips at 10, 80, 101 on track 1 (length 20 frames each)
// One clip at 20 on track 2
// Grouping clip at 80 on tid1 and at 20 on tid2, so the group move will be rejected for the 80 clip
std::unordered_set<int> ids = {cid2, cid4};
int gid = timeline->requestClipsGroup(ids);
REQUIRE(gid > -1);
REQUIRE(TimelineFunctions::requestDeleteAllBlanksFrom(timeline, tid1, 20));
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getClipsCount() == 4);
REQUIRE(timeline->getClipPosition(cid1) == 10);
REQUIRE(timeline->getClipPosition(cid2) == 80);
REQUIRE(timeline->getClipPosition(cid3) == 100);
REQUIRE(timeline->getClipPosition(cid4) == 20);
undoStack->undo();
state1();
undoStack->undo();
}
SECTION("Ensure remove spaces behaves correctly with a group on different tracks")
{
// We have clips at 10, 80, 101 on track 1 (length 20 frames each)
// One clip at 20 on track 2
// Grouping clip at 10 on tid1 and at 20 on tid2, so the clip on tid2 will be moved
std::unordered_set<int> ids = {cid1, cid4};
int gid = timeline->requestClipsGroup(ids);
REQUIRE(gid > -1);
REQUIRE(TimelineFunctions::requestDeleteAllBlanksFrom(timeline, tid1, 0));
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getClipsCount() == 4);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPosition(cid2) == 20);
REQUIRE(timeline->getClipPosition(cid3) == 40);
REQUIRE(timeline->getClipPosition(cid4) == 10);
undoStack->undo();
state1();
undoStack->undo();
}
binModel->clean();
pCore->m_projectManager = nullptr;
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment