diff --git a/CMakeLists.txt b/CMakeLists.txt index 2de62e944b8..fa304847eb5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1141,6 +1141,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/track/trackref.cpp src/util/battery/battery.cpp src/util/cache.cpp + src/util/clipboard.cpp src/util/cmdlineargs.cpp src/util/color/color.cpp src/util/color/colorpalette.cpp diff --git a/src/coreservices.cpp b/src/coreservices.cpp index c0e1ccc10fe..3a66ab5decc 100644 --- a/src/coreservices.cpp +++ b/src/coreservices.cpp @@ -30,6 +30,7 @@ #include "skin/skincontrols.h" #include "soundio/soundmanager.h" #include "sources/soundsourceproxy.h" +#include "util/clipboard.h" #include "util/db/dbconnectionpooled.h" #include "util/font.h" #include "util/logger.h" @@ -335,6 +336,7 @@ void CoreServices::initialize(QApplication* pApp) { emit initializationProgressUpdate(50, tr("library")); CoverArtCache::createInstance(); + Clipboard::createInstance(); m_pTrackCollectionManager = std::make_shared( this, @@ -580,6 +582,8 @@ void CoreServices::finalize() { // CoverArtCache is fairly independent of everything else. CoverArtCache::destroy(); + Clipboard::destroy(); + // PlayerManager depends on Engine, SoundManager, VinylControlManager, and Config // The player manager has to be deleted before the library to ensure // that all modified track metadata of loaded tracks is saved. diff --git a/src/library/autodj/autodjfeature.cpp b/src/library/autodj/autodjfeature.cpp index 59525f24a38..cdca01516af 100644 --- a/src/library/autodj/autodjfeature.cpp +++ b/src/library/autodj/autodjfeature.cpp @@ -14,6 +14,8 @@ #include "moc_autodjfeature.cpp" #include "sources/soundsourceproxy.h" #include "track/track.h" +#include "util/clipboard.h" +#include "util/dnd.h" #include "widget/wlibrary.h" #include "widget/wlibrarysidebar.h" @@ -161,6 +163,14 @@ void AutoDJFeature::activate() { emit enableCoverArtDisplay(true); } +void AutoDJFeature::clear() { + m_playlistDao.clearAutoDJQueue(); +} + +void AutoDJFeature::paste() { + emit pasteFromSidebar(); +} + bool AutoDJFeature::dropAccept(const QList& urls, QObject* pSource) { // If a track is dropped onto the Auto DJ tree node, but the track isn't in the // library, then add the track to the library before adding it to the diff --git a/src/library/autodj/autodjfeature.h b/src/library/autodj/autodjfeature.h index 8a9a9d2a4fd..70451a49d7a 100644 --- a/src/library/autodj/autodjfeature.h +++ b/src/library/autodj/autodjfeature.h @@ -32,6 +32,9 @@ class AutoDJFeature : public LibraryFeature { QVariant title() override; + void clear() override; + void paste() override; + bool dropAccept(const QList& urls, QObject* pSource) override; bool dragMoveAccept(const QUrl& url) override; diff --git a/src/library/autodj/dlgautodj.cpp b/src/library/autodj/dlgautodj.cpp index 4c8e3ced149..43c0dbb94fe 100644 --- a/src/library/autodj/dlgautodj.cpp +++ b/src/library/autodj/dlgautodj.cpp @@ -390,6 +390,10 @@ void DlgAutoDJ::setFocus() { m_pTrackTableView->setFocus(); } +void DlgAutoDJ::pasteFromSidebar() { + m_pTrackTableView->pasteFromSidebar(); +} + void DlgAutoDJ::keyPressEvent(QKeyEvent* pEvent) { // If we receive key events either the mode selector or the spinbox are focused. // Return, Enter and Escape move focus back to the previously focused diff --git a/src/library/autodj/dlgautodj.h b/src/library/autodj/dlgautodj.h index a9cc19a8f75..a2d976dc774 100644 --- a/src/library/autodj/dlgautodj.h +++ b/src/library/autodj/dlgautodj.h @@ -28,6 +28,7 @@ class DlgAutoDJ : public QWidget, public Ui::DlgAutoDJ, public LibraryView { void onShow() override; bool hasFocus() const override; void setFocus() override; + void pasteFromSidebar() override; void onSearch(const QString& text) override; void activateSelectedTrack() override; void loadSelectedTrackToGroup(const QString& group, bool play) override; diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp index 274c206b4f1..285891605b3 100644 --- a/src/library/basetracktablemodel.cpp +++ b/src/library/basetracktablemodel.cpp @@ -21,6 +21,7 @@ #include "moc_basetracktablemodel.cpp" #include "track/track.h" #include "util/assert.h" +#include "util/clipboard.h" #include "util/datetime.h" #include "util/db/sqlite.h" #include "util/logger.h" @@ -380,6 +381,59 @@ int BaseTrackTableModel::columnCount(const QModelIndex& parent) const { return countValidColumnHeaders(); } +void BaseTrackTableModel::cutTracks(const QModelIndexList& indices) { + copyTracks(indices); + removeTracks(indices); +} + +void BaseTrackTableModel::copyTracks(const QModelIndexList& indices) const { + Clipboard::start(); + for (const QModelIndex& index : indices) { + if (index.isValid()) { + Clipboard::add(QUrl::fromLocalFile(getTrackLocation(index))); + } + } + Clipboard::finish(); +} + +QList BaseTrackTableModel::pasteTracks(const QModelIndex& insertionIndex) { + // Don't paste into locked playlists and crates or into into History + if (isLocked() || !hasCapabilities(TrackModel::Capability::ReceiveDrops)) { + return QList{}; + } + + int insertionPos = 0; + const QList urls = Clipboard::urls(); + const QList trackIds = m_pTrackCollectionManager->resolveTrackIdsFromUrls(urls, true); + if (!trackIds.isEmpty()) { + addTracksWithTrackIds(insertionIndex, trackIds, &insertionPos); + } + + QList rows; + for (const auto& trackId : trackIds) { + const auto trackRows = getTrackRows(trackId); + for (int trackRow : trackRows) { + if (insertionPos == 0) { + rows.append(trackRow); + } else { + int pos = + index( + trackRow, + fieldIndex(ColumnCache:: + COLUMN_PLAYLISTTRACKSTABLE_POSITION)) + .data() + .toInt(); + // trackRows includes all instances in the table of the pasted + // tracks. We only want to select the ones we just inserted + if (pos >= insertionPos && pos < insertionPos + trackIds.size()) { + rows.append(trackRow); + } + } + } + } + return rows; +} + bool BaseTrackTableModel::isColumnHiddenByDefault( int column) { return column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST) || diff --git a/src/library/basetracktablemodel.h b/src/library/basetracktablemodel.h index b5d82568d1e..e46cc2569c8 100644 --- a/src/library/basetracktablemodel.h +++ b/src/library/basetracktablemodel.h @@ -81,6 +81,10 @@ class BaseTrackTableModel : public QAbstractTableModel, public TrackModel { return m_columnCache.fieldIndex(fieldName); } + void cutTracks(const QModelIndexList& indices) override; + void copyTracks(const QModelIndexList& indices) const override; + QList pasteTracks(const QModelIndex& index) override; + bool isColumnHiddenByDefault( int column) override; diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp index 561b2a3ecac..f6399267f70 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/browse/browsetablemodel.cpp @@ -14,6 +14,7 @@ #include "moc_browsetablemodel.cpp" #include "recording/recordingmanager.h" #include "track/track.h" +#include "util/clipboard.h" #include "widget/wlibrarytableview.h" namespace { @@ -314,6 +315,20 @@ bool BrowseTableModel::isColumnHiddenByDefault(int column) { void BrowseTableModel::moveTrack(const QModelIndex&, const QModelIndex&) { } +void BrowseTableModel::copyTracks(const QModelIndexList& indices) const { + Clipboard::start(); + for (const QModelIndex& index : indices) { + if (index.isValid()) { + Clipboard::add(QUrl::fromLocalFile(getTrackLocation(index))); + } + } + Clipboard::finish(); + + // TODO Investigate if we can also implement cut and paste (via QFile + // operations) so mixxx could manage files in the filesystem, rather than + // having to go switch between mixxx and the system file browser. +} + void BrowseTableModel::removeTracks(const QModelIndexList&) { } diff --git a/src/library/browse/browsetablemodel.h b/src/library/browse/browsetablemodel.h index fb4b8dbc7f8..e770096eee5 100644 --- a/src/library/browse/browsetablemodel.h +++ b/src/library/browse/browsetablemodel.h @@ -71,6 +71,7 @@ class BrowseTableModel final : public QStandardItemModel, public virtual TrackMo const QString currentSearch() const override; bool isColumnInternal(int) override; void moveTrack(const QModelIndex&, const QModelIndex&) override; + void copyTracks(const QModelIndexList& indices) const override; bool isLocked() override { return false; } bool isColumnHiddenByDefault(int column) override; const QList& searchColumns() const override; diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index 192c0002ae4..ede2f4eb569 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -796,6 +796,16 @@ int PlaylistDAO::insertTracksIntoPlaylist(const QList& trackIds, return tracksAdded; } +void PlaylistDAO::clearAutoDJQueue() { + const int iAutoDJPlaylistId = getPlaylistIdFromName(AUTODJ_TABLE); + // If the first track is already loaded to the player, + // alter the playlist only below the first track + const int position = + (m_pAutoDJProcessor && m_pAutoDJProcessor->nextTrackLoaded()) ? 2 : 1; + + removeTracksFromPlaylist(iAutoDJPlaylistId, position); +} + void PlaylistDAO::addPlaylistToAutoDJQueue(const int playlistId, AutoDJSendLoc loc) { //qDebug() << "Adding tracks from playlist " << playlistId << " to the Auto-DJ Queue"; diff --git a/src/library/dao/playlistdao.h b/src/library/dao/playlistdao.h index 58660626c27..3eb6def5ec2 100644 --- a/src/library/dao/playlistdao.h +++ b/src/library/dao/playlistdao.h @@ -107,6 +107,8 @@ class PlaylistDAO : public QObject, public virtual DAO { bool insertTrackIntoPlaylist(TrackId trackId, int playlistId, int position); // Inserts a list of tracks into playlist int insertTracksIntoPlaylist(const QList& trackIds, const int playlistId, int position); + // Remove all tracks from the Auto-DJ Queue + void clearAutoDJQueue(); // Add a playlist to the Auto-DJ Queue void addPlaylistToAutoDJQueue(const int playlistId, AutoDJSendLoc loc); // Add a list of tracks to the Auto-DJ Queue diff --git a/src/library/library.cpp b/src/library/library.cpp index 77bd315e1b4..48c2ca4bc63 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -389,6 +389,10 @@ void Library::bindLibraryWidget( &Library::showTrackModel, pTrackTableView, &WTrackTableView::loadTrackModel); + connect(this, + &Library::pasteFromSidebar, + m_pLibraryWidget, + &WLibrary::pasteFromSidebar); connect(pTrackTableView, &WTrackTableView::loadTrack, this, @@ -399,6 +403,10 @@ void Library::bindLibraryWidget( &Library::slotLoadTrackToPlayer); m_pLibraryWidget->registerView(m_sTrackViewName, pTrackTableView); + connect(m_pLibraryWidget, + &WLibrary::setLibraryFocus, + m_pLibraryControl, + &LibraryControl::setLibraryFocus); connect(this, &Library::switchToView, m_pLibraryWidget, @@ -461,6 +469,10 @@ void Library::addFeature(LibraryFeature* feature) { } m_features.push_back(feature); m_pSidebarModel->addLibraryFeature(feature); + connect(feature, + &LibraryFeature::pasteFromSidebar, + this, + &Library::pasteFromSidebar); connect(feature, &LibraryFeature::showTrackModel, this, @@ -517,7 +529,7 @@ void Library::onPlayerManagerTrackAnalyzerIdle() { } void Library::slotShowTrackModel(QAbstractItemModel* model) { - //qDebug() << "Library::slotShowTrackModel" << model; + // qDebug() << "Library::slotShowTrackModel" << model; TrackModel* trackModel = dynamic_cast(model); VERIFY_OR_DEBUG_ASSERT(trackModel) { return; @@ -528,7 +540,7 @@ void Library::slotShowTrackModel(QAbstractItemModel* model) { } void Library::slotSwitchToView(const QString& view) { - //qDebug() << "Library::slotSwitchToView" << view; + // qDebug() << "Library::slotSwitchToView" << view; emit switchToView(view); } diff --git a/src/library/library.h b/src/library/library.h index fe9bf0056c1..3cd08cc23e5 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -124,6 +124,7 @@ class Library: public QObject { void restoreSearch(const QString&); void search(const QString& text); void disableSearch(); + void pasteFromSidebar(); // emit this signal to enable/disable the cover art widget void enableCoverArtDisplay(bool); void selectTrack(const TrackId&); diff --git a/src/library/libraryfeature.h b/src/library/libraryfeature.h index 5ba3ffa7da2..61972192dcd 100644 --- a/src/library/libraryfeature.h +++ b/src/library/libraryfeature.h @@ -68,6 +68,13 @@ class LibraryFeature : public QObject { return false; } + virtual void clear() { + } + virtual void paste() { + } + virtual void pasteChild(const QModelIndex& index) { + Q_UNUSED(index); + } // Reimplement this to register custom views with the library widget. virtual void bindLibraryWidget(WLibrary* /* libraryWidget */, KeyboardEventFilter* /* keyboard */) {} @@ -135,6 +142,7 @@ class LibraryFeature : public QObject { void restoreModelState(); void restoreSearch(const QString&); void disableSearch(); + void pasteFromSidebar(); // emit this signal before you parse a large music collection, e.g., iTunes, Traktor. // The second arg indicates if the feature should be "selected" when loading starts void featureIsLoading(LibraryFeature*, bool selectFeature); diff --git a/src/library/libraryview.h b/src/library/libraryview.h index eec640e636a..ac6061b5ac8 100644 --- a/src/library/libraryview.h +++ b/src/library/libraryview.h @@ -22,6 +22,9 @@ class LibraryView { /// Reimplement if LibraryView should be able to search virtual void onSearch(const QString& text) {Q_UNUSED(text);} + virtual void pasteFromSidebar() { + } + /// If applicable, requests that the LibraryView load the selected /// track. Does nothing otherwise. virtual void activateSelectedTrack() { diff --git a/src/library/playlisttablemodel.cpp b/src/library/playlisttablemodel.cpp index 3c563657d8d..ce7c937b717 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/playlisttablemodel.cpp @@ -189,30 +189,35 @@ void PlaylistTableModel::selectPlaylist(int playlistId) { setSort(defaultSortColumn(), defaultSortOrder()); } -int PlaylistTableModel::addTracks(const QModelIndex& index, - const QList& locations) { - if (locations.isEmpty()) { +int PlaylistTableModel::addTracksWithTrackIds(const QModelIndex& insertionIndex, + const QList& trackIds, + int* pOutInsertionPos) { + if (trackIds.isEmpty()) { return 0; } - QList trackIds = m_pTrackCollectionManager->resolveTrackIdsFromLocations( - locations); - const int positionColumn = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); - int position = index.sibling(index.row(), positionColumn).data().toInt(); + int position = index(insertionIndex.row(), positionColumn).data().toInt(); // Handle weird cases like a drag and drop to an invalid index if (position <= 0) { position = rowCount() + 1; } + if (pOutInsertionPos) { + *pOutInsertionPos = position; + } + int tracksAdded = m_pTrackCollectionManager->internalCollection()->getPlaylistDAO().insertTracksIntoPlaylist( trackIds, m_iPlaylistId, position); - if (locations.size() - tracksAdded > 0) { + if (trackIds.size() - tracksAdded > 0) { + QString playlistName = m_pTrackCollectionManager->internalCollection() + ->getPlaylistDAO() + .getPlaylistName(m_iPlaylistId); qDebug() << "PlaylistTableModel::addTracks could not add" - << locations.size() - tracksAdded - << "to playlist" << m_iPlaylistId; + << trackIds.size() - tracksAdded + << "to playlist id" << m_iPlaylistId << "name" << playlistName; } return tracksAdded; } diff --git a/src/library/playlisttablemodel.h b/src/library/playlisttablemodel.h index 0443bd53577..c663f88bd4c 100644 --- a/src/library/playlisttablemodel.h +++ b/src/library/playlisttablemodel.h @@ -25,7 +25,9 @@ class PlaylistTableModel final : public TrackSetTableModel { /// This function should only be used by AUTODJ void removeTracks(const QModelIndexList& indices) final; /// Returns the number of successful additions. - int addTracks(const QModelIndex& index, const QList& locations) final; + int addTracksWithTrackIds(const QModelIndex& index, + const QList& trackIds, + int* pOutInsertionPos) final; bool isLocked() final; /// Get the total duration of all tracks referenced by the given model indices diff --git a/src/library/proxytrackmodel.cpp b/src/library/proxytrackmodel.cpp index 73c374e2838..1d0398c6ffe 100644 --- a/src/library/proxytrackmodel.cpp +++ b/src/library/proxytrackmodel.cpp @@ -113,6 +113,17 @@ void ProxyTrackModel::removeTracks(const QModelIndexList& indices) { } } +void ProxyTrackModel::copyTracks(const QModelIndexList& indices) const { + QModelIndexList translatedList; + foreach (QModelIndex index, indices) { + QModelIndex indexSource = mapToSource(index); + translatedList.append(indexSource); + } + if (m_pTrackModel) { + m_pTrackModel->copyTracks(translatedList); + } +} + void ProxyTrackModel::moveTrack(const QModelIndex& sourceIndex, const QModelIndex& destIndex) { QModelIndex sourceIndexSource = mapToSource(sourceIndex); diff --git a/src/library/proxytrackmodel.h b/src/library/proxytrackmodel.h index d56ef77eeb5..561449a31a8 100644 --- a/src/library/proxytrackmodel.h +++ b/src/library/proxytrackmodel.h @@ -36,6 +36,7 @@ class ProxyTrackModel : public QSortFilterProxyModel, public TrackModel { bool isColumnInternal(int column) final; bool isColumnHiddenByDefault(int column) final; void removeTracks(const QModelIndexList& indices) final; + void copyTracks(const QModelIndexList& indices) const final; void moveTrack(const QModelIndex& sourceIndex, const QModelIndex& destIndex) final; QAbstractItemDelegate* delegateForColumn(const int i, QObject* pParent) final; QString getModelSetting(const QString& name) final; diff --git a/src/library/sidebarmodel.cpp b/src/library/sidebarmodel.cpp index 1dd2786fbc4..49a13dd9bce 100644 --- a/src/library/sidebarmodel.cpp +++ b/src/library/sidebarmodel.cpp @@ -148,6 +148,24 @@ QModelIndex SidebarModel::getFeatureRootIndex(LibraryFeature* pFeature) { return ind; } +void SidebarModel::clear(const QModelIndex& index) { + if (index.internalPointer() == this) { + m_sFeatures[index.row()]->clear(); + } +} + +void SidebarModel::paste(const QModelIndex& index) { + if (index.internalPointer() == this) { + m_sFeatures[index.row()]->paste(); + } else { + TreeItem* pTreeItem = (TreeItem*)index.internalPointer(); + if (pTreeItem) { + LibraryFeature* feature = pTreeItem->feature(); + feature->pasteChild(index); + } + } +} + QModelIndex SidebarModel::parent(const QModelIndex& index) const { //qDebug() << "SidebarModel::parent index=" << index.getData(); if (index.isValid()) { @@ -475,7 +493,7 @@ QModelIndex SidebarModel::translateIndex( } void SidebarModel::slotDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { - //qDebug() << "slotDataChanged topLeft:" << topLeft << "bottomRight:" << bottomRight; + // qDebug() << "slotDataChanged topLeft:" << topLeft << "bottomRight:" << bottomRight; QModelIndex topLeftTranslated = translateSourceIndex(topLeft); QModelIndex bottomRightTranslated = translateSourceIndex(bottomRight); emit dataChanged(topLeftTranslated, bottomRightTranslated); @@ -499,8 +517,8 @@ void SidebarModel::slotRowsInserted(const QModelIndex& parent, int start, int en Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); - //qDebug() << "slotRowsInserted" << parent << start << end; - //QModelIndex newParent = translateSourceIndex(parent); + // qDebug() << "slotRowsInserted" << parent << start << end; + // QModelIndex newParent = translateSourceIndex(parent); endInsertRows(); } diff --git a/src/library/sidebarmodel.h b/src/library/sidebarmodel.h index c0d7a6ca063..c987dff72c7 100644 --- a/src/library/sidebarmodel.h +++ b/src/library/sidebarmodel.h @@ -47,6 +47,8 @@ class SidebarModel : public QAbstractItemModel { } QModelIndex getFeatureRootIndex(LibraryFeature* pFeature); + void clear(const QModelIndex& index); + void paste(const QModelIndex& index); public slots: void pressed(const QModelIndex& index); void clicked(const QModelIndex& index); diff --git a/src/library/trackmodel.h b/src/library/trackmodel.h index c811114709a..53bf53ddbc9 100644 --- a/src/library/trackmodel.h +++ b/src/library/trackmodel.h @@ -140,6 +140,16 @@ class TrackModel { virtual void removeTracks(const QModelIndexList& indices) { Q_UNUSED(indices); } + virtual void cutTracks(const QModelIndexList& indices) { + Q_UNUSED(indices); + } + virtual void copyTracks(const QModelIndexList& indices) const { + Q_UNUSED(indices); + } + virtual QList pasteTracks(const QModelIndex& index) { + Q_UNUSED(index); + return QList(); + } virtual void hideTracks(const QModelIndexList& indices) { Q_UNUSED(indices); } @@ -154,6 +164,14 @@ class TrackModel { Q_UNUSED(locations); return 0; } + virtual int addTracksWithTrackIds(const QModelIndex& index, + const QList& tracks, + int* pOutInsertionPos) { + Q_UNUSED(index); + Q_UNUSED(tracks); + Q_UNUSED(pOutInsertionPos); + return 0; + } virtual void moveTrack(const QModelIndex& sourceIndex, const QModelIndex& destIndex) { Q_UNUSED(sourceIndex); diff --git a/src/library/trackset/baseplaylistfeature.cpp b/src/library/trackset/baseplaylistfeature.cpp index 0b18a37b1ae..d26de9c0642 100644 --- a/src/library/trackset/baseplaylistfeature.cpp +++ b/src/library/trackset/baseplaylistfeature.cpp @@ -165,7 +165,7 @@ void BasePlaylistFeature::connectPlaylistDAO() { &BasePlaylistFeature::slotPlaylistTableRenamed); } -int BasePlaylistFeature::playlistIdFromIndex(const QModelIndex& index) { +int BasePlaylistFeature::playlistIdFromIndex(const QModelIndex& index) const { TreeItem* item = static_cast(index.internalPointer()); if (item == nullptr) { return kInvalidPlaylistId; diff --git a/src/library/trackset/baseplaylistfeature.h b/src/library/trackset/baseplaylistfeature.h index fbcbf16586a..273c052984e 100644 --- a/src/library/trackset/baseplaylistfeature.h +++ b/src/library/trackset/baseplaylistfeature.h @@ -86,7 +86,7 @@ class BasePlaylistFeature : public BaseTrackSetFeature { virtual void decorateChild(TreeItem* pChild, int playlistId) = 0; virtual void addToAutoDJ(PlaylistDAO::AutoDJSendLoc loc); - int playlistIdFromIndex(const QModelIndex& index); + int playlistIdFromIndex(const QModelIndex& index) const; // Get the QModelIndex of a playlist based on its id. Returns QModelIndex() // on failure. QModelIndex indexFromPlaylistId(int playlistId); diff --git a/src/library/trackset/basetracksetfeature.cpp b/src/library/trackset/basetracksetfeature.cpp index a50b9b94019..51f18f1f436 100644 --- a/src/library/trackset/basetracksetfeature.cpp +++ b/src/library/trackset/basetracksetfeature.cpp @@ -13,6 +13,10 @@ BaseTrackSetFeature::BaseTrackSetFeature( m_pSidebarModel(make_parented(this)) { } +void BaseTrackSetFeature::pasteChild(const QModelIndex&) { + emit pasteFromSidebar(); +} + void BaseTrackSetFeature::activate() { emit switchToView(m_rootViewName); emit disableSearch(); diff --git a/src/library/trackset/basetracksetfeature.h b/src/library/trackset/basetracksetfeature.h index 6869d97ac46..c647f9d046f 100644 --- a/src/library/trackset/basetracksetfeature.h +++ b/src/library/trackset/basetracksetfeature.h @@ -14,6 +14,7 @@ class BaseTrackSetFeature : public LibraryFeature { const QString& rootViewName, const QString& iconName); + void pasteChild(const QModelIndex& index) override; signals: void analyzeTracks(const QList&); diff --git a/src/library/trackset/crate/cratetablemodel.cpp b/src/library/trackset/crate/cratetablemodel.cpp index 28388f4bfb1..d574f90eb35 100644 --- a/src/library/trackset/crate/cratetablemodel.cpp +++ b/src/library/trackset/crate/cratetablemodel.cpp @@ -150,17 +150,22 @@ TrackModel::Capabilities CrateTableModel::getCapabilities() const { return caps; } -int CrateTableModel::addTracks( - const QModelIndex& index, const QList& locations) { +int CrateTableModel::addTracksWithTrackIds( + const QModelIndex& index, const QList& trackIds, int* pOutInsertionPos) { Q_UNUSED(index); + + if (pOutInsertionPos != nullptr) { + // crate insertion is not done by position, and no duplicates will be added,. + // 0 indicates this to the caller. + *pOutInsertionPos = 0; + } + // If a track is dropped but it isn't in the library, then add it because // the user probably dropped a file from outside Mixxx into this crate. - QList trackIds = - m_pTrackCollectionManager->resolveTrackIdsFromLocations(locations); if (!m_pTrackCollectionManager->internalCollection()->addCrateTracks( m_selectedCrate, trackIds)) { qWarning() << "CrateTableModel::addTracks could not add" - << locations.size() << "tracks to crate" << m_selectedCrate; + << trackIds.size() << "tracks to crate" << m_selectedCrate; return 0; } diff --git a/src/library/trackset/crate/cratetablemodel.h b/src/library/trackset/crate/cratetablemodel.h index fed1f1d4dc3..6e977b4a1d8 100644 --- a/src/library/trackset/crate/cratetablemodel.h +++ b/src/library/trackset/crate/cratetablemodel.h @@ -19,7 +19,9 @@ class CrateTableModel final : public TrackSetTableModel { void removeTracks(const QModelIndexList& indices) final; /// Returns the number of unsuccessful additions. - int addTracks(const QModelIndex& index, const QList& locations) final; + int addTracksWithTrackIds(const QModelIndex& index, + const QList& tracks, + int* pOutInsertionPos) final; bool isLocked() final; Capabilities getCapabilities() const final; diff --git a/src/library/trackset/tracksettablemodel.cpp b/src/library/trackset/tracksettablemodel.cpp index f27bd25efae..1e53c73ca60 100644 --- a/src/library/trackset/tracksettablemodel.cpp +++ b/src/library/trackset/tracksettablemodel.cpp @@ -1,5 +1,6 @@ #include "library/trackset/tracksettablemodel.h" +#include "library/trackcollectionmanager.h" #include "mixer/playermanager.h" #include "moc_tracksettablemodel.cpp" @@ -26,3 +27,15 @@ bool TrackSetTableModel::isColumnInternal(int column) { column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART_DIGEST) || column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART_HASH); } + +int TrackSetTableModel::addTracks(const QModelIndex& index, + const QList& locations) { + if (locations.isEmpty()) { + return 0; + } + + QList trackIds = m_pTrackCollectionManager->resolveTrackIdsFromLocations( + locations); + + return addTracksWithTrackIds(index, trackIds, nullptr); +} diff --git a/src/library/trackset/tracksettablemodel.h b/src/library/trackset/tracksettablemodel.h index 649d33ef325..94202c9a853 100644 --- a/src/library/trackset/tracksettablemodel.h +++ b/src/library/trackset/tracksettablemodel.h @@ -11,4 +11,6 @@ class TrackSetTableModel : public BaseSqlTableModel { const char* settingsNamespace); bool isColumnInternal(int column) override; + + int addTracks(const QModelIndex& index, const QList& locations) final; }; diff --git a/src/util/clipboard.cpp b/src/util/clipboard.cpp new file mode 100644 index 00000000000..f38c5f361c7 --- /dev/null +++ b/src/util/clipboard.cpp @@ -0,0 +1,44 @@ +#include "clipboard.h" + +#include +#include +#include + +namespace { + +QByteArray urlsToUtf8(const QList& urls) { + QByteArray result; + for (const QUrl& url : urls) { + result += url.toEncoded(); + result += '\n'; + } + if (!result.isEmpty()) { + result.chop(1); + } + return result; +} + +} // namespace + +void Clipboard::start() { + instance()->m_urls.clear(); +} + +void Clipboard::add(const QUrl& url) { + instance()->m_urls.append(url); +} + +void Clipboard::finish() { + QMimeData* pData = new QMimeData; + pData->setUrls(instance()->m_urls); + // "x-special/gnome-copied-files" is used for many file managers + // https://indigo.re/posts/2021-12-21-clipboard-data.html + pData->setData(QStringLiteral("x-special/gnome-copied-files"), + "copy\n" + urlsToUtf8(instance()->m_urls)); + QApplication::clipboard()->setMimeData(pData); +} + +QList Clipboard::urls() { + const QMimeData* data = QApplication::clipboard()->mimeData(); + return data ? data->urls() : QList(); +} diff --git a/src/util/clipboard.h b/src/util/clipboard.h new file mode 100644 index 00000000000..fee83879ede --- /dev/null +++ b/src/util/clipboard.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include "util/singleton.h" + +class Clipboard : public Singleton { + QList m_urls; + + public: + static QList urls(); + static void start(); + static void finish(); + static void add(const QUrl& url); +}; diff --git a/src/widget/wlibrary.cpp b/src/widget/wlibrary.cpp index e3aadc6f54d..b408c625c74 100644 --- a/src/widget/wlibrary.cpp +++ b/src/widget/wlibrary.cpp @@ -1,5 +1,6 @@ #include "widget/wlibrary.h" +#include #include #include "library/libraryview.h" @@ -31,20 +32,20 @@ void WLibrary::setup(const QDomNode& node, const SkinContext& context) { kMaxTrackTableBackgroundColorOpacity); } -bool WLibrary::registerView(const QString& name, QWidget* view) { +bool WLibrary::registerView(const QString& name, QWidget* pView) { //qDebug() << "WLibrary::registerView" << name; const auto lock = lockMutex(&m_mutex); if (m_viewMap.contains(name)) { return false; } - if (dynamic_cast(view) == nullptr) { + if (dynamic_cast(pView) == nullptr) { qDebug() << "WARNING: Attempted to register view" << name << "with WLibrary " << "which does not implement the LibraryView interface. " << "Ignoring."; return false; } - addWidget(view); - m_viewMap[name] = view; + addWidget(pView); + m_viewMap[name] = pView; return true; } @@ -52,41 +53,49 @@ void WLibrary::switchToView(const QString& name) { const auto lock = lockMutex(&m_mutex); //qDebug() << "WLibrary::switchToView" << name; - LibraryView* oldLibraryView = dynamic_cast( + LibraryView* pOldLibrartView = dynamic_cast( currentWidget()); - QWidget* widget = m_viewMap.value(name, nullptr); - if (widget != nullptr) { - LibraryView * lview = dynamic_cast(widget); - if (lview == nullptr) { + QWidget* pWidget = m_viewMap.value(name, nullptr); + if (pWidget != nullptr) { + LibraryView* pLibraryView = dynamic_cast(pWidget); + if (pLibraryView == nullptr) { qDebug() << "WARNING: Attempted to switch to view" << name << "with WLibrary " << "which does not implement the LibraryView interface. " << "Ignoring."; return; } - if (currentWidget() != widget) { - if (oldLibraryView) { - oldLibraryView->saveCurrentViewState(); + if (currentWidget() != pWidget) { + if (pOldLibrartView) { + pOldLibrartView->saveCurrentViewState(); } //qDebug() << "WLibrary::setCurrentWidget" << name; - setCurrentWidget(widget); - lview->onShow(); - lview->restoreCurrentViewState(); + setCurrentWidget(pWidget); + pLibraryView->onShow(); + pLibraryView->restoreCurrentViewState(); } } } +void WLibrary::pasteFromSidebar() { + QWidget* pCurrent = currentWidget(); + LibraryView* pView = dynamic_cast(pCurrent); + if (pView) { + pView->pasteFromSidebar(); + } +} + void WLibrary::search(const QString& name) { auto lock = lockMutex(&m_mutex); - QWidget* current = currentWidget(); - LibraryView* view = dynamic_cast(current); - if (view == nullptr) { + QWidget* pCurrent = currentWidget(); + LibraryView* pView = dynamic_cast(pCurrent); + if (pView == nullptr) { qDebug() << "WARNING: Attempted to search in view" << name << "with WLibrary " << "which does not implement the LibraryView interface. Ignoring."; return; } lock.unlock(); - view->onSearch(name); + pView->onSearch(name); } LibraryView* WLibrary::getActiveView() const { @@ -98,17 +107,17 @@ bool WLibrary::isTrackInCurrentView(const TrackId& trackId) { VERIFY_OR_DEBUG_ASSERT(trackId.isValid()) { return false; } - QWidget* current = currentWidget(); - WTrackTableView* tracksView = qobject_cast(current); - if (!tracksView) { + QWidget* pCurrent = currentWidget(); + WTrackTableView* pTracksView = qobject_cast(pCurrent); + if (!pTracksView) { // This view is no tracks view, but maybe a special tracks view with a // controls row (AutoDJ, Recording)? //qDebug() << " view is no tracks view. look for tracks view child"; - tracksView = current->findChild(); + pTracksView = pCurrent->findChild(); } - if (tracksView) { + if (pTracksView) { //qDebug() << " tracks view found"; - return tracksView->isTrackInCurrentView(trackId); + return pTracksView->isTrackInCurrentView(trackId); } else { // No tracks view, this is probably a root view WLibraryTextBrowser //qDebug() << " no tracks view found"; @@ -122,15 +131,15 @@ void WLibrary::slotSelectTrackInActiveTrackView(const TrackId& trackId) { return; } - QWidget* current = currentWidget(); - WTrackTableView* tracksView = qobject_cast(current); - if (!tracksView) { + QWidget* pCurrent = currentWidget(); + WTrackTableView* pTracksView = qobject_cast(pCurrent); + if (!pTracksView) { //qDebug() << " view is no tracks view. look for tracks view child"; - tracksView = current->findChild(); + pTracksView = pCurrent->findChild(); } - if (tracksView) { + if (pTracksView) { //qDebug() << " tracks view found"; - tracksView->slotSelectTrack(trackId); + pTracksView->slotSelectTrack(trackId); } else { //qDebug() << " no tracks view found"; } @@ -142,3 +151,10 @@ bool WLibrary::event(QEvent* pEvent) { } return QStackedWidget::event(pEvent); } + +void WLibrary::keyPressEvent(QKeyEvent* pEvent) { + if (pEvent->key() == Qt::Key_Left && pEvent->modifiers() & Qt::ControlModifier) { + emit setLibraryFocus(FocusWidget::Sidebar); + } + QStackedWidget::keyPressEvent(pEvent); +} diff --git a/src/widget/wlibrary.h b/src/widget/wlibrary.h index 531915975a6..0b99e6d90a7 100644 --- a/src/widget/wlibrary.h +++ b/src/widget/wlibrary.h @@ -4,6 +4,9 @@ #include #include +#include "library/library_decl.h" +#include "library/libraryview.h" +#include "skin/legacy/skincontext.h" #include "util/compatibility/qmutex.h" #include "widget/wbasewidget.h" @@ -48,17 +51,22 @@ class WLibrary : public QStackedWidget, public WBaseWidget { return m_bShowButtonText; } + signals: + FocusWidget setLibraryFocus(FocusWidget newFocus); + public slots: // Show the view registered with the given name. Does nothing if the current // view is the specified view, or if the name does not specify any // registered view. void switchToView(const QString& name); void slotSelectTrackInActiveTrackView(const TrackId& trackId); + void pasteFromSidebar(); void search(const QString&); protected: bool event(QEvent* pEvent) override; + void keyPressEvent(QKeyEvent* event) override; private: QT_RECURSIVE_MUTEX m_mutex; diff --git a/src/widget/wlibrarysidebar.cpp b/src/widget/wlibrarysidebar.cpp index c13ac5c7b48..663006112c6 100644 --- a/src/widget/wlibrarysidebar.cpp +++ b/src/widget/wlibrarysidebar.cpp @@ -239,12 +239,24 @@ void WLibrarySidebar::keyPressEvent(QKeyEvent* event) { // TODO(XXX) Should first keyEvent ensure previous item has focus? I.e. if the selected // item is not focused, require second press to perform the desired action. - // make the selected item the navigation starting point + SidebarModel* sidebarModel = qobject_cast(model()); + QModelIndexList selectedIndices = selectionModel()->selectedRows(); + if (sidebarModel && !selectedIndices.isEmpty()) { + QModelIndex index = selectedIndices.at(0); + if (event->matches(QKeySequence::Delete) || event->key() == Qt::Key_Backspace) { + sidebarModel->clear(index); + return; + } + if (event->matches(QKeySequence::Paste)) { + sidebarModel->paste(index); + return; + } + } + focusSelectedIndex(); switch (event->key()) { case Qt::Key_Return: - focusSelectedIndex(); toggleSelectedItem(); return; case Qt::Key_Down: @@ -268,6 +280,14 @@ void WLibrarySidebar::keyPressEvent(QKeyEvent* event) { emit pressed(selIndex); return; } + case Qt::Key_Right: { + if (event->modifiers() & Qt::ControlModifier) { + emit setLibraryFocus(FocusWidget::TracksTable); + } else { + QTreeView::keyPressEvent(event); + } + return; + } case Qt::Key_Left: { // If an expanded item is selected let QTreeView collapse it QModelIndex selIndex = selectedIndex(); diff --git a/src/widget/wlibrarytableview.cpp b/src/widget/wlibrarytableview.cpp index d4885207c3b..afdc6a42ce8 100644 --- a/src/widget/wlibrarytableview.cpp +++ b/src/widget/wlibrarytableview.cpp @@ -386,6 +386,14 @@ QModelIndex WLibraryTableView::moveCursor(CursorAction cursorAction, return pModel->index(pModel->rowCount() - 1, column); } } break; + case QAbstractItemView::MoveLeft: + case QAbstractItemView::MoveRight: + if (modifiers & Qt::ControlModifier) { + // Ignore, so it can be handled by WLibrary::keyEvent + // to navigate to the sidebar + return currentIndex(); + } + break; default: break; } diff --git a/src/widget/wlibrarytableview.h b/src/widget/wlibrarytableview.h index 7e0e8351dd3..04759b2a1f4 100644 --- a/src/widget/wlibrarytableview.h +++ b/src/widget/wlibrarytableview.h @@ -4,6 +4,7 @@ #include #include +#include "library/library_decl.h" #include "library/libraryview.h" #include "preferences/usersettings.h" #include "track/track_decl.h" @@ -61,6 +62,7 @@ class WLibraryTableView : public QTableView, public virtual LibraryView { void trackSelected(TrackPointer pTrack); void onlyCachedCoverArt(bool); void scrollValueChanged(int); + FocusWidget setLibraryFocus(FocusWidget newFocus); public slots: void setTrackTableFont(const QFont& font); diff --git a/src/widget/wlibrarytextbrowser.cpp b/src/widget/wlibrarytextbrowser.cpp index 82d35ed6e5b..ebc6484079e 100644 --- a/src/widget/wlibrarytextbrowser.cpp +++ b/src/widget/wlibrarytextbrowser.cpp @@ -1,5 +1,7 @@ #include "widget/wlibrarytextbrowser.h" +#include + #include "moc_wlibrarytextbrowser.cpp" WLibraryTextBrowser::WLibraryTextBrowser(QWidget* parent) @@ -13,3 +15,11 @@ bool WLibraryTextBrowser::hasFocus() const { void WLibraryTextBrowser::setFocus() { QWidget::setFocus(); } + +void WLibraryTextBrowser::keyPressEvent(QKeyEvent* event) { + if (event->key() == Qt::Key_Left && event->modifiers() & Qt::ControlModifier) { + event->ignore(); + return; + } + QTextBrowser::keyPressEvent(event); +} diff --git a/src/widget/wlibrarytextbrowser.h b/src/widget/wlibrarytextbrowser.h index 206664f0693..264ad90f2e6 100644 --- a/src/widget/wlibrarytextbrowser.h +++ b/src/widget/wlibrarytextbrowser.h @@ -2,6 +2,7 @@ #include +#include "library/library_decl.h" #include "library/libraryview.h" class WLibraryTextBrowser : public QTextBrowser, public LibraryView { @@ -11,4 +12,7 @@ class WLibraryTextBrowser : public QTextBrowser, public LibraryView { void onShow() override {} bool hasFocus() const override; void setFocus() override; + void keyPressEvent(QKeyEvent* event) override; + signals: + FocusWidget setLibraryFocus(FocusWidget newFocus); }; diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index fff5254d98f..49d4a601d01 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -115,7 +115,7 @@ void WTrackTableView::slotGuiTick50ms(double /*unused*/) { // slows down scrolling performance so we wait until the user has // stopped interacting first. if (m_selectionChangedSinceLastGuiTick) { - const QModelIndexList indices = selectionModel()->selectedRows(); + const QModelIndexList indices = getSelectedRows(); if (indices.size() == 1 && indices.first().isValid()) { // A single track has been selected TrackModel* trackModel = getTrackModel(); @@ -139,6 +139,11 @@ void WTrackTableView::slotGuiTick50ms(double /*unused*/) { } } +// slot +void WTrackTableView::pasteFromSidebar() { + pasteTracks(QModelIndex()); +} + // slot void WTrackTableView::loadTrackModel(QAbstractItemModel* model, bool restoreState) { qDebug() << "WTrackTableView::loadTrackModel()" << model; @@ -400,7 +405,7 @@ TrackModel::SortColumnId WTrackTableView::getColumnIdFromCurrentIndex() { } void WTrackTableView::assignPreviousTrackColor() { - QModelIndexList indices = selectionModel()->selectedRows(); + QModelIndexList indices = getSelectedRows(); if (indices.isEmpty()) { return; } @@ -421,7 +426,7 @@ void WTrackTableView::assignPreviousTrackColor() { } void WTrackTableView::assignNextTrackColor() { - QModelIndexList indices = selectionModel()->selectedRows(); + QModelIndexList indices = getSelectedRows(); if (indices.isEmpty()) { return; } @@ -442,7 +447,7 @@ void WTrackTableView::assignNextTrackColor() { } void WTrackTableView::slotPurge() { - QModelIndexList indices = selectionModel()->selectedRows(); + QModelIndexList indices = getSelectedRows(); if (indices.isEmpty()) { return; } @@ -456,7 +461,7 @@ void WTrackTableView::slotPurge() { } void WTrackTableView::slotDeleteTracksFromDisk() { - QModelIndexList indices = selectionModel()->selectedRows(); + QModelIndexList indices = getSelectedRows(); if (indices.isEmpty()) { return; } @@ -467,7 +472,7 @@ void WTrackTableView::slotDeleteTracksFromDisk() { } void WTrackTableView::slotUnhide() { - QModelIndexList indices = selectionModel()->selectedRows(); + QModelIndexList indices = getSelectedRows(); if (indices.isEmpty()) { return; } @@ -504,7 +509,7 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) { } event->accept(); // Update track indices in context menu - QModelIndexList indices = selectionModel()->selectedRows(); + QModelIndexList indices = getSelectedRows(); m_pTrackMenu->loadTrackModelIndices(indices); saveCurrentIndex(); @@ -568,7 +573,7 @@ void WTrackTableView::mouseMoveEvent(QMouseEvent* pEvent) { //qDebug() << "MouseMoveEvent"; // Iterate over selected rows and append each item's location url to a list. QList locations; - const QModelIndexList indices = selectionModel()->selectedRows(); + const QModelIndexList indices = getSelectedRows(); for (const QModelIndex& index : indices) { if (!index.isValid()) { @@ -675,7 +680,7 @@ void WTrackTableView::dropEvent(QDropEvent * event) { // Save a list of row (just plain ints) so we don't get screwed over // when the QModelIndexes all become invalid (eg. after moveTrack() // or addTrack()) - const QModelIndexList indices = selectionModel()->selectedRows(); + const QModelIndexList indices = getSelectedRows(); QList selectedRows; for (const QModelIndex& idx : indices) { @@ -814,11 +819,87 @@ void WTrackTableView::dropEvent(QDropEvent * event) { verticalScrollBar()->setValue(vScrollBarPos); } +QModelIndexList WTrackTableView::getSelectedRows() const { + QItemSelectionModel* pSelectionModel = selectionModel(); + VERIFY_OR_DEBUG_ASSERT(pSelectionModel != nullptr) { + qWarning() << "No selection model available"; + return {}; + } + return pSelectionModel->selectedRows(); +} + TrackModel* WTrackTableView::getTrackModel() const { TrackModel* trackModel = dynamic_cast(model()); return trackModel; } +namespace { +QModelIndex calculateCutIndex(const QModelIndex& currentIndex, + const QModelIndexList& removedIndices) { + if (removedIndices.empty()) { + return QModelIndex(); + } + const int row = currentIndex.row(); + int rowAfterRemove = row; + for (const auto& removeIndex : removedIndices) { + if (removeIndex.row() < row) { + rowAfterRemove--; + } + } + return currentIndex.siblingAtRow(rowAfterRemove); +} +} // namespace + +void WTrackTableView::removeSelectedTracks() { + const QModelIndexList indices = getSelectedRows(); + const QModelIndex newIndex = calculateCutIndex(currentIndex(), indices); + getTrackModel()->removeTracks(indices); + setCurrentIndex(newIndex); +} + +void WTrackTableView::cutSelectedTracks() { + const QModelIndexList indices = getSelectedRows(); + const QModelIndex newIndex = calculateCutIndex(currentIndex(), indices); + getTrackModel()->cutTracks(indices); + setCurrentIndex(newIndex); +} + +void WTrackTableView::copySelectedTracks() { + const QModelIndexList indices = getSelectedRows(); + getTrackModel()->copyTracks(indices); +} + +void WTrackTableView::pasteTracks(const QModelIndex& index) { + TrackModel* trackModel = getTrackModel(); + if (!trackModel) { + return; + } + + const QList rows = trackModel->pasteTracks(index); + if (rows.empty()) { + return; + } + + updateGeometries(); + + const auto lastVisibleRow = rowAt(height()); + + // Use selectRow to scroll to the first or last pasted row. We would use + // scrollTo but this is broken. This solution was already used elsewhere + // in this way. + if (rows.back() > lastVisibleRow) { + selectRow(rows.back()); + } else { + selectRow(rows.front()); + } + + // Select all the rows that we pasted + for (const auto row : rows) { + selectionModel()->select(model()->index(row, 0), + QItemSelectionModel::Select | QItemSelectionModel::Rows); + } +} + void WTrackTableView::keyPressEvent(QKeyEvent* event) { switch (event->key()) { case kPropertiesShortcutKey: { @@ -833,22 +914,47 @@ void WTrackTableView::keyPressEvent(QKeyEvent* event) { if (event->modifiers().testFlag(Qt::NoModifier)) { slotMouseDoubleClicked(currentIndex()); } else if ((event->modifiers() & kPropertiesShortcutModifier)) { - QModelIndexList indices = selectionModel()->selectedRows(); + QModelIndexList indices = getSelectedRows(); if (indices.length() == 1) { m_pTrackMenu->loadTrackModelIndices(indices); m_pTrackMenu->slotShowDlgTrackInfo(); } } - } break; + return; + } case kHideRemoveShortcutKey: { if (event->modifiers() == kHideRemoveShortcutModifier) { hideOrRemoveSelectedTracks(); - return; } - } break; + return; + } default: - QTableView::keyPressEvent(event); + break; + } + TrackModel* trackModel = getTrackModel(); + if (trackModel && !trackModel->isLocked()) { + if (event->matches(QKeySequence::Delete) || event->key() == Qt::Key_Backspace) { + removeSelectedTracks(); + return; + } + if (event->matches(QKeySequence::Cut)) { + cutSelectedTracks(); + return; + } + if (event->matches(QKeySequence::Copy)) { + copySelectedTracks(); + return; + } + if (event->matches(QKeySequence::Paste)) { + pasteTracks(currentIndex()); + return; + } + if (event->key() == Qt::Key_Escape) { + clearSelection(); + setCurrentIndex(QModelIndex()); + } } + QTableView::keyPressEvent(event); } void WTrackTableView::resizeEvent(QResizeEvent* event) { @@ -893,7 +999,7 @@ void WTrackTableView::resizeEvent(QResizeEvent* event) { } void WTrackTableView::hideOrRemoveSelectedTracks() { - QModelIndexList indices = selectionModel()->selectedRows(); + QModelIndexList indices = getSelectedRows(); if (indices.isEmpty()) { return; } @@ -975,7 +1081,7 @@ void WTrackTableView::hideOrRemoveSelectedTracks() { } void WTrackTableView::activateSelectedTrack() { - auto indices = selectionModel()->selectedRows(); + auto indices = getSelectedRows(); if (indices.isEmpty()) { return; } @@ -983,7 +1089,7 @@ void WTrackTableView::activateSelectedTrack() { } void WTrackTableView::loadSelectedTrackToGroup(const QString& group, bool play) { - auto indices = selectionModel()->selectedRows(); + auto indices = getSelectedRows(); if (indices.isEmpty()) { return; } @@ -1025,19 +1131,13 @@ void WTrackTableView::loadSelectedTrackToGroup(const QString& group, bool play) QList WTrackTableView::getSelectedTrackIds() const { QList trackIds; - QItemSelectionModel* pSelectionModel = selectionModel(); - VERIFY_OR_DEBUG_ASSERT(pSelectionModel != nullptr) { - qWarning() << "No selected tracks available"; - return trackIds; - } - TrackModel* pTrackModel = getTrackModel(); VERIFY_OR_DEBUG_ASSERT(pTrackModel != nullptr) { qWarning() << "No selected tracks available"; return trackIds; } - const QModelIndexList rows = selectionModel()->selectedRows(); + const QModelIndexList rows = getSelectedRows(); trackIds.reserve(rows.size()); for (const QModelIndex& row: rows) { const TrackId trackId = pTrackModel->getTrackId(row); @@ -1212,12 +1312,23 @@ void WTrackTableView::doSortByColumn(int headerSection, Qt::SortOrder sortOrder) sortByColumn(headerSection, sortOrder); + selectTracksById(selectedTrackIds, prevColum); + + // This seems to be broken since at least Qt 5.12: no scrolling is issued + // scrollTo(first, QAbstractItemView::EnsureVisible); + horizontalScrollBar()->setValue(savedHScrollBarPos); +} + +void WTrackTableView::selectTracksById(const QList& trackIds, int prevColum) { + TrackModel* trackModel = getTrackModel(); + QAbstractItemModel* itemModel = model(); + QItemSelectionModel* currentSelection = selectionModel(); currentSelection->reset(); // remove current selection // Find previously selected tracks and store respective rows for reselection. QMap selectedRows; - for (const auto& trackId : selectedTrackIds) { + for (const auto& trackId : trackIds) { // TODO(rryan) slowly fixing the issues with BaseSqlTableModel. This // code is broken for playlists because it assumes each trackid is in // the table once. This will erroneously select all instances of the @@ -1256,10 +1367,6 @@ void WTrackTableView::doSortByColumn(int headerSection, Qt::SortOrder sortOrder) QModelIndex tl = itemModel->index(i.key(), 0); currentSelection->select(tl, QItemSelectionModel::Rows | QItemSelectionModel::Select); } - - // This seems to be broken since at least Qt 5.12: no scrolling is issued - //scrollTo(first, QAbstractItemView::EnsureVisible); - horizontalScrollBar()->setValue(savedHScrollBarPos); } void WTrackTableView::applySortingIfVisible() { @@ -1330,7 +1437,7 @@ bool WTrackTableView::hasFocus() const { } void WTrackTableView::setFocus() { - QWidget::setFocus(); + QWidget::setFocus(Qt::OtherFocusReason); } QString WTrackTableView::getModelStateKey() const { diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h index a786f0d43c3..ea87853d3f7 100644 --- a/src/widget/wtracktableview.h +++ b/src/widget/wtracktableview.h @@ -34,6 +34,7 @@ class WTrackTableView : public WLibraryTableView { void onShow() override; bool hasFocus() const override; void setFocus() override; + void pasteFromSidebar() override; void keyPressEvent(QKeyEvent* event) override; void resizeEvent(QResizeEvent* event) override; void activateSelectedTrack() override; @@ -47,6 +48,12 @@ class WTrackTableView : public WLibraryTableView { TrackId getCurrentTrackId() const; bool setCurrentTrackId(const TrackId& trackId, int column = 0, bool scrollToTrack = false); + void removeSelectedTracks(); + void cutSelectedTracks(); + void copySelectedTracks(); + void pasteTracks(const QModelIndex& index); + void selectTracksById(const QList& tracks, int prevColumn); + double getBackgroundColorOpacity() const { return m_backgroundColorOpacity; } @@ -126,6 +133,9 @@ class WTrackTableView : public WLibraryTableView { // when dragging. void mouseMoveEvent(QMouseEvent *pEvent) override; + // Returns the list of selected rows, or an empty list if none are selected. + QModelIndexList getSelectedRows() const; + // Returns the current TrackModel, or returns NULL if none is set. TrackModel* getTrackModel() const;