File indexing completed on 2025-02-23 04:34:22
0001 /** 0002 * \file playlistview.cpp 0003 * List view for playlist items. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 25 Aug 2018 0008 * 0009 * Copyright (C) 2018-2024 Urs Fleisch 0010 * 0011 * This file is part of Kid3. 0012 * Contains adapted Python code from 0013 * http://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget 0014 * 0015 * Kid3 is free software; you can redistribute it and/or modify 0016 * it under the terms of the GNU General Public License as published by 0017 * the Free Software Foundation; either version 2 of the License, or 0018 * (at your option) any later version. 0019 * 0020 * Kid3 is distributed in the hope that it will be useful, 0021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0023 * GNU General Public License for more details. 0024 * 0025 * You should have received a copy of the GNU General Public License 0026 * along with this program. If not, see <http://www.gnu.org/licenses/>. 0027 */ 0028 0029 #include "playlistview.h" 0030 #include <algorithm> 0031 #include <QDragEnterEvent> 0032 #include <QDragMoveEvent> 0033 #include <QMimeData> 0034 #include <QAction> 0035 #include <QUrl> 0036 #include "filesystemmodel.h" 0037 0038 PlaylistView::PlaylistView(QWidget* parent) 0039 : QListView(parent), m_dropRole(FileSystemModel::FilePathRole) 0040 { 0041 auto deleteAction = new QAction(this); 0042 deleteAction->setShortcut(QKeySequence::Delete); 0043 deleteAction->setShortcutContext(Qt::WidgetShortcut); 0044 connect(deleteAction, &QAction::triggered, 0045 this, &PlaylistView::deleteCurrentRow); 0046 addAction(deleteAction); 0047 0048 auto moveUpAction = new QAction(this); 0049 moveUpAction->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Up); 0050 moveUpAction->setShortcutContext(Qt::WidgetShortcut); 0051 connect(moveUpAction, &QAction::triggered, 0052 this, &PlaylistView::moveUpCurrentRow); 0053 addAction(moveUpAction); 0054 0055 auto moveDownAction = new QAction(this); 0056 moveDownAction->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Down); 0057 moveDownAction->setShortcutContext(Qt::WidgetShortcut); 0058 connect(moveDownAction, &QAction::triggered, 0059 this, &PlaylistView::moveDownCurrentRow); 0060 addAction(moveDownAction); 0061 } 0062 0063 /** 0064 * Check if the drop index if child of a dropped item. 0065 * @param event drop event 0066 * @param index drop index 0067 * @return true if dropping on itself. 0068 */ 0069 bool PlaylistView::droppingOnItself(QDropEvent* event, const QModelIndex& index) 0070 { 0071 Qt::DropAction dropAction = event->dropAction(); 0072 if (dragDropMode() == InternalMove) { 0073 dropAction = Qt::MoveAction; 0074 } 0075 if (event->source() == this && 0076 (event->possibleActions() & Qt::MoveAction) != 0 && 0077 dropAction == Qt::MoveAction) { 0078 QModelIndexList selIndexes = selectedIndexes(); 0079 QModelIndex child = index; 0080 QModelIndex root = rootIndex(); 0081 while (child.isValid() && child != root) { 0082 if (selIndexes.contains(child)) { 0083 return true; 0084 } 0085 child = child.parent(); 0086 } 0087 } 0088 return false; 0089 } 0090 0091 /** 0092 * @brief Get row, column and index where item is dropped. 0093 * @param event drop event 0094 * @param dropRow the row is returned here 0095 * @param dropCol the column is returned here 0096 * @param dropIndex the parent model index is returned here 0097 * @return true if drop supported and not on itself. 0098 */ 0099 bool PlaylistView::dropOn( 0100 QDropEvent* event, int* dropRow, int* dropCol, QModelIndex* dropIndex) 0101 { 0102 if (event->isAccepted()) 0103 return false; 0104 0105 QModelIndex index; 0106 QModelIndex root = rootIndex(); 0107 #if QT_VERSION >= 0x060000 0108 if (viewport()->rect().contains(event->position().toPoint())) { 0109 index = indexAt(event->position().toPoint()); 0110 if (!index.isValid() || !visualRect(index).contains(event->position().toPoint())) { 0111 index = root; 0112 } 0113 } 0114 #else 0115 if (viewport()->rect().contains(event->pos())) { 0116 index = indexAt(event->pos()); 0117 if (!index.isValid() || !visualRect(index).contains(event->pos())) { 0118 index = root; 0119 } 0120 } 0121 #endif 0122 0123 if (model()->supportedDropActions() & event->dropAction()) { 0124 int row = -1; 0125 int col = -1; 0126 if (index != root) { 0127 #if QT_VERSION >= 0x060000 0128 DropIndicatorPosition dropIndicatorPosition = 0129 position(event->position().toPoint(), visualRect(index), index); 0130 #else 0131 DropIndicatorPosition dropIndicatorPosition = 0132 position(event->pos(), visualRect(index), index); 0133 #endif 0134 switch (dropIndicatorPosition) { 0135 case QAbstractItemView::AboveItem: 0136 row = index.row(); 0137 col = index.column(); 0138 index = index.parent(); 0139 break; 0140 case QAbstractItemView::BelowItem: 0141 row = index.row() + 1; 0142 col = index.column(); 0143 index = index.parent(); 0144 break; 0145 case QAbstractItemView::OnItem: 0146 case QAbstractItemView::OnViewport: 0147 row = index.row(); 0148 col = index.column(); 0149 index = index.parent(); 0150 break; 0151 } 0152 } 0153 *dropIndex = index; 0154 *dropRow = row; 0155 *dropCol = col; 0156 if (!droppingOnItself(event, index)) { 0157 return true; 0158 } 0159 } 0160 return false; 0161 } 0162 0163 /** 0164 * Get drop indicator position. 0165 * @param pos drop position 0166 * @param rect visual rectangle of @a idx 0167 * @param idx parent index 0168 * @return drop indicator position. 0169 */ 0170 QAbstractItemView::DropIndicatorPosition PlaylistView::position( 0171 const QPoint& pos, const QRect& rect, const QModelIndex& idx) const 0172 { 0173 QAbstractItemView::DropIndicatorPosition r = QAbstractItemView::OnViewport; 0174 if (constexpr int margin = 2; pos.y() - rect.top() < margin) { 0175 r = QAbstractItemView::AboveItem; 0176 } else if (rect.bottom() - pos.y() < margin) { 0177 r = QAbstractItemView::BelowItem; 0178 } else if (rect.contains(pos, true)) { 0179 r = QAbstractItemView::OnItem; 0180 } 0181 0182 if (r == QAbstractItemView::OnItem && 0183 !(model()->flags(idx) & Qt::ItemIsDropEnabled)) { 0184 r = pos.y() < rect.center().y() 0185 ? QAbstractItemView::AboveItem : QAbstractItemView::BelowItem; 0186 } 0187 return r; 0188 } 0189 0190 /** 0191 * Get sorted list of selected rows. 0192 * @return list of rows. 0193 */ 0194 QList<int> PlaylistView::getSelectedRows() const 0195 { 0196 QSet<int> selRows; 0197 const auto idxs = selectedIndexes(); 0198 for (const QModelIndex& idx : idxs) { 0199 selRows.insert(idx.row()); 0200 } 0201 0202 #if QT_VERSION >= 0x050e00 0203 QList result(selRows.constBegin(), selRows.constEnd()); 0204 #else 0205 QList result = selRows.toList(); 0206 #endif 0207 std::sort(result.begin(), result.end()); 0208 return result; 0209 } 0210 0211 void PlaylistView::dropEvent(QDropEvent* event) 0212 { 0213 if (event->dropAction() == Qt::MoveAction || 0214 event->dropAction() == Qt::CopyAction || 0215 dragDropMode() == InternalMove) { 0216 if (event->source() == this) { 0217 // Internal drop. 0218 QModelIndex index; 0219 int col = -1; 0220 int row = -1; 0221 if (dropOn(event, &row, &col, &index)) { 0222 if (QAbstractItemModel* mdl = model()) { 0223 if (const QList<int> selRows = getSelectedRows(); 0224 !selRows.isEmpty()) { 0225 int top = selRows.first(); 0226 int dropRow = row; 0227 if (dropRow == -1) { 0228 dropRow = mdl->rowCount(index); 0229 } 0230 int offset = dropRow - top; 0231 for (int theRow : selRows) { 0232 int r = theRow + offset; 0233 if (r > mdl->rowCount(index) || r < 0) { 0234 r = 0; 0235 } 0236 mdl->insertRow(r, index); 0237 } 0238 0239 if (const QList<int> newSelRows = getSelectedRows(); 0240 !newSelRows.isEmpty()) { 0241 top = newSelRows.first(); 0242 offset = dropRow - top; 0243 for (int theRow : newSelRows) { 0244 int r = theRow + offset; 0245 if (r > mdl->rowCount(index) || r < 0) { 0246 r = 0; 0247 } 0248 for (int j = 0; j < mdl->columnCount(index); ++j) { 0249 QVariant source = mdl->index(theRow, j, index) 0250 .data(m_dropRole); 0251 mdl->setData(mdl->index(r, j, index), source, m_dropRole); 0252 } 0253 } 0254 event->accept(); 0255 } 0256 } 0257 } 0258 } else { 0259 QListView::dropEvent(event); 0260 } 0261 } else if (event->mimeData()->hasUrls()) { 0262 // External file drop. 0263 int row, col; 0264 QModelIndex index; 0265 if (dropOn(event, &row, &col, &index)) { 0266 QList<QUrl> urls = event->mimeData()->urls(); 0267 if (QAbstractItemModel* mdl = model()) { 0268 if (row == -1) { 0269 row = mdl->rowCount(index); 0270 } 0271 if (!urls.isEmpty()) { 0272 QListIterator it(urls); 0273 it.toBack(); 0274 while (it.hasPrevious()) { 0275 if (const QUrl& url = it.previous(); url.isLocalFile()) { 0276 QString path = url.toLocalFile(); 0277 mdl->insertRow(row, index); 0278 QModelIndex idx = mdl->index(row, 0, index); 0279 mdl->setData(idx, path, m_dropRole); 0280 if (idx.data(m_dropRole).toString() != path) { 0281 qWarning("Playlist: Failed to set path %s", qPrintable(path)); 0282 mdl->removeRow(row, index); 0283 } 0284 } 0285 } 0286 event->accept(); 0287 } 0288 } 0289 } 0290 } 0291 } 0292 } 0293 0294 void PlaylistView::dragEnterEvent(QDragEnterEvent* event) 0295 { 0296 QListView::dragEnterEvent(event); 0297 if (!event->isAccepted() && event->mimeData()->hasUrls()) { 0298 event->acceptProposedAction(); 0299 } 0300 } 0301 0302 void PlaylistView::dragMoveEvent(QDragMoveEvent* event) 0303 { 0304 QListView::dragMoveEvent(event); 0305 if (!event->isAccepted() && event->mimeData()->hasUrls()) { 0306 event->acceptProposedAction(); 0307 } 0308 } 0309 0310 void PlaylistView::dragLeaveEvent(QDragLeaveEvent* event) 0311 { 0312 event->accept(); 0313 } 0314 0315 void PlaylistView::deleteCurrentRow() 0316 { 0317 if (QAbstractItemModel* mdl = model()) { 0318 if (QModelIndex idx = currentIndex(); idx.isValid()) { 0319 int row = idx.row(); 0320 mdl->removeRow(row); 0321 if (int numRows = mdl->rowCount(); row < numRows) { 0322 setCurrentIndex(mdl->index(row, 0)); 0323 } else if (row > 0 && row - 1 < numRows) { 0324 setCurrentIndex(mdl->index(row - 1, 0)); 0325 } 0326 } 0327 } 0328 } 0329 0330 void PlaylistView::moveUpCurrentRow() 0331 { 0332 swapRows(-1, 0); 0333 } 0334 0335 void PlaylistView::moveDownCurrentRow() 0336 { 0337 swapRows(0, 1); 0338 } 0339 0340 void PlaylistView::swapRows(int offset1, int offset2) 0341 { 0342 if (QAbstractItemModel* mdl = model()) { 0343 if (QModelIndex idx = currentIndex(); idx.isValid()) { 0344 int row1 = idx.row() + offset1; 0345 int row2 = idx.row() + offset2; 0346 int numRows = mdl->rowCount(); 0347 if (row1 >= 0 && row2 >= 0 && row1 < numRows && row2 < numRows) { 0348 QModelIndex idx1 = mdl->index(row1, 0); 0349 QModelIndex idx2 = mdl->index(row2, 0); 0350 QVariant val1 = idx1.data(m_dropRole); 0351 QVariant val2 = idx2.data(m_dropRole); 0352 mdl->setData(idx1, val2, m_dropRole); 0353 mdl->setData(idx2, val1, m_dropRole); 0354 if (offset1 == 0) { 0355 setCurrentIndex(idx2); 0356 } else if (offset2 == 0) { 0357 setCurrentIndex(idx1); 0358 } 0359 } 0360 } 0361 } 0362 }