diff --git a/CMakeLists.txt b/CMakeLists.txt index 5053489dca0d..0d8c5f9385bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -342,12 +342,14 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/banshee/bansheedbconnection.cpp src/library/banshee/bansheefeature.cpp src/library/banshee/bansheeplaylistmodel.cpp + src/library/basecoverartdelegate.cpp src/library/baseexternallibraryfeature.cpp src/library/baseexternalplaylistmodel.cpp src/library/baseexternaltrackmodel.cpp src/library/baseplaylistfeature.cpp src/library/basesqltablemodel.cpp src/library/basetrackcache.cpp + src/library/basetracktablemodel.cpp src/library/bpmdelegate.cpp src/library/browse/browsefeature.cpp src/library/browse/browsetablemodel.cpp diff --git a/build/depends.py b/build/depends.py index cc6572f84521..b68c7f85cdf1 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1027,6 +1027,7 @@ def sources(self, build): "src/library/externaltrackcollection.cpp", "src/library/basesqltablemodel.cpp", "src/library/basetrackcache.cpp", + "src/library/basetracktablemodel.cpp", "src/library/columncache.cpp", "src/library/librarytablemodel.cpp", "src/library/searchquery.cpp", @@ -1121,6 +1122,7 @@ def sources(self, build): "src/library/bpmdelegate.cpp", "src/library/previewbuttondelegate.cpp", "src/library/colordelegate.cpp", + "src/library/basecoverartdelegate.cpp", "src/library/coverartdelegate.cpp", "src/library/locationdelegate.cpp", "src/library/tableitemdelegate.cpp", diff --git a/src/library/banshee/bansheeplaylistmodel.cpp b/src/library/banshee/bansheeplaylistmodel.cpp index 68c52afa6d07..d1b125a57e55 100644 --- a/src/library/banshee/bansheeplaylistmodel.cpp +++ b/src/library/banshee/bansheeplaylistmodel.cpp @@ -221,13 +221,6 @@ void BansheePlaylistModel::setTableModel(int playlistId) { setSort(defaultSortColumn(), defaultSortOrder()); } -bool BansheePlaylistModel::setData(const QModelIndex& index, const QVariant& value, int role) { - Q_UNUSED(index); - Q_UNUSED(value); - Q_UNUSED(role); - return false; -} - TrackModel::CapabilitiesFlags BansheePlaylistModel::getCapabilities() const { return TRACKMODELCAPS_NONE | TRACKMODELCAPS_ADDTOPLAYLIST @@ -238,65 +231,23 @@ TrackModel::CapabilitiesFlags BansheePlaylistModel::getCapabilities() const { } Qt::ItemFlags BansheePlaylistModel::flags(const QModelIndex &index) const { - return readWriteFlags(index); -} - -Qt::ItemFlags BansheePlaylistModel::readWriteFlags(const QModelIndex &index) const { - if (!index.isValid()) { - return Qt::ItemIsEnabled; - } - - Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); - - // Enable dragging songs from this data model to elsewhere (like the waveform - // widget to load a track into a Player). - defaultFlags |= Qt::ItemIsDragEnabled; - - return defaultFlags; -} - -Qt::ItemFlags BansheePlaylistModel::readOnlyFlags(const QModelIndex &index) const -{ - Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); - if (!index.isValid()) - return Qt::ItemIsEnabled; - - //Enable dragging songs from this data model to elsewhere (like the waveform widget to - //load a track into a Player). - defaultFlags |= Qt::ItemIsDragEnabled; - - return defaultFlags; + return readOnlyFlags(index); } void BansheePlaylistModel::tracksChanged(QSet trackIds) { Q_UNUSED(trackIds); } -void BansheePlaylistModel::trackLoaded(QString group, TrackPointer pTrack) { - if (group == m_previewDeckGroup) { - // If there was a previously loaded track, refresh its rows so the - // preview state will update. - if (m_previewDeckTrackId.isValid()) { - const int numColumns = columnCount(); - QLinkedList rows = getTrackRows(m_previewDeckTrackId); - m_previewDeckTrackId = TrackId(); // invalidate - foreach (int row, rows) { - QModelIndex left = index(row, 0); - QModelIndex right = index(row, numColumns); - emit dataChanged(left, right); - } - } - if (pTrack) { - for (int row = 0; row < rowCount(); ++row) { - const QUrl rowUrl(getFieldString(index(row, 0), CLM_URI)); - if (TrackFile::fromUrl(rowUrl) == pTrack->getFileInfo()) { - m_previewDeckTrackId = - TrackId(getFieldVariant(index(row, 0), CLM_VIEW_ORDER)); - break; - } +TrackId BansheePlaylistModel::doGetTrackId(const TrackPointer& pTrack) const { + if (pTrack) { + for (int row = 0; row < rowCount(); ++row) { + const QUrl rowUrl(getFieldString(index(row, 0), CLM_URI)); + if (TrackFile::fromUrl(rowUrl) == pTrack->getFileInfo()) { + return TrackId(getFieldVariant(index(row, 0), CLM_VIEW_ORDER)); } } } + return TrackId(); } QVariant BansheePlaylistModel::getFieldVariant(const QModelIndex& index, diff --git a/src/library/banshee/bansheeplaylistmodel.h b/src/library/banshee/bansheeplaylistmodel.h index 6e7149a710a3..d6cad06cf5a0 100644 --- a/src/library/banshee/bansheeplaylistmodel.h +++ b/src/library/banshee/bansheeplaylistmodel.h @@ -28,19 +28,12 @@ class BansheePlaylistModel : public BaseSqlTableModel { Qt::ItemFlags flags(const QModelIndex &index) const final; CapabilitiesFlags getCapabilities() const final; - bool setData(const QModelIndex& index, const QVariant& value, int role=Qt::EditRole) final; - - protected: - // Use this if you want a model that is read-only. - Qt::ItemFlags readOnlyFlags(const QModelIndex &index) const final; - // Use this if you want a model that can be changed - Qt::ItemFlags readWriteFlags(const QModelIndex &index) const final; - private slots: void tracksChanged(QSet trackIds); - void trackLoaded(QString group, TrackPointer pTrack); private: + TrackId doGetTrackId(const TrackPointer& pTrack) const final; + QString getFieldString(const QModelIndex& index, const QString& fieldName) const; QVariant getFieldVariant(const QModelIndex& index, const QString& fieldName) const; void dropTempTable(); diff --git a/src/library/basecoverartdelegate.cpp b/src/library/basecoverartdelegate.cpp new file mode 100644 index 000000000000..afb31d03b343 --- /dev/null +++ b/src/library/basecoverartdelegate.cpp @@ -0,0 +1,143 @@ +#include "library/coverartdelegate.h" + +#include +#include + +#include "library/coverartcache.h" +#include "library/dao/trackschema.h" +#include "library/trackmodel.h" +#include "util/logger.h" +#include "widget/wlibrarytableview.h" + +namespace { + +const mixxx::Logger kLogger("BaseCoverArtDelegate"); + +inline TrackModel* asTrackModel( + QTableView* pTableView) { + auto* pTrackModel = + dynamic_cast(pTableView->model()); + DEBUG_ASSERT(pTrackModel); + return pTrackModel; +} + +} // anonymous namespace + +BaseCoverArtDelegate::BaseCoverArtDelegate(QTableView* parent) + : TableItemDelegate(parent), + m_pTrackModel(asTrackModel(parent)), + m_pCache(CoverArtCache::instance()), + m_inhibitLazyLoading(false) { + if (m_pCache) { + connect(m_pCache, + &CoverArtCache::coverFound, + this, + &BaseCoverArtDelegate::slotCoverFound); + } else { + kLogger.warning() + << "Caching of cover art is not available"; + } +} + +void BaseCoverArtDelegate::emitRowsChanged( + QList&& rows) { + if (rows.isEmpty()) { + return; + } + // Sort in ascending order... + std::sort(rows.begin(), rows.end()); + // ...and then deduplicate... + rows.erase(std::unique(rows.begin(), rows.end()), rows.end()); + // ...before emitting the signal. + DEBUG_ASSERT(!rows.isEmpty()); + emit rowsChanged(std::move(rows)); +} + +void BaseCoverArtDelegate::slotInhibitLazyLoading( + bool inhibitLazyLoading) { + m_inhibitLazyLoading = inhibitLazyLoading; + if (m_inhibitLazyLoading || m_cacheMissRows.isEmpty()) { + return; + } + // If we can request non-cache covers now, request updates + // for all rows that were cache misses since the last time. + auto staleRows = m_cacheMissRows; + // Reset the member variable before mutating the aggregated + // rows list (-> implicit sharing) and emitting a signal that + // in turn may trigger new signals for BaseCoverArtDelegate! + m_cacheMissRows = QList(); + emitRowsChanged(std::move(staleRows)); +} + +void BaseCoverArtDelegate::slotCoverFound( + const QObject* pRequestor, + const CoverInfo& coverInfo, + const QPixmap& pixmap, + mixxx::cache_key_t requestedImageHash, + bool coverInfoUpdated) { + Q_UNUSED(pixmap); + if (pRequestor != this) { + return; + } + if (coverInfoUpdated) { + const auto pTrack = + loadTrackByLocation(coverInfo.trackLocation); + if (pTrack) { + kLogger.info() + << "Updating cover info of track" + << coverInfo.trackLocation; + pTrack->setCoverInfo(coverInfo); + } + } + QList refreshRows = m_pendingCacheRows.values(requestedImageHash); + m_pendingCacheRows.remove(requestedImageHash); + emitRowsChanged(std::move(refreshRows)); +} + +TrackPointer BaseCoverArtDelegate::loadTrackByLocation( + const QString& trackLocation) const { + VERIFY_OR_DEBUG_ASSERT(m_pTrackModel) { + return TrackPointer(); + } + return m_pTrackModel->getTrackByRef( + TrackRef::fromFileInfo(trackLocation)); +} + +void BaseCoverArtDelegate::paintItem( + QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const { + paintItemBackground(painter, option, index); + + CoverInfo coverInfo = coverInfoForIndex(index); + if (CoverImageUtils::isValidHash(coverInfo.hash)) { + VERIFY_OR_DEBUG_ASSERT(m_pCache) { + return; + } + const double scaleFactor = + getDevicePixelRatioF(static_cast(parent())); + QPixmap pixmap = m_pCache->tryLoadCover( + this, + coverInfo, + option.rect.width() * scaleFactor, + m_inhibitLazyLoading ? CoverArtCache::Loading::CachedOnly : CoverArtCache::Loading::Default); + if (pixmap.isNull()) { + // Cache miss + if (m_inhibitLazyLoading) { + // We are requesting cache-only covers and got a cache + // miss. Record this row so that when we switch to requesting + // non-cache we can request an update. + m_cacheMissRows.append(index.row()); + } else { + // If we asked for a non-cache image and got a null pixmap, + // then our request was queued. + m_pendingCacheRows.insertMulti(coverInfo.hash, index.row()); + } + } else { + // Cache hit + pixmap.setDevicePixelRatio(scaleFactor); + painter->drawPixmap(option.rect.topLeft(), pixmap); + return; + } + } +} diff --git a/src/library/basecoverartdelegate.h b/src/library/basecoverartdelegate.h new file mode 100644 index 000000000000..cf7745705cdb --- /dev/null +++ b/src/library/basecoverartdelegate.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include + +#include "library/tableitemdelegate.h" +#include "track/track.h" +#include "util/cache.h" + +class CoverArtCache; +class TrackModel; + +class BaseCoverArtDelegate : public TableItemDelegate { + Q_OBJECT + + public: + explicit BaseCoverArtDelegate( + QTableView* parent); + ~BaseCoverArtDelegate() override = default; + + void paintItem( + QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const final; + + signals: + // Sent when rows need to be refreshed + void rowsChanged( + QList rows); + + public slots: + // Advise the delegate to temporarily inhibit lazy loading + // of cover images and to only display those cover images + // that have already been cached. Otherwise only the solid + // (background) color is painted. + // + // It is useful to handle cases when the user scroll down + // very fast or when they hold an arrow key. In thise case + // it is NOT desirable to start multiple expensive file + // system operations in worker threads for loading and + // scaling cover images that are not even displayed after + // scrolling beyond them. + void slotInhibitLazyLoading( + bool inhibitLazyLoading); + + private slots: + void slotCoverFound( + const QObject* pRequestor, + const CoverInfo& coverInfo, + const QPixmap& pixmap, + mixxx::cache_key_t requestedImageHash, + bool coverInfoUpdated); + + protected: + TrackModel* const m_pTrackModel; + + private: + void emitRowsChanged( + QList&& rows); + + TrackPointer loadTrackByLocation( + const QString& trackLocation) const; + + virtual CoverInfo coverInfoForIndex( + const QModelIndex& index) const = 0; + + CoverArtCache* const m_pCache; + bool m_inhibitLazyLoading; + + // We need to record rows in paint() (which is const) so + // these are marked mutable. + mutable QList m_cacheMissRows; + mutable QHash m_pendingCacheRows; +}; diff --git a/src/library/baseexternalplaylistmodel.cpp b/src/library/baseexternalplaylistmodel.cpp index fd560fbc59b3..0a2b17807cb5 100644 --- a/src/library/baseexternalplaylistmodel.cpp +++ b/src/library/baseexternalplaylistmodel.cpp @@ -145,34 +145,19 @@ void BaseExternalPlaylistModel::setPlaylist(QString playlist_path) { setSearch(""); } -void BaseExternalPlaylistModel::trackLoaded(QString group, TrackPointer pTrack) { - if (group == m_previewDeckGroup) { - // If there was a previously loaded track, refresh its rows so the - // preview state will update. - if (m_previewDeckTrackId.isValid()) { - const int numColumns = columnCount(); - QLinkedList rows = getTrackRows(m_previewDeckTrackId); - m_previewDeckTrackId = TrackId(); // invalidate - foreach (int row, rows) { - QModelIndex left = index(row, 0); - QModelIndex right = index(row, numColumns); - emit dataChanged(left, right); - } - } - if (pTrack) { - // The external table has foreign Track IDs, so we need to compare - // by location - for (int row = 0; row < rowCount(); ++row) { - QString nativeLocation = index(row, fieldIndex("location")).data().toString(); - QString location = QDir::fromNativeSeparators(nativeLocation); - if (location == pTrack->getLocation()) { - m_previewDeckTrackId = TrackId(index(row, 0).data()); - //Debug() << "foreign track id" << m_previewDeckTrackId; - break; - } +TrackId BaseExternalPlaylistModel::doGetTrackId(const TrackPointer& pTrack) const { + if (pTrack) { + // The external table has foreign Track IDs, so we need to compare + // by location + for (int row = 0; row < rowCount(); ++row) { + QString nativeLocation = index(row, fieldIndex("location")).data().toString(); + QString location = QDir::fromNativeSeparators(nativeLocation); + if (location == pTrack->getLocation()) { + return TrackId(index(row, 0).data()); } } } + return TrackId(); } TrackModel::CapabilitiesFlags BaseExternalPlaylistModel::getCapabilities() const { diff --git a/src/library/baseexternalplaylistmodel.h b/src/library/baseexternalplaylistmodel.h index 8d0321ff9c02..b1b19e9f0057 100644 --- a/src/library/baseexternalplaylistmodel.h +++ b/src/library/baseexternalplaylistmodel.h @@ -28,10 +28,11 @@ class BaseExternalPlaylistModel : public BaseSqlTableModel { TrackId getTrackId(const QModelIndex& index) const override; bool isColumnInternal(int column) override; Qt::ItemFlags flags(const QModelIndex &index) const override; - void trackLoaded(QString group, TrackPointer pTrack) override; CapabilitiesFlags getCapabilities() const override; private: + TrackId doGetTrackId(const TrackPointer& pTrack) const override; + QString m_playlistsTable; QString m_playlistTracksTable; QSharedPointer m_trackSource; diff --git a/src/library/baseexternaltrackmodel.cpp b/src/library/baseexternaltrackmodel.cpp index 6d4d552522ab..68f2aa87948e 100644 --- a/src/library/baseexternaltrackmodel.cpp +++ b/src/library/baseexternaltrackmodel.cpp @@ -88,34 +88,19 @@ TrackId BaseExternalTrackModel::getTrackId(const QModelIndex& index) const { } } -void BaseExternalTrackModel::trackLoaded(QString group, TrackPointer pTrack) { - if (group == m_previewDeckGroup) { - // If there was a previously loaded track, refresh its rows so the - // preview state will update. - if (m_previewDeckTrackId.isValid()) { - const int numColumns = columnCount(); - QLinkedList rows = getTrackRows(m_previewDeckTrackId); - m_previewDeckTrackId = TrackId(); // invalidate - foreach (int row, rows) { - QModelIndex left = index(row, 0); - QModelIndex right = index(row, numColumns); - emit dataChanged(left, right); - } - } - if (pTrack) { - // The external table has foreign Track IDs, so we need to compare - // by location - for (int row = 0; row < rowCount(); ++row) { - QString nativeLocation = index(row, fieldIndex("location")).data().toString(); - QString location = QDir::fromNativeSeparators(nativeLocation); - if (location == pTrack->getLocation()) { - m_previewDeckTrackId = TrackId(index(row, 0).data()); - //qDebug() << "foreign track id" << m_previewDeckTrackId; - break; - } +TrackId BaseExternalTrackModel::doGetTrackId(const TrackPointer& pTrack) const { + if (pTrack) { + // The external table has foreign Track IDs, so we need to compare + // by location + for (int row = 0; row < rowCount(); ++row) { + QString nativeLocation = index(row, fieldIndex("location")).data().toString(); + QString location = QDir::fromNativeSeparators(nativeLocation); + if (location == pTrack->getLocation()) { + return TrackId(index(row, 0).data()); } } } + return TrackId(); } bool BaseExternalTrackModel::isColumnInternal(int column) { diff --git a/src/library/baseexternaltrackmodel.h b/src/library/baseexternaltrackmodel.h index 16db84caabfa..23ed68092c4f 100644 --- a/src/library/baseexternaltrackmodel.h +++ b/src/library/baseexternaltrackmodel.h @@ -23,9 +23,11 @@ class BaseExternalTrackModel : public BaseSqlTableModel { CapabilitiesFlags getCapabilities() const override; TrackId getTrackId(const QModelIndex& index) const override; TrackPointer getTrack(const QModelIndex& index) const override; - void trackLoaded(QString group, TrackPointer pTrack) override; bool isColumnInternal(int column) override; Qt::ItemFlags flags(const QModelIndex &index) const override; + + private: + TrackId doGetTrackId(const TrackPointer& pTrack) const override; }; #endif /* BASEEXTERNALTRACKMODEL_H */ diff --git a/src/library/basesqltablemodel.cpp b/src/library/basesqltablemodel.cpp index a45fa51965b1..2c9538465344 100644 --- a/src/library/basesqltablemodel.cpp +++ b/src/library/basesqltablemodel.cpp @@ -1,31 +1,25 @@ // Created by RJ Ryan (rryan@mit.edu) 1/29/2010 -#include -#include -#include - #include "library/basesqltablemodel.h" -#include "library/bpmdelegate.h" -#include "library/colordelegate.h" +#include +#include +#include + #include "library/coverartdelegate.h" -#include "library/locationdelegate.h" -#include "library/previewbuttondelegate.h" +#include "library/dao/trackschema.h" +#include "library/queryutil.h" +#include "library/starrating.h" #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" -#include "library/stardelegate.h" -#include "library/starrating.h" -#include "library/queryutil.h" #include "mixer/playermanager.h" -#include "mixer/playerinfo.h" #include "track/keyutils.h" #include "track/trackmetadata.h" +#include "util/assert.h" #include "util/db/dbconnection.h" #include "util/duration.h" -#include "util/assert.h" #include "util/performancetimer.h" #include "util/platform.h" -#include "widget/wlibrarytableview.h" namespace { @@ -40,95 +34,36 @@ const int kMaxSortColumns = 3; // Constant for getModelSetting(name) const QString COLUMNS_SORTING = QStringLiteral("ColumnsSorting"); -// Alpha value for row color background (range 0 - 255) -constexpr int kTrackColorRowBackgroundOpacity = 0x20; // 12.5% opacity +const QString kEmptyString = QStringLiteral(""); } // anonymous namespace -BaseSqlTableModel::BaseSqlTableModel(QObject* pParent, - TrackCollectionManager* pTrackCollectionManager, - const char* settingsNamespace) - : QAbstractTableModel(pParent), - TrackModel(pTrackCollectionManager->internalCollection()->database(), settingsNamespace), +BaseSqlTableModel::BaseSqlTableModel( + QObject* parent, + TrackCollectionManager* pTrackCollectionManager, + const char* settingsNamespace) + : BaseTrackTableModel( + settingsNamespace, + pTrackCollectionManager, + parent), m_pTrackCollectionManager(pTrackCollectionManager), m_database(pTrackCollectionManager->internalCollection()->database()), - m_previewDeckGroup(PlayerManager::groupForPreviewDeck(0)), m_bInitialized(false), - m_currentSearch("") { - connect(&PlayerInfo::instance(), - &PlayerInfo::trackLoaded, - this, - &BaseSqlTableModel::trackLoaded); - connect(&pTrackCollectionManager->internalCollection()->getTrackDAO(), - &TrackDAO::forceModelUpdate, - this, - &BaseSqlTableModel::select); - // TODO(rryan): This is a virtual function call from a constructor. - trackLoaded(m_previewDeckGroup, PlayerInfo::instance().getTrackInfo(m_previewDeckGroup)); + m_currentSearch(kEmptyString) { } BaseSqlTableModel::~BaseSqlTableModel() { } -void BaseSqlTableModel::initHeaderData() { - // Set the column heading labels, rename them for translations and have - // proper capitalization - - // TODO(owilliams): Clean this up to make it readable. - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED, - tr("Played"), 50); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST, - tr("Artist"), 200); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_TITLE, - tr("Title"), 300); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM, - tr("Album"), 200); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST, - tr("Album Artist"), 100); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_GENRE, - tr("Genre"), 100); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER, - tr("Composer"), 50); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING, - tr("Grouping"), 10); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_YEAR, - tr("Year"), 40); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE, - tr("Type"), 50); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION, - tr("Location"), 100); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT, - tr("Comment"), 250); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_DURATION, - tr("Duration"), 70); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_RATING, - tr("Rating"), 100); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE, - tr("Bitrate"), 50); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_BPM, - tr("BPM"), 70); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER, - tr("Track #"), 10); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED, - tr("Date Added"), 90); +void BaseSqlTableModel::initHeaderProperties() { + BaseTrackTableModel::initHeaderProperties(); + // Add playlist columns setHeaderProperties(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION, - tr("#"), 30); + tr("#"), + 30); setHeaderProperties(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_DATETIMEADDED, - tr("Timestamp"), 80); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_KEY, - tr("Key"), 50); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK, - tr("BPM Lock"), 10); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW, - tr("Preview"), 50); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_COVERART, - tr("Cover Art"), 90); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_COLOR, - tr("Color"), 10); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN, - tr("ReplayGain"), 50); - setHeaderProperties(ColumnCache::COLUMN_LIBRARYTABLE_SAMPLERATE, - tr("Samplerate"), 50); + tr("Timestamp"), + 80); } void BaseSqlTableModel::initSortColumnMapping() { @@ -168,79 +103,6 @@ void BaseSqlTableModel::initSortColumnMapping() { } } -void BaseSqlTableModel::setHeaderProperties( - ColumnCache::Column column, QString title, int defaultWidth) { - int fi = fieldIndex(column); - setHeaderData(fi, Qt::Horizontal, m_tableColumnCache.columnName(column), - TrackModel::kHeaderNameRole); - setHeaderData(fi, Qt::Horizontal, title, Qt::DisplayRole); - setHeaderData(fi, Qt::Horizontal, defaultWidth, TrackModel::kHeaderWidthRole); -} - -bool BaseSqlTableModel::setHeaderData(int section, Qt::Orientation orientation, - const QVariant &value, int role) { - int numColumns = columnCount(); - if (section < 0 || section >= numColumns) { - return false; - } - - if (orientation != Qt::Horizontal) { - // We only care about horizontal headers. - return false; - } - - if (m_headerInfo.size() != numColumns) { - m_headerInfo.resize(numColumns); - } - - m_headerInfo[section][role] = value; - emit headerDataChanged(orientation, section, section); - return true; -} - -QVariant BaseSqlTableModel::headerData(int section, Qt::Orientation orientation, - int role) const { - if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { - QVariant headerValue = m_headerInfo.value(section).value(role); - if (!headerValue.isValid()) { - // Try EditRole if DisplayRole wasn't present - headerValue = m_headerInfo.value(section).value(Qt::EditRole); - } - if (!headerValue.isValid()) { - headerValue = QVariant(section).toString(); - } - return headerValue; - } else if (role == TrackModel::kHeaderWidthRole && orientation == Qt::Horizontal) { - QVariant widthValue = m_headerInfo.value(section).value(role); - if (!widthValue.isValid()) { - return 50; - } - return widthValue; - } else if (role == TrackModel::kHeaderNameRole && orientation == Qt::Horizontal) { - return m_headerInfo.value(section).value(role); - } else if (role == Qt::ToolTipRole && orientation == Qt::Horizontal) { - QVariant tooltip = m_headerInfo.value(section).value(role); - if (tooltip.isValid()) return tooltip; - } - return QAbstractTableModel::headerData(section, orientation, role); -} - - -bool BaseSqlTableModel::isColumnHiddenByDefault(int column) { - if ((column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER)) || - (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER)) || - (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR)) || - (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING)) || - (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION)) || - (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST)) || - (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN)) || - (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) || - (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_SAMPLERATE))) { - return true; - } - return false; -} - void BaseSqlTableModel::clearRows() { DEBUG_ASSERT(m_rowInfo.empty() == m_trackIdToRows.empty()); DEBUG_ASSERT(m_rowInfo.size() >= m_trackIdToRows.size()); @@ -255,8 +117,8 @@ void BaseSqlTableModel::clearRows() { } void BaseSqlTableModel::replaceRows( - QVector&& rows, - TrackId2Rows&& trackIdToRows) { + QVector&& rows, + TrackId2Rows&& trackIdToRows) { // NOTE(uklotzde): Use r-value references for parameters here, because // conceptually those parameters should replace the corresponding internal // member variables. Currently Qt4/5 doesn't support move semantics and @@ -300,7 +162,7 @@ void BaseSqlTableModel::select() { // Prepare query for id and all columns not in m_trackSource QString queryString = QString("SELECT %1 FROM %2 %3") - .arg(m_tableColumns.join(","), m_tableName, m_tableOrderBy); + .arg(m_tableColumns.join(","), m_tableName, m_tableOrderBy); if (sDebug) { qDebug() << this << "select() executing:" << queryString; @@ -354,7 +216,7 @@ void BaseSqlTableModel::select() { // current position defines the ordering rowInfo.order = rowInfos.size(); rowInfo.metadata.reserve(sqlRecord.count()); - for (int i = 0; i < m_tableColumns.size(); ++i) { + for (int i = 0; i < m_tableColumns.size(); ++i) { rowInfo.metadata.push_back(sqlRecord.value(i)); } rowInfos.push_back(rowInfo); @@ -366,16 +228,16 @@ void BaseSqlTableModel::select() { if (m_trackSource) { m_trackSource->filterAndSort(trackIds, - m_currentSearch, - m_currentSearchFilter, - m_trackSourceOrderBy, - m_sortColumns, - m_tableColumns.size() - 1, // exclude the 1st column with the id - &m_trackSortOrder); + m_currentSearch, + m_currentSearchFilter, + m_trackSourceOrderBy, + m_sortColumns, + m_tableColumns.size() - 1, // exclude the 1st column with the id + &m_trackSortOrder); // Re-sort the track IDs since filterAndSort can change their order or mark // them for removal (by setting their row to -1). - for (auto& rowInfo: rowInfos) { + for (auto& rowInfo : rowInfos) { // If the sort is not a track column then we will sort only to // separate removed tracks (order == -1) from present tracks (order == // 0). Otherwise we sort by the order that filterAndSort returned to us. @@ -424,9 +286,9 @@ void BaseSqlTableModel::select() { } void BaseSqlTableModel::setTable(const QString& tableName, - const QString& idColumn, - const QStringList& tableColumns, - QSharedPointer trackSource) { + const QString& idColumn, + const QStringList& tableColumns, + QSharedPointer trackSource) { if (sDebug) { qDebug() << this << "setTable" << tableName << tableColumns << idColumn; } @@ -456,10 +318,7 @@ void BaseSqlTableModel::setTable(const QString& tableName, Qt::QueuedConnection); } - // Build a map from the column names to their indices, used by fieldIndex() - m_tableColumnCache.setColumns(m_tableColumns); - - initHeaderData(); + initTableColumnsAndHeaderProperties(m_tableColumns); initSortColumnMapping(); m_bInitialized = true; @@ -477,7 +336,6 @@ TrackModel::SortColumnId BaseSqlTableModel::sortColumnIdFromColumnIndex(int inde return m_sortColumnIdByColumnIndex.value(index, TrackModel::SortColumnId::SORTCOLUMN_INVALID); } - const QString BaseSqlTableModel::currentSearch() const { return m_currentSearch; } @@ -534,7 +392,9 @@ void BaseSqlTableModel::setSort(int column, Qt::SortOrder order) { in >> name >> ordI; int col = fieldIndex(name); - if (col < 0) continue; + if (col < 0) { + continue; + } Qt::SortOrder ord; ord = ordI > 0 ? Qt::AscendingOrder : Qt::DescendingOrder; @@ -543,8 +403,8 @@ void BaseSqlTableModel::setSort(int column, Qt::SortOrder order) { } } if (m_sortColumns.size() > 0 && m_sortColumns.at(0).m_column == column) { - // Only the order has changed - m_sortColumns.replace(0, SortColumn(column, order)); + // Only the order has changed + m_sortColumns.replace(0, SortColumn(column, order)); } else { // Remove column if already in history // As reverse loop to not skip an entry when removing the previous @@ -567,7 +427,6 @@ void BaseSqlTableModel::setSort(int column, Qt::SortOrder order) { QString val; QTextStream out(&val); for (SortColumn& sc : m_sortColumns) { - QString name; if (sc.m_column > 0 && sc.m_column < m_tableColumns.size()) { name = m_tableColumns[sc.m_column]; @@ -587,7 +446,6 @@ void BaseSqlTableModel::setSort(int column, Qt::SortOrder order) { qDebug() << "setSort() sortColumns:" << val; } - // we have two selects for sorting, since keeping the select history // across the two selects is hard, we do this only for the trackSource // this is OK, because the columns of the table are virtual in case of @@ -614,7 +472,7 @@ void BaseSqlTableModel::setSort(int column, Qt::SortOrder order) { m_sortColumns.prepend(SortColumn(column, order)); } else if (m_trackSource) { bool first = true; - for (const SortColumn &sc : m_sortColumns) { + for (const SortColumn& sc : m_sortColumns) { QString sort_field; if (sc.m_column < m_tableColumns.size()) { if (sc.m_column == kIdColumn) { @@ -636,10 +494,9 @@ void BaseSqlTableModel::setSort(int column, Qt::SortOrder order) { continue; } - m_trackSourceOrderBy.append(first ? "ORDER BY ": ", "); + m_trackSourceOrderBy.append(first ? "ORDER BY " : ", "); m_trackSourceOrderBy.append(mixxx::DbConnection::collateLexicographically(sort_field)); - m_trackSourceOrderBy.append((sc.m_order == Qt::AscendingOrder) ? - " ASC" : " DESC"); + m_trackSourceOrderBy.append((sc.m_order == Qt::AscendingOrder) ? " ASC" : " DESC"); //qDebug() << m_trackSourceOrderBy; first = false; } @@ -661,22 +518,20 @@ int BaseSqlTableModel::rowCount(const QModelIndex& parent) const { } int BaseSqlTableModel::columnCount(const QModelIndex& parent) const { - if (parent.isValid()) { + VERIFY_OR_DEBUG_ASSERT(!parent.isValid()) { return 0; } - // Subtract one from trackSource::columnCount to ignore the id column int count = m_tableColumns.size() + - (m_trackSource ? m_trackSource->columnCount() - 1: 0); + (m_trackSource ? m_trackSource->columnCount() - 1 : 0); return count; } int BaseSqlTableModel::fieldIndex(ColumnCache::Column column) const { - int tableIndex = m_tableColumnCache.fieldIndex(column); - if (tableIndex > -1) { + int tableIndex = BaseTrackTableModel::fieldIndex(column); + if (tableIndex >= 0) { return tableIndex; } - if (m_trackSource) { // We need to account for the case where the field name is not a table // column or a source column. @@ -686,15 +541,14 @@ int BaseSqlTableModel::fieldIndex(ColumnCache::Column column) const { return m_tableColumns.size() + sourceTableIndex - 1; } } - return -1; + return tableIndex; } int BaseSqlTableModel::fieldIndex(const QString& fieldName) const { - int tableIndex = m_tableColumnCache.fieldIndex(fieldName); - if (tableIndex > -1) { + int tableIndex = BaseTrackTableModel::fieldIndex(fieldName); + if (tableIndex >= 0) { return tableIndex; } - if (m_trackSource) { // We need to account for the case where the field name is not a table // column or a source column. @@ -704,318 +558,93 @@ int BaseSqlTableModel::fieldIndex(const QString& fieldName) const { return m_tableColumns.size() + sourceTableIndex - 1; } } - return -1; + return tableIndex; } -QVariant BaseSqlTableModel::data(const QModelIndex& index, int role) const { - //qDebug() << this << "data()"; - if (!index.isValid() || ( - role != Qt::BackgroundRole && - role != Qt::DisplayRole && - role != Qt::EditRole && - role != Qt::CheckStateRole && - role != Qt::ToolTipRole)) { - return QVariant(); - } +QVariant BaseSqlTableModel::rawValue( + const QModelIndex& index) const { + DEBUG_ASSERT(index.isValid()); - int row = index.row(); - int column = index.column(); - - // This value is the value in its most raw form. It was looked up either - // from the SQL table or from the cached track layer. - QVariant value = getBaseValue(index, role); - - // Format the value based on whether we are in a tooltip, display, or edit - // role - switch (role) { - case Qt::BackgroundRole: { - QModelIndex colorIndex = index.sibling( - index.row(), - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR)); - QColor color = mixxx::RgbColor::toQColor( - mixxx::RgbColor::fromQVariant(getBaseValue(colorIndex, role))); - if (color.isValid()) { - color.setAlpha(kTrackColorRowBackgroundOpacity); - value = QBrush(color); - } else { - value = QVariant(); - } - break; - } - case Qt::ToolTipRole: - if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR)) { - value = mixxx::RgbColor::toQString(mixxx::RgbColor::fromQVariant(value)); - } - M_FALLTHROUGH_INTENDED; - case Qt::DisplayRole: - if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION)) { - int duration = value.toInt(); - if (duration > 0) { - value = mixxx::Duration::formatTime(duration); - } else { - value = QString(); - } - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING)) { - if (value.canConvert(QMetaType::Int)) - value = QVariant::fromValue(StarRating(value.toInt())); - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { - if (value.canConvert(QMetaType::Int)) - value = QString("(%1)").arg(value.toInt()); - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED)) { - value = value.toBool(); - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED)) { - QDateTime gmtDate = value.toDateTime(); - gmtDate.setTimeSpec(Qt::UTC); - value = gmtDate.toLocalTime(); - } else if (column == fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_DATETIMEADDED)) { - QDateTime gmtDate = value.toDateTime(); - gmtDate.setTimeSpec(Qt::UTC); - value = gmtDate.toLocalTime(); - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { - if (role == Qt::DisplayRole) { - value = value.toDouble() == 0.0 - ? "-" : QString("%1").arg(value.toDouble(), 0, 'f', 1); - } - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) { - value = value.toBool(); - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR)) { - value = mixxx::TrackMetadata::formatCalendarYear(value.toString()); - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER)) { - int track_number = value.toInt(); - if (track_number <= 0) { - // clear invalid values - value = QString(); - } - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE)) { - int bitrate = value.toInt(); - if (bitrate <= 0) { - // clear invalid values - value = QString(); - } - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY)) { - // If we know the semantic key via the LIBRARYTABLE_KEY_ID - // column (as opposed to the string representation of the key - // currently stored in the DB) then lookup the key and render it - // using the user's selected notation. - int keyIdColumn = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY_ID); - if (keyIdColumn != -1) { - mixxx::track::io::key::ChromaticKey key = - KeyUtils::keyFromNumericValue( - index.sibling(row, keyIdColumn).data().toInt()); - - if (key != mixxx::track::io::key::INVALID) { - // Render this key with the user-provided notation. - value = KeyUtils::keyToString(key); - } - } - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN)) { - value = mixxx::ReplayGain::ratioToString(value.toDouble()); - } // Otherwise, just use the column value. - - break; - case Qt::EditRole: - if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { - value = value.toDouble(); - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { - value = index.sibling( - row, fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED)).data().toBool(); - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING)) { - if (value.canConvert(QMetaType::Int)) { - value = QVariant::fromValue(StarRating(value.toInt())); - } - } - break; - case Qt::CheckStateRole: - if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { - bool played = index.sibling( - row, fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED)).data().toBool(); - value = played ? Qt::Checked : Qt::Unchecked; - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { - bool locked = index.sibling( - row, fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)).data().toBool(); - value = locked ? Qt::Checked : Qt::Unchecked; - } - break; - default: - break; + const int row = index.row(); + DEBUG_ASSERT(row >= 0); + if (row >= m_rowInfo.size()) { + return QVariant(); } - return value; -} -bool BaseSqlTableModel::setData( - const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid()) - return false; + const int column = index.column(); + DEBUG_ASSERT(column >= 0); + // TODO(rryan) check range on column - int row = index.row(); - int column = index.column(); + const RowInfo& rowInfo = m_rowInfo[row]; + const TrackId trackId = rowInfo.trackId; - if (sDebug) { - qDebug() << this << "setData() column:" << column << "value:" << value << "role:" << role; - } + // If the row info has the row-specific column, return that. + if (column < m_tableColumns.size()) { + // Special case for preview column. Return whether trackId is the + // current preview deck track. + if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) { + return previewDeckTrackId() == trackId; + } - // Over-ride sets to TIMESPLAYED and re-direct them to PLAYED - if (role == Qt::CheckStateRole) { - QString val = value.toInt() > 0 ? QString("true") : QString("false"); - if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { - QModelIndex playedIndex = index.sibling(index.row(), fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED)); - return setData(playedIndex, val, Qt::EditRole); - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { - QModelIndex bpmLockindex = index.sibling(index.row(), fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)); - return setData(bpmLockindex, val, Qt::EditRole); + const QVector& columns = rowInfo.metadata; + if (sDebug) { + qDebug() << "Returning table-column value" + << columns.at(column) + << "for column" << column; } - return false; + return columns[column]; } - if (row < 0 || row >= m_rowInfo.size()) { + // Otherwise, return the information from the track record cache for the + // given track ID + if (!m_trackSource) { + return QVariant(); + } + // Subtract table columns from index to get the track source column + // number and add 1 to skip over the id column. + int trackSourceColumn = column - m_tableColumns.size() + 1; + if (!m_trackSource->isCached(trackId)) { + // Ideally Mixxx would have notified us of this via a signal, but in + // the case that a track is not in the cache, we attempt to load it + // on the fly. This will be a steep penalty to pay if there are tons + // of these tracks in the table that are not cached. + qDebug() << __FILE__ << __LINE__ + << "Track" << trackId + << "was not present in cache and had to be manually fetched."; + m_trackSource->ensureCached(trackId); + } + return m_trackSource->data(trackId, trackSourceColumn); +} + +QVariant BaseSqlTableModel::roleValue( + const QModelIndex& index, + QVariant&& rawValue, + int role) const { + if (role == Qt::DisplayRole && + index.column() == fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_DATETIMEADDED)) { + QDateTime gmtDate = rawValue.toDateTime(); + gmtDate.setTimeSpec(Qt::UTC); + return gmtDate.toLocalTime(); + } + return BaseTrackTableModel::roleValue(index, std::move(rawValue), role); +} + +bool BaseSqlTableModel::setTrackValueForColumn( + const TrackPointer& pTrack, + int column, + const QVariant& value, + int role) { + if (role != Qt::EditRole) { return false; } - - const RowInfo& rowInfo = m_rowInfo[row]; - TrackId trackId(rowInfo.trackId); - // You can't set something in the table columns because we have no way of // persisting it. if (column < m_tableColumns.size()) { return false; } - // TODO(rryan) ugly and only works because the mixxx library tables are the - // only ones that aren't read-only. This should be moved into BTC. - TrackPointer pTrack = m_pTrackCollectionManager->internalCollection()->getTrackById(trackId); - if (!pTrack) { - return false; - } - setTrackValueForColumn(pTrack, column, value); - - // Do not save the track here. Changing the track dirties it and the caching - // system will automatically save the track once it is unloaded from - // memory. rryan 10/2010 - - return true; -} - -Qt::ItemFlags BaseSqlTableModel::flags(const QModelIndex &index) const { - return readWriteFlags(index); -} - -Qt::ItemFlags BaseSqlTableModel::readWriteFlags( - const QModelIndex &index) const { - if (!index.isValid()) - return Qt::ItemIsEnabled; - - Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); - - // Enable dragging songs from this data model to elsewhere (like the - // waveform widget to load a track into a Player). - defaultFlags |= Qt::ItemIsDragEnabled; - - int column = index.column(); - - if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR)) { - return defaultFlags; - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { - return defaultFlags | Qt::ItemIsUserCheckable; - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) { - return defaultFlags | Qt::ItemIsUserCheckable; - } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { - // Allow checking of the BPM-locked indicator. - defaultFlags |= Qt::ItemIsUserCheckable; - // Disable editing of BPM field when BPM is locked - bool locked = index.sibling( - index.row(), fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) - .data().toBool(); - return locked ? defaultFlags : defaultFlags | Qt::ItemIsEditable; - } else { - return defaultFlags | Qt::ItemIsEditable; - } -} - -Qt::ItemFlags BaseSqlTableModel::readOnlyFlags(const QModelIndex &index) const { - Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); - if (!index.isValid()) - return Qt::ItemIsEnabled; - - // Enable dragging songs from this data model to elsewhere (like the - // waveform widget to load a track into a Player). - defaultFlags |= Qt::ItemIsDragEnabled; - - return defaultFlags; -} - -TrackId BaseSqlTableModel::getTrackId(const QModelIndex& index) const { - if (index.isValid()) { - return TrackId(index.sibling(index.row(), fieldIndex(m_idColumn)).data()); - } else { - return TrackId(); - } -} - -TrackPointer BaseSqlTableModel::getTrack(const QModelIndex& index) const { - return m_pTrackCollectionManager->internalCollection()->getTrackById(getTrackId(index)); -} - -TrackPointer BaseSqlTableModel::getTrackByRef( - const TrackRef& trackRef) const { - return m_pTrackCollectionManager->internalCollection()->getTrackByRef(trackRef); -} - -QString BaseSqlTableModel::getTrackLocation(const QModelIndex& index) const { - if (!index.isValid()) { - return QString(); - } - QString nativeLocation = - index.sibling(index.row(), - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION)) - .data().toString(); - return QDir::fromNativeSeparators(nativeLocation); -} - -void BaseSqlTableModel::trackLoaded(QString group, TrackPointer pTrack) { - if (group == m_previewDeckGroup) { - // If there was a previously loaded track, refresh its rows so the - // preview state will update. - if (m_previewDeckTrackId.isValid()) { - const int numColumns = columnCount(); - QLinkedList rows = getTrackRows(m_previewDeckTrackId); - m_previewDeckTrackId = TrackId(); // invalidate - foreach (int row, rows) { - QModelIndex left = index(row, 0); - QModelIndex right = index(row, numColumns); - emit dataChanged(left, right); - } - } - m_previewDeckTrackId = pTrack ? pTrack->getId() : TrackId(); - } -} - -void BaseSqlTableModel::tracksChanged(QSet trackIds) { - if (sDebug) { - qDebug() << this << "trackChanged" << trackIds.size(); - } - - const int numColumns = columnCount(); - for (const auto& trackId : trackIds) { - QLinkedList rows = getTrackRows(trackId); - foreach (int row, rows) { - //qDebug() << "Row in this result set was updated. Signalling update. track:" << trackId << "row:" << row; - QModelIndex left = index(row, 0); - QModelIndex right = index(row, numColumns); - emit dataChanged(left, right); - } - } -} - -void BaseSqlTableModel::setTrackValueForColumn(TrackPointer pTrack, int column, - QVariant value) { // TODO(XXX) Qt properties could really help here. + DEBUG_ASSERT(pTrack); if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST) == column) { pTrack->setArtist(value.toString()); } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE) == column) { @@ -1037,7 +666,6 @@ void BaseSqlTableModel::setTrackValueForColumn(TrackPointer pTrack, int column, } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT) == column) { pTrack->setComment(value.toString()); } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM) == column) { - // QVariant::toFloat needs >= QT 4.6.x pTrack->setBpm(static_cast(value.toDouble())); } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED) == column) { // Update both the played flag and the number of times played @@ -1058,133 +686,76 @@ void BaseSqlTableModel::setTrackValueForColumn(TrackPointer pTrack, int column, pTrack->setRating(starRating.starCount()); } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY) == column) { pTrack->setKeyText(value.toString(), - mixxx::track::io::key::USER); + mixxx::track::io::key::USER); } else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK) == column) { pTrack->setBpmLocked(value.toBool()); } else { // We never should get up to this point! VERIFY_OR_DEBUG_ASSERT(false) { qWarning() << "Column" - << m_tableColumnCache.columnNameForFieldIndex(column) - << "is not editable!"; + << columnNameForFieldIndex(column) + << "is not editable!"; } + return false; } + return true; } -QVariant BaseSqlTableModel::getBaseValue( - const QModelIndex& index, int role) const { - if (role != Qt::BackgroundRole && - role != Qt::DisplayRole && - role != Qt::ToolTipRole && - role != Qt::EditRole) { - return QVariant(); - } - - int row = index.row(); - int column = index.column(); - - if (row < 0 || row >= m_rowInfo.size()) { - return QVariant(); - } - - // TODO(rryan) check range on column - - const RowInfo& rowInfo = m_rowInfo[row]; - TrackId trackId(rowInfo.trackId); - - // If the row info has the row-specific column, return that. - if (column < m_tableColumns.size()) { - // Special case for preview column. Return whether trackId is the - // current preview deck track. - if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) { - if (role == Qt::ToolTipRole) { - return ""; - } - return m_previewDeckTrackId == trackId; - } +TrackPointer BaseSqlTableModel::getTrack(const QModelIndex& index) const { + return m_pTrackCollectionManager->internalCollection()->getTrackById(getTrackId(index)); +} - const QVector& columns = rowInfo.metadata; - if (sDebug) { - qDebug() << "Returning table-column value" << columns.at(column) - << "for column" << column << "role" << role; - } - return columns[column]; +TrackId BaseSqlTableModel::getTrackId(const QModelIndex& index) const { + if (index.isValid()) { + return TrackId(index.sibling(index.row(), fieldIndex(m_idColumn)).data()); + } else { + return TrackId(); } +} - // Otherwise, return the information from the track record cache for the - // given track ID - if (m_trackSource) { - // Subtract table columns from index to get the track source column - // number and add 1 to skip over the id column. - int trackSourceColumn = column - m_tableColumns.size() + 1; - if (!m_trackSource->isCached(trackId)) { - // Ideally Mixxx would have notified us of this via a signal, but in - // the case that a track is not in the cache, we attempt to load it - // on the fly. This will be a steep penalty to pay if there are tons - // of these tracks in the table that are not cached. - qDebug() << __FILE__ << __LINE__ - << "Track" << trackId - << "was not present in cache and had to be manually fetched."; - m_trackSource->ensureCached(trackId); - } - return m_trackSource->data(trackId, trackSourceColumn); +QString BaseSqlTableModel::getTrackLocation(const QModelIndex& index) const { + if (!index.isValid()) { + return QString(); } - return QVariant(); + QString nativeLocation = + index.sibling(index.row(), + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION)) + .data() + .toString(); + return QDir::fromNativeSeparators(nativeLocation); } -QMimeData* BaseSqlTableModel::mimeData(const QModelIndexList &indexes) const { - QMimeData *mimeData = new QMimeData(); - QList urls; - - // The list of indexes we're given contains separates indexes for each - // column, so even if only one row is selected, we'll have columnCount() - // indices. We need to only count each row once: - QSet rows; +void BaseSqlTableModel::tracksChanged(QSet trackIds) { + if (sDebug) { + qDebug() << this << "trackChanged" << trackIds.size(); + } - foreach (QModelIndex index, indexes) { - if (!index.isValid() || rows.contains(index.row())) { - continue; - } - rows.insert(index.row()); - QUrl url = TrackFile(getTrackLocation(index)).toUrl(); - if (!url.isValid()) { - qDebug() << this << "ERROR: invalid url" << url; - continue; + const int numColumns = columnCount(); + for (const auto& trackId : trackIds) { + QLinkedList rows = getTrackRows(trackId); + foreach (int row, rows) { + //qDebug() << "Row in this result set was updated. Signalling update. track:" << trackId << "row:" << row; + QModelIndex topLeft = index(row, 0); + QModelIndex bottomRight = index(row, numColumns); + emit dataChanged(topLeft, bottomRight); } - urls.append(url); } - mimeData->setUrls(urls); - return mimeData; } -QAbstractItemDelegate* BaseSqlTableModel::delegateForColumn(const int i, QObject* pParent) { - auto* pTableView = qobject_cast(pParent); - DEBUG_ASSERT(pTableView); - - if (i == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING)) { - return new StarDelegate(pTableView); - } else if (i == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { - return new BPMDelegate(pTableView); - } else if (PlayerManager::numPreviewDecks() > 0 && i == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) { - return new PreviewButtonDelegate(pTableView, i); - } else if (i == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION)) { - return new LocationDelegate(pTableView); - } else if (i == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR)) { - return new ColorDelegate(pTableView); - } else if (i == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART)) { - CoverArtDelegate* pCoverDelegate = new CoverArtDelegate(pTableView); - connect(pCoverDelegate, - &CoverArtDelegate::coverReadyForCell, - this, - &BaseSqlTableModel::refreshCell); - return pCoverDelegate; - } - return nullptr; +BaseCoverArtDelegate* BaseSqlTableModel::doCreateCoverArtDelegate( + QTableView* pTableView) const { + return new CoverArtDelegate(pTableView); } -void BaseSqlTableModel::refreshCell(int row, int column) { - QModelIndex coverIndex = index(row, column); - emit dataChanged(coverIndex, coverIndex); +void BaseSqlTableModel::slotRefreshCoverRows(QList rows) { + if (rows.isEmpty()) { + return; + } + const int column = fieldIndex(LIBRARYTABLE_COVERART); + VERIFY_OR_DEBUG_ASSERT(column >= 0) { + return; + } + emitDataChangedForMultipleRowsInColumn(rows, column); } void BaseSqlTableModel::hideTracks(const QModelIndexList& indices) { diff --git a/src/library/basesqltablemodel.h b/src/library/basesqltablemodel.h index 55e3e0d38d71..be574d22c57c 100644 --- a/src/library/basesqltablemodel.h +++ b/src/library/basesqltablemodel.h @@ -5,7 +5,7 @@ #include "library/basetrackcache.h" #include "library/dao/trackdao.h" -#include "library/trackmodel.h" +#include "library/basetracktablemodel.h" #include "library/columncache.h" #include "util/class.h" @@ -13,12 +13,13 @@ class TrackCollectionManager; // BaseSqlTableModel is a custom-written SQL-backed table which aggressively // caches the contents of the table and supports lightweight updates. -class BaseSqlTableModel : public QAbstractTableModel, public TrackModel { +class BaseSqlTableModel : public BaseTrackTableModel { Q_OBJECT public: - BaseSqlTableModel(QObject* pParent, - TrackCollectionManager* pTrackCollectionManager, - const char* settingsNamespace); + BaseSqlTableModel( + QObject* parent, + TrackCollectionManager* pTrackCollectionManager, + const char* settingsNamespace); ~BaseSqlTableModel() override; // Returns true if the BaseSqlTableModel has been initialized. Calling data @@ -31,74 +32,66 @@ class BaseSqlTableModel : public QAbstractTableModel, public TrackModel { void setSearch(const QString& searchText, const QString& extraFilter = QString()); void setSort(int column, Qt::SortOrder order); - int fieldIndex(ColumnCache::Column column) const; - - /////////////////////////////////////////////////////////////////////////// - // Inherited from TrackModel - /////////////////////////////////////////////////////////////////////////// - int fieldIndex(const QString& fieldName) const final; - /////////////////////////////////////////////////////////////////////////// // Inherited from QAbstractItemModel /////////////////////////////////////////////////////////////////////////// - void sort(int column, Qt::SortOrder order) final; - int rowCount(const QModelIndex& parent=QModelIndex()) const final; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const final; + int rowCount(const QModelIndex& parent = QModelIndex()) const final; int columnCount(const QModelIndex& parent = QModelIndex()) const final; - bool setHeaderData(int section, Qt::Orientation orientation, - const QVariant &value, int role = Qt::DisplayRole) final; - QVariant headerData(int section, Qt::Orientation orientation, - int role=Qt::DisplayRole) const final; - QMimeData* mimeData(const QModelIndexList &indexes) const final; - /////////////////////////////////////////////////////////////////////////// - // Functions that might be reimplemented/overridden in derived classes - /////////////////////////////////////////////////////////////////////////// - // This class also has protected variables that should be used in children - // m_database, m_pTrackCollection - - // calls readWriteFlags() by default, reimplement this if the child calls - // should be readOnly - Qt::ItemFlags flags(const QModelIndex &index) const override; + void sort(int column, Qt::SortOrder order) final; /////////////////////////////////////////////////////////////////////////// // Inherited from TrackModel /////////////////////////////////////////////////////////////////////////// - bool isColumnHiddenByDefault(int column) override; + int fieldIndex(const QString& fieldName) const final; + TrackPointer getTrack(const QModelIndex& index) const override; - TrackPointer getTrackByRef(const TrackRef& trackRef) const override; TrackId getTrackId(const QModelIndex& index) const override; + QString getTrackLocation(const QModelIndex& index) const override; + const QLinkedList getTrackRows(TrackId trackId) const override { return m_trackIdToRows.value(trackId); } - QString getTrackLocation(const QModelIndex& index) const override; - void hideTracks(const QModelIndexList& indices) override; + void search(const QString& searchText, const QString& extraFilter = QString()) override; const QString currentSearch() const override; - QAbstractItemDelegate* delegateForColumn(const int i, QObject* pParent) override; + TrackModel::SortColumnId sortColumnIdFromColumnIndex(int column) override; int columnIndexFromSortColumnId(TrackModel::SortColumnId sortColumn) override; - /////////////////////////////////////////////////////////////////////////// - // Inherited from QAbstractItemModel - /////////////////////////////////////////////////////////////////////////// - bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + void hideTracks(const QModelIndexList& indices) override; - public slots: void select() override; + /////////////////////////////////////////////////////////////////////////// + // Inherited from BaseTrackTableModel + /////////////////////////////////////////////////////////////////////////// + int fieldIndex( + ColumnCache::Column column) const final; + protected: + /////////////////////////////////////////////////////////////////////////// + // Inherited from BaseTrackTableModel + /////////////////////////////////////////////////////////////////////////// + QVariant rawValue( + const QModelIndex& index) const override; + QVariant roleValue( + const QModelIndex& index, + QVariant&& rawValue, + int role) const override; + + bool setTrackValueForColumn( + const TrackPointer& pTrack, + int column, + const QVariant& value, + int role) final; + void setTable(const QString& tableName, const QString& trackIdColumn, const QStringList& tableColumns, QSharedPointer trackSource); - void initHeaderData(); + void initHeaderProperties() override; virtual void initSortColumnMapping(); - // Use this if you want a model that is read-only. - virtual Qt::ItemFlags readOnlyFlags(const QModelIndex &index) const; - // Use this if you want a model that can be changed - virtual Qt::ItemFlags readWriteFlags(const QModelIndex &index) const; - TrackCollectionManager* const m_pTrackCollectionManager; protected: @@ -106,24 +99,22 @@ class BaseSqlTableModel : public QAbstractTableModel, public TrackModel { QSqlDatabase m_database; - QString m_previewDeckGroup; - TrackId m_previewDeckTrackId; QString m_tableOrderBy; int m_columnIndexBySortColumnId[NUM_SORTCOLUMNIDS]; QMap m_sortColumnIdByColumnIndex; private slots: - virtual void tracksChanged(QSet trackIds); - virtual void trackLoaded(QString group, TrackPointer pTrack); - void refreshCell(int row, int column); + void tracksChanged(QSet trackIds); + + void slotRefreshCoverRows(QList rows); private: - // A simple helper function for initializing header title and width. Note - // that the ideal width of a column is based on the width of its data, - // not the title string itself. - void setHeaderProperties(ColumnCache::Column column, QString title, int defaultWidth); - inline void setTrackValueForColumn(TrackPointer pTrack, int column, QVariant value); - QVariant getBaseValue(const QModelIndex& index, int role = Qt::DisplayRole) const; + BaseCoverArtDelegate* doCreateCoverArtDelegate( + QTableView* pTableView) const final; + + void setTrackValueForColumn( + TrackPointer pTrack, int column, QVariant value); + // Set the columns used for searching. Names must correspond to the column // names in the table provided to setTable. Must be called after setTable is // called. @@ -158,7 +149,6 @@ class BaseSqlTableModel : public QAbstractTableModel, public TrackModel { QString m_idColumn; QSharedPointer m_trackSource; QStringList m_tableColumns; - ColumnCache m_tableColumnCache; QList m_sortColumns; bool m_bInitialized; QHash m_trackSortOrder; diff --git a/src/library/basetrackcache.cpp b/src/library/basetrackcache.cpp index 29d7e7dbfd49..99a54c88105a 100644 --- a/src/library/basetrackcache.cpp +++ b/src/library/basetrackcache.cpp @@ -74,7 +74,7 @@ QString BaseTrackCache::columnSortForFieldIndex(int index) const { return m_columnCache.columnSortForFieldIndex(index); } -void BaseTrackCache::slotTracksAdded(QSet trackIds) { +void BaseTrackCache::slotTracksAddedOrChanged(QSet trackIds) { if (sDebug) { qDebug() << this << "slotTracksAdded" << trackIds.size(); } @@ -113,9 +113,7 @@ void BaseTrackCache::slotTrackChanged(TrackId trackId) { if (sDebug) { qDebug() << this << "slotTrackChanged" << trackId; } - QSet trackIds; - trackIds.insert(trackId); - emit tracksChanged(trackIds); + emit tracksChanged(QSet{trackId}); } void BaseTrackCache::slotTrackClean(TrackId trackId) { diff --git a/src/library/basetrackcache.h b/src/library/basetrackcache.h index b66b3519191d..96e5b91a8ac9 100644 --- a/src/library/basetrackcache.h +++ b/src/library/basetrackcache.h @@ -75,7 +75,7 @@ class BaseTrackCache : public QObject { void tracksChanged(QSet trackIds); public slots: - void slotTracksAdded(QSet trackId); + void slotTracksAddedOrChanged(QSet trackId); void slotTracksRemoved(QSet trackId); void slotTrackDirty(TrackId trackId); void slotTrackClean(TrackId trackId); diff --git a/src/library/basetracktablemodel.cpp b/src/library/basetracktablemodel.cpp new file mode 100644 index 000000000000..ee92114ae146 --- /dev/null +++ b/src/library/basetracktablemodel.cpp @@ -0,0 +1,795 @@ +#include "library/basetracktablemodel.h" + +#include "library/basecoverartdelegate.h" +#include "library/bpmdelegate.h" +#include "library/colordelegate.h" +#include "library/dao/trackschema.h" +#include "library/locationdelegate.h" +#include "library/previewbuttondelegate.h" +#include "library/stardelegate.h" +#include "library/starrating.h" +#include "library/trackcollection.h" +#include "library/trackcollectionmanager.h" +#include "mixer/playerinfo.h" +#include "mixer/playermanager.h" +#include "util/assert.h" +#include "util/compatibility.h" +#include "util/logger.h" +#include "widget/wlibrarytableview.h" + +namespace { + +const mixxx::Logger kLogger("BaseTrackTableModel"); + +const QString kEmptyString = QStringLiteral(""); + +// Alpha value for row color background (range 0 - 255) +constexpr int kTrackColorRowBackgroundOpacity = 0x20; // 12.5% opacity + +const QStringList kDefaultTableColumns = { + LIBRARYTABLE_ALBUM, + LIBRARYTABLE_ALBUMARTIST, + LIBRARYTABLE_ARTIST, + LIBRARYTABLE_BPM, + LIBRARYTABLE_BPM_LOCK, + LIBRARYTABLE_BITRATE, + LIBRARYTABLE_CHANNELS, + LIBRARYTABLE_COLOR, + LIBRARYTABLE_COMMENT, + LIBRARYTABLE_COMPOSER, + LIBRARYTABLE_COVERART, + LIBRARYTABLE_DATETIMEADDED, + LIBRARYTABLE_DURATION, + LIBRARYTABLE_FILETYPE, + LIBRARYTABLE_GENRE, + LIBRARYTABLE_GROUPING, + LIBRARYTABLE_KEY, + LIBRARYTABLE_LOCATION, + LIBRARYTABLE_PLAYED, + LIBRARYTABLE_PREVIEW, + LIBRARYTABLE_RATING, + LIBRARYTABLE_REPLAYGAIN, + LIBRARYTABLE_SAMPLERATE, + LIBRARYTABLE_TIMESPLAYED, + LIBRARYTABLE_TITLE, + LIBRARYTABLE_TRACKNUMBER, + LIBRARYTABLE_YEAR, +}; + +inline QSqlDatabase cloneDatabase( + const QSqlDatabase& prototype) { + const auto connectionName = + uuidToStringWithoutBraces(QUuid::createUuid()); + auto cloned = QSqlDatabase::cloneDatabase( + prototype, + connectionName); + if (prototype.isOpen() && !cloned.open()) { + kLogger.warning() + << "Failed to open cloned database connection" + << cloned + << cloned.lastError(); + } + return cloned; +} + +QSqlDatabase cloneDatabase( + TrackCollectionManager* pTrackCollectionManager) { + DEBUG_ASSERT(pTrackCollectionManager); + DEBUG_ASSERT(pTrackCollectionManager->internalCollection()); + const auto connectionName = + uuidToStringWithoutBraces(QUuid::createUuid()); + return cloneDatabase( + pTrackCollectionManager->internalCollection()->database()); +} + +} // anonymous namespace + +//static +QStringList BaseTrackTableModel::defaultTableColumns() { + return kDefaultTableColumns; +} + +BaseTrackTableModel::BaseTrackTableModel( + const char* settingsNamespace, + TrackCollectionManager* pTrackCollectionManager, + QObject* parent) + : QAbstractTableModel(parent), + TrackModel( + cloneDatabase(pTrackCollectionManager), + settingsNamespace), + m_pTrackCollectionManager(pTrackCollectionManager), + m_previewDeckGroup(PlayerManager::groupForPreviewDeck(0)) { + connect(&pTrackCollectionManager->internalCollection()->getTrackDAO(), + &TrackDAO::forceModelUpdate, + this, + &BaseTrackTableModel::slotRefreshAllRows); + connect(&PlayerInfo::instance(), + &PlayerInfo::trackLoaded, + this, + &BaseTrackTableModel::slotTrackLoaded); +} + +void BaseTrackTableModel::initTableColumnsAndHeaderProperties( + const QStringList& tableColumns) { + m_columnCache.setColumns(tableColumns); + if (m_columnHeaders.size() < tableColumns.size()) { + m_columnHeaders.resize(tableColumns.size()); + } + initHeaderProperties(); +} + +void BaseTrackTableModel::initHeaderProperties() { + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_ALBUM, + tr("Album"), + defaultColumnWidth() * 4); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST, + tr("Album Artist"), + defaultColumnWidth() * 4); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_ARTIST, + tr("Artist"), + defaultColumnWidth() * 4); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_BITRATE, + tr("Bitrate"), + defaultColumnWidth()); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_BPM, + tr("BPM"), + defaultColumnWidth() * 2); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_CHANNELS, + tr("Channels"), + defaultColumnWidth() / 2); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_COLOR, + tr("Color"), + defaultColumnWidth() / 2); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_COMMENT, + tr("Comment"), + defaultColumnWidth() * 6); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER, + tr("Composer"), + defaultColumnWidth() * 4); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_COVERART, + tr("Cover Art"), + defaultColumnWidth() / 2); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED, + tr("Date Added"), + defaultColumnWidth() * 3); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_DURATION, + tr("Duration"), + defaultColumnWidth()); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE, + tr("Type"), + defaultColumnWidth()); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_GENRE, + tr("Genre"), + defaultColumnWidth() * 4); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_GROUPING, + tr("Grouping"), + defaultColumnWidth() * 4); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_KEY, + tr("Key"), + defaultColumnWidth()); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION, + tr("Location"), + defaultColumnWidth() * 6); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW, + tr("Preview"), + defaultColumnWidth() / 2); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_RATING, + tr("Rating"), + defaultColumnWidth() * 2); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN, + tr("ReplayGain"), + defaultColumnWidth() * 2); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_SAMPLERATE, + tr("Samplerate"), + defaultColumnWidth()); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED, + tr("Played"), + defaultColumnWidth() * 2); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_TITLE, + tr("Title"), + defaultColumnWidth() * 4); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER, + tr("Track #"), + defaultColumnWidth()); + setHeaderProperties( + ColumnCache::COLUMN_LIBRARYTABLE_YEAR, + tr("Year"), + defaultColumnWidth()); +} + +void BaseTrackTableModel::setHeaderProperties( + ColumnCache::Column column, + QString title, + int defaultWidth) { + int section = fieldIndex(column); + if (section < 0) { + kLogger.debug() + << "Skipping header properties for unsupported column" + << column + << title; + return; + } + if (section >= m_columnHeaders.size()) { + m_columnHeaders.resize(section + 1); + } + m_columnHeaders[section].column = column; + setHeaderData( + section, + Qt::Horizontal, + m_columnCache.columnName(column), + TrackModel::kHeaderNameRole); + setHeaderData( + section, + Qt::Horizontal, + title, + Qt::DisplayRole); + setHeaderData( + section, + Qt::Horizontal, + defaultWidth, + TrackModel::kHeaderWidthRole); +} + +bool BaseTrackTableModel::setHeaderData( + int section, + Qt::Orientation orientation, + const QVariant& value, + int role) { + VERIFY_OR_DEBUG_ASSERT(section >= 0) { + return false; + } + VERIFY_OR_DEBUG_ASSERT(section < m_columnHeaders.size()) { + return false; + } + if (orientation != Qt::Horizontal) { + // We only care about horizontal headers. + return false; + } + m_columnHeaders[section].header[role] = value; + emit headerDataChanged(orientation, section, section); + return true; +} + +QVariant BaseTrackTableModel::headerData( + int section, + Qt::Orientation orientation, + int role) const { + if (orientation == Qt::Horizontal) { + switch (role) { + case Qt::DisplayRole: { + QVariant headerValue = + m_columnHeaders.value(section).header.value(role); + if (!headerValue.isValid()) { + // Try EditRole if DisplayRole wasn't present + headerValue = m_columnHeaders.value(section).header.value(Qt::EditRole); + } + if (headerValue.isValid()) { + return headerValue; + } else { + return QVariant(section).toString(); + } + } + case TrackModel::kHeaderWidthRole: { + QVariant widthValue = m_columnHeaders.value(section).header.value(role); + if (widthValue.isValid()) { + return widthValue; + } else { + return defaultColumnWidth(); + } + } + case TrackModel::kHeaderNameRole: { + return m_columnHeaders.value(section).header.value(role); + } + case Qt::ToolTipRole: { + QVariant tooltip = m_columnHeaders.value(section).header.value(role); + if (tooltip.isValid()) { + return tooltip; + } + break; + } + default: + break; + } + } + return QAbstractTableModel::headerData(section, orientation, role); +} + +int BaseTrackTableModel::countValidColumnHeaders() const { + int count = 0; + for (const auto& columnHeader : m_columnHeaders) { + if (columnHeader.column != + ColumnCache::COLUMN_LIBRARYTABLE_INVALID) { + ++count; + } + } + return count; +} + +int BaseTrackTableModel::columnCount(const QModelIndex& parent) const { + VERIFY_OR_DEBUG_ASSERT(!parent.isValid()) { + return 0; + } + return countValidColumnHeaders(); +} + +bool BaseTrackTableModel::isColumnHiddenByDefault( + int column) { + return column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_CHANNELS) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_SAMPLERATE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); +} + +QAbstractItemDelegate* BaseTrackTableModel::delegateForColumn( + const int index, QObject* pParent) { + auto* pTableView = qobject_cast(pParent); + VERIFY_OR_DEBUG_ASSERT(pTableView) { + return nullptr; + } + if (index == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING)) { + return new StarDelegate(pTableView); + } else if (index == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { + return new BPMDelegate(pTableView); + } else if (PlayerManager::numPreviewDecks() > 0 && + index == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) { + return new PreviewButtonDelegate(pTableView, index); + } else if (index == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION)) { + return new LocationDelegate(pTableView); + } else if (index == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR)) { + return new ColorDelegate(pTableView); + } else if (index == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART)) { + auto* pCoverArtDelegate = + doCreateCoverArtDelegate(pTableView); + // WLibraryTableView -> BaseCoverArtDelegate + connect(pTableView, + &WLibraryTableView::onlyCachedCoverArt, + pCoverArtDelegate, + &BaseCoverArtDelegate::slotInhibitLazyLoading); + // BaseCoverArtDelegate -> BaseTrackTableModel + connect(pCoverArtDelegate, + &BaseCoverArtDelegate::rowsChanged, + this, + &BaseTrackTableModel::slotRefreshCoverRows); + return pCoverArtDelegate; + } + return nullptr; +} + +QVariant BaseTrackTableModel::data( + const QModelIndex& index, + int role) const { + if (!index.isValid()) { + return QVariant(); + } + + if (role == Qt::BackgroundRole) { + QModelIndex colorIndex = index.sibling( + index.row(), + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR)); + if (!colorIndex.isValid()) { + return QVariant(); + } + const auto trackColor = + mixxx::RgbColor::fromQVariant( + rawValue(colorIndex)); + if (!trackColor) { + return QVariant(); + } + auto bgColor = mixxx::RgbColor::toQColor(trackColor); + DEBUG_ASSERT(bgColor.isValid()); + bgColor.setAlpha(kTrackColorRowBackgroundOpacity); + return QBrush(bgColor); + } + + // Only retrieve a value for supported roles + if (role != Qt::DisplayRole && + role != Qt::EditRole && + role != Qt::CheckStateRole && + role != Qt::ToolTipRole) { + return QVariant(); + } + + return roleValue(index, rawValue(index), role); +} + +bool BaseTrackTableModel::setData( + const QModelIndex& index, + const QVariant& value, + int role) { + const int column = index.column(); + + // Override sets to TIMESPLAYED and redirect them to PLAYED + if (role == Qt::CheckStateRole) { + const auto val = value.toInt() > 0; + if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { + QModelIndex playedIndex = index.sibling(index.row(), fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED)); + return setData(playedIndex, val, Qt::EditRole); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { + QModelIndex bpmLockindex = index.sibling(index.row(), fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)); + return setData(bpmLockindex, val, Qt::EditRole); + } + return false; + } + + TrackPointer pTrack = getTrack(index); + if (!pTrack) { + return false; + } + + // Do not save the track here. Changing the track dirties it and the caching + // system will automatically save the track once it is unloaded from + // memory. rryan 10/2010 + return setTrackValueForColumn(pTrack, column, value, role); +} + +QVariant BaseTrackTableModel::roleValue( + const QModelIndex& index, + QVariant&& rawValue, + int role) const { + const int column = index.column(); + // Format the value based on whether we are in a tooltip, + // display, or edit role + switch (role) { + case Qt::ToolTipRole: + if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR)) { + return mixxx::RgbColor::toQString(mixxx::RgbColor::fromQVariant(rawValue)); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) { + return kEmptyString; + } + M_FALLTHROUGH_INTENDED; + case Qt::DisplayRole: + if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION)) { + bool ok; + const auto duration = rawValue.toDouble(&ok); + if (ok && duration >= 0) { + return mixxx::Duration::formatTime( + duration, + mixxx::Duration::Precision::SECONDS); + } else { + return QVariant(); + } + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING)) { + VERIFY_OR_DEBUG_ASSERT(rawValue.canConvert(QMetaType::Int)) { + return QVariant(); + } + return QVariant::fromValue(StarRating(rawValue.toInt())); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED)) { + return rawValue.toBool(); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { + VERIFY_OR_DEBUG_ASSERT(rawValue.canConvert(QMetaType::Int)) { + return QVariant(); + } + return QString("(%1)").arg(rawValue.toInt()); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED)) { + QDateTime gmtDate = rawValue.toDateTime(); + gmtDate.setTimeSpec(Qt::UTC); + return gmtDate.toLocalTime(); + } else if (column == fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_DATETIMEADDED)) { + QDateTime gmtDate = rawValue.toDateTime(); + gmtDate.setTimeSpec(Qt::UTC); + return gmtDate.toLocalTime(); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { + bool ok; + const auto bpmValue = rawValue.toDouble(&ok); + if (ok && bpmValue > 0.0) { + return QString("%1").arg(bpmValue, 0, 'f', 1); + } else { + return QChar('-'); + } + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) { + return rawValue.toBool(); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR)) { + return mixxx::TrackMetadata::formatCalendarYear(rawValue.toString()); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER)) { + const auto trackNumber = rawValue.toInt(0); + if (trackNumber > 0) { + return std::move(rawValue); + } else { + // clear invalid values + return QVariant(); + } + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE)) { + int bitrateValue = rawValue.toInt(0); + if (bitrateValue > 0) { + return std::move(rawValue); + } else { + // clear invalid values + return QVariant(); + } + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY)) { + // If we know the semantic key via the LIBRARYTABLE_KEY_ID + // column (as opposed to the string representation of the key + // currently stored in the DB) then lookup the key and render it + // using the user's selected notation. + int keyIdColumn = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY_ID); + if (keyIdColumn != -1) { + mixxx::track::io::key::ChromaticKey key = + KeyUtils::keyFromNumericValue( + index.sibling(index.row(), keyIdColumn).data().toInt()); + if (key != mixxx::track::io::key::INVALID) { + // Render this key with the user-provided notation. + return KeyUtils::keyToString(key); + } + } + // clear invalid values + return QVariant(); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN)) { + bool ok; + const auto gainValue = rawValue.toDouble(&ok); + return ok ? mixxx::ReplayGain::ratioToString(gainValue) : QString(); + } + // Otherwise, just use the column value + break; + case Qt::EditRole: + if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { + bool ok; + const auto bpmValue = rawValue.toDouble(&ok); + return ok ? bpmValue : 0.0; + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { + return index.sibling( + index.row(), fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED)) + .data() + .toBool(); + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING)) { + VERIFY_OR_DEBUG_ASSERT(rawValue.canConvert(QMetaType::Int)) { + return QVariant(); + } + return QVariant::fromValue(StarRating(rawValue.toInt())); + } + // Otherwise, just use the column value + break; + case Qt::CheckStateRole: + if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { + bool played = index.sibling( + index.row(), fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED)) + .data() + .toBool(); + return played ? Qt::Checked : Qt::Unchecked; + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { + bool locked = index.sibling( + index.row(), fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) + .data() + .toBool(); + return locked ? Qt::Checked : Qt::Unchecked; + } + // No check state supported + return QVariant(); + default: + DEBUG_ASSERT(!"unexpected role"); + break; + } + return std::move(rawValue); + +} + +bool BaseTrackTableModel::isBpmLocked( + const QModelIndex& index) const { + const auto bpmLockIndex = + index.sibling( + index.row(), + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)); + return bpmLockIndex.data().toBool(); +} + +Qt::ItemFlags BaseTrackTableModel::defaultItemFlags( + const QModelIndex& index) const { + if (index.isValid()) { + return QAbstractItemModel::flags(index) | + // Enable dragging songs from this data model to elsewhere + // like the waveform widget to load a track into a Player + Qt::ItemIsDragEnabled; + } else { + return Qt::ItemIsEnabled; + } +} + +Qt::ItemFlags BaseTrackTableModel::readOnlyFlags( + const QModelIndex& index) const { + return defaultItemFlags(index); +} + +Qt::ItemFlags BaseTrackTableModel::readWriteFlags( + const QModelIndex& index) const { + if (!index.isValid()) { + return defaultItemFlags(index); + } + + const int column = index.column(); + if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_CHANNELS) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_SAMPLERATE)) { + return readOnlyFlags(index); + } + + Qt::ItemFlags itemFlags = defaultItemFlags(index); + if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PLAYED) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) { + // Checkable cells + itemFlags |= Qt::ItemIsUserCheckable; + } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) { + // Always allow checking of the BPM-locked indicator + itemFlags |= Qt::ItemIsUserCheckable; + // Allow editing of BPM only if not locked + if (!isBpmLocked(index)) { + itemFlags |= Qt::ItemIsEditable; + } + } else { + // Cells are editable by default + itemFlags |= Qt::ItemIsEditable; + } + return itemFlags; +} + +Qt::ItemFlags BaseTrackTableModel::flags( + const QModelIndex& index) const { + return readWriteFlags(index); +} + +QList BaseTrackTableModel::collectUrls( + const QModelIndexList& indexes) const { + QList urls; + urls.reserve(indexes.size()); + // The list of indexes we're given contains separates indexes for each + // column, so even if only one row is selected, we'll have columnCount() + // indices. We need to only count each row once: + QSet visitedRows; + for (const auto& index : indexes) { + if (visitedRows.contains(index.row())) { + continue; + } + visitedRows.insert(index.row()); + QUrl url = TrackFile(getTrackLocation(index)).toUrl(); + if (url.isValid()) { + urls.append(url); + } + } + return urls; +} + +QMimeData* BaseTrackTableModel::mimeData( + const QModelIndexList& indexes) const { + const auto urls = collectUrls(indexes); + if (urls.isEmpty()) { + return nullptr; + } else { + QMimeData* mimeData = new QMimeData(); + mimeData->setUrls(urls); + return mimeData; + } +} + +void BaseTrackTableModel::slotTrackLoaded( + QString group, + TrackPointer pTrack) { + if (group == m_previewDeckGroup) { + // If there was a previously loaded track, refresh its rows so the + // preview state will update. + if (m_previewDeckTrackId.isValid()) { + const int numColumns = columnCount(); + QLinkedList rows = getTrackRows(m_previewDeckTrackId); + m_previewDeckTrackId = TrackId(); // invalidate + foreach (int row, rows) { + QModelIndex topLeft = index(row, 0); + QModelIndex bottomRight = index(row, numColumns); + emit dataChanged(topLeft, bottomRight); + } + } + m_previewDeckTrackId = doGetTrackId(pTrack); + } +} + +void BaseTrackTableModel::slotRefreshCoverRows( + QList rows) { + if (rows.isEmpty()) { + return; + } + const int column = fieldIndex(LIBRARYTABLE_COVERART); + VERIFY_OR_DEBUG_ASSERT(column >= 0) { + return; + } + emitDataChangedForMultipleRowsInColumn(rows, column); +} + +void BaseTrackTableModel::slotRefreshAllRows() { + select(); +} + +void BaseTrackTableModel::emitDataChangedForMultipleRowsInColumn( + const QList& rows, + int column, + const QVector& roles) { + DEBUG_ASSERT(column >= 0); + DEBUG_ASSERT(column < columnCount()); + int beginRow = -1; + int endRow = -1; + for (const int row : rows) { + DEBUG_ASSERT(row >= rows.first()); + DEBUG_ASSERT(row <= rows.last()); + DEBUG_ASSERT(row >= 0); + if (row >= rowCount()) { + // The number of rows might have changed since the signal + // has been emitted. This case seems to occur after switching + // to a different view with less rows. + continue; + } + if (beginRow < 0) { + // Start the first stride + DEBUG_ASSERT(beginRow == endRow); + DEBUG_ASSERT(row == rows.first()); + beginRow = row; + endRow = row + 1; + } else if (row == endRow) { + // Continue the current stride + ++endRow; + } else { + // Finish the current stride... + DEBUG_ASSERT(beginRow >= rows.first()); + DEBUG_ASSERT(beginRow < endRow); + DEBUG_ASSERT(endRow - 1 <= rows.last()); + QModelIndex topLeft = index(beginRow, column); + QModelIndex bottomRight = index(endRow - 1, column); + emit dataChanged(topLeft, bottomRight, roles); + // ...before starting the next stride + // Rows are expected to be sorted in ascending order + // without duplicates! + DEBUG_ASSERT(row >= endRow); + beginRow = row; + endRow = row + 1; + } + } + if (beginRow < endRow) { + // Finish the final stride + DEBUG_ASSERT(beginRow >= rows.first()); + DEBUG_ASSERT(endRow - 1 <= rows.last()); + QModelIndex topLeft = index(beginRow, column); + QModelIndex bottomRight = index(endRow - 1, column); + emit dataChanged(topLeft, bottomRight, roles); + } +} + +TrackPointer BaseTrackTableModel::getTrackByRef( + const TrackRef& trackRef) const { + return m_pTrackCollectionManager->internalCollection()->getTrackByRef(trackRef); +} diff --git a/src/library/basetracktablemodel.h b/src/library/basetracktablemodel.h new file mode 100644 index 000000000000..16aeff967fb6 --- /dev/null +++ b/src/library/basetracktablemodel.h @@ -0,0 +1,214 @@ +#pragma once + +#include +#include +#include +#include + +#include "library/columncache.h" +#include "library/trackmodel.h" + +class BaseCoverArtDelegate; +class TrackCollectionManager; + +class BaseTrackTableModel : public QAbstractTableModel, public TrackModel { + Q_OBJECT + DISALLOW_COPY_AND_ASSIGN(BaseTrackTableModel); + + public: + explicit BaseTrackTableModel( + const char* settingsNamespace, + TrackCollectionManager* const pTrackCollectionManager, + QObject* parent = nullptr); + ~BaseTrackTableModel() override = default; + + /////////////////////////////////////////////////////// + // Overridable functions + /////////////////////////////////////////////////////// + + virtual int fieldIndex( + ColumnCache::Column column) const { + return m_columnCache.fieldIndex(column); + } + + /////////////////////////////////////////////////////// + // Inherited from QAbstractItemModel + /////////////////////////////////////////////////////// + + QVariant headerData( + int section, + Qt::Orientation orientation, + int role = Qt::DisplayRole) const final; + bool setHeaderData( + int section, + Qt::Orientation orientation, + const QVariant& value, + int role = Qt::DisplayRole) final; + + QMimeData* mimeData( + const QModelIndexList& indexes) const final; + + QVariant data( + const QModelIndex& index, + int role = Qt::DisplayRole) const final; + bool setData( + const QModelIndex& index, + const QVariant& value, + int role = Qt::EditRole) final; + + // Calculate the number of columns from all valid + // column headers. + // Reimplement in derived classes if a more efficient + // implementation is available. + int columnCount( + const QModelIndex& parent = QModelIndex()) const override; + + // Calls readWriteFlags() by default + // Reimplement in derived classes if the table model + // should be readOnly + Qt::ItemFlags flags( + const QModelIndex& index) const override; + + /////////////////////////////////////////////////////// + // Inherited from TrackModel + /////////////////////////////////////////////////////// + + QAbstractItemDelegate* delegateForColumn( + const int column, + QObject* pParent) final; + + int fieldIndex( + const QString& fieldName) const override { + return m_columnCache.fieldIndex(fieldName); + } + + bool isColumnHiddenByDefault( + int column) override; + + TrackPointer getTrackByRef( + const TrackRef& trackRef) const override; + + protected: + static constexpr int defaultColumnWidth() { + return 50; + } + static QStringList defaultTableColumns(); + + // Build a map from the column names to their indices + // used by fieldIndex(). This function has to be called + void initTableColumnsAndHeaderProperties( + const QStringList& tableColumns = defaultTableColumns()); + + QString columnNameForFieldIndex(int index) const { + return m_columnCache.columnNameForFieldIndex(index); + } + + // A simple helper function for initializing header title and width. + // Note that the ideal width of a column is based on the width of + // its data, not the title string itself. + void setHeaderProperties( + ColumnCache::Column column, + QString title, + int defaultWidth = 0); + + ColumnCache::Column mapColumn(int column) const { + if (column >= 0 && column < m_columnHeaders.size()) { + return m_columnHeaders[column].column; + } else { + return ColumnCache::COLUMN_LIBRARYTABLE_INVALID; + } + } + + // Emit the dataChanged() signal for multiple rows in + // a single column. The list of rows must be sorted in + // ascending order without duplicates! + void emitDataChangedForMultipleRowsInColumn( + const QList& rows, + int column, + const QVector& roles = QVector()); + + const TrackId previewDeckTrackId() const { + return m_previewDeckTrackId; + } + + bool isBpmLocked( + const QModelIndex& index) const; + + const QPointer m_pTrackCollectionManager; + + /////////////////////////////////////////////////////// + // Overridable functions + /////////////////////////////////////////////////////// + + virtual void initHeaderProperties(); + + // Use this if you want a model that is read-only. + virtual Qt::ItemFlags readOnlyFlags( + const QModelIndex& index) const; + // Use this if you want a model that can be changed + virtual Qt::ItemFlags readWriteFlags( + const QModelIndex& index) const; + + virtual QVariant rawValue( + const QModelIndex& index) const = 0; + + // Reimplement in derived classes to handle columns other + // then COLUMN_LIBRARYTABLE + virtual QVariant roleValue( + const QModelIndex& index, + QVariant&& rawValue, + int role) const; + + virtual bool setTrackValueForColumn( + const TrackPointer& pTrack, + int column, + const QVariant& value, + int role) { + Q_UNUSED(pTrack); + Q_UNUSED(column); + Q_UNUSED(value); + Q_UNUSED(role); + return false; + } + + private slots: + void slotTrackLoaded( + QString group, + TrackPointer pTrack); + + void slotRefreshCoverRows( + QList rows); + + void slotRefreshAllRows(); + + private: + // Track models may reference tracks by an external id + // TODO: TrackId should only be used for tracks from + // the internal database. + virtual TrackId doGetTrackId( + const TrackPointer& pTrack) const { + return pTrack ? pTrack->getId() : TrackId(); + } + virtual BaseCoverArtDelegate* doCreateCoverArtDelegate( + QTableView* pTableView) const = 0; + + Qt::ItemFlags defaultItemFlags( + const QModelIndex& index) const; + + QList collectUrls( + const QModelIndexList& indexes) const; + + const QString m_previewDeckGroup; + + ColumnCache m_columnCache; + + struct ColumnHeader { + ColumnCache::Column column = ColumnCache::COLUMN_LIBRARYTABLE_INVALID; + QHash header; + }; + QVector m_columnHeaders; + + int countValidColumnHeaders() const; + + TrackId m_previewDeckTrackId; +}; diff --git a/src/library/coverartdelegate.cpp b/src/library/coverartdelegate.cpp index 732624df6d48..a82f490e8948 100644 --- a/src/library/coverartdelegate.cpp +++ b/src/library/coverartdelegate.cpp @@ -1,166 +1,43 @@ -#include - #include "library/coverartdelegate.h" -#include "library/coverartcache.h" + #include "library/dao/trackschema.h" #include "library/trackmodel.h" -#include "widget/wlibrarytableview.h" -#include "util/compatibility.h" -#include "util/logger.h" -#include "util/math.h" - -namespace { - -const mixxx::Logger kLogger("CoverArtDelegate"); - -} // anonymous namespace - -CoverArtDelegate::CoverArtDelegate(WLibraryTableView* parent) - : TableItemDelegate(parent), - m_pTableView(parent), - m_pTrackModel(nullptr), - m_bOnlyCachedCover(false), - m_iCoverColumn(-1), - m_iCoverSourceColumn(-1), - m_iCoverTypeColumn(-1), - m_iCoverLocationColumn(-1), - m_iCoverHashColumn(-1), - m_iTrackLocationColumn(-1), - m_iIdColumn(-1) { - // This assumes that the parent is wtracktableview - connect(parent, - &WLibraryTableView::onlyCachedCoverArt, - this, - &CoverArtDelegate::slotOnlyCachedCoverArt); - - CoverArtCache* pCache = CoverArtCache::instance(); - if (pCache) { - connect(pCache, - &CoverArtCache::coverFound, - this, - &CoverArtDelegate::slotCoverFound); - } - - QTableView* pTableView = qobject_cast(parent); - if (pTableView) { - m_pTrackModel = dynamic_cast(pTableView->model()); - } - - if (m_pTrackModel) { - m_iCoverColumn = m_pTrackModel->fieldIndex( - LIBRARYTABLE_COVERART); - m_iCoverSourceColumn = m_pTrackModel->fieldIndex( - LIBRARYTABLE_COVERART_SOURCE); - m_iCoverTypeColumn = m_pTrackModel->fieldIndex( - LIBRARYTABLE_COVERART_TYPE); - m_iCoverHashColumn = m_pTrackModel->fieldIndex( - LIBRARYTABLE_COVERART_HASH); - m_iCoverLocationColumn = m_pTrackModel->fieldIndex( - LIBRARYTABLE_COVERART_LOCATION); - m_iTrackLocationColumn = m_pTrackModel->fieldIndex( - TRACKLOCATIONSTABLE_LOCATION); - m_iIdColumn = m_pTrackModel->fieldIndex( - LIBRARYTABLE_ID); - } -} - -void CoverArtDelegate::slotOnlyCachedCoverArt(bool b) { - m_bOnlyCachedCover = b; - - // If we can request non-cache covers now, request updates for all rows that - // were cache misses since the last time. - if (!m_bOnlyCachedCover) { - foreach (int row, m_cacheMissRows) { - emit coverReadyForCell(row, m_iCoverColumn); - } - m_cacheMissRows.clear(); - } +#include "util/assert.h" + +CoverArtDelegate::CoverArtDelegate( + QTableView* parent) + : BaseCoverArtDelegate(parent), + m_iCoverSourceColumn(m_pTrackModel->fieldIndex( + LIBRARYTABLE_COVERART_SOURCE)), + m_iCoverTypeColumn(m_pTrackModel->fieldIndex( + LIBRARYTABLE_COVERART_TYPE)), + m_iCoverLocationColumn(m_pTrackModel->fieldIndex( + LIBRARYTABLE_COVERART_LOCATION)), + m_iCoverHashColumn(m_pTrackModel->fieldIndex( + LIBRARYTABLE_COVERART_HASH)), + m_iTrackIdColumn(m_pTrackModel->fieldIndex( + LIBRARYTABLE_ID)), + m_iTrackLocationColumn(m_pTrackModel->fieldIndex( + TRACKLOCATIONSTABLE_LOCATION)) { + DEBUG_ASSERT(m_iCoverSourceColumn >= 0); + DEBUG_ASSERT(m_iCoverTypeColumn >= 0); + DEBUG_ASSERT(m_iCoverLocationColumn >= 0); + DEBUG_ASSERT(m_iCoverHashColumn >= 0); + DEBUG_ASSERT(m_iTrackIdColumn >= 0); + DEBUG_ASSERT(m_iTrackLocationColumn >= 0); } -void CoverArtDelegate::slotCoverFound( - const QObject* pRequestor, - const CoverInfo& coverInfo, - const QPixmap& pixmap, - quint16 requestedHash, - bool coverInfoUpdated) { - Q_UNUSED(pixmap); - if (pRequestor != this) { - return; - } - const QLinkedList rows = - m_hashToRow.take(requestedHash); - foreach(int row, rows) { - emit coverReadyForCell(row, m_iCoverColumn); - } - if (m_pTrackModel && coverInfoUpdated) { - const auto pTrack = - m_pTrackModel->getTrackByRef( - TrackRef::fromFileInfo(coverInfo.trackLocation)); - if (pTrack) { - kLogger.info() - << "Updating cover info of track" - << coverInfo.trackLocation; - pTrack->setCoverInfo(coverInfo); - } - } -} - -void CoverArtDelegate::paintItem(QPainter *painter, - const QStyleOptionViewItem &option, - const QModelIndex &index) const { - paintItemBackground(painter, option, index); - - if (m_iIdColumn < 0 || - m_iCoverSourceColumn == -1 || - m_iCoverTypeColumn == -1 || - m_iCoverLocationColumn == -1 || - m_iCoverHashColumn == -1) { - return; - } - - CoverInfo info; - info.type = static_cast( - index.sibling(index.row(), m_iCoverTypeColumn).data().toInt()); - - // We don't support types other than METADATA or FILE currently. - if (info.type != CoverInfo::METADATA && info.type != CoverInfo::FILE) { - return; - } - - info.source = static_cast( - index.sibling(index.row(), m_iCoverSourceColumn).data().toInt()); - info.coverLocation = index.sibling(index.row(), m_iCoverLocationColumn).data().toString(); - info.hash = index.sibling(index.row(), m_iCoverHashColumn).data().toUInt(); - info.trackLocation = index.sibling(index.row(), m_iTrackLocationColumn).data().toString(); - - double scaleFactor = getDevicePixelRatioF(static_cast(parent())); - // We listen for updates via slotCoverFound above and signal to - // BaseSqlTableModel when a row's cover is ready. - CoverArtCache* const pCache = CoverArtCache::instance(); - VERIFY_OR_DEBUG_ASSERT(pCache) { - return; - } - QPixmap pixmap = pCache->tryLoadCover( - this, - info, - option.rect.width() * scaleFactor, - m_bOnlyCachedCover ? CoverArtCache::Loading::CachedOnly : CoverArtCache::Loading::Default); - if (!pixmap.isNull()) { - // Cache hit - pixmap.setDevicePixelRatio(scaleFactor); - painter->drawPixmap(option.rect.topLeft(), pixmap); - return; - } - - if (m_bOnlyCachedCover) { - // We are requesting cache-only covers and got a cache - // miss. Record this row so that when we switch to requesting - // non-cache we can request an update. - m_cacheMissRows.append(index.row()); - } else { - // If we asked for a non-cache image and got a null pixmap, then our - // request was queued. We cannot use the cover image hash, because this - // might be refreshed while loading the image! - m_hashToRow[info.hash].append(index.row()); - } +CoverInfo CoverArtDelegate::coverInfoForIndex( + const QModelIndex& index) const { + CoverInfo coverInfo; + coverInfo.hash = index.sibling(index.row(), m_iCoverHashColumn).data().toUInt(); + coverInfo.type = static_cast( + index.sibling(index.row(), m_iCoverTypeColumn).data().toInt()); + coverInfo.source = static_cast( + index.sibling(index.row(), m_iCoverSourceColumn).data().toInt()); + coverInfo.coverLocation = + index.sibling(index.row(), m_iCoverLocationColumn).data().toString(); + coverInfo.trackLocation = + index.sibling(index.row(), m_iTrackLocationColumn).data().toString(); + return coverInfo; } diff --git a/src/library/coverartdelegate.h b/src/library/coverartdelegate.h index 1d3e00ac8f30..dd4e31ed0ea1 100644 --- a/src/library/coverartdelegate.h +++ b/src/library/coverartdelegate.h @@ -1,65 +1,23 @@ -#ifndef COVERARTDELEGATE_H -#define COVERARTDELEGATE_H +#pragma once -#include -#include -#include +#include "library/basecoverartdelegate.h" -#include "library/tableitemdelegate.h" - -class CoverInfo; -class TrackModel; -class WLibraryTableView; - -class CoverArtDelegate : public TableItemDelegate { +class CoverArtDelegate : public BaseCoverArtDelegate { Q_OBJECT - public: - explicit CoverArtDelegate(WLibraryTableView* parent); - ~CoverArtDelegate() override = default; - - void paintItem(QPainter* painter, - const QStyleOptionViewItem& option, - const QModelIndex& index) const override; - - signals: - void coverReadyForCell(int row, int column); - private slots: - // If it is true, it must not try to load and search covers. - // - // It means that in this cases it will just draw - // covers which are already in the pixmapcache. - // - // It is useful to handle cases when the user scroll down - // very fast or when they hold an arrow key, because - // in these cases 'paint()' would be called very often - // and it might make CoverDelegate starts many searches, - // which could bring performance issues. - void slotOnlyCachedCoverArt(bool b); - - void slotCoverFound( - const QObject* pRequestor, - const CoverInfo& coverInfo, - const QPixmap& pixmap, - quint16 requestedHash, - bool coverInfoUpdated); + public: + explicit CoverArtDelegate( + QTableView* parent); + ~CoverArtDelegate() final = default; private: - QTableView* m_pTableView; - TrackModel* m_pTrackModel; - bool m_bOnlyCachedCover; - int m_iCoverColumn; + CoverInfo coverInfoForIndex( + const QModelIndex& index) const final; + int m_iCoverSourceColumn; int m_iCoverTypeColumn; int m_iCoverLocationColumn; int m_iCoverHashColumn; + int m_iTrackIdColumn; int m_iTrackLocationColumn; - int m_iIdColumn; - - // We need to record rows in paint() (which is const) so these are marked - // mutable. - mutable QList m_cacheMissRows; - mutable QHash > m_hashToRow; }; - -#endif // COVERARTDELEGATE_H diff --git a/src/library/dao/autodjcratesdao.cpp b/src/library/dao/autodjcratesdao.cpp index c56f9051c88e..62d5a86d4048 100644 --- a/src/library/dao/autodjcratesdao.cpp +++ b/src/library/dao/autodjcratesdao.cpp @@ -198,8 +198,8 @@ void AutoDJCratesDAO::createAndConnectAutoDjCratesDatabase() { // Be notified when a track is modified. // We only care when the number of times it's been played changes. - connect(&m_pTrackCollection->getTrackDAO(), - &TrackDAO::trackDirty, + connect(m_pTrackCollection, + &TrackCollection::trackDirty, this, &AutoDJCratesDAO::slotTrackDirty); diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 32d0beba39ea..85b7cfa2ad21 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -297,7 +297,7 @@ void TrackDAO::databaseTrackAdded(TrackPointer pTrack) { void TrackDAO::databaseTracksChanged(QSet changedTracks) { // results in a call of BaseTrackCache::updateTracksInIndex(trackIds); if (!changedTracks.isEmpty()) { - emit tracksAdded(changedTracks); + emit tracksChanged(changedTracks); } } diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index 3948a1730d46..a596b38a23b7 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -76,6 +76,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC void trackDirty(TrackId trackId) const; void trackClean(TrackId trackId) const; void trackChanged(TrackId trackId); + void tracksChanged(QSet trackIds); void tracksAdded(QSet trackIds); void tracksRemoved(QSet trackIds); void dbTrackAdded(TrackPointer pTrack); diff --git a/src/library/tableitemdelegate.cpp b/src/library/tableitemdelegate.cpp index 617b805d0180..0176555ebf40 100644 --- a/src/library/tableitemdelegate.cpp +++ b/src/library/tableitemdelegate.cpp @@ -9,6 +9,7 @@ TableItemDelegate::TableItemDelegate(QTableView* pTableView) : QStyledItemDelegate(pTableView), m_pTableView(pTableView) { + DEBUG_ASSERT(m_pTableView); } void TableItemDelegate::paint( @@ -16,19 +17,15 @@ void TableItemDelegate::paint( const QStyleOptionViewItem& option, const QModelIndex& index) const { PainterScope painterScope(painter); - painter->setClipRect(option.rect); // Set the palette appropriately based on whether the row is selected or // not. We also have to check if it is inactive or not and use the // appropriate ColorGroup. - QPalette::ColorGroup cg = QPalette::Normal; - if (option.state & QStyle::State_Enabled) { - if (!(option.state & QStyle::State_Active)) { - cg = QPalette::Disabled; - } - } else { - cg = QPalette::Disabled; + QPalette::ColorGroup cg = QPalette::Disabled; + if ((option.state & QStyle::State_Enabled) && + (option.state & QStyle::State_Active)) { + cg = QPalette::Normal; } if (option.state & QStyle::State_Selected) { @@ -37,15 +34,13 @@ void TableItemDelegate::paint( painter->setBrush(option.palette.color(cg, QPalette::Text)); } - if (m_pTableView) { - QStyle* style = m_pTableView->style(); - if (style) { - style->drawControl( - QStyle::CE_ItemViewItem, - &option, - painter, - m_pTableView); - } + QStyle* style = m_pTableView->style(); + if (style) { + style->drawControl( + QStyle::CE_ItemViewItem, + &option, + painter, + m_pTableView); } paintItem(painter, option, index); @@ -61,11 +56,15 @@ void TableItemDelegate::paintItemBackground( const QModelIndex& index) { // If the row is not selected, paint the desired background color before // painting the delegate item - if (!option.showDecorationSelected || !(option.state & QStyle::State_Selected)) { - QVariant bgValue = index.data(Qt::BackgroundRole); - if (bgValue.isValid()) { - DEBUG_ASSERT(bgValue.canConvert()); - painter->fillRect(option.rect, qvariant_cast(bgValue)); - } + if (option.showDecorationSelected && + (option.state & QStyle::State_Selected)) { + return; + } + QVariant bgValue = index.data(Qt::BackgroundRole); + if (!bgValue.isValid()) { + return; } + DEBUG_ASSERT(bgValue.canConvert()); + const auto bgBrush = qvariant_cast(bgValue); + painter->fillRect(option.rect, bgBrush); } diff --git a/src/library/tableitemdelegate.h b/src/library/tableitemdelegate.h index 01f6fd233569..0ac0bdc38770 100644 --- a/src/library/tableitemdelegate.h +++ b/src/library/tableitemdelegate.h @@ -3,22 +3,22 @@ #include #include - class TableItemDelegate : public QStyledItemDelegate { Q_OBJECT public: - explicit TableItemDelegate(QTableView* pTableView); + explicit TableItemDelegate( + QTableView* pTableView); ~TableItemDelegate() override = default; void paint( - QPainter *painter, - const QStyleOptionViewItem &option, - const QModelIndex &index) const override; + QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const override; virtual void paintItem( - QPainter *painter, - const QStyleOptionViewItem &option, - const QModelIndex &index) const = 0; + QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const = 0; protected: static void paintItemBackground( diff --git a/src/library/trackcollection.cpp b/src/library/trackcollection.cpp index a6f4f07bc459..2ef5a410e940 100644 --- a/src/library/trackcollection.cpp +++ b/src/library/trackcollection.cpp @@ -23,6 +23,44 @@ TrackCollection::TrackCollection( m_analysisDao(pConfig), m_trackDao(m_cueDao, m_playlistDao, m_analysisDao, m_libraryHashDao, pConfig) { + // Forward signals from TrackDAO + connect(&m_trackDao, + &TrackDAO::trackClean, + this, + &TrackCollection::trackClean, + /*signal-to-signal*/ Qt::DirectConnection); + connect(&m_trackDao, + &TrackDAO::trackDirty, + this, + &TrackCollection::trackDirty, + /*signal-to-signal*/ Qt::DirectConnection); + connect(&m_trackDao, + &TrackDAO::trackChanged, + this, + [this](TrackId trackId) { + emit tracksChanged(QSet{trackId}); + }, + /*signal-to-signal*/ Qt::DirectConnection); + connect(&m_trackDao, + &TrackDAO::tracksAdded, + this, + &TrackCollection::tracksAdded, + /*signal-to-signal*/ Qt::DirectConnection); + connect(&m_trackDao, + &TrackDAO::tracksChanged, + this, + &TrackCollection::tracksChanged, + /*signal-to-signal*/ Qt::DirectConnection); + connect(&m_trackDao, + &TrackDAO::tracksRemoved, + this, + &TrackCollection::tracksRemoved, + /*signal-to-signal*/ Qt::DirectConnection); + connect(&m_trackDao, + &TrackDAO::forceModelUpdate, + this, + &TrackCollection::multipleTracksChanged, + /*signal-to-signal*/ Qt::DirectConnection); } TrackCollection::~TrackCollection() { @@ -87,7 +125,11 @@ void TrackCollection::connectTrackSource(QSharedPointer pTrackSo connect(&m_trackDao, &TrackDAO::tracksAdded, m_pTrackSource.data(), - &BaseTrackCache::slotTracksAdded); + &BaseTrackCache::slotTracksAddedOrChanged); + connect(&m_trackDao, + &TrackDAO::tracksChanged, + m_pTrackSource.data(), + &BaseTrackCache::slotTracksAddedOrChanged); connect(&m_trackDao, &TrackDAO::tracksRemoved, m_pTrackSource.data(), diff --git a/src/library/trackcollection.h b/src/library/trackcollection.h index 870e35edb2ec..2be31a0bbae7 100644 --- a/src/library/trackcollection.h +++ b/src/library/trackcollection.h @@ -99,6 +99,14 @@ class TrackCollection : public QObject, bool unremove); signals: + // Forwarded signals from TrackDAO + void trackClean(TrackId trackId); + void trackDirty(TrackId trackId); + void tracksAdded(QSet trackIds); + void tracksChanged(QSet trackIds); + void tracksRemoved(QSet trackIds); + void multipleTracksChanged(); + void crateInserted(CrateId id); void crateUpdated(CrateId id); void crateDeleted(CrateId id);