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 }