diff --git a/src/library/crate/cratestorage.cpp b/src/library/crate/cratestorage.cpp index a5e1343a3324..05afdfcde2e8 100644 --- a/src/library/crate/cratestorage.cpp +++ b/src/library/crate/cratestorage.cpp @@ -81,6 +81,26 @@ class CrateQueryBinder { FwdSqlQuery& m_query; }; +const QChar kSqlListSeparator(','); + +// It is not possible to bind multiple values as a list to a query. +// The list of track ids has to be transformed into a single list +// string before it can be used in an SQL query. +QString joinSqlStringList(const QList& trackIds) { + QString joinedTrackIds; + // Reserve memory up front to prevent reallocation. Here we + // assume that all track ids fit into 6 decimal digits and + // add 1 character for the list separator. + joinedTrackIds.reserve((6 + 1) * trackIds.size()); + for (const auto& trackId: trackIds) { + if (!joinedTrackIds.isEmpty()) { + joinedTrackIds += kSqlListSeparator; + } + joinedTrackIds += trackId.toString(); + } + return joinedTrackIds; +} + } // anonymous namespace @@ -468,6 +488,29 @@ CrateTrackSelectResult CrateStorage::selectTrackCratesSorted(TrackId trackId) co } } +CrateSummarySelectResult CrateStorage::selectCratesWithTrackCount(const QList& trackIds) const { + FwdSqlQuery query(m_database, QString( + "SELECT *, (" + " SELECT COUNT(*) FROM %1 WHERE %2.%3 = %1.%4 and %1.%5 in (%9)" + " ) AS %6, 0 as %7 FROM %2 ORDER BY %8").arg( + CRATE_TRACKS_TABLE, + CRATE_TABLE, + CRATETABLE_ID, + CRATETRACKSTABLE_CRATEID, + CRATETRACKSTABLE_TRACKID, + CRATESUMMARY_TRACK_COUNT, + CRATESUMMARY_TRACK_DURATION, + CRATETABLE_NAME, + joinSqlStringList(trackIds))); + + if (query.execPrepared()) { + return CrateSummarySelectResult(std::move(query)); + } else { + return CrateSummarySelectResult(); + } +} + + CrateTrackSelectResult CrateStorage::selectTracksSortedByCrateNameLike(const QString& crateNameLike) const { FwdSqlQuery query(m_database, QString( diff --git a/src/library/crate/cratestorage.h b/src/library/crate/cratestorage.h index 1dfa16ce3b8e..9c484e58e2d8 100644 --- a/src/library/crate/cratestorage.h +++ b/src/library/crate/cratestorage.h @@ -276,6 +276,8 @@ class CrateStorage: public virtual /*implements*/ SqlStorage { CrateId crateId) const; CrateTrackSelectResult selectTrackCratesSorted( TrackId trackId) const; + CrateSummarySelectResult selectCratesWithTrackCount( + const QList& trackIds) const; CrateTrackSelectResult selectTracksSortedByCrateNameLike( const QString& crateNameLike) const; @@ -284,7 +286,6 @@ class CrateStorage: public virtual /*implements*/ SqlStorage { QSet collectCrateIdsOfTracks( const QList& trackIds) const; - ///////////////////////////////////////////////////////////////////////// // CrateSummary view operations (read-only, const) ///////////////////////////////////////////////////////////////////////// diff --git a/src/library/setlogfeature.cpp b/src/library/setlogfeature.cpp index 530e157c21e6..dc95240048d5 100644 --- a/src/library/setlogfeature.cpp +++ b/src/library/setlogfeature.cpp @@ -10,12 +10,15 @@ #include "library/treeitem.h" #include "mixer/playerinfo.h" #include "mixer/playermanager.h" +#include "widget/wtracktableview.h" +#include "widget/wlibrary.h" SetlogFeature::SetlogFeature(QObject* parent, UserSettingsPointer pConfig, TrackCollection* pTrackCollection) : BasePlaylistFeature(parent, pConfig, pTrackCollection, "SETLOGHOME"), - m_playlistId(-1) { + m_playlistId(-1), + m_libraryWidget(nullptr) { m_pPlaylistTableModel = new PlaylistTableModel(this, pTrackCollection, "mixxx.db.model.setlog", true); //show all tracks @@ -60,6 +63,8 @@ void SetlogFeature::bindWidget(WLibrary* libraryWidget, keyboard); connect(&PlayerInfo::instance(), SIGNAL(currentPlayingTrackChanged(TrackPointer)), this, SLOT(slotPlayingTrackChanged(TrackPointer))); + m_libraryWidget = libraryWidget; + } void SetlogFeature::onRightClick(const QPoint& globalPos) { @@ -268,7 +273,19 @@ void SetlogFeature::slotPlayingTrackChanged(TrackPointer currentPlayingTrack) { if (m_pPlaylistTableModel->getPlaylist() == m_playlistId) { // View needs a refresh - m_pPlaylistTableModel->appendTrack(currentPlayingTrackId); + + WTrackTableView* view = dynamic_cast(m_libraryWidget->getActiveView()); + if (view != nullptr) { + // We have a active view on the history. The user may have some + // important active selection. For example putting track into crates + // while the song changes trough autodj. The selection is then lost + // and dataloss occures + const QList trackIds = view->getSelectedTrackIds(); + m_pPlaylistTableModel->appendTrack(currentPlayingTrackId); + view->setSelectedTracks(trackIds); + } else { + m_pPlaylistTableModel->appendTrack(currentPlayingTrackId); + } } else { // TODO(XXX): Care whether the append succeeded. m_playlistDao.appendTrackToPlaylist(currentPlayingTrackId, diff --git a/src/library/setlogfeature.h b/src/library/setlogfeature.h index d02b25e4e6ea..22833affa2cc 100644 --- a/src/library/setlogfeature.h +++ b/src/library/setlogfeature.h @@ -49,6 +49,7 @@ class SetlogFeature : public BasePlaylistFeature { QAction* m_pJoinWithPreviousAction; QAction* m_pGetNewPlaylist; int m_playlistId; + WLibrary* m_libraryWidget; }; #endif // SETLOGFEATURE_H diff --git a/src/track/track.cpp b/src/track/track.cpp index 8ab68fafe650..da88fdd4d84a 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -746,6 +746,25 @@ void Track::removeCue(const CuePointer& pCue) { emit(cuesUpdated()); } +void Track::removeCuesOfType(Cue::CueType type) { + QMutexLocker lock(&m_qMutex); + bool dirty = false; + QMutableListIterator it(m_cuePoints); + while (it.hasNext()) { + CuePointer pCue = it.next(); + // FIXME: Why does this only work for the CUE CueType? + if (pCue->getType() == type) { + disconnect(pCue.get(), 0, this, 0); + it.remove(); + dirty = true; + } + } + if (dirty) { + markDirtyAndUnlock(&lock); + emit(cuesUpdated()); + } +} + QList Track::getCuePoints() const { QMutexLocker lock(&m_qMutex); return m_cuePoints; diff --git a/src/track/track.h b/src/track/track.h index f388fd65333c..1c4d1a833341 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -246,6 +246,7 @@ class Track : public QObject { // Calls for managing the track's cue points CuePointer createAndAddCue(); void removeCue(const CuePointer& pCue); + void removeCuesOfType(Cue::CueType); QList getCuePoints() const; void setCuePoints(const QList& cuePoints); diff --git a/src/util/parented_ptr.h b/src/util/parented_ptr.h index e54bdb7f958e..7acfc4877e02 100644 --- a/src/util/parented_ptr.h +++ b/src/util/parented_ptr.h @@ -100,7 +100,7 @@ inline bool operator== (const parented_ptr& lhs, const U* rhs) { } template -inline bool operator== (const parented_ptr& lhs, const parented_ptr& rhs) const { +inline bool operator== (const parented_ptr& lhs, const parented_ptr& rhs) { return lhs.get() == rhs.get(); } @@ -115,7 +115,7 @@ inline bool operator!= (const parented_ptr& lhs, const U* rhs) { } template -inline bool operator!= (const parented_ptr& lhs, const parented_ptr& rhs) const { +inline bool operator!= (const parented_ptr& lhs, const parented_ptr& rhs) { return !(lhs.get() == rhs.get()); } diff --git a/src/widget/wcoverartmenu.cpp b/src/widget/wcoverartmenu.cpp index 8a47eff32667..51a6ce1a5035 100644 --- a/src/widget/wcoverartmenu.cpp +++ b/src/widget/wcoverartmenu.cpp @@ -22,7 +22,7 @@ void WCoverArtMenu::createActions() { connect(m_pChange, SIGNAL(triggered()), this, SLOT(slotChange())); addAction(m_pChange); - m_pUnset = new QAction(tr("Unset cover", + m_pUnset = new QAction(tr("Clear cover", "clears the set cover art -- does not touch files on disk"), this); connect(m_pUnset, SIGNAL(triggered()), this, SLOT(slotUnset())); addAction(m_pUnset); diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index 8a030daedefa..b8fa24f16022 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -4,6 +4,9 @@ #include #include #include +#include +#include +#include #include "widget/wtracktableview.h" @@ -28,6 +31,7 @@ #include "util/dnd.h" #include "util/time.h" #include "util/assert.h" +#include "util/parented_ptr.h" WTrackTableView::WTrackTableView(QWidget * parent, UserSettingsPointer pConfig, @@ -44,7 +48,9 @@ WTrackTableView::WTrackTableView(QWidget * parent, m_iCoverHashColumn(-1), m_iCoverColumn(-1), m_selectionChangedSinceLastGuiTick(true), - m_loadCachedOnly(false) { + m_loadCachedOnly(false), + m_bPlaylistMenuLoaded(false), + m_bCrateMenuLoaded(false) { connect(&m_loadTrackMapper, SIGNAL(mapped(QString)), this, SLOT(loadSelectionToGroup(QString))); @@ -65,16 +71,35 @@ WTrackTableView::WTrackTableView(QWidget * parent, m_pMenu = new QMenu(this); + m_pLoadToMenu = new QMenu(this); + m_pLoadToMenu->setTitle(tr("Load to")); + m_pDeckMenu = new QMenu(this); + m_pDeckMenu->setTitle(tr("Deck")); m_pSamplerMenu = new QMenu(this); - m_pSamplerMenu->setTitle(tr("Load to Sampler")); + m_pSamplerMenu->setTitle(tr("Sampler")); + m_pPlaylistMenu = new QMenu(this); m_pPlaylistMenu->setTitle(tr("Add to Playlist")); + connect(m_pPlaylistMenu, SIGNAL(aboutToShow()), + this, SLOT(slotPopulatePlaylistMenu())); m_pCrateMenu = new QMenu(this); - m_pCrateMenu->setTitle(tr("Add to Crate")); + m_pCrateMenu->setTitle(tr("Crates")); + connect(m_pCrateMenu, SIGNAL(aboutToShow()), + this, SLOT(slotPopulateCrateMenu())); + + m_pMetadataMenu = new QMenu(this); + m_pMetadataMenu->setTitle("Metadata"); + m_pBPMMenu = new QMenu(this); - m_pBPMMenu->setTitle(tr("BPM Options")); + m_pBPMMenu->setTitle(tr("Change BPM")); + + m_pClearMetadataMenu = new QMenu(this); + //: Clear metadata in right click track context menu in library + m_pClearMetadataMenu->setTitle(tr("Clear")); + m_pCoverMenu = new WCoverArtMenu(this); m_pCoverMenu->setTitle(tr("Cover Art")); + connect(m_pCoverMenu, SIGNAL(coverInfoSelected(const CoverInfo&)), this, SLOT(slotCoverInfoSelected(const CoverInfo&))); connect(m_pCoverMenu, SIGNAL(reloadCoverArt()), @@ -94,8 +119,9 @@ WTrackTableView::WTrackTableView(QWidget * parent, connect(&m_playlistMapper, SIGNAL(mapped(int)), this, SLOT(addSelectionToPlaylist(int))); - connect(&m_crateMapper, SIGNAL(mapped(int)), - this, SLOT(addSelectionToCrate(int))); + + connect(&m_crateMapper, SIGNAL(mapped(QWidget *)), + this, SLOT(updateSelectionCrates(QWidget *))); m_pCOTGuiTick = new ControlProxy("[Master]", "guiTick50ms", this); m_pCOTGuiTick->connectValueChanged(SLOT(slotGuiTick50ms(double))); @@ -128,9 +154,14 @@ WTrackTableView::~WTrackTableView() { delete m_pUnhideAct; delete m_pPropertiesAct; delete m_pMenu; + delete m_pLoadToMenu; + delete m_pDeckMenu; + delete m_pSamplerMenu; delete m_pPlaylistMenu; - delete m_pCoverMenu; delete m_pCrateMenu; + delete m_pMetadataMenu; + delete m_pClearMetadataMenu; + delete m_pCoverMenu; delete m_pBpmLockAction; delete m_pBpmUnlockAction; delete m_pBpmDoubleAction; @@ -141,12 +172,15 @@ WTrackTableView::~WTrackTableView() { delete m_pBpmThreeHalvesAction; delete m_pBPMMenu; delete m_pClearBeatsAction; + delete m_pClearPlayCountAction; + delete m_pClearMainCueAction; + delete m_pClearHotCuesAction; + delete m_pClearLoopAction; + delete m_pClearReplayGainAction; delete m_pClearWaveformAction; - delete m_pReplayGainResetAction; + delete m_pClearAllMetadataAction; delete m_pPurgeAct; delete m_pFileBrowserAct; - delete m_pResetPlayedAct; - delete m_pSamplerMenu; } void WTrackTableView::enableCachedOnly() { @@ -401,40 +435,71 @@ void WTrackTableView::createActions() { connect(m_pFileBrowserAct, SIGNAL(triggered()), this, SLOT(slotOpenInFileBrowser())); - m_pAutoDJBottomAct = new QAction(tr("Add to Auto DJ Queue (bottom)"), this); + m_pAutoDJBottomAct = new QAction(tr("Add to Auto DJ Queue (Bottom)"), this); connect(m_pAutoDJBottomAct, SIGNAL(triggered()), this, SLOT(slotSendToAutoDJBottom())); - m_pAutoDJTopAct = new QAction(tr("Add to Auto DJ Queue (top)"), this); + m_pAutoDJTopAct = new QAction(tr("Add to Auto DJ Queue (Top)"), this); connect(m_pAutoDJTopAct, SIGNAL(triggered()), this, SLOT(slotSendToAutoDJTop())); - m_pAutoDJReplaceAct = new QAction(tr("Add to Auto DJ Queue (replace)"), this); + m_pAutoDJReplaceAct = new QAction(tr("Add to Auto DJ Queue (Replace)"), this); connect(m_pAutoDJReplaceAct, SIGNAL(triggered()), this, SLOT(slotSendToAutoDJReplace())); - m_pImportMetadataFromFileAct = new QAction(tr("Import Metadata from File"), this); + m_pImportMetadataFromFileAct = new QAction(tr("Import From File Tags"), this); connect(m_pImportMetadataFromFileAct, SIGNAL(triggered()), this, SLOT(slotImportTrackMetadataFromFileTags())); - m_pImportMetadataFromMusicBrainzAct = new QAction(tr("Import Metadata from MusicBrainz"),this); + m_pImportMetadataFromMusicBrainzAct = new QAction(tr("Import From MusicBrainz"),this); connect(m_pImportMetadataFromMusicBrainzAct, SIGNAL(triggered()), this, SLOT(slotShowDlgTagFetcher())); - m_pExportMetadataAct = new QAction(tr("Export Metadata into File"), this); + m_pExportMetadataAct = new QAction(tr("Export To File Tags"), this); connect(m_pExportMetadataAct, SIGNAL(triggered()), this, SLOT(slotExportTrackMetadataIntoFileTags())); - m_pAddToPreviewDeck = new QAction(tr("Load to Preview Deck"), this); + m_pAddToPreviewDeck = new QAction(tr("Preview Deck"), this); // currently there is only one preview deck so just map it here. QString previewDeckGroup = PlayerManager::groupForPreviewDeck(0); m_deckMapper.setMapping(m_pAddToPreviewDeck, previewDeckGroup); connect(m_pAddToPreviewDeck, SIGNAL(triggered()), &m_deckMapper, SLOT(map())); - m_pResetPlayedAct = new QAction(tr("Reset Play Count"), this); - connect(m_pResetPlayedAct, SIGNAL(triggered()), - this, SLOT(slotResetPlayed())); + + // Clear metadata actions + m_pClearBeatsAction = new QAction(tr("BPM and Beatgrid"), this); + connect(m_pClearBeatsAction, SIGNAL(triggered()), + this, SLOT(slotClearBeats())); + + m_pClearPlayCountAction = new QAction(tr("Play Count"), this); + connect(m_pClearPlayCountAction, SIGNAL(triggered()), + this, SLOT(slotClearPlayCount())); + + m_pClearMainCueAction = new QAction(tr("Cue Point"), this); + connect(m_pClearMainCueAction, SIGNAL(triggered()), + this, SLOT(slotClearMainCue())); + + m_pClearHotCuesAction = new QAction(tr("Hotcues"), this); + connect(m_pClearHotCuesAction, SIGNAL(triggered()), + this, SLOT(slotClearHotCues())); + + m_pClearLoopAction = new QAction(tr("Loop"), this); + connect(m_pClearLoopAction, SIGNAL(triggered()), + this, SLOT(slotClearLoop())); + + m_pClearReplayGainAction = new QAction(tr("ReplayGain"), this); + connect(m_pClearReplayGainAction, SIGNAL(triggered()), + this, SLOT(slotClearReplayGain())); + + m_pClearWaveformAction = new QAction(tr("Waveform"), this); + connect(m_pClearWaveformAction, SIGNAL(triggered()), + this, SLOT(slotClearWaveform())); + + m_pClearAllMetadataAction = new QAction(tr("All"), this); + connect(m_pClearAllMetadataAction, SIGNAL(triggered()), + this, SLOT(slotClearAllMetadata())); + m_pBpmLockAction = new QAction(tr("Lock BPM"), this); m_pBpmUnlockAction = new QAction(tr("Unlock BPM"), this); @@ -443,7 +508,7 @@ void WTrackTableView::createActions() { connect(m_pBpmUnlockAction, SIGNAL(triggered()), this, SLOT(slotUnlockBpm())); - //new BPM actions + //BPM edit actions m_pBpmDoubleAction = new QAction(tr("Double BPM"), this); m_pBpmHalveAction = new QAction(tr("Halve BPM"), this); m_pBpmTwoThirdsAction = new QAction(tr("2/3 BPM"), this); @@ -470,18 +535,6 @@ void WTrackTableView::createActions() { &m_BpmMapper, SLOT(map())); connect(m_pBpmThreeHalvesAction, SIGNAL(triggered()), &m_BpmMapper, SLOT(map())); - - m_pClearBeatsAction = new QAction(tr("Clear BPM and Beatgrid"), this); - connect(m_pClearBeatsAction, SIGNAL(triggered()), - this, SLOT(slotClearBeats())); - - m_pClearWaveformAction = new QAction(tr("Clear Waveform"), this); - connect(m_pClearWaveformAction, SIGNAL(triggered()), - this, SLOT(slotClearWaveform())); - - m_pReplayGainResetAction = new QAction(tr("Reset ReplayGain"), this); - connect(m_pReplayGainResetAction, SIGNAL(triggered()), - this, SLOT(slotReplayGainReset())); } // slot @@ -757,14 +810,17 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) { m_pMenu->clear(); if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_ADDTOAUTODJ)) { + m_pMenu->clear(); m_pMenu->addAction(m_pAutoDJBottomAct); m_pMenu->addAction(m_pAutoDJTopAct); m_pMenu->addAction(m_pAutoDJReplaceAct); m_pMenu->addSeparator(); } + m_pLoadToMenu->clear(); if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_LOADTODECK)) { int iNumDecks = m_pNumDecks->get(); + m_pDeckMenu->clear(); if (iNumDecks > 0) { for (int i = 1; i <= iNumDecks; ++i) { // PlayerManager::groupForDeck is 0-indexed. @@ -774,19 +830,20 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) { bool loadTrackIntoPlayingDeck = m_pConfig->getValue( ConfigKey("[Controls]", "AllowTrackLoadToPlayingDeck")); bool deckEnabled = (!deckPlaying || loadTrackIntoPlayingDeck) && oneSongSelected; - QAction* pAction = new QAction(tr("Load to Deck %1").arg(i), m_pMenu); + QAction* pAction = new QAction(tr("Deck %1").arg(i), m_pMenu); pAction->setEnabled(deckEnabled); - m_pMenu->addAction(pAction); + m_pDeckMenu->addAction(pAction); m_deckMapper.setMapping(pAction, deckGroup); connect(pAction, SIGNAL(triggered()), &m_deckMapper, SLOT(map())); } } + m_pLoadToMenu->addMenu(m_pDeckMenu); } if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_LOADTOSAMPLER)) { int iNumSamplers = m_pNumSamplers->get(); if (iNumSamplers > 0) { - m_pSamplerMenu->clear(); + m_pSamplerMenu->clear(); for (int i = 1; i <= iNumSamplers; ++i) { // PlayerManager::groupForSampler is 0-indexed. QString samplerGroup = PlayerManager::groupForSampler(i - 1); @@ -799,70 +856,74 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) { m_samplerMapper.setMapping(pAction, samplerGroup); connect(pAction, SIGNAL(triggered()), &m_samplerMapper, SLOT(map())); } - m_pMenu->addMenu(m_pSamplerMenu); + m_pLoadToMenu->addMenu(m_pSamplerMenu); } } if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_LOADTOPREVIEWDECK) && m_pNumPreviewDecks->get() > 0.0) { - m_pMenu->addAction(m_pAddToPreviewDeck); + m_pLoadToMenu->addAction(m_pAddToPreviewDeck); } + m_pMenu->addMenu(m_pLoadToMenu); m_pMenu->addSeparator(); if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_ADDTOPLAYLIST)) { - m_pPlaylistMenu->clear(); - PlaylistDAO& playlistDao = m_pTrackCollection->getPlaylistDAO(); - QMap playlists; - int numPlaylists = playlistDao.playlistCount(); - for (int i = 0; i < numPlaylists; ++i) { - int iPlaylistId = playlistDao.getPlaylistId(i); - playlists.insert(playlistDao.getPlaylistName(iPlaylistId), iPlaylistId); - } - QMapIterator it(playlists); - while (it.hasNext()) { - it.next(); - if (!playlistDao.isHidden(it.value())) { - // No leak because making the menu the parent means they will be - // auto-deleted - auto pAction = new QAction(it.key(), m_pPlaylistMenu); - bool locked = playlistDao.isPlaylistLocked(it.value()); - pAction->setEnabled(!locked); - m_pPlaylistMenu->addAction(pAction); - m_playlistMapper.setMapping(pAction, it.value()); - connect(pAction, SIGNAL(triggered()), &m_playlistMapper, SLOT(map())); - } - } - m_pPlaylistMenu->addSeparator(); - QAction* newPlaylistAction = new QAction(tr("Create New Playlist"), m_pPlaylistMenu); - m_pPlaylistMenu->addAction(newPlaylistAction); - m_playlistMapper.setMapping(newPlaylistAction, -1);// -1 to signify new playlist - connect(newPlaylistAction, SIGNAL(triggered()), &m_playlistMapper, SLOT(map())); - + // Playlist menu is lazy loaded on hover by slotPopulatePlaylistMenu + // to avoid unnecessary database queries + m_bPlaylistMenuLoaded = false; m_pMenu->addMenu(m_pPlaylistMenu); } if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_ADDTOCRATE)) { - m_pCrateMenu->clear(); - CrateSelectResult allCrates(m_pTrackCollection->crates().selectCrates()); - Crate crate; - while (allCrates.populateNext(&crate)) { - auto pAction = std::make_unique(crate.getName(), m_pCrateMenu); - pAction->setEnabled(!crate.isLocked()); - m_crateMapper.setMapping(pAction.get(), crate.getId().toInt()); - connect(pAction.get(), SIGNAL(triggered()), &m_crateMapper, SLOT(map())); - m_pCrateMenu->addAction(pAction.get()); - pAction.release(); - } - m_pCrateMenu->addSeparator(); - QAction* newCrateAction = new QAction(tr("Create New Crate"), m_pCrateMenu); - m_pCrateMenu->addAction(newCrateAction); - m_crateMapper.setMapping(newCrateAction, CrateId().toInt());// invalid crate id for new crate - connect(newCrateAction, SIGNAL(triggered()), &m_crateMapper, SLOT(map())); - + // Crate menu is lazy loaded on hover by slotPopulateCrateMenu + // to avoid unnecessary database queries + m_bCrateMenuLoaded = false; m_pMenu->addMenu(m_pCrateMenu); } + + m_pMenu->addSeparator(); + m_pMetadataMenu->clear(); + + if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_IMPORTMETADATA)) { + m_pMetadataMenu->addAction(m_pImportMetadataFromFileAct); + m_pImportMetadataFromMusicBrainzAct->setEnabled(oneSongSelected); + m_pMetadataMenu->addAction(m_pImportMetadataFromMusicBrainzAct); + m_pMetadataMenu->addAction(m_pExportMetadataAct); + } + + m_pClearMetadataMenu->clear(); + + if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_CLEAR_BEATS)) { + if (trackModel == nullptr) { + return; + } + bool allowClear = true; + int column = trackModel->fieldIndex("bpm_lock"); + for (int i = 0; i < indices.size() && allowClear; ++i) { + int row = indices.at(i).row(); + QModelIndex index = indices.at(i).sibling(row,column); + if (index.data().toBool()) { + allowClear = false; + } + } + m_pClearBeatsAction->setEnabled(allowClear); + m_pClearMetadataMenu->addAction(m_pClearBeatsAction); + } + + if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_RESETPLAYED)) { + m_pClearMetadataMenu->addAction(m_pClearPlayCountAction); + } + + //FIXME: Why are clearning the main cue and loop not working? +// m_pClearMetadataMenu->addAction(m_pClearMainCueAction); + m_pClearMetadataMenu->addAction(m_pClearHotCuesAction); +// m_pClearMetadataMenu->addAction(m_pClearLoopAction); + m_pClearMetadataMenu->addAction(m_pClearReplayGainAction); + m_pClearMetadataMenu->addAction(m_pClearWaveformAction); + m_pClearMetadataMenu->addSeparator(); + m_pClearMetadataMenu->addAction(m_pClearAllMetadataAction); //start of BPM section of menu if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_MANIPULATEBEATS)) { @@ -933,38 +994,9 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) { } } //add BPM menu to pMenu - m_pMenu->addMenu(m_pBPMMenu); + m_pMetadataMenu->addMenu(m_pBPMMenu); //end of BPM section of menu - if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_CLEAR_BEATS)) { - if (trackModel == nullptr) { - return; - } - bool allowClear = true; - int column = trackModel->fieldIndex("bpm_lock"); - for (int i = 0; i < indices.size() && allowClear; ++i) { - int row = indices.at(i).row(); - QModelIndex index = indices.at(i).sibling(row,column); - if (index.data().toBool()) { - allowClear = false; - } - } - m_pClearBeatsAction->setEnabled(allowClear); - m_pBPMMenu->addAction(m_pClearBeatsAction); - } - - m_pMenu->addAction(m_pClearWaveformAction); - - m_pMenu->addAction(m_pReplayGainResetAction); - - m_pMenu->addSeparator(); - if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_IMPORTMETADATA)) { - m_pMenu->addAction(m_pImportMetadataFromFileAct); - m_pImportMetadataFromMusicBrainzAct->setEnabled(oneSongSelected); - m_pMenu->addAction(m_pImportMetadataFromMusicBrainzAct); - m_pMenu->addAction(m_pExportMetadataAct); - } - // Cover art menu only applies if at least one track is selected. if (indices.size()) { // We load a single track to get the necessary context for the cover (we use @@ -981,9 +1013,12 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) { info.coverLocation = last.sibling( last.row(), m_iCoverLocationColumn).data().toString(); m_pCoverMenu->setCoverArt(info); - m_pMenu->addMenu(m_pCoverMenu); + m_pMetadataMenu->addMenu(m_pCoverMenu); } + m_pMenu->addMenu(m_pMetadataMenu); + m_pMenu->addMenu(m_pClearMetadataMenu); + // REMOVE and HIDE should not be at the first menu position to avoid accidental clicks m_pMenu->addSeparator(); bool locked = modelHasCapabilities(TrackModel::TRACKMODELCAPS_LOCKED); @@ -1003,9 +1038,6 @@ void WTrackTableView::contextMenuEvent(QContextMenuEvent* event) { m_pPurgeAct->setEnabled(!locked); m_pMenu->addAction(m_pPurgeAct); } - if (modelHasCapabilities(TrackModel::TRACKMODELCAPS_RESETPLAYED)) { - m_pMenu->addAction(m_pResetPlayedAct); - } m_pMenu->addAction(m_pFileBrowserAct); m_pMenu->addSeparator(); m_pPropertiesAct->setEnabled(oneSongSelected); @@ -1373,6 +1405,31 @@ QList WTrackTableView::getSelectedTrackIds() const { return trackIds; } +void WTrackTableView::setSelectedTracks(const QList& trackIds) { + QItemSelectionModel* pSelectionModel = selectionModel(); + VERIFY_OR_DEBUG_ASSERT(pSelectionModel != nullptr) { + qWarning() << "No selected tracks available"; + return; + } + + TrackModel* pTrackModel = getTrackModel(); + VERIFY_OR_DEBUG_ASSERT(pTrackModel != nullptr) { + qWarning() << "No selected tracks available"; + return; + } + + for (const auto& trackId : trackIds) { + const QLinkedList gts = pTrackModel->getTrackRows(trackId); + + QLinkedList::const_iterator i; + for (i = gts.constBegin(); i != gts.constEnd(); ++i) { + pSelectionModel->select(model()->index(*i, 0), + QItemSelectionModel::Select | QItemSelectionModel::Rows); + } + } +} + + void WTrackTableView::sendToAutoDJ(PlaylistDAO::AutoDJSendLoc loc) { if (!modelHasCapabilities(TrackModel::TRACKMODELCAPS_ADDTOAUTODJ)) { return; @@ -1446,7 +1503,7 @@ void WTrackTableView::slotExportTrackMetadataIntoFileTags() { } //slot for reset played count, sets count to 0 of one or more tracks -void WTrackTableView::slotResetPlayed() { +void WTrackTableView::slotClearPlayCount() { QModelIndexList indices = selectionModel()->selectedRows(); TrackModel* trackModel = getTrackModel(); @@ -1462,6 +1519,43 @@ void WTrackTableView::slotResetPlayed() { } } +void WTrackTableView::slotPopulatePlaylistMenu() { + // The user may open the Playlist submenu, move their cursor away, then + // return to the Playlist submenu before exiting the track context menu. + // Avoid querying the database multiple times in that case. + if (m_bPlaylistMenuLoaded) { + return; + } + m_pPlaylistMenu->clear(); + PlaylistDAO& playlistDao = m_pTrackCollection->getPlaylistDAO(); + QMap playlists; + int numPlaylists = playlistDao.playlistCount(); + for (int i = 0; i < numPlaylists; ++i) { + int iPlaylistId = playlistDao.getPlaylistId(i); + playlists.insert(playlistDao.getPlaylistName(iPlaylistId), iPlaylistId); + } + QMapIterator it(playlists); + while (it.hasNext()) { + it.next(); + if (!playlistDao.isHidden(it.value())) { + // No leak because making the menu the parent means they will be + // auto-deleted + auto pAction = new QAction(it.key(), m_pPlaylistMenu); + bool locked = playlistDao.isPlaylistLocked(it.value()); + pAction->setEnabled(!locked); + m_pPlaylistMenu->addAction(pAction); + m_playlistMapper.setMapping(pAction, it.value()); + connect(pAction, SIGNAL(triggered()), &m_playlistMapper, SLOT(map())); + } + } + m_pPlaylistMenu->addSeparator(); + QAction* newPlaylistAction = new QAction(tr("Create New Playlist"), m_pPlaylistMenu); + m_pPlaylistMenu->addAction(newPlaylistAction); + m_playlistMapper.setMapping(newPlaylistAction, -1);// -1 to signify new playlist + connect(newPlaylistAction, SIGNAL(triggered()), &m_playlistMapper, SLOT(map())); + m_bPlaylistMenuLoaded = true; +} + void WTrackTableView::addSelectionToPlaylist(int iPlaylistId) { const QList trackIds = getSelectedTrackIds(); if (trackIds.isEmpty()) { @@ -1513,22 +1607,104 @@ void WTrackTableView::addSelectionToPlaylist(int iPlaylistId) { playlistDao.appendTracksToPlaylist(trackIds, iPlaylistId); } -void WTrackTableView::addSelectionToCrate(int iCrateId) { +void WTrackTableView::slotPopulateCrateMenu() { + // The user may open the Crate submenu, move their cursor away, then + // return to the Crate submenu before exiting the track context menu. + // Avoid querying the database multiple times in that case. + if (m_bCrateMenuLoaded) { + return; + } + m_pCrateMenu->clear(); + const QList trackIds = getSelectedTrackIds(); + + CrateSummarySelectResult allCrates(m_pTrackCollection->crates().selectCratesWithTrackCount(trackIds)); + + CrateSummary crate; + while (allCrates.populateNext(&crate)) { + auto pAction = make_parented(m_pCrateMenu); + auto pCheckBox = make_parented(m_pCrateMenu); + + pCheckBox->setText(crate.getName()); + pCheckBox->setProperty("crateId", + QVariant::fromValue(crate.getId())); + pCheckBox->setEnabled(!crate.isLocked()); + pAction->setEnabled(!crate.isLocked()); + pAction->setDefaultWidget(pCheckBox.get()); + + if (crate.getTrackCount() == 0) { + pCheckBox->setChecked(false); + } else if (crate.getTrackCount() == (uint)trackIds.length()) { + pCheckBox->setChecked(true); + } else { + pCheckBox->setTristate(true); + pCheckBox->setCheckState(Qt::PartiallyChecked); + } + + m_crateMapper.setMapping(pAction.get(), pCheckBox.get()); + m_crateMapper.setMapping(pCheckBox.get(), pCheckBox.get()); + m_pCrateMenu->addAction(pAction.get()); + connect(pAction.get(), SIGNAL(triggered()), + &m_crateMapper, SLOT(map())); + connect(pCheckBox.get(), SIGNAL(stateChanged(int)), + &m_crateMapper, SLOT(map())); + + } + m_pCrateMenu->addSeparator(); + QAction* newCrateAction = new QAction(tr("Create New Crate"), m_pCrateMenu); + m_pCrateMenu->addAction(newCrateAction); + connect(newCrateAction, SIGNAL(triggered()), this, SLOT(addSelectionToNewCrate())); + m_bCrateMenuLoaded = true; +} + +void WTrackTableView::updateSelectionCrates(QWidget* pWidget) { + auto pCheckBox = qobject_cast(pWidget); + VERIFY_OR_DEBUG_ASSERT(pCheckBox) { + qWarning() << "crateId is not of CrateId type"; + return; + } + CrateId crateId = pCheckBox->property("crateId").value(); + const QList trackIds = getSelectedTrackIds(); + if (trackIds.isEmpty()) { qWarning() << "No tracks selected for crate"; return; } - CrateId crateId(iCrateId); - if (!crateId.isValid()) { // i.e. a new crate is suppose to be created - crateId = CrateFeatureHelper( - m_pTrackCollection, m_pConfig).createEmptyCrate(); + // we need to disable tristate again as the mixed state will now be gone and can't be brought back + pCheckBox->setTristate(false); + if(!pCheckBox->isChecked()) { + if (crateId.isValid()) { + m_pTrackCollection->removeCrateTracks(crateId, trackIds); + } + } else { + if (!crateId.isValid()) { // i.e. a new crate is suppose to be created + crateId = CrateFeatureHelper( + m_pTrackCollection, m_pConfig).createEmptyCrate(); + } + if (crateId.isValid()) { + m_pTrackCollection->unhideTracks(trackIds); + m_pTrackCollection->addCrateTracks(crateId, trackIds); + } + } +} + +void WTrackTableView::addSelectionToNewCrate() { + const QList trackIds = getSelectedTrackIds(); + + if (trackIds.isEmpty()) { + qWarning() << "No tracks selected for crate"; + return; } + + CrateId crateId = CrateFeatureHelper( + m_pTrackCollection, m_pConfig).createEmptyCrate(); + if (crateId.isValid()) { m_pTrackCollection->unhideTracks(trackIds); m_pTrackCollection->addCrateTracks(crateId, trackIds); } + } void WTrackTableView::doSortByColumn(int headerSection) { @@ -1648,26 +1824,55 @@ void WTrackTableView::slotClearBeats() { } } -void WTrackTableView::slotClearWaveform() { +void WTrackTableView::slotClearMainCue() { + QModelIndexList indices = selectionModel()->selectedRows(); TrackModel* trackModel = getTrackModel(); + if (trackModel == nullptr) { return; } - AnalysisDao& analysisDao = m_pTrackCollection->getAnalysisDAO(); + for (const QModelIndex& index : indices) { + TrackPointer pTrack = trackModel->getTrack(index); + if (pTrack) { + pTrack->removeCuesOfType(Cue::LOAD); + } + } +} + +void WTrackTableView::slotClearHotCues() { QModelIndexList indices = selectionModel()->selectedRows(); + TrackModel* trackModel = getTrackModel(); + + if (trackModel == nullptr) { + return; + } + for (const QModelIndex& index : indices) { TrackPointer pTrack = trackModel->getTrack(index); - if (!pTrack) { - continue; + if (pTrack) { + pTrack->removeCuesOfType(Cue::CUE); } - analysisDao.deleteAnalysesForTrack(pTrack->getId()); - pTrack->setWaveform(WaveformPointer()); - pTrack->setWaveformSummary(WaveformPointer()); } } -void WTrackTableView::slotReplayGainReset() { +void WTrackTableView::slotClearLoop() { + QModelIndexList indices = selectionModel()->selectedRows(); + TrackModel* trackModel = getTrackModel(); + + if (trackModel == nullptr) { + return; + } + + for (const QModelIndex& index : indices) { + TrackPointer pTrack = trackModel->getTrack(index); + if (pTrack) { + pTrack->removeCuesOfType(Cue::LOOP); + } + } +} + +void WTrackTableView::slotClearReplayGain() { QModelIndexList indices = selectionModel()->selectedRows(); TrackModel* trackModel = getTrackModel(); @@ -1683,6 +1888,34 @@ void WTrackTableView::slotReplayGainReset() { } } +void WTrackTableView::slotClearWaveform() { + TrackModel* trackModel = getTrackModel(); + if (trackModel == nullptr) { + return; + } + + AnalysisDao& analysisDao = m_pTrackCollection->getAnalysisDAO(); + QModelIndexList indices = selectionModel()->selectedRows(); + for (const QModelIndex& index : indices) { + TrackPointer pTrack = trackModel->getTrack(index); + if (!pTrack) { + continue; + } + analysisDao.deleteAnalysesForTrack(pTrack->getId()); + pTrack->setWaveform(WaveformPointer()); + pTrack->setWaveformSummary(WaveformPointer()); + } +} + +void WTrackTableView::slotClearAllMetadata() { + slotClearBeats(); + slotClearMainCue(); + slotClearHotCues(); + slotClearLoop(); + slotClearReplayGain(); + slotClearWaveform(); +} + void WTrackTableView::slotCoverInfoSelected(const CoverInfo& coverInfo) { TrackModel* trackModel = getTrackModel(); if (trackModel == nullptr) { diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h index db4660eb9b14..8959e3ee53f0 100644 --- a/src/widget/wtracktableview.h +++ b/src/widget/wtracktableview.h @@ -37,7 +37,8 @@ class WTrackTableView : public WLibraryTableView { void keyPressEvent(QKeyEvent* event) override; void loadSelectedTrack() override; void loadSelectedTrackToGroup(QString group, bool play) override; - + QList getSelectedTrackIds() const; + void setSelectedTracks(const QList& tracks); void saveCurrentVScrollBarPos(); void restoreCurrentVScrollBarPos(); @@ -65,17 +66,26 @@ class WTrackTableView : public WLibraryTableView { void slotShowTrackInTagFetcher(TrackPointer track); void slotImportTrackMetadataFromFileTags(); void slotExportTrackMetadataIntoFileTags(); - void slotResetPlayed(); + void slotPopulatePlaylistMenu(); void addSelectionToPlaylist(int iPlaylistId); - void addSelectionToCrate(int iCrateId); + void updateSelectionCrates(QWidget* qc); + void slotPopulateCrateMenu(); + void addSelectionToNewCrate(); void loadSelectionToGroup(QString group, bool play = false); void doSortByColumn(int headerSection); void slotLockBpm(); void slotUnlockBpm(); void slotScaleBpm(int); + void slotClearBeats(); + void slotClearPlayCount(); + void slotClearMainCue(); + void slotClearHotCues(); + void slotClearLoop(); + void slotClearReplayGain(); void slotClearWaveform(); - void slotReplayGainReset(); + void slotClearAllMetadata(); + // Signalled 20 times per second (every 50ms) by GuiTick. void slotGuiTick50ms(double); void slotScrollValueChanged(int); @@ -108,8 +118,6 @@ class WTrackTableView : public WLibraryTableView { TrackModel* getTrackModel() const; bool modelHasCapabilities(TrackModel::CapabilitiesFlags capabilities) const; - QList getSelectedTrackIds() const; - UserSettingsPointer m_pConfig; TrackCollection* m_pTrackCollection; @@ -126,7 +134,19 @@ class WTrackTableView : public WLibraryTableView { ControlProxy* m_pNumPreviewDecks; // Context menu machinery - QMenu *m_pMenu, *m_pPlaylistMenu, *m_pCrateMenu, *m_pSamplerMenu, *m_pBPMMenu; + QMenu *m_pMenu; + + QMenu *m_pLoadToMenu; + QMenu *m_pDeckMenu; + QMenu *m_pSamplerMenu; + + QMenu *m_pPlaylistMenu; + QMenu *m_pCrateMenu; + QMenu *m_pMetadataMenu; + QMenu *m_pClearMetadataMenu; + QMenu *m_pBPMMenu; + + WCoverArtMenu* m_pCoverMenu; QSignalMapper m_playlistMapper, m_crateMapper, m_deckMapper, m_samplerMapper; @@ -151,9 +171,6 @@ class WTrackTableView : public WLibraryTableView { QAction *m_pUnhideAct; QAction *m_pPurgeAct; - // Reset the played count of selected track or tracks - QAction* m_pResetPlayedAct; - // Show track-editor action QAction *m_pPropertiesAct; QAction *m_pFileBrowserAct; @@ -169,14 +186,15 @@ class WTrackTableView : public WLibraryTableView { QAction *m_pBpmFourThirdsAction; QAction *m_pBpmThreeHalvesAction; - // Clear track beats + // Clear track metadata actions QAction* m_pClearBeatsAction; - - // Clear track waveform + QAction* m_pClearPlayCountAction; + QAction* m_pClearMainCueAction; + QAction* m_pClearHotCuesAction; + QAction* m_pClearLoopAction; QAction* m_pClearWaveformAction; - - // Replay Gain feature - QAction *m_pReplayGainResetAction; + QAction* m_pClearReplayGainAction; + QAction* m_pClearAllMetadataAction; bool m_sorting; @@ -192,6 +210,8 @@ class WTrackTableView : public WLibraryTableView { mixxx::Duration m_lastUserAction; bool m_selectionChangedSinceLastGuiTick; bool m_loadCachedOnly; + bool m_bPlaylistMenuLoaded; + bool m_bCrateMenuLoaded; ControlProxy* m_pCOTGuiTick; };