Skip to content

Commit

Permalink
Playlists: move tracks with Alt + Up/Down/PageUp/PageDown/Home/End
Browse files Browse the repository at this point in the history
  • Loading branch information
ronso0 committed Apr 14, 2024
1 parent e351c24 commit 1b1aa5a
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 96 deletions.
1 change: 1 addition & 0 deletions .codespellignorelines
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ void EngineEffectsDelay::process(CSAMPLE* pInOut,
// ALAC/CAF has been added in version 1.0.26
QStringLiteral("caf"),
void EngineFilter::process(CSAMPLE* pInOut, const int iBufferSize)
// Note(RRyan/Max Linke):
314 changes: 219 additions & 95 deletions src/widget/wtracktableview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -639,15 +639,15 @@ void WTrackTableView::dragMoveEvent(QDragMoveEvent * event) {

// Drag-and-drop "drop" event. Occurs when something is dropped onto the track table view
void WTrackTableView::dropEvent(QDropEvent * event) {
TrackModel* trackModel = getTrackModel();
TrackModel* pTrackModel = getTrackModel();

// We only do things to the TrackModel in this method so if we don't have
// one we should just bail.
if (!trackModel) {
if (!pTrackModel) {
return;
}

if (!event->mimeData()->hasUrls() || trackModel->isLocked()) {
if (!event->mimeData()->hasUrls() || pTrackModel->isLocked()) {
event->ignore();
return;
}
Expand Down Expand Up @@ -677,105 +677,27 @@ void WTrackTableView::dropEvent(QDropEvent * event) {

// Drag and drop within this widget (track reordering)
if (event->source() == this &&
trackModel->hasCapabilities(TrackModel::Capability::Reorder)) {
pTrackModel->hasCapabilities(TrackModel::Capability::Reorder)) {
// Note the above code hides an ambiguous case when a
// playlist is empty. For that reason, we can't factor that
// code out to be common for both internal reordering
// and external drag-and-drop. With internal reordering,
// you can't have an empty playlist. :)

//qDebug() << "track reordering" << __FILE__ << __LINE__;

// Save a list of row (just plain ints) so we don't get screwed over
// Save a list of rows (just plain ints) so we don't get screwed over
// when the QModelIndexes all become invalid (eg. after moveTrack()
// or addTrack())
const QModelIndexList indices = getSelectedRows();

QList<int> selectedRows;
for (const QModelIndex& idx : indices) {
selectedRows.append(idx.row());
}

// Note: The biggest subtlety in the way I've done this track reordering code
// is that as soon as we've moved ANY track, all of our QModelIndexes probably
// get screwed up. The starting point for the logic below is to say screw it to
// the QModelIndexes, and just keep a list of row numbers to work from. That
// ends up making the logic simpler and the behavior totally predictable,
// which lets us do nice things like "restore" the selection model.

// The model indices are sorted so that we remove the tracks from the table
// in ascending order. This is necessary because if track A is above track B in
// the table, and you remove track A, the model index for track B will change.
// Sorting the indices first means we don't have to worry about this.
//std::sort(m_selectedIndices.begin(), m_selectedIndices.end(), std::greater<QModelIndex>());
std::sort(selectedRows.begin(), selectedRows.end());
int maxRow = 0;
int minRow = 0;
if (!selectedRows.isEmpty()) {
maxRow = selectedRows.last();
minRow = selectedRows.first();
}

// Destination row, if destIndex is invalid we set it to last row + 1
int destRow = destIndex.row() < 0 ? model()->rowCount() : destIndex.row();

int selectedRowCount = selectedRows.count();
int selectionRestoreStartRow = destRow;

// Adjust first row of new selection
if (destRow >= minRow && destRow <= maxRow) {
// If you drag a contiguous selection of multiple tracks and drop
// them somewhere inside that same selection, do nothing.
QList<int> selectedRows = getSelectedRowNumbers();
if (selectedRows.isEmpty()) {
return;
} else {
if (destRow < minRow) {
// If we're moving the tracks _up_,
// then reverse the order of the row selection
// to make the algorithm below work as it is
std::sort(selectedRows.begin(),
selectedRows.end(),
std::greater<int>());
} else {
if (destRow > maxRow) {
// If we're moving the tracks _down_,
// adjust the first row to reselect
selectionRestoreStartRow =
selectionRestoreStartRow - selectedRowCount;
}
}
}

// For each row that needs to be moved...
while (!selectedRows.isEmpty()) {
int movedRow = selectedRows.takeFirst(); // Remember it's row index
// Move it
trackModel->moveTrack(model()->index(movedRow, 0), destIndex);

// Move the row indices for rows that got bumped up
// into the void we left, or down because of the new spot
// we're taking.
for (int i = 0; i < selectedRows.count(); i++) {
if ((selectedRows[i] > movedRow) && (
(destRow > selectedRows[i]) )) {
selectedRows[i] = selectedRows[i] - 1;
} else if ((selectedRows[i] < movedRow) &&
(destRow < selectedRows[i])) {
selectedRows[i] = selectedRows[i] + 1;
}
}
}
std::sort(selectedRows.begin(), selectedRows.end());


// Highlight the moved rows again (restoring the selection)
//QModelIndex newSelectedIndex = destIndex;
for (int i = 0; i < selectedRowCount; i++) {
this->selectionModel()->select(model()->index(selectionRestoreStartRow + i, 0),
QItemSelectionModel::Select | QItemSelectionModel::Rows);
}
moveRows(selectedRows, destIndex.row());
} else { // Drag and drop inside Mixxx is only for few rows, bulks happen here
// Reset the selected tracks (if you had any tracks highlighted, it
// clears them)
this->selectionModel()->clear();
selectionModel()->clear();

// Have to do this here because the index is invalid after
// addTrack
Expand Down Expand Up @@ -807,18 +729,19 @@ void WTrackTableView::dropEvent(QDropEvent * event) {
for (const auto& fileInfo : trackFileInfos) {
trackLocations.append(fileInfo.location());
}
numNewRows = trackModel->addTracks(destIndex, trackLocations);
numNewRows = pTrackModel->addTracks(destIndex, trackLocations);
DEBUG_ASSERT(numNewRows >= 0);
DEBUG_ASSERT(numNewRows <= trackFileInfos.size());
}

// Create the selection, but only if the track model supports
// reordering. (eg. crates don't support reordering/indexes)
if (trackModel->hasCapabilities(TrackModel::Capability::Reorder)) {
if (pTrackModel->hasCapabilities(TrackModel::Capability::Reorder)) {
// TODO Also set current index to have good starting point for navigation?
for (int i = selectionStartRow; i < selectionStartRow + numNewRows; i++) {
this->selectionModel()->select(model()->index(i, 0),
QItemSelectionModel::Select |
QItemSelectionModel::Rows);
selectionModel()->select(model()->index(i, 0),
QItemSelectionModel::Select |
QItemSelectionModel::Rows);
}
}
}
Expand All @@ -837,6 +760,23 @@ QModelIndexList WTrackTableView::getSelectedRows() const {
return pSelectionModel->selectedRows();
}

QList<int> WTrackTableView::getSelectedRowNumbers() const {
QItemSelectionModel* pSelectionModel = selectionModel();
VERIFY_OR_DEBUG_ASSERT(pSelectionModel != nullptr) {
qWarning() << "No selection model available";
return {};
}
const QModelIndexList indices = pSelectionModel->selectedRows();
if (indices.isEmpty()) {
return {};
}
QList<int> selectedRows;
for (const QModelIndex& idx : indices) {
selectedRows.append(idx.row());
}
return selectedRows;
}

TrackModel* WTrackTableView::getTrackModel() const {
TrackModel* trackModel = dynamic_cast<TrackModel*>(model());
return trackModel;
Expand Down Expand Up @@ -917,6 +857,179 @@ void WTrackTableView::pasteTracks(const QModelIndex& index) {
}
}

void WTrackTableView::moveRows(const QList<int>& selectedRowsIn, int destRow) {
TrackModel* pTrackModel = getTrackModel();
if (!pTrackModel) {
return;
}
if (selectedRowsIn.isEmpty()) {
return;
}

// Note(RRyan/Max Linke):
// The biggest subtlety in the way I've done this track reordering code
// is that as soon as we've moved ANY track, all of our QModelIndexes probably
// get screwed up. The starting point for the logic below is to say screw
// the QModelIndexes, and just keep a list of row numbers to work from.
// That ends up making the logic simpler and the behavior totally predictable,
// which lets us do nice things like "restore" the selection model.

// The model indices are sorted so that we remove the tracks from the table
// in ascending order. This is necessary because if track A is above track B in
// the table, and you remove track A, the model index for track B will change.
// Sorting the indices first means we don't have to worry about this.
QList<int> selectedRows = selectedRowsIn;

destRow = destRow < 0 ? model()->rowCount() : destRow;
// Required for refocusing the correct column and restoring the selection
// after we moved. Use 0 if the index is invalid for some reason.
int idxCol = std::max(0, currentIndex().column());
int selectedRowCount = selectedRows.count();
int selectionRestoreStartRow = destRow;
int firstSelRow = selectedRows.first();
int lastSelRow = selectedRows.last();

if (destRow == firstSelRow && selectedRowCount == 1) {
return; // no-op
}

// Adjust first row of new selection
if (destRow >= firstSelRow && destRow <= lastSelRow) {
bool continuous = selectedRowCount == lastSelRow - firstSelRow + 1;
qWarning() << " destRow in selection, cont:" << QString(continuous ? "YES" : "NO");
// If we drag a contiguous selection of multiple tracks and drop them
// somewhere inside that same selection, we obviously have nothing to do.
// This is also a good way to abort accidental drags.
if (continuous) {
return;
}
// Non-continuous selection:
// consolidate selection at firstSelRow
if (destRow == firstSelRow) {
// Remove consecutive rows (they are already in place) until we find
// the first gap in the selection.
// Use the reow after that continuous part of the selection as destination.
while (destRow == firstSelRow) {
selectedRows.removeFirst();
firstSelRow = selectedRows.first();
destRow++;
}
} else {
return;
}
}

if (destRow < firstSelRow) {
// If we're moving the tracks _up_, reverse the order of the row selection
// to make the algorithm below work as it is
std::sort(selectedRows.begin(),
selectedRows.end(),
std::greater<int>());
} else {
if (destRow > lastSelRow) {
// If we're moving the tracks _down_, adjust the first row to reselect
selectionRestoreStartRow =
selectionRestoreStartRow - selectedRowCount;
}
}

// For each row that needs to be moved...
while (!selectedRows.isEmpty()) {
int movedRow = selectedRows.takeFirst(); // Remember it's row index
// Move it
pTrackModel->moveTrack(model()->index(movedRow, 0), model()->index(destRow, 0));

// Move the row indices for rows that got bumped up
// into the void we left, or down because of the new spot
// we're taking.
for (int i = 0; i < selectedRows.count(); i++) {
if ((selectedRows[i] > movedRow) && ((destRow > selectedRows[i]))) {
selectedRows[i] = selectedRows[i] - 1;
} else if ((selectedRows[i] < movedRow) &&
(destRow < selectedRows[i])) {
selectedRows[i] = selectedRows[i] + 1;
}
}
}

// TODO Try to make all moved rows visible?

// Set current index.
// TODO If we moved down, pick the last selected row?
// int idxRow = destRow < firstSelRow
// ? selectionRestoreStartRow
// : selectionRestoreStartRow + selectedRowCount - 1;
const auto idx = model()->index(selectionRestoreStartRow, idxCol);
QItemSelectionModel* pSelectionModel = selectionModel();
if (pSelectionModel && idx.isValid()) {
pSelectionModel->setCurrentIndex(idx,
QItemSelectionModel::SelectCurrent | QItemSelectionModel::Select);
}

// Highlight the moved rows again (restoring the selection)
for (int i = 0; i < selectedRowCount; i++) {
selectionModel()->select(model()->index(selectionRestoreStartRow + i, idxCol),
QItemSelectionModel::Select | QItemSelectionModel::Rows);
}
}

void WTrackTableView::moveSelectedTracks(QKeyEvent* event) {
QList<int> selectedRows = getSelectedRowNumbers();
if (selectedRows.isEmpty()) {
return;
}
std::sort(selectedRows.begin(), selectedRows.end());

bool up = event->key() == Qt::Key_Up;
bool pageUp = event->key() == Qt::Key_PageUp;
bool down = event->key() == Qt::Key_Down;
bool pageDown = event->key() == Qt::Key_PageDown;
bool top = event->key() == Qt::Key_Home;
bool bottom = event->key() == Qt::Key_End;

// Check if we have a continuous selection.
int firstSelRow = selectedRows.first();
int lastSelRow = selectedRows.last();
int rowCount = model()->rowCount();
bool continuous = selectedRows.length() == lastSelRow - firstSelRow + 1;
if (continuous &&
(((up || pageUp || top) && firstSelRow == 0) ||
((down || pageDown || bottom) && lastSelRow == rowCount - 1))) {
// Continuous selection with no more rows to skip in the desired
// direction, further Up/Down would wrap around the current index.
// Ignore.
return;
}

int destRow = 0;
if (top) {
destRow = 0;
} else if (bottom || ((bottom || down || pageDown) && lastSelRow == rowCount - 1)) {
// In case of End or non-continuous and lastSelRow already at the end
// we simply paste at the end by invalidating the index.
destRow = -1;
} else if (up || pageUp) {
// currentIndex can be anywhere inside or outside the selection.
// Set it top or bottom of the selection, then pass through the key event
// to get us the desired destination index.
setCurrentIndex(model()->index(firstSelRow, currentIndex().column()));
QTableView::keyPressEvent(event);
destRow = currentIndex().row();
} else {
// Same when moving down.
setCurrentIndex(model()->index(lastSelRow, currentIndex().column()));
QTableView::keyPressEvent(event);
destRow = currentIndex().row() + 1;
if (pageDown && destRow >= rowCount) {
// PageDown hit the end of the list. Explicitly paste at the
destRow = -1;
} else {
}
}

moveRows(selectedRows, destRow);
}

void WTrackTableView::keyPressEvent(QKeyEvent* event) {
switch (event->key()) {
case kPropertiesShortcutKey: {
Expand Down Expand Up @@ -948,8 +1061,8 @@ void WTrackTableView::keyPressEvent(QKeyEvent* event) {
default:
break;
}
TrackModel* trackModel = getTrackModel();
if (trackModel && !trackModel->isLocked()) {
TrackModel* pTrackModel = getTrackModel();
if (pTrackModel && !pTrackModel->isLocked()) {
if (event->matches(QKeySequence::Delete) || event->key() == Qt::Key_Backspace) {
removeSelectedTracks();
return;
Expand All @@ -966,6 +1079,17 @@ void WTrackTableView::keyPressEvent(QKeyEvent* event) {
pasteTracks(currentIndex());
return;
}
if (event->modifiers().testFlag(Qt::AltModifier) &&
(event->key() == Qt::Key_Up ||
event->key() == Qt::Key_Down ||
event->key() == Qt::Key_PageUp ||
event->key() == Qt::Key_PageDown ||
event->key() == Qt::Key_Home ||
event->key() == Qt::Key_End) &&
pTrackModel->hasCapabilities(TrackModel::Capability::Reorder)) {
moveSelectedTracks(event);
return;
}
if (event->key() == Qt::Key_Escape) {
clearSelection();
setCurrentIndex(QModelIndex());
Expand Down
Loading

0 comments on commit 1b1aa5a

Please sign in to comment.