Skip to content

Commit 7391034

Browse files
committed
Fix the selection after a column insert
1 parent 4ad9ee5 commit 7391034

8 files changed

Lines changed: 171 additions & 4 deletions

File tree

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ New features:
1515

1616
Bug fixes:
1717

18+
* Fix the selection covering the wrong columns after a column was inserted or
19+
deleted: a column's index is its identity, not its place on screen, so the
20+
selection now spans the columns actually dragged across
21+
- Velocity interpolation over a selection was reaching the wrong columns too
22+
1823
* Sort the effects gallery alphabetically: the list had drifted out of order as
1924
effects were added over the years
2025

src/application/service/editor_service.cpp

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ EditorService::EditorService(SelectionServiceS selectionService, SettingsService
7575
initialize();
7676
m_undoStack->setCanUndoChangedCallback([this] { emit canUndoChanged(); });
7777
m_undoStack->setCanRedoChangedCallback([this] { emit canRedoChanged(); });
78+
79+
// The selection spans places on screen, and only the song knows which index sits in which place.
80+
// Read through the song on every call so that it stays right when a new one is loaded. Cleared
81+
// in the destructor, because the selection service can be shared and outlive this.
82+
if (m_selectionService) {
83+
m_selectionService->setColumnOrderResolver([this](size_t trackIndex) {
84+
SelectionService::ColumnIndexList order;
85+
const auto indices = columnIndices(static_cast<quint64>(trackIndex));
86+
std::ranges::transform(indices, std::back_inserter(order), [](quint64 index) { return static_cast<size_t>(index); });
87+
return order;
88+
});
89+
}
7890
}
7991

