diff --git a/res/skins/Deere/style.qss b/res/skins/Deere/style.qss index bfe72df5aff..f2d06b87589 100644 --- a/res/skins/Deere/style.qss +++ b/res/skins/Deere/style.qss @@ -229,6 +229,14 @@ WLibrarySidebar::item:selected, color: #D6D6D6; background-color: #006596; } +WLibrarySidebar::item:selected:focus { + outline: none; + border: 0px; +} +WLibrarySidebar::item:!selected:focus { + outline: none; + border: 1px solid white; +} /* Use the native focus decoration */ /* This is for all cells including Played and Location */ WTrackTableView, @@ -412,8 +420,7 @@ WSearchLineEdit { qproperty-layoutSpacing: 0; } -WLibrarySidebar, -WLibrarySidebar::item:focus { +WLibrarySidebar { outline: none; /* Spacing between treeview and preview deck/search bar */ margin: 0px; diff --git a/res/skins/LateNight/style_classic.qss b/res/skins/LateNight/style_classic.qss index 8d444a44cbe..0d7354b0bac 100644 --- a/res/skins/LateNight/style_classic.qss +++ b/res/skins/LateNight/style_classic.qss @@ -2071,6 +2071,14 @@ WTrackTableView::item:selected, color: #fff; background-color: #5e4507; } +WLibrarySidebar::item:selected:focus { + outline: none; + border: 0px; +} +WLibrarySidebar::item:!selected:focus { + outline: none; + border: 1px solid white; +} /* Use the native focus decoration */ /* This is for all cells including Played and Location */ @@ -2088,9 +2096,6 @@ WTrackTableView { WLibrarySidebar { show-decoration-selected: 0; -} -WLibrarySidebar, -WLibrarySidebar::item:focus { outline: none; } diff --git a/res/skins/LateNight/style_palemoon.qss b/res/skins/LateNight/style_palemoon.qss index 99d07136412..ef6449ddf8a 100644 --- a/res/skins/LateNight/style_palemoon.qss +++ b/res/skins/LateNight/style_palemoon.qss @@ -2544,6 +2544,9 @@ WLibrarySidebar { /* background of Color delegate in selected row */ selection-background-color: #2c454f; } +WLibrarySidebar { + outline: none; +} /* Selected rows in Tree and Tracks table */ WLibrarySidebar::item:selected, WTrackTableView::item:selected, @@ -2551,9 +2554,13 @@ WTrackTableView::item:selected, color: #fff; background-color: #2c454f; } -WLibrarySidebar, -WLibrarySidebar::item:focus { +WLibrarySidebar::item:selected:focus { + outline: none; + border: 0px; +} +WLibrarySidebar::item:!selected:focus { outline: none; + border: 1px solid white; } WTrackTableView:focus, diff --git a/res/skins/Shade/style.qss b/res/skins/Shade/style.qss index ae60e8a8c9f..0f6f4c3c636 100644 --- a/res/skins/Shade/style.qss +++ b/res/skins/Shade/style.qss @@ -470,9 +470,16 @@ WLibrarySidebar::branch:selected, WSearchLineEdit::indicator { width: 0; } -WLibrarySidebar, -WLibrarySidebar::item:focus { +WLibrarySidebar { + outline: none; +} +WLibrarySidebar::item:selected:focus { + outline: none; + border: 0px; +} +WLibrarySidebar::item:!selected:focus { outline: none; + border: 1px solid white; } /* Use the native focus decoration */ diff --git a/res/skins/Tango/style.qss b/res/skins/Tango/style.qss index 496fb41eb09..2570e895eb5 100644 --- a/res/skins/Tango/style.qss +++ b/res/skins/Tango/style.qss @@ -2650,6 +2650,14 @@ WLibrarySidebar::item:!selected { background-color: #0f0f0f; color: #999; } +WLibrarySidebar::item:selected:focus { + outline: none; + border: 0px; +} +WLibrarySidebar::item:!selected:focus { + outline: none; + border: 1px solid white; +} WTrackTableViewHeader { /* Don't set a font size to pick up the system font size. */ diff --git a/src/library/banshee/bansheeplaylistmodel.cpp b/src/library/banshee/bansheeplaylistmodel.cpp index 8e9b802d4b3..0115f1e63f1 100644 --- a/src/library/banshee/bansheeplaylistmodel.cpp +++ b/src/library/banshee/bansheeplaylistmodel.cpp @@ -66,7 +66,7 @@ const QString kCrate = QStringLiteral(CLM_CRATE); BansheePlaylistModel::BansheePlaylistModel(QObject* pParent, TrackCollectionManager* pTrackCollectionManager, BansheeDbConnection* pConnection) : BaseSqlTableModel(pParent, pTrackCollectionManager, "mixxx.db.model.banshee_playlist"), m_pConnection(pConnection), - m_playlistId(-1) { + m_playlistId(kInvalidPlaylistId) { m_tempTableName = BANSHEE_TABLE + QString::number(sTableNumber.fetchAndAddAcquire(1)); } @@ -77,7 +77,7 @@ BansheePlaylistModel::~BansheePlaylistModel() { void BansheePlaylistModel::dropTempTable() { if (m_playlistId >= 0) { // Clear old playlist - m_playlistId = -1; + m_playlistId = kInvalidPlaylistId; QSqlQuery query(m_database); QString strQuery("DROP TABLE IF EXISTS %1"); if (!query.exec(strQuery.arg(m_tempTableName))) { diff --git a/src/library/baseexternallibraryfeature.cpp b/src/library/baseexternallibraryfeature.cpp index 53d34909c67..7fe4c8751eb 100644 --- a/src/library/baseexternallibraryfeature.cpp +++ b/src/library/baseexternallibraryfeature.cpp @@ -114,7 +114,7 @@ void BaseExternalLibraryFeature::slotImportAsMixxxPlaylist() { int playlistId = playlistDao.createUniquePlaylist(&playlist); - if (playlistId != -1) { + if (playlistId != kInvalidPlaylistId) { playlistDao.appendTracksToPlaylist(trackIds, playlistId); } else { // Do not change strings here without also changing strings in diff --git a/src/library/baseexternalplaylistmodel.cpp b/src/library/baseexternalplaylistmodel.cpp index d2c0bfb7b98..c858b65b1c6 100644 --- a/src/library/baseexternalplaylistmodel.cpp +++ b/src/library/baseexternalplaylistmodel.cpp @@ -24,7 +24,7 @@ BaseExternalPlaylistModel::BaseExternalPlaylistModel(QObject* parent, m_playlistsTable(playlistsTable), m_playlistTracksTable(playlistTracksTable), m_trackSource(trackSource), - m_currentPlaylistId(-1) { + m_currentPlaylistId(kInvalidPlaylistId) { } BaseExternalPlaylistModel::~BaseExternalPlaylistModel() { @@ -100,20 +100,20 @@ void BaseExternalPlaylistModel::setPlaylist(const QString& playlist_path) { } // TODO(XXX): Why not last-insert id? - int playlistId = -1; + int playlistId = kInvalidPlaylistId; QSqlRecord finder_query_record = finder_query.record(); while (finder_query.next()) { playlistId = finder_query.value(finder_query_record.indexOf("id")).toInt(); } - if (playlistId == -1) { + if (playlistId == kInvalidPlaylistId) { qWarning() << "ERROR: Could not get the playlist ID for playlist:" << playlist_path; return; } // Store search text QString currSearch = currentSearch(); - if (m_currentPlaylistId != -1) { + if (m_currentPlaylistId != kInvalidPlaylistId) { if (!currSearch.trimmed().isEmpty()) { m_searchTexts.insert(m_currentPlaylistId, currSearch); } else { diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index c5bc96d4e96..d9adb4aab42 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -62,7 +62,7 @@ int PlaylistDAO::createPlaylist(const QString& name, const HiddenType hidden) { if (!query.exec()) { LOG_FAILED_QUERY(query); - return -1; + return kInvalidPlaylistId; } //Get the id of the last playlist. @@ -83,7 +83,7 @@ int PlaylistDAO::createPlaylist(const QString& name, const HiddenType hidden) { if (!query.exec()) { LOG_FAILED_QUERY(query); - return -1; + return kInvalidPlaylistId; } int playlistId = query.lastInsertId().toInt(); @@ -97,10 +97,10 @@ int PlaylistDAO::createUniquePlaylist(QString* pName, const HiddenType hidden) { int playlistId = getPlaylistIdFromName(*pName); int i = 1; - if (playlistId != -1) { + if (playlistId != kInvalidPlaylistId) { // Calculate a unique name *pName += "(%1)"; - while (playlistId != -1) { + while (playlistId != kInvalidPlaylistId) { i++; playlistId = getPlaylistIdFromName(pName->arg(i)); } @@ -165,7 +165,7 @@ int PlaylistDAO::getPlaylistIdFromName(const QString& name) const { } else { LOG_FAILED_QUERY(query); } - return -1; + return kInvalidPlaylistId; } void PlaylistDAO::deletePlaylist(const int playlistId) { @@ -227,10 +227,60 @@ void PlaylistDAO::deletePlaylist(const int playlistId) { } } -int PlaylistDAO::deleteAllUnlockedPlaylistsWithFewerTracks( +bool PlaylistDAO::deletePlaylists(const QStringList& idStringList) { + if (idStringList.isEmpty()) { + return false; + } + const QString idString = idStringList.join(","); + + qInfo() << "Deleting" << idStringList.size() << "playlists"; + + // delete tracks assigned to these playlists + auto deleteTracks = FwdSqlQuery(m_database, + QString("DELETE FROM PlaylistTracks WHERE playlist_id IN (%1)") + .arg(idString)); + if (!deleteTracks.execPrepared()) { + return false; + } + + // delete the playlists + auto deletePlaylists = FwdSqlQuery(m_database, + QString("DELETE FROM Playlists WHERE id IN (%1)").arg(idString)); + if (!deletePlaylists.execPrepared()) { + return false; + } + + emit deleted(kInvalidPlaylistId); + return true; +} + +bool PlaylistDAO::deleteUnlockedPlaylists(QStringList&& idStringList) { + if (idStringList.isEmpty()) { + return false; + } + + idStringList.removeDuplicates(); + const QString idString = idStringList.join(","); + QSqlQuery query(m_database); + // select locked playlists + query.prepare(QStringLiteral( + "SELECT id FROM Playlists WHERE id IN (%1) AND locked = 1") + .arg(idString)); + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; + } + // remove locked playlists from list + while (query.next()) { + idStringList.removeOne(query.value(0).toString()); + } + return deletePlaylists(idStringList); +} + +bool PlaylistDAO::deleteAllUnlockedPlaylistsWithFewerTracks( PlaylistDAO::HiddenType type, int minNumberOfTracks) { VERIFY_OR_DEBUG_ASSERT(minNumberOfTracks > 0) { - return 0; // nothing to do, probably unintended invocation + return false; // nothing to do, probably unintended invocation } QSqlQuery query(m_database); @@ -243,35 +293,17 @@ int PlaylistDAO::deleteAllUnlockedPlaylistsWithFewerTracks( query.bindValue(":length", minNumberOfTracks); if (!query.exec()) { LOG_FAILED_QUERY(query); - return -1; + return false; } QStringList idStringList; while (query.next()) { idStringList.append(query.value(0).toString()); } - if (idStringList.isEmpty()) { - return 0; - } - QString idString = idStringList.join(","); - - qInfo() << "Deleting" << idStringList.size() << "playlists of type" << type + qInfo() << "Prepared deletion of" << idStringList.size() << "playlists of type" << type << "that contain fewer than" << minNumberOfTracks << "tracks"; - auto deleteTracks = FwdSqlQuery(m_database, - QString("DELETE FROM PlaylistTracks WHERE playlist_id IN (%1)") - .arg(idString)); - if (!deleteTracks.execPrepared()) { - return -1; - } - - auto deletePlaylists = FwdSqlQuery(m_database, - QString("DELETE FROM Playlists WHERE id IN (%1)").arg(idString)); - if (!deletePlaylists.execPrepared()) { - return -1; - } - - return idStringList.length(); + return deletePlaylists(idStringList); } void PlaylistDAO::renamePlaylist(const int playlistId, const QString& newName) { @@ -300,10 +332,44 @@ bool PlaylistDAO::setPlaylistLocked(const int playlistId, const bool locked) { LOG_FAILED_QUERY(query); return false; } - emit lockChanged(playlistId); + emit lockChanged(QSet{playlistId}); return true; } +int PlaylistDAO::setPlaylistsLocked(const QSet& playlistIds, const bool lock) { + if (playlistIds.isEmpty()) { + return 0; + } + if (lock) { + qInfo() << "Locking" << playlistIds.size() << "playlists"; + } else { + qInfo() << "Unlocking" << playlistIds.size() << "playlists"; + } + + QString idsString; + for (int id : playlistIds) { + idsString += QString::number(id) + ','; + } + // strip the last comma + idsString.chop(1); + + QSqlQuery query(m_database); + query.prepare(QStringLiteral( + "UPDATE Playlists SET locked = :lock WHERE id IN (%1)") + .arg(idsString)); + // SQLite3 doesn't support boolean value. Using integer instead. + int iLock = lock ? 1 : 0; + query.bindValue(":lock", iLock); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return -1; + } + + emit lockChanged(playlistIds); + return playlistIds.size(); +} + bool PlaylistDAO::isPlaylistLocked(const int playlistId) const { QSqlQuery query(m_database); query.prepare(QStringLiteral( @@ -441,7 +507,7 @@ int PlaylistDAO::getPlaylistId(const int index) const { if (!query.exec()) { LOG_FAILED_QUERY(query); - return -1; + return kInvalidPlaylistId; } int currentRow = 0; @@ -451,7 +517,7 @@ int PlaylistDAO::getPlaylistId(const int index) const { return id; } } - return -1; + return kInvalidPlaylistId; } PlaylistDAO::HiddenType PlaylistDAO::getHiddenType(const int playlistId) const { @@ -754,8 +820,6 @@ void PlaylistDAO::addPlaylistToAutoDJQueue(const int playlistId, AutoDJSendLoc l } int PlaylistDAO::getPreviousPlaylist(const int currentPlaylistId, HiddenType hidden) const { - // Find out the highest position existing in the playlist so we know what - // position this track should have. QSqlQuery query(m_database); query.prepare(QStringLiteral( "SELECT max(id) as id FROM Playlists " @@ -765,15 +829,32 @@ int PlaylistDAO::getPreviousPlaylist(const int currentPlaylistId, HiddenType hid if (!query.exec()) { LOG_FAILED_QUERY(query); - return -1; + return kInvalidPlaylistId; + } + + if (query.next()) { + return query.value(query.record().indexOf("id")).toInt(); + } + return kInvalidPlaylistId; +} + +int PlaylistDAO::getNextPlaylist(const int currentPlaylistId, HiddenType hidden) const { + QSqlQuery query(m_database); + query.prepare(QStringLiteral( + "SELECT max(id) as id FROM Playlists " + "WHERE id > :id AND hidden = :hidden")); + query.bindValue(":id", currentPlaylistId); + query.bindValue(":hidden", hidden); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return kInvalidPlaylistId; } - // Get the id of the highest playlist - int previousPlaylistId = -1; if (query.next()) { - previousPlaylistId = query.value(query.record().indexOf("id")).toInt(); + return query.value(query.record().indexOf("id")).toInt(); } - return previousPlaylistId; + return kInvalidPlaylistId; } bool PlaylistDAO::copyPlaylistTracks(const int sourcePlaylistID, const int targetPlaylistID) { @@ -1196,7 +1277,7 @@ void PlaylistDAO::setAutoDJProcessor(AutoDJProcessor* pAutoDJProcessor) { void PlaylistDAO::addTracksToAutoDJQueue(const QList& trackIds, AutoDJSendLoc loc) { int iAutoDJPlaylistId = getPlaylistIdFromName(AUTODJ_TABLE); - if (iAutoDJPlaylistId == -1) { + if (iAutoDJPlaylistId == kInvalidPlaylistId) { return; } diff --git a/src/library/dao/playlistdao.h b/src/library/dao/playlistdao.h index 16903dd76c8..b32048a79ea 100644 --- a/src/library/dao/playlistdao.h +++ b/src/library/dao/playlistdao.h @@ -28,6 +28,8 @@ const QString PLAYLISTTRACKSTABLE_DATETIMEADDED = QStringLiteral("pl_datetime_ad class AutoDJProcessor; +constexpr int kInvalidPlaylistId = -1; + class PlaylistDAO : public QObject, public virtual DAO { Q_OBJECT public: @@ -55,15 +57,19 @@ class PlaylistDAO : public QObject, public virtual DAO { int createUniquePlaylist(QString* pName, const HiddenType type = PLHT_NOT_HIDDEN); // Delete a playlist void deletePlaylist(const int playlistId); - /// Delete Playlists with fewer entries then "length" + // Delete a set of playlists. + bool deletePlaylists(const QStringList& idStringList); + bool deleteUnlockedPlaylists(QStringList&& idStringList); + /// Delete Playlists with fewer entries then "minNumberOfTracks" /// Needs to be called inside a transaction. - /// @return number of deleted playlists, -1 on error - int deleteAllUnlockedPlaylistsWithFewerTracks(PlaylistDAO::HiddenType type, + /// @return true on success, flase on error + bool deleteAllUnlockedPlaylistsWithFewerTracks(const PlaylistDAO::HiddenType type, int minNumberOfTracks); // Rename a playlist void renamePlaylist(const int playlistId, const QString& newName); // Lock or unlock a playlist bool setPlaylistLocked(const int playlistId, const bool locked); + int setPlaylistsLocked(const QSet& playlistIds, const bool lock); // Find out the state of a playlist lock bool isPlaylistLocked(const int playlistId) const; // Append a list of tracks to a playlist @@ -108,6 +114,9 @@ class PlaylistDAO : public QObject, public virtual DAO { // Get the preceding playlist of currentPlaylistId with the HiddenType // hidden. Returns -1 if no such playlist exists. int getPreviousPlaylist(const int currentPlaylistId, HiddenType hidden) const; + // Get the following playlist of currentPlaylistId with the HiddenType + // hidden. Returns -1 if no such playlist exists. + int getNextPlaylist(const int currentPlaylistId, HiddenType hidden) const; // Append all the tracks in the source playlist to the target playlist. bool copyPlaylistTracks(const int sourcePlaylistID, const int targetPlaylistID); // Returns the number of tracks in the given playlist. @@ -127,7 +136,7 @@ class PlaylistDAO : public QObject, public virtual DAO { void added(int playlistId); void deleted(int playlistId); void renamed(int playlistId, const QString& newName); - void lockChanged(int playlistId); + void lockChanged(const QSet& playlistIds); void trackAdded(int playlistId, TrackId trackId, int position); void trackRemoved(int playlistId, TrackId trackId, int position); void tracksChanged(const QSet& playlistIds); // added/removed/reordered diff --git a/src/library/itunes/itunesfeature.h b/src/library/itunes/itunesfeature.h index 39598f3789a..0417e67bf35 100644 --- a/src/library/itunes/itunesfeature.h +++ b/src/library/itunes/itunesfeature.h @@ -45,14 +45,7 @@ class ITunesFeature : public BaseExternalLibraryFeature { std::unique_ptr makeImporter(); // returns the invisible rootItem for the sidebar model TreeItem* importLibrary(); - void guessMusicLibraryMountpoint(QXmlStreamReader& xml); - void parseTracks(QXmlStreamReader& xml); - void parseTrack(QXmlStreamReader& xml, QSqlQuery& query); - TreeItem* parsePlaylists(QXmlStreamReader &xml); - void parsePlaylist(QXmlStreamReader& xml, QSqlQuery& query1, - QSqlQuery &query2, TreeItem*); void clearTable(const QString& table_name); - bool readNextStartElement(QXmlStreamReader& xml); /// Presents an 'open file' dialog for selecting an iTunes library XML and /// returns the file path. diff --git a/src/library/playlisttablemodel.cpp b/src/library/playlisttablemodel.cpp index d4124d668ce..db7f37a0edd 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/playlisttablemodel.cpp @@ -18,7 +18,7 @@ PlaylistTableModel::PlaylistTableModel(QObject* parent, const char* settingsNamespace, bool keepDeletedTracks) : TrackSetTableModel(parent, pTrackCollectionManager, settingsNamespace), - m_iPlaylistId(-1), + m_iPlaylistId(kInvalidPlaylistId), m_keepDeletedTracks(keepDeletedTracks) { connect(&m_pTrackCollectionManager->internalCollection()->getPlaylistDAO(), &PlaylistDAO::tracksChanged, @@ -129,7 +129,7 @@ void PlaylistTableModel::setTableModel(int playlistId) { } // Store search text QString currSearch = currentSearch(); - if (m_iPlaylistId != -1) { + if (m_iPlaylistId != kInvalidPlaylistId) { if (!currSearch.trimmed().isEmpty()) { m_searchTexts.insert(m_iPlaylistId, currSearch); } else { diff --git a/src/library/rekordbox/rekordboxfeature.cpp b/src/library/rekordbox/rekordboxfeature.cpp index 75ed800d019..c38c37d8c6b 100644 --- a/src/library/rekordbox/rekordboxfeature.cpp +++ b/src/library/rekordbox/rekordboxfeature.cpp @@ -289,7 +289,7 @@ QString getText(rekordbox_pdb_t::device_sql_string_t* deviceString) { } int createDevicePlaylist(QSqlDatabase& database, const QString& devicePath) { - int playlistID = -1; + int playlistID = kInvalidPlaylistId; QSqlQuery queryInsertIntoDevicePlaylist(database); queryInsertIntoDevicePlaylist.prepare( @@ -726,7 +726,7 @@ void buildPlaylistTree( return; } - int playlistID = -1; + int playlistID = kInvalidPlaylistId; while (idQuery.next()) { playlistID = idQuery.value(idQuery.record().indexOf("id")).toInt(); } @@ -796,7 +796,7 @@ void clearDeviceTables(QSqlDatabase& database, TreeItem* child) { ScopedTransaction transaction(database); int trackID = -1; - int playlistID = -1; + int playlistID = kInvalidPlaylistId; QSqlQuery tracksQuery(database); tracksQuery.prepare("select id from " + kRekordboxLibraryTable + " where device=:device"); tracksQuery.bindValue(":device", child->getLabel()); diff --git a/src/library/sidebarmodel.cpp b/src/library/sidebarmodel.cpp index 1cef8f14b4c..e0539fd95d9 100644 --- a/src/library/sidebarmodel.cpp +++ b/src/library/sidebarmodel.cpp @@ -13,10 +13,10 @@ namespace { -// The time between selecting and activating (= clicking) a feature item -// in the sidebar tree. This is essential to allow smooth scrolling through -// a list of items with an encoder or the keyboard! A value of 300 ms has -// been chosen as a compromise between usability and responsiveness. +/// The time between selecting and activating (= clicking) a feature item +/// in the sidebar tree. This is essential to allow smooth scrolling through +/// a list of items with an encoder or the keyboard! A value of 300 ms has +/// been chosen as a compromise between usability and responsiveness. constexpr int kPressedUntilClickedTimeoutMillis = 300; } // anonymous namespace @@ -138,6 +138,18 @@ QModelIndex SidebarModel::index(int row, int column, return createIndex(row, column, const_cast(this)); } +QModelIndex SidebarModel::getFeatureRootIndex(LibraryFeature* pFeature) { + // qDebug() << "SidebarModel::getFeatureRootIndex for" << pFeature->title().toString(); + QModelIndex ind; + for (int i = 0; i < m_sFeatures.size(); ++i) { + if (m_sFeatures[i] == pFeature) { + ind = index(i, 0); + break; + } + } + return ind; +} + QModelIndex SidebarModel::parent(const QModelIndex& index) const { //qDebug() << "SidebarModel::parent index=" << index.getData(); if (index.isValid()) { @@ -290,6 +302,8 @@ void SidebarModel::slotPressedUntilClickedTimeout() { } } +/// Connected to WLibrarySidebar::pressed signal, called after left click and +/// selection change via keyboard or controller void SidebarModel::pressed(const QModelIndex& index) { stopPressedUntilClickedTimer(); if (index.isValid()) { @@ -317,6 +331,7 @@ void SidebarModel::clicked(const QModelIndex& index) { } } +/// Invoked by double click and click on tree node expand icons void SidebarModel::doubleClicked(const QModelIndex& index) { stopPressedUntilClickedTimer(); if (index.isValid()) { @@ -337,9 +352,7 @@ void SidebarModel::rightClicked(const QPoint& globalPos, const QModelIndex& inde if (index.isValid()) { if (index.internalPointer() == this) { m_sFeatures[index.row()]->onRightClick(globalPos); - } - else - { + } else { TreeItem* pTreeItem = static_cast(index.internalPointer()); if (pTreeItem) { LibraryFeature* pFeature = pTreeItem->feature(); @@ -406,7 +419,7 @@ bool SidebarModel::dropAccept(const QModelIndex& index, const QList& urls, bool SidebarModel::hasTrackTable(const QModelIndex& index) const { if (index.internalPointer() == this) { - return m_sFeatures[index.row()]->hasTrackTable(); + return m_sFeatures[index.row()]->hasTrackTable(); } return false; } @@ -429,7 +442,7 @@ bool SidebarModel::dragMoveAccept(const QModelIndex& index, const QUrl& url) { return result; } -// Translates an index from the child models to an index of the sidebar models +/// Translates an index from the child models to an index of the sidebar models QModelIndex SidebarModel::translateSourceIndex(const QModelIndex& index) { /* These method is called from the slot functions below. * QObject::sender() return the object which emitted the signal @@ -454,8 +467,6 @@ QModelIndex SidebarModel::translateIndex( TreeItem* pItem = static_cast(index.internalPointer()); translatedIndex = createIndex(index.row(), index.column(), pItem); } else { - //Comment from Tobias Rafreider --> Dead Code???? - for (int i = 0; i < m_sFeatures.size(); ++i) { if (m_sFeatures[i]->sidebarModel() == pModel) { translatedIndex = createIndex(i, 0, this); diff --git a/src/library/sidebarmodel.h b/src/library/sidebarmodel.h index 921d93dbd78..debaa338a6b 100644 --- a/src/library/sidebarmodel.h +++ b/src/library/sidebarmodel.h @@ -45,6 +45,7 @@ class SidebarModel : public QAbstractItemModel { QModelIndex translateChildIndex(const QModelIndex& index) { return translateIndex(index, index.model()); } + QModelIndex getFeatureRootIndex(LibraryFeature* pFeature); public slots: void pressed(const QModelIndex& index); diff --git a/src/library/trackset/baseplaylistfeature.cpp b/src/library/trackset/baseplaylistfeature.cpp index f7f7bc70813..46da3f86197 100644 --- a/src/library/trackset/baseplaylistfeature.cpp +++ b/src/library/trackset/baseplaylistfeature.cpp @@ -52,6 +52,18 @@ BasePlaylistFeature::BasePlaylistFeature( pModel->setParent(this); initActions(); + connectPlaylistDAO(); + connect(m_pLibrary, + &Library::trackSelected, + this, + [this](const TrackPointer& pTrack) { + const auto trackId = pTrack ? pTrack->getId() : TrackId{}; + slotTrackSelected(trackId); + }); + connect(m_pLibrary, + &Library::switchToView, + this, + &BasePlaylistFeature::slotResetSelectedTrack); } void BasePlaylistFeature::initActions() { @@ -130,15 +142,17 @@ void BasePlaylistFeature::initActions() { &QAction::triggered, this, &BasePlaylistFeature::slotExportTrackFiles); +} +void BasePlaylistFeature::connectPlaylistDAO() { connect(&m_playlistDao, &PlaylistDAO::added, this, - &BasePlaylistFeature::slotPlaylistTableChangedAndSelect); + &BasePlaylistFeature::slotPlaylistTableChangedAndScrollTo); connect(&m_playlistDao, &PlaylistDAO::lockChanged, this, - &BasePlaylistFeature::slotPlaylistTableChangedAndScrollTo); + &BasePlaylistFeature::slotPlaylistContentOrLockChanged); connect(&m_playlistDao, &PlaylistDAO::deleted, this, @@ -146,23 +160,13 @@ void BasePlaylistFeature::initActions() { connect(&m_playlistDao, &PlaylistDAO::tracksChanged, this, - &BasePlaylistFeature::slotPlaylistContentChanged); + &BasePlaylistFeature::slotPlaylistContentOrLockChanged); connect(&m_playlistDao, &PlaylistDAO::renamed, this, + // In "History") just the item is renamed, while in "Playlists" the + // entire sidebar model is rebuilt to resort items by name &BasePlaylistFeature::slotPlaylistTableRenamed); - - connect(m_pLibrary, - &Library::trackSelected, - this, - [this](const TrackPointer& pTrack) { - const auto trackId = pTrack ? pTrack->getId() : TrackId{}; - slotTrackSelected(trackId); - }); - connect(m_pLibrary, - &Library::switchToView, - this, - &BasePlaylistFeature::slotResetSelectedTrack); } int BasePlaylistFeature::playlistIdFromIndex(const QModelIndex& index) { @@ -188,7 +192,7 @@ void BasePlaylistFeature::selectPlaylistInSidebar(int playlistId, bool select) { return; } QModelIndex index = indexFromPlaylistId(playlistId); - if (index.isValid() && m_pSidebarWidget) { + if (index.isValid()) { m_pSidebarWidget->selectChildIndex(index, select); } } @@ -197,10 +201,13 @@ void BasePlaylistFeature::activateChild(const QModelIndex& index) { //qDebug() << "BasePlaylistFeature::activateChild()" << index; int playlistId = playlistIdFromIndex(index); if (playlistId == kInvalidPlaylistId) { - // This happens if user clicks on group nodes - // like the year folder in the history feature + // This happens if user clicks on group nodes. + // Doesn't apply to YEAR nodes in the history feature, they are linked to + // a dummy playlist. return; } + m_lastClickedIndex = index; + m_lastRightClickedIndex = QModelIndex(); emit saveModelState(); m_pPlaylistTableModel->setTableModel(playlistId); emit showTrackModel(m_pPlaylistTableModel); @@ -216,17 +223,14 @@ void BasePlaylistFeature::activatePlaylist(int playlistId) { VERIFY_OR_DEBUG_ASSERT(index.isValid()) { return; } + m_lastClickedIndex = index; + m_lastRightClickedIndex = QModelIndex(); emit saveModelState(); - m_lastRightClickedIndex = index; m_pPlaylistTableModel->setTableModel(playlistId); emit showTrackModel(m_pPlaylistTableModel); emit enableCoverArtDisplay(true); // Update selection - emit featureSelect(this, m_lastRightClickedIndex); - if (!m_pSidebarWidget) { - return; - } - m_pSidebarWidget->selectChildIndex(m_lastRightClickedIndex); + emit featureSelect(this, m_lastClickedIndex); } void BasePlaylistFeature::renameItem(const QModelIndex& index) { @@ -244,7 +248,7 @@ void BasePlaylistFeature::slotRenamePlaylist() { if (locked) { qDebug() << "Skipping playlist rename because playlist" << playlistId - << "is locked."; + << oldName << "is locked."; return; } QString newName; @@ -323,9 +327,8 @@ void BasePlaylistFeature::slotDuplicatePlaylist() { int newPlaylistId = m_playlistDao.createPlaylist(name); - if (newPlaylistId != kInvalidPlaylistId && - m_playlistDao.copyPlaylistTracks(oldPlaylistId, newPlaylistId)) { - activatePlaylist(newPlaylistId); + if (newPlaylistId != kInvalidPlaylistId) { + m_playlistDao.copyPlaylistTracks(oldPlaylistId, newPlaylistId); } } @@ -375,9 +378,7 @@ void BasePlaylistFeature::slotCreatePlaylist() { int playlistId = m_playlistDao.createPlaylist(name); - if (playlistId != kInvalidPlaylistId) { - activatePlaylist(playlistId); - } else { + if (playlistId == kInvalidPlaylistId) { QMessageBox::warning(nullptr, tr("Playlist Creation Failed"), tr("An unknown error occurred while creating playlist: ") + name); @@ -418,21 +419,12 @@ void BasePlaylistFeature::slotDeletePlaylist() { return; } - // we will switch to the sibling if the deleted playlist is currently active - bool wasActive = m_pPlaylistTableModel->getPlaylist() == playlistId; - - VERIFY_OR_DEBUG_ASSERT(playlistId >= 0) { - return; - } - bool locked = m_playlistDao.isPlaylistLocked(playlistId); if (locked) { qDebug() << "Skipping playlist deletion because playlist" << playlistId << "is locked."; return; } - int siblingId = getSiblingPlaylistIdOf(m_lastRightClickedIndex); - QMessageBox::StandardButton btn = QMessageBox::question(nullptr, tr("Confirm Deletion"), tr("Do you really want to delete playlist %1?") @@ -444,15 +436,6 @@ void BasePlaylistFeature::slotDeletePlaylist() { } m_playlistDao.deletePlaylist(playlistId); - - if (siblingId == kInvalidPlaylistId) { - return; - } - if (wasActive) { - activatePlaylist(siblingId); - } else if (m_pSidebarWidget) { - m_pSidebarWidget->selectChildIndex(indexFromPlaylistId(siblingId), false); - } } void BasePlaylistFeature::slotImportPlaylist() { @@ -461,6 +444,10 @@ void BasePlaylistFeature::slotImportPlaylist() { if (playlistFile.isEmpty()) { return; } + int playlistId = playlistIdFromIndex(m_lastRightClickedIndex); + if (playlistId == kInvalidPlaylistId) { + return; + } // Update the import/export playlist directory QString fileDirectory(playlistFile); @@ -468,19 +455,37 @@ void BasePlaylistFeature::slotImportPlaylist() { m_pConfig->set(kConfigKeyLastImportExportPlaylistDirectory, ConfigValue(fileDirectory)); - slotImportPlaylistFile(playlistFile); - activateChild(m_lastRightClickedIndex); + slotImportPlaylistFile(playlistFile, playlistId); } -void BasePlaylistFeature::slotImportPlaylistFile(const QString& playlist_file) { +void BasePlaylistFeature::slotImportPlaylistFile(const QString& playlistFile, + int playlistId) { + if (playlistFile.isEmpty()) { + return; + } // The user has picked a new directory via a file dialog. This means the // system sandboxer (if we are sandboxed) has granted us permission to this // folder. We don't need access to this file on a regular basis so we do not // register a security bookmark. - QList locations = Parser::parse(playlist_file); + // Create a temporary PlaylistTableModel for the Playlist the entries shall be imported to. + // This is used as a proxy object to write to the database. + // We cannot use m_pPlaylistTableModel since it might have another playlist selected which + // is not the playlist that received the right-click. + QScopedPointer pPlaylistTableModel( + new PlaylistTableModel(this, + m_pLibrary->trackCollectionManager(), + "mixxx.db.model.playlist_export")); + pPlaylistTableModel->setTableModel(playlistId); + pPlaylistTableModel->setSort( + pPlaylistTableModel->fieldIndex( + ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION), + Qt::AscendingOrder); + pPlaylistTableModel->select(); + + QList locations = Parser::parse(playlistFile); // Iterate over the List that holds locations of playlist entries - m_pPlaylistTableModel->addTracks(QModelIndex(), locations); + pPlaylistTableModel->addTracks(QModelIndex(), locations); } void BasePlaylistFeature::slotCreateImportPlaylist() { @@ -521,17 +526,14 @@ void BasePlaylistFeature::slotCreateImportPlaylist() { } lastPlaylistId = m_playlistDao.createPlaylist(name); - if (lastPlaylistId != kInvalidPlaylistId) { - emit saveModelState(); - m_pPlaylistTableModel->setTableModel(lastPlaylistId); - } else { + if (lastPlaylistId == kInvalidPlaylistId) { QMessageBox::warning(nullptr, tr("Playlist Creation Failed"), tr("An unknown error occurred while creating playlist: ") + name); return; } - slotImportPlaylistFile(playlistFile); + slotImportPlaylistFile(playlistFile, lastPlaylistId); } activatePlaylist(lastPlaylistId); } @@ -583,7 +585,7 @@ void BasePlaylistFeature::slotExportPlaylist() { "mixxx.db.model.playlist_export")); emit saveModelState(); - pPlaylistTableModel->setTableModel(m_pPlaylistTableModel->getPlaylist()); + pPlaylistTableModel->setTableModel(playlistId); pPlaylistTableModel->setSort( pPlaylistTableModel->fieldIndex( ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION), @@ -619,13 +621,17 @@ void BasePlaylistFeature::slotExportPlaylist() { } void BasePlaylistFeature::slotExportTrackFiles() { + int playlistId = playlistIdFromIndex(m_lastRightClickedIndex); + if (playlistId == kInvalidPlaylistId) { + return; + } QScopedPointer pPlaylistTableModel( new PlaylistTableModel(this, m_pLibrary->trackCollectionManager(), "mixxx.db.model.playlist_export")); emit saveModelState(); - pPlaylistTableModel->setTableModel(m_pPlaylistTableModel->getPlaylist()); + pPlaylistTableModel->setTableModel(playlistId); pPlaylistTableModel->setSort(pPlaylistTableModel->fieldIndex( ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION), Qt::AscendingOrder); @@ -713,27 +719,43 @@ void BasePlaylistFeature::htmlLinkClicked(const QUrl& link) { } } -void BasePlaylistFeature::updateChildModel(int playlistId) { - QString playlistLabel = fetchPlaylistLabel(playlistId); +void BasePlaylistFeature::updateChildModel(const QSet& playlistIds) { + // qDebug() << "BasePlaylistFeature::updateChildModel"; + if (playlistIds.isEmpty()) { + return; + } - QVariant variantId = QVariant(playlistId); + int id = kInvalidPlaylistId; + QString label; + bool ok = false; for (int row = 0; row < m_pSidebarModel->rowCount(); ++row) { QModelIndex index = m_pSidebarModel->index(row, 0); TreeItem* pTreeItem = m_pSidebarModel->getItem(index); DEBUG_ASSERT(pTreeItem != nullptr); - if (!pTreeItem->hasChildren() && // leaf node - pTreeItem->getData() == variantId) { - pTreeItem->setLabel(playlistLabel); - decorateChild(pTreeItem, playlistId); + if (pTreeItem->hasChildren()) { + for (TreeItem* pChild : pTreeItem->children()) { + id = pChild->getData().toInt(&ok); + if (ok && id != kInvalidPlaylistId && playlistIds.contains(id)) { + label = fetchPlaylistLabel(id); + pChild->setLabel(label); + decorateChild(pChild, id); + } + } + } else { + id = pTreeItem->getData().toInt(&ok); + if (ok && id != kInvalidPlaylistId && playlistIds.contains(id)) { + label = fetchPlaylistLabel(id); + pTreeItem->setLabel(label); + decorateChild(pTreeItem, id); + } } } } -/** - * Clears the child model dynamically, but the invisible root item remains - */ +/// Clears the child model dynamically, but the invisible root item remains void BasePlaylistFeature::clearChildModel() { + m_lastClickedIndex = QModelIndex(); m_lastRightClickedIndex = QModelIndex(); m_pSidebarModel->removeRows(0, m_pSidebarModel->rowCount()); } @@ -752,6 +774,10 @@ QModelIndex BasePlaylistFeature::indexFromPlaylistId(int playlistId) { return QModelIndex(); } +bool BasePlaylistFeature::isChildIndexSelectedInSidebar(const QModelIndex& index) { + return m_pSidebarWidget && m_pSidebarWidget->isChildIndexSelected(index); +}; + void BasePlaylistFeature::slotTrackSelected(TrackId trackId) { m_selectedTrackId = trackId; m_playlistDao.getPlaylistsTrackIsIn(m_selectedTrackId, &m_playlistIdsOfSelectedTrack); diff --git a/src/library/trackset/baseplaylistfeature.h b/src/library/trackset/baseplaylistfeature.h index 044a4c81d34..3eb0829e6cb 100644 --- a/src/library/trackset/baseplaylistfeature.h +++ b/src/library/trackset/baseplaylistfeature.h @@ -21,8 +21,6 @@ class TrackCollectionManager; class TreeItem; class WLibrarySidebar; -constexpr int kInvalidPlaylistId = -1; - class BasePlaylistFeature : public BaseTrackSetFeature { Q_OBJECT @@ -56,8 +54,8 @@ class BasePlaylistFeature : public BaseTrackSetFeature { slotPlaylistTableChanged(playlistId); selectPlaylistInSidebar(playlistId, false); }; + virtual void slotPlaylistContentOrLockChanged(const QSet& playlistIds) = 0; virtual void slotPlaylistTableRenamed(int playlistId, const QString& newName) = 0; - virtual void slotPlaylistContentChanged(QSet playlistIds) = 0; void slotCreatePlaylist(); void renameItem(const QModelIndex& index) override; void deleteItem(const QModelIndex& index) override; @@ -71,7 +69,7 @@ class BasePlaylistFeature : public BaseTrackSetFeature { void slotRenamePlaylist(); void slotTogglePlaylistLock(); void slotImportPlaylist(); - void slotImportPlaylistFile(const QString& playlist_file); + void slotImportPlaylistFile(const QString& playlistFile, int playlistId); void slotCreateImportPlaylist(); void slotExportPlaylist(); // Copy all of the tracks in a playlist to a new directory. @@ -84,7 +82,7 @@ class BasePlaylistFeature : public BaseTrackSetFeature { QString label; }; - virtual void updateChildModel(int selected_id); + virtual void updateChildModel(const QSet& playlistIds); virtual void clearChildModel(); virtual QString fetchPlaylistLabel(int playlistId) = 0; @@ -96,8 +94,10 @@ class BasePlaylistFeature : public BaseTrackSetFeature { // Get the QModelIndex of a playlist based on its id. Returns QModelIndex() // on failure. QModelIndex indexFromPlaylistId(int playlistId); + bool isChildIndexSelectedInSidebar(const QModelIndex& index); PlaylistDAO& m_playlistDao; + QModelIndex m_lastClickedIndex; QModelIndex m_lastRightClickedIndex; QPointer m_pSidebarWidget; @@ -124,6 +124,7 @@ class BasePlaylistFeature : public BaseTrackSetFeature { private: void initActions(); + void connectPlaylistDAO(); virtual QString getRootViewHtml() const = 0; void markTreeItem(TreeItem* pTreeItem); diff --git a/src/library/trackset/crate/cratefeature.cpp b/src/library/trackset/crate/cratefeature.cpp index fb44211fde5..f4f46fba506 100644 --- a/src/library/trackset/crate/cratefeature.cpp +++ b/src/library/trackset/crate/cratefeature.cpp @@ -164,11 +164,11 @@ void CrateFeature::connectLibrary(Library* pLibrary) { } void CrateFeature::connectTrackCollection() { - connect(m_pTrackCollection, + connect(m_pTrackCollection, // created new, duplicated or imported playlist to new crate &TrackCollection::crateInserted, this, &CrateFeature::slotCrateTableChanged); - connect(m_pTrackCollection, + connect(m_pTrackCollection, // renamed, un/locked, toggled AutoDJ source &TrackCollection::crateUpdated, this, &CrateFeature::slotCrateTableChanged); @@ -176,7 +176,7 @@ void CrateFeature::connectTrackCollection() { &TrackCollection::crateDeleted, this, &CrateFeature::slotCrateTableChanged); - connect(m_pTrackCollection, + connect(m_pTrackCollection, // crate tracks hidden, unhidden or purged &TrackCollection::crateTracksChanged, this, &CrateFeature::slotCrateContentChanged); @@ -295,12 +295,20 @@ TreeItemModel* CrateFeature::sidebarModel() const { return m_pSidebarModel; } +void CrateFeature::activate() { + m_lastClickedIndex = QModelIndex(); + BaseTrackSetFeature::activate(); +} + void CrateFeature::activateChild(const QModelIndex& index) { - //qDebug() << "CrateFeature::activateChild()" << index; + qDebug() << " CrateFeature::activateChild()" << index; CrateId crateId(crateIdFromIndex(index)); VERIFY_OR_DEBUG_ASSERT(crateId.isValid()) { return; } + m_lastClickedIndex = index; + m_lastRightClickedIndex = QModelIndex(); + m_prevSiblingCrate = CrateId(); emit saveModelState(); m_crateTableModel.selectCrate(crateId); emit showTrackModel(&m_crateTableModel); @@ -312,18 +320,24 @@ bool CrateFeature::activateCrate(CrateId crateId) { VERIFY_OR_DEBUG_ASSERT(crateId.isValid()) { return false; } + if (!m_pTrackCollection->crates().readCrateSummaryById(crateId)) { + // this may happen if called by slotCrateTableChanged() + // and the crate has just been deleted + return false; + } QModelIndex index = indexFromCrateId(crateId); VERIFY_OR_DEBUG_ASSERT(index.isValid()) { return false; } + m_lastClickedIndex = index; + m_lastRightClickedIndex = QModelIndex(); + m_prevSiblingCrate = CrateId(); emit saveModelState(); - m_lastRightClickedIndex = index; m_crateTableModel.selectCrate(crateId); emit showTrackModel(&m_crateTableModel); emit enableCoverArtDisplay(true); // Update selection - emit featureSelect(this, m_lastRightClickedIndex); - activateChild(m_lastRightClickedIndex); + emit featureSelect(this, m_lastClickedIndex); return true; } @@ -341,6 +355,10 @@ bool CrateFeature::readLastRightClickedCrate(Crate* pCrate) const { return true; } +bool CrateFeature::isChildIndexSelectedInSidebar(const QModelIndex& index) { + return m_pSidebarWidget && m_pSidebarWidget->isChildIndexSelected(index); +} + void CrateFeature::onRightClick(const QPoint& globalPos) { m_lastRightClickedIndex = QModelIndex(); QMenu menu(m_pSidebarWidget); @@ -403,7 +421,8 @@ void CrateFeature::slotCreateCrate() { CrateFeatureHelper(m_pTrackCollection, m_pConfig) .createEmptyCrate(); if (crateId.isValid()) { - activateCrate(crateId); + // expand Crates and scroll to new crate + m_pSidebarWidget->selectChildIndex(indexFromCrateId(crateId), false); } } @@ -422,7 +441,11 @@ void CrateFeature::slotDeleteCrate() { CrateId crateId = crate.getId(); // Store sibling id to restore selection after crate was deleted // to avoid the scroll position being reset to Crate root item. - storePrevSiblingCrateId(crateId); + m_prevSiblingCrate = CrateId(); + if (isChildIndexSelectedInSidebar(m_lastRightClickedIndex)) { + storePrevSiblingCrateId(crateId); + } + QMessageBox::StandardButton btn = QMessageBox::question(nullptr, tr("Confirm Deletion"), tr("Do you really want to delete crate %1?") @@ -492,15 +515,15 @@ void CrateFeature::slotRenameCrate() { void CrateFeature::slotDuplicateCrate() { Crate crate; if (readLastRightClickedCrate(&crate)) { - CrateId crateId = + CrateId newCrateId = CrateFeatureHelper(m_pTrackCollection, m_pConfig) .duplicateCrate(crate); - if (crateId.isValid()) { - activateCrate(crateId); + if (newCrateId.isValid()) { + qDebug() << "Duplicate crate" << crate << ", new crate:" << newCrateId; + return; } - } else { - qDebug() << "Failed to duplicate selected crate"; } + qDebug() << "Failed to duplicate selected crate"; } void CrateFeature::slotToggleCrateLock() { @@ -547,7 +570,7 @@ QModelIndex CrateFeature::rebuildChildModel(CrateId selectedCrateId) { modelRows.push_back(newTreeItemForCrateSummary(crateSummary)); if (selectedCrateId == crateSummary.getId()) { // save index for selection - selectedRow = modelRows.size() - 1; + selectedRow = static_cast(modelRows.size()) - 1; } } @@ -630,11 +653,22 @@ void CrateFeature::slotImportPlaylist() { m_pConfig->set(kConfigKeyLastImportExportCrateDirectoryKey, ConfigValue(fileDirectory)); - slotImportPlaylistFile(playlistFile); + CrateId crateId = crateIdFromIndex(m_lastRightClickedIndex); + Crate crate; + if (m_pTrackCollection->crates().readCrateById(crateId, &crate)) { + qDebug() << "Importing playlist file" << playlistFile << "into crate" + << crateId << crate; + } else { + qDebug() << "Importing playlist file" << playlistFile << "into crate" + << crateId << crate << "failed!"; + return; + } + + slotImportPlaylistFile(playlistFile, crateId); activateChild(m_lastRightClickedIndex); } -void CrateFeature::slotImportPlaylistFile(const QString& playlistFile) { +void CrateFeature::slotImportPlaylistFile(const QString& playlistFile, CrateId crateId) { // The user has picked a new directory via a file dialog. This means the // system sandboxer (if we are sandboxed) has granted us permission to this // folder. We don't need access to this file on a regular basis so we do not @@ -645,7 +679,19 @@ void CrateFeature::slotImportPlaylistFile(const QString& playlistFile) { if (locations.empty()) { return; } - m_crateTableModel.addTracks(QModelIndex(), locations); + + if (crateId == m_crateTableModel.selectedCrate()) { + // Add tracks directly to the model + m_crateTableModel.addTracks(QModelIndex(), locations); + } else { + // Create a temporary table model since the main one might have another + // crate selected which is not the crate that received the right-click. + QScopedPointer pCrateTableModel( + new CrateTableModel(this, m_pLibrary->trackCollectionManager())); + pCrateTableModel->selectCrate(crateId); + pCrateTableModel->select(); + pCrateTableModel->addTracks(QModelIndex(), locations); + } } void CrateFeature::slotCreateImportCrate() { @@ -663,7 +709,7 @@ void CrateFeature::slotCreateImportCrate() { CrateId lastCrateId; - // For each selected file + // For each selected file create a new crate for (const QString& playlistFile : playlistFiles) { const QFileInfo fileInfo(playlistFile); @@ -687,9 +733,7 @@ void CrateFeature::slotCreateImportCrate() { } } - if (m_pTrackCollection->insertCrate(crate, &lastCrateId)) { - m_crateTableModel.selectCrate(lastCrateId); - } else { + if (!m_pTrackCollection->insertCrate(crate, &lastCrateId)) { QMessageBox::warning(nullptr, tr("Crate Creation Failed"), tr("An unknown error occurred while creating crate: ") + @@ -697,7 +741,7 @@ void CrateFeature::slotCreateImportCrate() { return; } - slotImportPlaylistFile(playlistFile); + slotImportPlaylistFile(playlistFile, lastCrateId); } activateCrate(lastCrateId); } @@ -723,12 +767,13 @@ void CrateFeature::slotAnalyzeCrate() { } void CrateFeature::slotExportPlaylist() { - CrateId crateId = m_crateTableModel.selectedCrate(); + CrateId crateId = crateIdFromIndex(m_lastRightClickedIndex); Crate crate; if (m_pTrackCollection->crates().readCrateById(crateId, &crate)) { - qDebug() << "Exporting crate" << crate; + qDebug() << "Exporting crate" << crateId << crate; } else { qDebug() << "Failed to export crate" << crateId; + return; } QString lastCrateDirectory = m_pConfig->getValue( @@ -768,7 +813,7 @@ void CrateFeature::slotExportPlaylist() { // Create a new table model since the main one might have an active search. QScopedPointer pCrateTableModel( new CrateTableModel(this, m_pLibrary->trackCollectionManager())); - pCrateTableModel->selectCrate(m_crateTableModel.selectedCrate()); + pCrateTableModel->selectCrate(crateId); pCrateTableModel->select(); if (fileLocation.endsWith(".csv", Qt::CaseInsensitive)) { @@ -780,8 +825,8 @@ void CrateFeature::slotExportPlaylist() { QList playlistItems; int rows = pCrateTableModel->rowCount(); for (int i = 0; i < rows; ++i) { - QModelIndex index = m_crateTableModel.index(i, 0); - playlistItems << m_crateTableModel.getTrackLocation(index); + QModelIndex index = pCrateTableModel->index(i, 0); + playlistItems << pCrateTableModel->getTrackLocation(index); } exportPlaylistItemsIntoFile( fileLocation, @@ -810,6 +855,7 @@ void CrateFeature::slotExportTrackFiles() { void CrateFeature::storePrevSiblingCrateId(CrateId crateId) { QModelIndex actIndex = indexFromCrateId(crateId); + m_prevSiblingCrate = CrateId(); for (int i = (actIndex.row() + 1); i >= (actIndex.row() - 1); i -= 2) { QModelIndex newIndex = actIndex.sibling(i, actIndex.column()); if (newIndex.isValid()) { @@ -823,15 +869,14 @@ void CrateFeature::storePrevSiblingCrateId(CrateId crateId) { } void CrateFeature::slotCrateTableChanged(CrateId crateId) { - if (m_lastRightClickedIndex.isValid() && - (crateIdFromIndex(m_lastRightClickedIndex) == crateId)) { - // Try to restore previous selection - m_lastRightClickedIndex = rebuildChildModel(crateId); - if (m_lastRightClickedIndex.isValid()) { - // Select last active crate - activateCrate(crateId); - } else if (m_prevSiblingCrate.isValid()) { - // Select neighbour of deleted crate + Q_UNUSED(crateId); + if (isChildIndexSelectedInSidebar(m_lastClickedIndex)) { + // If the previously selected crate was loaded to the tracks table and + // selected in the sidebar try to activate that or a sibling + rebuildChildModel(); + if (!activateCrate(m_crateTableModel.selectedCrate())) { + // probably last clicked crate was deleted, try to + // select the stored sibling activateCrate(m_prevSiblingCrate); } } else { diff --git a/src/library/trackset/crate/cratefeature.h b/src/library/trackset/crate/cratefeature.h index 35d8f0adf80..b7e746d201b 100644 --- a/src/library/trackset/crate/cratefeature.h +++ b/src/library/trackset/crate/cratefeature.h @@ -42,6 +42,7 @@ class CrateFeature : public BaseTrackSetFeature { TreeItemModel* sidebarModel() const override; public slots: + void activate() override; void activateChild(const QModelIndex& index) override; void onRightClick(const QPoint& globalPos) override; void onRightClickChild(const QPoint& globalPos, const QModelIndex& index) override; @@ -62,7 +63,7 @@ class CrateFeature : public BaseTrackSetFeature { void slotAutoDjTrackSourceChanged(); void slotToggleCrateLock(); void slotImportPlaylist(); - void slotImportPlaylistFile(const QString& playlist_file); + void slotImportPlaylistFile(const QString& playlistFile, CrateId crateId); void slotCreateImportCrate(); void slotExportPlaylist(); // Copy all of the tracks in a crate to a new directory (like a thumbdrive). @@ -94,6 +95,7 @@ class CrateFeature : public BaseTrackSetFeature { CrateId crateIdFromIndex(const QModelIndex& index) const; QModelIndex indexFromCrateId(CrateId crateId) const; + bool isChildIndexSelectedInSidebar(const QModelIndex& index); bool readLastRightClickedCrate(Crate* pCrate) const; QString formatRootViewHtml() const; @@ -109,6 +111,7 @@ class CrateFeature : public BaseTrackSetFeature { // Can be used to restore a similar selection after the sidebar model was rebuilt. CrateId m_prevSiblingCrate; + QModelIndex m_lastClickedIndex; QModelIndex m_lastRightClickedIndex; TrackId m_selectedTrackId; diff --git a/src/library/trackset/playlistfeature.cpp b/src/library/trackset/playlistfeature.cpp index 076bbc72e5e..435442a31f4 100644 --- a/src/library/trackset/playlistfeature.cpp +++ b/src/library/trackset/playlistfeature.cpp @@ -276,36 +276,53 @@ void PlaylistFeature::decorateChild(TreeItem* item, int playlistId) { void PlaylistFeature::slotPlaylistTableChanged(int playlistId) { //qDebug() << "slotPlaylistTableChanged() playlistId:" << playlistId; enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); - if (type == PlaylistDAO::PLHT_NOT_HIDDEN || - type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - clearChildModel(); - m_lastRightClickedIndex = constructChildModel(playlistId); + if (type != PlaylistDAO::PLHT_NOT_HIDDEN && // not a regular playlist + type != PlaylistDAO::PLHT_UNKNOWN) { // not a deleted playlist + return; + } + + // Store current selection + int selectedPlaylistId = kInvalidPlaylistId; + if (isChildIndexSelectedInSidebar(m_lastClickedIndex)) { + if (playlistId == playlistIdFromIndex(m_lastClickedIndex) && + type == PlaylistDAO::PLHT_UNKNOWN) { + // if the selected playlist was deleted, find a sibling to select + selectedPlaylistId = getSiblingPlaylistIdOf(m_lastClickedIndex); + } else { + // just restore the current selection + selectedPlaylistId = playlistIdFromIndex(m_lastClickedIndex); + } + } + + clearChildModel(); + QModelIndex newIndex = constructChildModel(selectedPlaylistId); + if (newIndex.isValid()) { + // If a child index was selected and we got a new valid index select that. + // Else (root item was selected or for some reason no index could be created) + // there's nothing to do: either no child was selected earlier, or the root + // was selected and will remain selected after the child model was rebuilt. + activateChild(newIndex); + emit featureSelect(this, newIndex); } } -void PlaylistFeature::slotPlaylistContentChanged(QSet playlistIds) { +void PlaylistFeature::slotPlaylistContentOrLockChanged(const QSet& playlistIds) { + // qDebug() << "slotPlaylistContentOrLockChanged() playlistId:" << playlistId; + QSet idsToBeUpdated; for (const auto playlistId : qAsConst(playlistIds)) { - enum PlaylistDAO::HiddenType type = - m_playlistDao.getHiddenType(playlistId); - if (type == PlaylistDAO::PLHT_NOT_HIDDEN || - type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - updateChildModel(playlistId); + if (m_playlistDao.getHiddenType(playlistId) == PlaylistDAO::PLHT_NOT_HIDDEN) { + idsToBeUpdated.insert(playlistId); } } + updateChildModel(idsToBeUpdated); } void PlaylistFeature::slotPlaylistTableRenamed( int playlistId, const QString& newName) { Q_UNUSED(newName); - //qDebug() << "slotPlaylistTableChanged() playlistId:" << playlistId; - enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); - if (type == PlaylistDAO::PLHT_NOT_HIDDEN || - type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - clearChildModel(); - m_lastRightClickedIndex = constructChildModel(playlistId); - if (type != PlaylistDAO::PLHT_UNKNOWN) { - activatePlaylist(playlistId); - } + // qDebug() << "slotPlaylistTableRenamed() playlistId:" << playlistId; + if (m_playlistDao.getHiddenType(playlistId) == PlaylistDAO::PLHT_NOT_HIDDEN) { + slotPlaylistTableChanged(playlistId); } } diff --git a/src/library/trackset/playlistfeature.h b/src/library/trackset/playlistfeature.h index 66e1f39638b..9f4d04db7dc 100644 --- a/src/library/trackset/playlistfeature.h +++ b/src/library/trackset/playlistfeature.h @@ -37,7 +37,7 @@ class PlaylistFeature : public BasePlaylistFeature { private slots: void slotPlaylistTableChanged(int playlistId) override; - void slotPlaylistContentChanged(QSet playlistIds) override; + void slotPlaylistContentOrLockChanged(const QSet& playlistIds) override; void slotPlaylistTableRenamed(int playlistId, const QString& newName) override; protected: diff --git a/src/library/trackset/setlogfeature.cpp b/src/library/trackset/setlogfeature.cpp index c9a1b892692..54f1c4d5a9f 100644 --- a/src/library/trackset/setlogfeature.cpp +++ b/src/library/trackset/setlogfeature.cpp @@ -40,11 +40,22 @@ SetlogFeature::SetlogFeature( QStringLiteral("SETLOGHOME"), QStringLiteral("history")), m_playlistId(kInvalidPlaylistId), + m_placeholderId(kInvalidPlaylistId), m_pLibrary(pLibrary), m_pConfig(pConfig) { // remove unneeded entries deleteAllUnlockedPlaylistsWithFewerTracks(); + // Create empty placeholder playlist for YEAR items + QString placeholderName = "historyPlaceholder"; + m_placeholderId = m_playlistDao.createUniquePlaylist(&placeholderName, + PlaylistDAO::PLHT_UNKNOWN); + VERIFY_OR_DEBUG_ASSERT(m_placeholderId != kInvalidPlaylistId) { + qWarning() << "Failed to create empty History placeholder playlist!"; + } + // just to be safe + m_playlistDao.setPlaylistLocked(m_placeholderId, true); + //construct child model m_pSidebarModel->setRootItem(TreeItem::newRoot(this)); constructChildModel(kInvalidPlaylistId); @@ -61,19 +72,31 @@ SetlogFeature::SetlogFeature( this, &SetlogFeature::slotGetNewPlaylist); + m_pLockAllChildPlaylists = new QAction(tr("Lock all child playlists"), this); + connect(m_pLockAllChildPlaylists, + &QAction::triggered, + this, + &SetlogFeature::slotLockAllChildPlaylists); + + m_pUnlockAllChildPlaylists = new QAction(tr("Unlock all child playlists"), this); + connect(m_pUnlockAllChildPlaylists, + &QAction::triggered, + this, + &SetlogFeature::slotUnlockAllChildPlaylists); + + m_pDeleteAllChildPlaylists = new QAction(tr("Delete all unlocked child playlists"), this); + connect(m_pDeleteAllChildPlaylists, + &QAction::triggered, + this, + &SetlogFeature::slotDeleteAllUnlockedChildPlaylists); + // initialized in a new generic slot(get new history playlist purpose) slotGetNewPlaylist(); } SetlogFeature::~SetlogFeature() { - // If the history playlist we created doesn't have any tracks in it then - // delete it so we don't end up with tons of empty playlists. This is mostly - // for developers since they regularly open Mixxx without loading a track. - if (m_playlistId != kInvalidPlaylistId && - m_playlistDao.tracksInPlaylist(m_playlistId) == 0) { - m_playlistDao.deletePlaylist(m_playlistId); - } - // Also clean history up when shutting down in case the track threshold changed + // Clean up history when shutting down in case the track threshold changed, + // incl. the empty placeholder playlist and potentially empty current playlist deleteAllUnlockedPlaylistsWithFewerTracks(); } @@ -111,9 +134,13 @@ void SetlogFeature::slotDeletePlaylist() { if (playlistId == m_playlistId) { // the current setlog must not be deleted return; + } else if (playlistId == m_placeholderId) { + // this is a YEAR node + slotDeleteAllUnlockedChildPlaylists(); + } else { + // regular setlog, call the base implementation + BasePlaylistFeature::slotDeletePlaylist(); } - // regular setlog, call the base implementation - BasePlaylistFeature::slotDeletePlaylist(); } void SetlogFeature::onRightClick(const QPoint& globalPos) { @@ -145,28 +172,42 @@ void SetlogFeature::onRightClickChild(const QPoint& globalPos, const QModelIndex m_pLockPlaylistAction->setText(locked ? tr("Unlock") : tr("Lock")); QMenu menu(m_pSidebarWidget); - //menu.addAction(m_pCreatePlaylistAction); - //menu.addSeparator(); - menu.addAction(m_pAddToAutoDJAction); - menu.addAction(m_pAddToAutoDJTopAction); - menu.addSeparator(); - menu.addAction(m_pRenamePlaylistAction); - if (playlistId != m_playlistId) { - // Todays playlist should not be locked or deleted - menu.addAction(m_pDeletePlaylistAction); - menu.addAction(m_pLockPlaylistAction); - } - if (index.sibling(index.row() + 1, index.column()).isValid()) { - // The very first setlog cannot be joint - menu.addAction(m_pJoinWithPreviousAction); - } - if (playlistId == m_playlistId) { - // Todays playlists can change ! - m_pStartNewPlaylist->setEnabled(m_playlistDao.tracksInPlaylist(m_playlistId) > 0); - menu.addAction(m_pStartNewPlaylist); + if (playlistId == m_placeholderId) { + // this is a YEAR item + menu.addAction(m_pLockAllChildPlaylists); + menu.addAction(m_pUnlockAllChildPlaylists); + menu.addSeparator(); + menu.addAction(m_pDeleteAllChildPlaylists); + } else { + // this is a playlist + bool locked = m_playlistDao.isPlaylistLocked(playlistId); + m_pDeletePlaylistAction->setEnabled(!locked); + m_pRenamePlaylistAction->setEnabled(!locked); + m_pJoinWithPreviousAction->setEnabled(!locked); + + m_pLockPlaylistAction->setText(locked ? tr("Unlock") : tr("Lock")); + menu.addAction(m_pAddToAutoDJAction); + menu.addAction(m_pAddToAutoDJTopAction); + menu.addSeparator(); + menu.addAction(m_pRenamePlaylistAction); + if (playlistId != m_playlistId) { + // Todays playlist should not be locked or deleted + menu.addAction(m_pDeletePlaylistAction); + menu.addAction(m_pLockPlaylistAction); + } + if (index.sibling(index.row() + 1, index.column()).isValid()) { + // The very first (oldest) setlog cannot be joint + menu.addAction(m_pJoinWithPreviousAction); + } + if (playlistId == m_playlistId) { + // Todays playlists can change ! + m_pStartNewPlaylist->setEnabled( + m_playlistDao.tracksInPlaylist(m_playlistId) > 0); + menu.addAction(m_pStartNewPlaylist); + } + menu.addSeparator(); + menu.addAction(m_pExportPlaylistAction); } - menu.addSeparator(); - menu.addAction(m_pExportPlaylistAction); menu.exec(globalPos); } @@ -192,6 +233,8 @@ QModelIndex SetlogFeature::constructChildModel(int selectedId) { int idColumn = record.indexOf("id"); int createdColumn = record.indexOf("date_created"); + // Nice to have: restore previous expanded/collapsed state of YEAR items + clearChildModel(); QMap groups; std::vector> itemList; // Generous estimate (number of years the db is used ;)) @@ -212,16 +255,22 @@ QModelIndex SetlogFeature::constructChildModel(int selectedId) { .toDateTime(); // Create the TreeItem whose parent is the invisible root item + // Show only [kNumToplevelHistoryEntries] recent playlists at the top level + // before grouping them by year. if (row >= kNumToplevelHistoryEntries) { + // group by year int yearCreated = dateCreated.date().year(); auto i = groups.find(yearCreated); TreeItem* pGroupItem; - if (i != groups.end()) { + if (i != groups.end() && i.key() == yearCreated) { + // get YEAR item the playlist will sorted into pGroupItem = i.value(); } else { + // create YEAR item the playlist will sorted into + // store id of empty placeholder playlist auto pNewGroupItem = std::make_unique( - QString::number(yearCreated), kInvalidPlaylistId); + QString::number(yearCreated), m_placeholderId); pGroupItem = pNewGroupItem.get(); groups.insert(yearCreated, pGroupItem); itemList.push_back(std::move(pNewGroupItem)); @@ -231,6 +280,7 @@ QModelIndex SetlogFeature::constructChildModel(int selectedId) { pItem->setBold(m_playlistIdsOfSelectedTrack.contains(id)); decorateChild(pItem, id); } else { + // add most recent top-level playlist auto pItem = std::make_unique(name, id); pItem->setBold(m_playlistIdsOfSelectedTrack.contains(id)); decorateChild(pItem.get(), id); @@ -242,14 +292,12 @@ QModelIndex SetlogFeature::constructChildModel(int selectedId) { // Append all the newly created TreeItems in a dynamic way to the childmodel m_pSidebarModel->insertTreeItemRows(std::move(itemList), 0); - if (selectedId) { - return indexFromPlaylistId(selectedId); - } - return QModelIndex(); + return indexFromPlaylistId(selectedId); } QString SetlogFeature::fetchPlaylistLabel(int playlistId) { // Setup the sidebar playlist model + // TODO(ronso0) Why not m_playlistDao.getPlaylistName(id) ?? QSqlTableModel playlistTableModel(this, m_pLibrary->trackCollectionManager()->internalCollection()->database()); playlistTableModel.setTable("Playlists"); @@ -280,6 +328,7 @@ void SetlogFeature::decorateChild(TreeItem* item, int playlistId) { } } +/// Invoked on startup to create new current playlist and by "Finish current and start new" void SetlogFeature::slotGetNewPlaylist() { //qDebug() << "slotGetNewPlaylist() successfully triggered !"; @@ -308,9 +357,10 @@ void SetlogFeature::slotGetNewPlaylist() { m_recentTracks.clear(); } - reloadChildModel(m_playlistId); // For moving selection - emit showTrackModel(m_pPlaylistTableModel); - activatePlaylist(m_playlistId); + // reload child model again because the 'added' signal fired by PlaylistDAO + // might have triggered slotPlaylistTableChanged() before m_playlistId was set, + // which causes the wrong playlist being decorated as 'current' + slotPlaylistTableChanged(m_playlistId); } void SetlogFeature::slotJoinWithPrevious() { @@ -360,17 +410,105 @@ void SetlogFeature::slotJoinWithPrevious() { << " previous:" << previousPlaylistId; if (m_playlistDao.copyPlaylistTracks( currentPlaylistId, previousPlaylistId)) { - m_lastRightClickedIndex = constructChildModel(previousPlaylistId); m_playlistDao.deletePlaylist(currentPlaylistId); - reloadChildModel(previousPlaylistId); // For moving selection - emit showTrackModel(m_pPlaylistTableModel); - activatePlaylist(previousPlaylistId); } } } } } +void SetlogFeature::slotLockAllChildPlaylists() { + lockOrUnlockAllChildPlaylists(true); +} + +void SetlogFeature::slotUnlockAllChildPlaylists() { + lockOrUnlockAllChildPlaylists(false); +} + +void SetlogFeature::lockOrUnlockAllChildPlaylists(bool lock) { + if (!m_lastRightClickedIndex.isValid()) { + return; + } + if (lock) { + qWarning() << "lock all child playlists of" << m_lastRightClickedIndex.data().toString(); + } else { + qWarning() << "unlock all child playlists of" << m_lastRightClickedIndex.data().toString(); + } + TreeItem* item = static_cast(m_lastRightClickedIndex.internalPointer()); + if (!item) { + return; + } + const QList yearChildren = item->children(); + if (yearChildren.isEmpty()) { + return; + } + + QSet ids; + for (const auto& pChild : yearChildren) { + bool ok = false; + int childId = pChild->getData().toInt(&ok); + if (ok && childId != kInvalidPlaylistId) { + ids.insert(childId); + } + } + m_playlistDao.setPlaylistsLocked(ids, lock); +} + +void SetlogFeature::slotDeleteAllUnlockedChildPlaylists() { + if (!m_lastRightClickedIndex.isValid()) { + return; + } + TreeItem* item = static_cast(m_lastRightClickedIndex.internalPointer()); + if (!item) { + return; + } + const QList yearChildren = item->children(); + if (yearChildren.isEmpty()) { + return; + } + QString year = m_lastRightClickedIndex.data().toString(); + + QMessageBox::StandardButton btn = QMessageBox::question(nullptr, + tr("Confirm Deletion"), + //: %1 is the year + //: + are used to make the text in between bold in the popup + //:
is a linebreak + tr("Do you really want to delete all unlocked playlist from %1?

") + .arg(year), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (btn != QMessageBox::Yes) { + return; + } + + QStringList ids; + int count = 0; + for (const auto& pChild : yearChildren) { + bool ok = false; + int childId = pChild->getData().toInt(&ok); + if (ok && childId != kInvalidPlaylistId) { + ids.append(pChild->getData().toString()); + count++; + } + } + // Double-check, this is a weighty decision + btn = QMessageBox::warning(nullptr, + tr("Confirm Deletion"), + //: %1 is the number of playlists to be deleted + //: %2 is the year + //: + are used to make the text in between bold in the popup + //:
is a linebreak + tr("Deleting %1 playlists from %2.

") + .arg(QString::number(count), year), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Cancel); + if (btn != QMessageBox::Ok) { + return; + } + qDebug() << "History: deleting all unlocked playlists of" << year; + m_playlistDao.deleteUnlockedPlaylists(std::move(ids)); +} + void SetlogFeature::slotPlayingTrackChanged(TrackPointer currentPlayingTrack) { if (!currentPlayingTrack) { return; @@ -447,69 +585,122 @@ void SetlogFeature::slotPlayingTrackChanged(TrackPointer currentPlayingTrack) { } void SetlogFeature::slotPlaylistTableChanged(int playlistId) { - reloadChildModel(playlistId); -} - -void SetlogFeature::reloadChildModel(int playlistId) { //qDebug() << "updateChildModel() playlistId:" << playlistId; PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); - if (type == PlaylistDAO::PLHT_SET_LOG || - type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - clearChildModel(); - m_lastRightClickedIndex = constructChildModel(playlistId); + if (type != PlaylistDAO::PLHT_SET_LOG && + type != PlaylistDAO::PLHT_UNKNOWN) { // deleted Playlist + return; + } + + // save currently selected History sidebar item (if any) + int selectedYearIndexRow = -1; + int selectedPlaylistId = kInvalidPlaylistId; + bool rootWasSelected = false; + if (isChildIndexSelectedInSidebar(m_lastClickedIndex)) { + // a child index was selected (actual playlist or YEAR item) + int lastClickedPlaylistId = m_pPlaylistTableModel->getPlaylist(); + if (lastClickedPlaylistId == m_placeholderId) { + // a YEAR item was selected + selectedYearIndexRow = m_lastClickedIndex.row(); + } else if (playlistId == lastClickedPlaylistId && + type == PlaylistDAO::PLHT_UNKNOWN) { + // selected playlist was deleted, find a sibling. + // prev/next works here because history playlists are always + // sorted by date of creation. + selectedPlaylistId = m_playlistDao.getPreviousPlaylist( + lastClickedPlaylistId, + PlaylistDAO::PLHT_SET_LOG); + if (selectedPlaylistId == kInvalidPlaylistId) { + // no previous playlist, try to get the next playlist + selectedPlaylistId = m_playlistDao.getNextPlaylist( + lastClickedPlaylistId, + PlaylistDAO::PLHT_SET_LOG); + } + } else { + selectedPlaylistId = lastClickedPlaylistId; + } + } else { + rootWasSelected = m_pSidebarWidget && + m_pSidebarWidget->isFeatureRootIndexSelected(this); + } + + QModelIndex newIndex = constructChildModel(selectedPlaylistId); + + // restore selection + if (selectedYearIndexRow != -1) { + // if row is valid this means newIndex is invalid anyway + newIndex = m_pSidebarModel->index(selectedYearIndexRow, 0); + if (!newIndex.isValid()) { + // seems like we deleted the oldest (bottom) YEAR node while it was + // selected. Try to pick the row above + newIndex = m_pSidebarModel->index(selectedYearIndexRow - 1, 0); + } + } + if (newIndex.isValid()) { + emit featureSelect(this, newIndex); + activateChild(newIndex); + return; + } else if (rootWasSelected) { + // calling featureSelect with invalid index will select the root item + emit featureSelect(this, newIndex); + activate(); // to reload the new current playlist } } -void SetlogFeature::slotPlaylistContentChanged(QSet playlistIds) { +void SetlogFeature::slotPlaylistContentOrLockChanged(const QSet& playlistIds) { + // qDebug() << "slotPlaylistContentOrLockChanged() playlistId:" << playlistId; + QSet idsToBeUpdated; for (const auto playlistId : qAsConst(playlistIds)) { - enum PlaylistDAO::HiddenType type = - m_playlistDao.getHiddenType(playlistId); - if (type == PlaylistDAO::PLHT_SET_LOG || - type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - updateChildModel(playlistId); + if (m_playlistDao.getHiddenType(playlistId) == PlaylistDAO::PLHT_SET_LOG) { + idsToBeUpdated.insert(playlistId); } } + updateChildModel(idsToBeUpdated); } void SetlogFeature::slotPlaylistTableRenamed(int playlistId, const QString& newName) { Q_UNUSED(newName); //qDebug() << "slotPlaylistTableRenamed() playlistId:" << playlistId; - enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); - if (type == PlaylistDAO::PLHT_SET_LOG || - type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist - clearChildModel(); - m_lastRightClickedIndex = constructChildModel(playlistId); - if (type != PlaylistDAO::PLHT_UNKNOWN) { - if (playlistId == m_pPlaylistTableModel->getPlaylist()) { - activatePlaylist(playlistId); - } else if (m_pSidebarWidget) { - m_pSidebarWidget->selectChildIndex(indexFromPlaylistId(playlistId), false); - } - } + if (m_playlistDao.getHiddenType(playlistId) == PlaylistDAO::PLHT_SET_LOG) { + updateChildModel(QSet{playlistId}); } } void SetlogFeature::activate() { + // The root item was clicked, so actuvate the current playlist. + m_lastClickedIndex = m_pSidebarModel->getRootIndex(); activatePlaylist(m_playlistId); } void SetlogFeature::activatePlaylist(int playlistId) { - //qDebug() << "BasePlaylistFeature::activatePlaylist()" << playlistId; + // qDebug() << "SetlogFeature::activatePlaylist()" << playlistId; + if (playlistId == kInvalidPlaylistId) { + return; + } QModelIndex index = indexFromPlaylistId(playlistId); - if (playlistId != kInvalidPlaylistId && index.isValid()) { - m_pPlaylistTableModel->setTableModel(playlistId); - emit showTrackModel(m_pPlaylistTableModel); - emit enableCoverArtDisplay(true); - // Update selection only, if it is not the current playlist - // since we want the root item to be the current playlist as well - if (playlistId != m_playlistId) { - emit featureSelect(this, index); - activateChild(index); - } + VERIFY_OR_DEBUG_ASSERT(index.isValid()) { + return; + } + emit saveModelState(); + m_pPlaylistTableModel->setTableModel(playlistId); + emit showTrackModel(m_pPlaylistTableModel); + emit enableCoverArtDisplay(true); + // Update sidebar selection only if this is a child, incl. current playlist. + // indexFromPlaylistId() can't be used because, in case the root item was + // selected, that would switch to the 'current' child. + if (m_lastClickedIndex != m_pSidebarModel->getRootIndex()) { + m_lastClickedIndex = index; + emit featureSelect(this, index); + // redundant + // activateChild(index); + + // TODO(ronso0) Disable search for YEAR items + // emit disableSearch(); + // emit enableCoverArtDisplay(false); } } QString SetlogFeature::getRootViewHtml() const { - // Instead of the help text, the history shows the current entry instead + // Instead of the help text, the history shows the current playlist return QString(); } diff --git a/src/library/trackset/setlogfeature.h b/src/library/trackset/setlogfeature.h index d0cf14ca246..9d6c622920b 100644 --- a/src/library/trackset/setlogfeature.h +++ b/src/library/trackset/setlogfeature.h @@ -27,6 +27,8 @@ class SetlogFeature : public BasePlaylistFeature { void onRightClick(const QPoint& globalPos) override; void onRightClickChild(const QPoint& globalPos, const QModelIndex& index) override; void slotJoinWithPrevious(); + void slotLockAllChildPlaylists(); + void slotUnlockAllChildPlaylists(); void slotDeletePlaylist() override; void slotGetNewPlaylist(); void activate() override; @@ -39,18 +41,25 @@ class SetlogFeature : public BasePlaylistFeature { private slots: void slotPlayingTrackChanged(TrackPointer currentPlayingTrack); void slotPlaylistTableChanged(int playlistId) override; - void slotPlaylistContentChanged(QSet playlistIds) override; + void slotPlaylistContentOrLockChanged(const QSet& playlistIds) override; void slotPlaylistTableRenamed(int playlistId, const QString& newName) override; + void slotDeleteAllUnlockedChildPlaylists(); private: void deleteAllUnlockedPlaylistsWithFewerTracks(); - void reloadChildModel(int playlistId); + void lockOrUnlockAllChildPlaylists(bool lock); QString getRootViewHtml() const override; std::list m_recentTracks; QAction* m_pJoinWithPreviousAction; QAction* m_pStartNewPlaylist; + QAction* m_pLockAllChildPlaylists; + QAction* m_pUnlockAllChildPlaylists; + QAction* m_pDeleteAllChildPlaylists; + int m_playlistId; + int m_placeholderId; + QPointer m_libraryWidget; Library* m_pLibrary; UserSettingsPointer m_pConfig; diff --git a/src/library/traktor/traktorfeature.cpp b/src/library/traktor/traktorfeature.cpp index e6d524d6d5c..0cb61d2dd6d 100644 --- a/src/library/traktor/traktorfeature.cpp +++ b/src/library/traktor/traktorfeature.cpp @@ -513,7 +513,7 @@ void TraktorFeature::parsePlaylistEntries( } //playlist_id = id_query.lastInsertId().toInt(); - int playlist_id = -1; + int playlist_id = kInvalidPlaylistId; const int idColumn = id_query.record().indexOf("id"); while (id_query.next()) { playlist_id = id_query.value(idColumn).toInt(); diff --git a/src/widget/wlibrarysidebar.cpp b/src/widget/wlibrarysidebar.cpp index 33db934f438..6bcbef9584f 100644 --- a/src/widget/wlibrarysidebar.cpp +++ b/src/widget/wlibrarysidebar.cpp @@ -34,8 +34,13 @@ WLibrarySidebar::WLibrarySidebar(QWidget* parent) void WLibrarySidebar::contextMenuEvent(QContextMenuEvent *event) { //if (event->state() & Qt::RightButton) { //Dis shiz don werk on windowze - QModelIndex clickedItem = indexAt(event->pos()); - emit rightClicked(event->globalPos(), clickedItem); + const QModelIndex clickedIndex = indexAt(event->pos()); + if (!clickedIndex.isValid()) { + return; + } + // Use this instead of setCurrentIndex() to keep current selection + selectionModel()->setCurrentIndex(clickedIndex, QItemSelectionModel::NoUpdate); + emit rightClicked(event->globalPos(), clickedIndex); //} } @@ -173,20 +178,18 @@ void WLibrarySidebar::dropEvent(QDropEvent * event) { } void WLibrarySidebar::toggleSelectedItem() { - QModelIndexList selectedIndices = this->selectionModel()->selectedRows(); - if (selectedIndices.size() > 0) { - QModelIndex index = selectedIndices.at(0); + QModelIndex index = selectedIndex(); + if (index.isValid()) { // Activate the item so its content shows in the main library. - emit pressed(index); + emit clicked(index); // Expand or collapse the item as necessary. setExpanded(index, !isExpanded(index)); } } bool WLibrarySidebar::isLeafNodeSelected() { - QModelIndexList selectedIndices = this->selectionModel()->selectedRows(); - if (selectedIndices.size() > 0) { - QModelIndex index = selectedIndices.at(0); + QModelIndex index = selectedIndex(); + if (index.isValid()) { if(!index.model()->hasChildren(index)) { return true; } @@ -198,9 +201,48 @@ bool WLibrarySidebar::isLeafNodeSelected() { return false; } +bool WLibrarySidebar::isChildIndexSelected(const QModelIndex& index) { + // qDebug() << "WLibrarySidebar::isChildIndexSelected" << index; + QModelIndex selIndex = selectedIndex(); + if (!selIndex.isValid()) { + return false; + } + SidebarModel* sidebarModel = qobject_cast(model()); + VERIFY_OR_DEBUG_ASSERT(sidebarModel) { + // qDebug() << " >> model() is not SidebarModel"; + return false; + } + QModelIndex translated = sidebarModel->translateChildIndex(index); + if (!translated.isValid()) { + // qDebug() << " >> index can't be translated"; + return false; + } + return translated == selIndex; +} + +bool WLibrarySidebar::isFeatureRootIndexSelected(LibraryFeature* pFeature) { + // qDebug() << "WLibrarySidebar::isFeatureRootIndexSelected"; + QModelIndex selIndex = selectedIndex(); + if (!selIndex.isValid()) { + return false; + } + SidebarModel* sidebarModel = qobject_cast(model()); + VERIFY_OR_DEBUG_ASSERT(sidebarModel) { + return false; + } + const QModelIndex rootIndex = sidebarModel->getFeatureRootIndex(pFeature); + return rootIndex == selIndex; +} + +/// Invoked by actual keypresses (requires widget focus) and emulated keypresses +/// sent by LibraryControl 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. + switch (event->key()) { case Qt::Key_Return: + focusSelectedIndex(); toggleSelectedItem(); return; case Qt::Key_Down: @@ -209,19 +251,20 @@ void WLibrarySidebar::keyPressEvent(QKeyEvent* event) { case Qt::Key_PageUp: case Qt::Key_End: case Qt::Key_Home: { + // make the selected item the navigation starting point + focusSelectedIndex(); // Let the tree view move up and down for us. QTreeView::keyPressEvent(event); // After the selection changed force-activate (click) the newly selected // item to save us from having to push "Enter". - QModelIndexList selectedIndices = selectionModel()->selectedRows(); - if (selectedIndices.isEmpty()) { - return; - } - QModelIndex selIndex = selectedIndices.first(); - VERIFY_OR_DEBUG_ASSERT(selIndex.isValid()) { - qDebug() << "invalid sidebar index"; + QModelIndex selIndex = selectedIndex(); + if (!selIndex.isValid()) { return; } + // Ensure the new selection is visible even if it was already selected/ + // focused, like when the topmost item was selected but out of sight and + // we pressed Up, Home or PageUp. + scrollTo(selIndex); emit pressed(selIndex); return; } @@ -231,7 +274,7 @@ void WLibrarySidebar::keyPressEvent(QKeyEvent* event) { return; } // If an expanded item is selected let QTreeView collapse it - QModelIndex selIndex = selectedIndices.first(); + QModelIndex selIndex = selectedIndex(); VERIFY_OR_DEBUG_ASSERT(selIndex.isValid()) { qDebug() << "invalid sidebar index"; return; @@ -254,39 +297,26 @@ void WLibrarySidebar::keyPressEvent(QKeyEvent* event) { emit setLibraryFocus(FocusWidget::TracksTable); return; case kRenameSidebarItemShortcutKey: { // F2 - // Rename crate or playlist (internal, external, history - QModelIndexList selectedIndices = selectionModel()->selectedRows(); - if (selectedIndices.isEmpty()) { - return; - } - // If an expanded item is selected let QTreeView collapse it - QModelIndex selIndex = selectedIndices.first(); + // Rename crate or playlist (internal, external, history) + QModelIndex selIndex = selectedIndex(); VERIFY_OR_DEBUG_ASSERT(selIndex.isValid()) { qDebug() << "invalid sidebar index"; return; } if (isExpanded(selIndex)) { + // Root views / knots can not be renamed return; } emit renameItem(selIndex); return; } - case kHideRemoveShortcutKey: { // Del / Cmd+Backspace deletes items - // Rename crate or playlist (internal, external, history + case kHideRemoveShortcutKey: { // Del (macOS: Cmd+Backspace) + // Delete crate or playlist (internal, external, history) if (event->modifiers() != kHideRemoveShortcutModifier) { return; } - QModelIndexList selectedIndices = selectionModel()->selectedRows(); - if (selectedIndices.isEmpty()) { - return; - } - // If an expanded item is selected let QTreeView collapse it - QModelIndex selIndex = selectedIndices.first(); - VERIFY_OR_DEBUG_ASSERT(selIndex.isValid()) { - qDebug() << "invalid sidebar index"; - return; - } - if (isExpanded(selIndex)) { + QModelIndex selIndex = selectedIndex(); + if (!selIndex.isValid()) { return; } emit deleteItem(selIndex); @@ -297,6 +327,20 @@ void WLibrarySidebar::keyPressEvent(QKeyEvent* event) { } } +void WLibrarySidebar::mousePressEvent(QMouseEvent* event) { + // handle right click only in contextMenuEvent() to not select the clicked index + if (event->buttons().testFlag(Qt::RightButton)) { + return; + } + QTreeView::mousePressEvent(event); +} + +void WLibrarySidebar::focusInEvent(QFocusEvent* event) { + // Clear the current index, i.e. remove the focus indicator + selectionModel()->clearCurrentIndex(); + QTreeView::focusInEvent(event); +} + void WLibrarySidebar::selectIndex(const QModelIndex& index) { //qDebug() << "WLibrarySidebar::selectIndex" << index; if (!index.isValid()) { @@ -345,6 +389,27 @@ void WLibrarySidebar::selectChildIndex(const QModelIndex& index, bool selectItem scrollTo(translated, EnsureVisible); } +QModelIndex WLibrarySidebar::selectedIndex() { + QModelIndexList selectedIndices = selectionModel()->selectedRows(); + if (selectedIndices.isEmpty()) { + return QModelIndex(); + } + QModelIndex selIndex = selectedIndices.first(); + DEBUG_ASSERT(selIndex.isValid()); + return selIndex; +} + +/// Refocus the selected item after right-click +void WLibrarySidebar::focusSelectedIndex() { + // After the context menu was activated (and closed, with or without clicking + // an action), the currentIndex is the right-clicked item. + // If if the currentIndex is not selected, make the selection the currentIndex + QModelIndex selIndex = selectedIndex(); + if (selIndex.isValid() && selIndex != selectionModel()->currentIndex()) { + setCurrentIndex(selIndex); + } +} + bool WLibrarySidebar::event(QEvent* pEvent) { if (pEvent->type() == QEvent::ToolTip) { updateTooltip(); diff --git a/src/widget/wlibrarysidebar.h b/src/widget/wlibrarysidebar.h index 0bc67fc61b4..ece49c6fca9 100644 --- a/src/widget/wlibrarysidebar.h +++ b/src/widget/wlibrarysidebar.h @@ -14,6 +14,8 @@ #include "library/library_decl.h" #include "widget/wbasewidget.h" +class LibraryFeature; + class WLibrarySidebar : public QTreeView, public WBaseWidget { Q_OBJECT public: @@ -24,9 +26,13 @@ class WLibrarySidebar : public QTreeView, public WBaseWidget { void dragEnterEvent(QDragEnterEvent * event) override; void dropEvent(QDropEvent * event) override; void keyPressEvent(QKeyEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void focusInEvent(QFocusEvent* event) override; void timerEvent(QTimerEvent* event) override; void toggleSelectedItem(); bool isLeafNodeSelected(); + bool isChildIndexSelected(const QModelIndex& index); + bool isFeatureRootIndexSelected(LibraryFeature* pFeature); public slots: void selectIndex(const QModelIndex&); @@ -43,6 +49,9 @@ class WLibrarySidebar : public QTreeView, public WBaseWidget { bool event(QEvent* pEvent) override; private: + void focusSelectedIndex(); + QModelIndex selectedIndex(); + QBasicTimer m_expandTimer; QModelIndex m_hoverIndex; };