8092
void EditorService::initialize()
@@ -2508,7 +2520,11 @@ void EditorService::requestLinearVelocityInterpolationOnSelection(quint64 startL
25082520
{
25092521
if (m_selectionService->isValidSelection()) {
25102522
std::map<Position, NoteData> oldNoteDataMap;
2511-
for (auto column = m_selectionService->minColumn(); column <= m_selectionService->maxColumn(); column++) {
2523+
// The selected columns rather than a count from the lowest index to the highest: an index is
2524+
// a column's identity, so after an insert they no longer run left to right and counting
2525+
// through them would reach columns the selection never covered.
2526+
const auto selectedColumns = m_selectionService->selectedColumns();
2527+
for (auto && column : selectedColumns) {
25122528
auto start = position();
25132529
start.column = column;
25142530
for (quint64 line = startLine; line <= endLine; ++line) {
@@ -2522,7 +2538,7 @@ void EditorService::requestLinearVelocityInterpolationOnSelection(quint64 startL
25222538

25232539
NoteEditCommand::ChangeList changes;
25242540

2525-
for (auto column = m_selectionService->minColumn(); column <= m_selectionService->maxColumn(); column++) {
2541+
for (auto && column : selectedColumns) {
25262542

25272543
auto start = position();
25282544
start.column = column;
@@ -3186,6 +3202,11 @@ quint64 EditorService::totalTicks() const
31863202
return 0;
31873203
}
31883204

3189-
EditorService::~EditorService() = default;
3205+
EditorService::~EditorService()
3206+
{
3207+
if (m_selectionService) {
3208+
m_selectionService->setColumnOrderResolver({});
3209+
}
3210+
}
31903211

31913212
} // namespace noteahead

src/application/service/selection_service.cpp

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
#include "../../contrib/SimpleLogger/src/simple_logger.hpp"
1919

20+
#include <algorithm>
21+
#include <utility>
22+
2023
namespace noteahead {
2124

2225
static const auto TAG = "SelectionService";
@@ -60,14 +63,49 @@ size_t SelectionService::track() const
6063
return m_startPosition.has_value() ? m_startPosition->track : 0;
6164
}
6265

66+
void SelectionService::setColumnOrderResolver(ColumnOrderResolver resolver)
67+
{
68+
m_columnOrderResolver = std::move(resolver);
69+
}
70+
71+
SelectionService::ColumnIndexList SelectionService::selectedColumns(size_t trackIndex, size_t startColumn, size_t endColumn) const
72+
{
73+
if (m_columnOrderResolver) {
74+
if (const auto order = m_columnOrderResolver(trackIndex); !order.empty()) {
75+
const auto startPosition = std::ranges::find(order, startColumn);
76+
const auto endPosition = std::ranges::find(order, endColumn);
77+
if (startPosition != order.end() && endPosition != order.end()) {
78+
const auto first = std::min(startPosition, endPosition);
79+
const auto last = std::max(startPosition, endPosition);
80+
return { first, last + 1 };
81+
}
82+
}
83+
}
84+
85+
// No order to go by: the indices are the only one there is.
86+
ColumnIndexList columns;
87+
for (size_t column = std::min(startColumn, endColumn); column <= std::max(startColumn, endColumn); column++) {
88+
columns.push_back(column);
89+
}
90+
return columns;
91+
}
92+
93+
SelectionService::ColumnIndexList SelectionService::selectedColumns() const
94+
{
95+
if (!isValidSelection()) {
96+
return {};
97+
}
98+
return selectedColumns(m_startPosition->track, m_startPosition->column, m_endPosition->column);
99+
}
100+
63101
SelectionService::PositionList SelectionService::selectedPositions() const
64102
{
65103
PositionList positions;
66104

67105
if (isValidSelection()) {
68106
auto start = *m_startPosition;
69107
auto end = *m_endPosition;
70-
for (size_t column = std::min(start.column, end.column); column <= std::max(start.column, end.column); column++) {
108+
for (auto && column : selectedColumns(start.track, start.column, end.column)) {
71109
for (size_t line = std::min(start.line, end.line); line <= std::max(start.line, end.line); line++) {
72110
positions.push_back({ start.pattern, start.track, column, line, start.lineColumn });
73111
}

src/application/service/selection_service.hpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
#include <QObject>
2020

21+
#include <functional>
2122
#include <optional>
23+
#include <vector>
2224

2325
#include "../position.hpp"
2426

@@ -52,12 +54,36 @@ class SelectionService : public QObject
5254
using PositionList = std::vector<Position>;
5355
PositionList selectedPositions() const;
5456

57+
using ColumnIndexList = std::vector<size_t>;
58+
//! Gives back a track's column indices in the order they are drawn.
59+
using ColumnOrderResolver = std::function<ColumnIndexList(size_t trackIndex)>;
60+
61+
//! Teaches the selection what the columns' order on screen is.
62+
//!
63+
//! A column's index is its identity, not its place: inserting one gives it the next free index
64+
//! wherever it lands, so after an insert the indices no longer run left to right. A selection
65+
//! spans what the user dragged across, which is a range of places, and without this there is no
66+
//! way to tell which indices those places hold.
67+
//!
68+
//! Unset -- and for a track the resolver does not know -- the indices are taken as the order,
69+
//! which is what they are until something is inserted or deleted.
70+
void setColumnOrderResolver(ColumnOrderResolver resolver);
71+
72+
//! The selected columns' indices, left to right on screen. What anything walking a selection
73+
//! across columns has to iterate: the indices themselves are not a range and cannot be counted
74+
//! through once a column has been inserted or deleted.
75+
Q_INVOKABLE ColumnIndexList selectedColumns() const;
76+
5577
signals:
5678
void isValidSelectionChanged();
5779
void selectionCleared(const Position & startPosition, const Position & endPosition);
5880
void selectionChanged(const Position & startPosition, const Position & endPosition);
5981

6082
private:
83+
//! The indices between the two ends of the selection, in display order, both ends included.
84+
ColumnIndexList selectedColumns(size_t trackIndex, size_t startColumn, size_t endColumn) const;
85+
86+
ColumnOrderResolver m_columnOrderResolver;
6187
std::optional<Position> m_startPosition;
6288
std::optional<Position> m_endPosition;
6389
};

src/unit_tests/editor_service_test/editor_service_test.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,39 @@ void EditorServiceTest::test_removePatternFromMiddle_shouldNotRemoveLastPattern(
194194
QCOMPARE(editorService.patternAtSongPosition(1), 2);
195195
}
196196

197+
void EditorServiceTest::test_selection_afterColumnInsert_shouldFollowDisplayOrder()
198+
{
199+
// The reported bug, against a real song rather than a stand-in order: insert a column and the
200+
// selection rectangle stops matching the columns dragged across.
201+
const auto selectionService = std::make_shared<SelectionService>();
202+
EditorService editorService { selectionService, std::make_shared<SettingsService>(), std::make_shared<AutomationService>(std::make_shared<PropertyService>()), std::make_shared<DataService>() };
203+
204+
editorService.requestNewColumn(0);
205+
editorService.requestNewColumn(0);
206+
QCOMPARE(editorService.columnCount(0), static_cast<quint64>(3));
207+
208+
// A new column on the left of the track. It takes the next free index and lands at the front,
209+
// so the indices stop running left to right.
210+
editorService.requestPosition(0, 0, 0, 0, 0);
211+
editorService.requestNewColumnToLeft();
212+
213+
const auto order = editorService.columnIndices(0);
214+
QCOMPARE(order.size(), 4);
215+
216+
// Drag across the first three columns on screen.
217+
selectionService->requestSelectionStart(0, 0, static_cast<size_t>(order.at(0)), 0);
218+
selectionService->requestSelectionEnd(0, 0, static_cast<size_t>(order.at(2)), 0);
219+
220+
for (int i = 0; i < 3; i++) {
221+
QVERIFY2(selectionService->isSelected(0, 0, static_cast<size_t>(order.at(i)), 0),
222+
qPrintable(QString { "Column at display position %1 (index %2) is not selected" }.arg(i).arg(order.at(i))));
223+
}
224+
225+
// And the fourth, which the drag never reached, must stay out of it.
226+
QVERIFY2(!selectionService->isSelected(0, 0, static_cast<size_t>(order.at(3)), 0),
227+
qPrintable(QString { "Column at display position 3 (index %1) should not be selected" }.arg(order.at(3))));
228+
}
229+
197230
void EditorServiceTest::test_columnCutPaste_equalSizes_shouldCopyColumn()
198231
{
199232
EditorService editorService { std::make_shared<SelectionService>(), std::make_shared<SettingsService>(), std::make_shared<AutomationService>(std::make_shared<PropertyService>()), std::make_shared<DataService>() };

src/unit_tests/editor_service_test/editor_service_test.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ private slots:
3636
void test_removePattern_shouldRemovePattern();
3737
void test_removePatternFromMiddle_shouldNotRemoveLastPattern();
3838

39+
void test_selection_afterColumnInsert_shouldFollowDisplayOrder();
40+
3941
void test_columnCutPaste_equalSizes_shouldCopyColumn();
4042
void test_columnCutPaste_shorterTarget_shouldCopyColumn();
4143
void test_columnCopyPaste_equalSizes_shouldCopyColumn();

src/unit_tests/selection_service_test/selection_service_test.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,46 @@ void SelectionServiceTest::test_selectedPositions_reversed_shouldReturnCorrectRa
5454
QCOMPARE(positions.at(2).line, 6);
5555
}
5656

57+
void SelectionServiceTest::test_selectedPositions_reorderedColumns_shouldFollowDisplayOrder()
58+
{
59+
SelectionService service;
60+
61+
// A track that had columns 0, 1, 2 and then got a new one inserted on the left. The new column
62+
// takes the next free index, 3, and sits at display position 0: an index is an identity, not a
63+
// place on screen, so after an insert the two stop agreeing.
64+
service.setColumnOrderResolver([](size_t) { return SelectionService::ColumnIndexList { 3, 0, 1, 2 }; });
65+
66+
// Dragging across the first three columns on screen, which are indices 3, 0 and 1.
67+
service.requestSelectionStart(0, 0, 3, 0);
68+
service.requestSelectionEnd(0, 0, 1, 0);
69+
70+
QVERIFY(service.isSelected(0, 0, 3, 0));
71+
QVERIFY(service.isSelected(0, 0, 0, 0));
72+
QVERIFY(service.isSelected(0, 0, 1, 0));
73+
74+
// Index 2 is the fourth column on screen, past the end of the drag. Walking indices numerically
75+
// from 1 to 3 would have taken it in and left out index 0, which is what made the rectangle
76+
// cover the wrong columns.
77+
QVERIFY(!service.isSelected(0, 0, 2, 0));
78+
79+
// Left to right on screen, which is what anything walking the selection has to follow.
80+
const SelectionService::ColumnIndexList expected { 3, 0, 1 };
81+
QCOMPARE(service.selectedColumns(), expected);
82+
}
83+
84+
void SelectionServiceTest::test_selectedPositions_noResolver_shouldFallBackOnIndexOrder()
85+
{
86+
// Without a resolver there is nothing to say what the display order is, so the indices are the
87+
// only order there is. Keeps a bare SelectionService behaving as it always did.
88+
SelectionService service;
89+
service.requestSelectionStart(0, 0, 1, 0);
90+
service.requestSelectionEnd(0, 0, 3, 0);
91+
92+
QVERIFY(service.isSelected(0, 0, 1, 0));
93+
QVERIFY(service.isSelected(0, 0, 2, 0));
94+
QVERIFY(service.isSelected(0, 0, 3, 0));
95+
}
96+
5797
void SelectionServiceTest::test_isValidSelection_shouldReturnFalseForIncompleteSelection()
5898
{
5999
SelectionService service;

src/unit_tests/selection_service_test/selection_service_test.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ private slots:
2828
void test_selectedPositions_shouldReturnEmptyIfInvalid();
2929
void test_selectedPositions_shouldReturnCorrectRange();
3030
void test_selectedPositions_reversed_shouldReturnCorrectRange();
31+
void test_selectedPositions_reorderedColumns_shouldFollowDisplayOrder();
32+
void test_selectedPositions_noResolver_shouldFallBackOnIndexOrder();
3133

3234
void test_isValidSelection_shouldReturnFalseForIncompleteSelection();
3335
void test_isValidSelection_shouldReturnTrueForValidSelection();

0 commit comments

Comments
 (0)