diff --git a/src/playlist/playlistlistcontainer.cpp b/src/playlist/playlistlistcontainer.cpp index fe5ff5010..35ff61780 100644 --- a/src/playlist/playlistlistcontainer.cpp +++ b/src/playlist/playlistlistcontainer.cpp @@ -33,9 +33,17 @@ #include #include -class PlaylistListSortFilterModel : public QSortFilterProxyModel { +#include + +/* This filter proxy will: + - Accept all ancestors if at least a single child matches + - Accept all children if at least a single ancestor matches + + The tree is then expanded only to the level at which the match occurs +*/ +class PlaylistListFilterProxyModel : public QSortFilterProxyModel { public: - explicit PlaylistListSortFilterModel(QObject* parent) + explicit PlaylistListFilterProxyModel(QObject* parent) : QSortFilterProxyModel(parent) {} bool lessThan(const QModelIndex& left, const QModelIndex& right) const { @@ -49,6 +57,83 @@ class PlaylistListSortFilterModel : public QSortFilterProxyModel { // deterministic sorting even when two items are named the same. return left.row() < right.row(); } + + QList expandList; + + void setFilterRegExp(const QRegExp & regExp) { + expandList.clear(); + QSortFilterProxyModel::setFilterRegExp(regExp); + } + + void refreshExpanded(QTreeView *tree) { + tree->collapseAll(); + for(QModelIndex sourceIndex : expandList ) { + QModelIndex mappedIndex = mapFromSource( sourceIndex ); + tree->setExpanded( mappedIndex, true ); + } + } + + // Depth first search of all the items + bool hasAcceptedChildren(int source_row, const QModelIndex &source_parent) const { + QModelIndex item = sourceModel()->index(source_row,0,source_parent); + if (!item.isValid()) { + return false; + } + + //check if there are children + int childCount = item.model()->rowCount(item); + if (childCount == 0) + return false; + + for (int i = 0; i < childCount; ++i) { + if (filterAcceptsRowItself(i, item)) + return true; + + if (hasAcceptedChildren(i, item)) + return true; + } + return false; + } + + bool filterAcceptsRowItself(int source_row, const QModelIndex &source_parent) const { + bool rv = QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); + if(rv) { + if(sourceModel()->hasIndex(source_row,0,source_parent)) { + QModelIndex idx = sourceModel()->index(source_row,0,source_parent); + + // Bit of a hack to get around the const in this function + auto * me = const_cast(this); + + QModelIndex pidx = sourceModel()->parent(idx); + while(pidx.isValid()) { + me->expandList.append(pidx); + pidx = sourceModel()->parent(pidx); + } + } + } + return rv; + } + + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { + if (filterAcceptsRowItself(source_row, source_parent)) { + return true; + } + + //accept if any of the parents is accepted on it's own merits + QModelIndex parent = source_parent; + while (parent.isValid()) { + if (filterAcceptsRowItself(parent.row(), parent.parent())) + return true; + parent = parent.parent(); + } + + //accept if any of the children is accepted on it's own merits + if (hasAcceptedChildren(source_row, source_parent)) { + return true; + } + + return false; + } }; PlaylistListContainer::PlaylistListContainer(QWidget* parent) @@ -60,11 +145,12 @@ PlaylistListContainer::PlaylistListContainer(QWidget* parent) action_remove_(new QAction(this)), action_save_playlist_(new QAction(this)), model_(new PlaylistListModel(this)), - proxy_(new PlaylistListSortFilterModel(this)), + proxy_(new PlaylistListFilterProxyModel(this)), loaded_icons_(false), active_playlist_id_(-1) { ui_->setupUi(this); ui_->tree->setAttribute(Qt::WA_MacShowFocusRect, false); + ui_->tree->SetAutoOpen(false); action_new_folder_->setText(tr("New folder")); action_remove_->setText(tr("Delete")); @@ -91,6 +177,8 @@ PlaylistListContainer::PlaylistListContainer(QWidget* parent) model_->invisibleRootItem()->setData(PlaylistListModel::Type_Folder, PlaylistListModel::Role_Type); + + connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(SearchTextEdited(QString))); } PlaylistListContainer::~PlaylistListContainer() { delete ui_; } @@ -128,6 +216,10 @@ void PlaylistListContainer::RecursivelySetIcons(QStandardItem* parent) const { case PlaylistListModel::Type_Playlist: child->setIcon(model_->playlist_icon()); break; + + case PlaylistListModel::Type_Track: + child->setIcon(model_->track_icon()); + break; } } } @@ -161,6 +253,10 @@ void PlaylistListContainer::SetApplication(Application* app) { QStandardItem* playlist_item = model_->NewPlaylist(p.name, p.id); QStandardItem* parent_folder = model_->FolderByPath(p.ui_path); parent_folder->appendRow(playlist_item); + for (const Song s : app->playlist_backend()->GetPlaylistSongs(p.id)) { + QStandardItem* track_item = model_->NewTrack(s); + playlist_item->appendRow(track_item); + } } } @@ -267,6 +363,18 @@ void PlaylistListContainer::CurrentChanged(Playlist* new_playlist) { ui_->tree->scrollTo(index); } +void PlaylistListContainer::SearchTextEdited(const QString& text) { + QRegExp regexp(text); + regexp.setCaseSensitivity(Qt::CaseInsensitive); + + if(regexp.isEmpty()) { + ui_->tree->collapseAll(); + } else { + proxy_->setFilterRegExp(regexp); + proxy_->refreshExpanded(ui_->tree); + } +} + void PlaylistListContainer::PlaylistPathChanged(int id, const QString& new_path) { // Update the path in the database diff --git a/src/playlist/playlistlistcontainer.h b/src/playlist/playlistlistcontainer.h index 2c1cc8486..77d47390e 100644 --- a/src/playlist/playlistlistcontainer.h +++ b/src/playlist/playlistlistcontainer.h @@ -31,6 +31,8 @@ class Playlist; class PlaylistListModel; class Ui_PlaylistListContainer; +class PlaylistListFilterProxyModel; + class PlaylistListContainer : public QWidget { Q_OBJECT @@ -49,6 +51,7 @@ class PlaylistListContainer : public QWidget { void NewFolderClicked(); void DeleteClicked(); void ItemDoubleClicked(const QModelIndex& index); + void SearchTextEdited(const QString& text); // From the model void PlaylistPathChanged(int id, const QString& new_path); @@ -87,7 +90,7 @@ class PlaylistListContainer : public QWidget { QAction* action_save_playlist_; PlaylistListModel* model_; - QSortFilterProxyModel* proxy_; + PlaylistListFilterProxyModel* proxy_; bool loaded_icons_; QIcon padded_play_icon_; diff --git a/src/playlist/playlistlistcontainer.ui b/src/playlist/playlistlistcontainer.ui index 9f9cafb95..5539e535b 100644 --- a/src/playlist/playlistlistcontainer.ui +++ b/src/playlist/playlistlistcontainer.ui @@ -41,6 +41,13 @@ 0 + + + + Search for anything + + + @@ -72,19 +79,6 @@ - - - - Qt::Horizontal - - - - 70 - 20 - - - - @@ -125,6 +119,11 @@ + + QSearchField + QWidget +
3rdparty/qocoa/qsearchfield.h
+
AutoExpandingTreeView QTreeView diff --git a/src/playlist/playlistlistmodel.cpp b/src/playlist/playlistlistmodel.cpp index 89a5326d9..478630c50 100644 --- a/src/playlist/playlistlistmodel.cpp +++ b/src/playlist/playlistlistmodel.cpp @@ -71,6 +71,12 @@ void PlaylistListModel::AddRowMappings(const QModelIndex& begin, void PlaylistListModel::AddRowItem(QStandardItem* item, const QString& parent_path) { switch (item->data(Role_Type).toInt()) { + case Type_Track: { + // const int id = item->data(Role_TrackId).toInt(); + // TODO + break; + } + case Type_Playlist: { const int id = item->data(Role_PlaylistId).toInt(); playlists_by_id_[id] = item; @@ -172,6 +178,16 @@ QStandardItem* PlaylistListModel::NewPlaylist(const QString& name, return ret; } +QStandardItem* PlaylistListModel::NewTrack(const Song& song) const { + QStandardItem* ret = new QStandardItem; + ret->setText(song.artist() + " - " + song.title()); + ret->setData(PlaylistListModel::Type_Track, PlaylistListModel::Role_Type); + ret->setData(song.id(), PlaylistListModel::Role_TrackId); + ret->setIcon(track_icon_); + ret->setFlags(Qt::ItemIsDragEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable); + return ret; +} + bool PlaylistListModel::setData(const QModelIndex& index, const QVariant& value, int role) { if (!QStandardItemModel::setData(index, value, role)) { diff --git a/src/playlist/playlistlistmodel.h b/src/playlist/playlistlistmodel.h index 0c8dcccb7..590586fd3 100644 --- a/src/playlist/playlistlistmodel.h +++ b/src/playlist/playlistlistmodel.h @@ -2,16 +2,16 @@ #define PLAYLISTLISTMODEL_H #include - +#include "core/song.h" class PlaylistListModel : public QStandardItemModel { Q_OBJECT public: PlaylistListModel(QObject* parent = nullptr); - enum Types { Type_Folder, Type_Playlist }; + enum Types { Type_Folder, Type_Playlist, Type_Track }; - enum Roles { Role_Type = Qt::UserRole, Role_PlaylistId }; + enum Roles { Role_Type = Qt::UserRole, Role_PlaylistId, Role_TrackId }; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); @@ -19,6 +19,7 @@ class PlaylistListModel : public QStandardItemModel { // These icons will be used for newly created playlists and folders. // The caller will need to set these icons on existing items if there are any. void SetIcons(const QIcon& playlist_icon, const QIcon& folder_icon); + const QIcon& track_icon() const { return track_icon_; } const QIcon& playlist_icon() const { return playlist_icon_; } const QIcon& folder_icon() const { return folder_icon_; } @@ -41,6 +42,9 @@ class PlaylistListModel : public QStandardItemModel { // added to the model yet. QStandardItem* NewPlaylist(const QString& name, int id) const; + // Returns a new track item. The item isn't added to the model yet. + QStandardItem* NewTrack(const Song& song) const; + // QStandardItemModel bool setData(const QModelIndex& index, const QVariant& value, int role); @@ -61,6 +65,7 @@ signals: private: bool dropping_rows_; + QIcon track_icon_; QIcon playlist_icon_; QIcon folder_icon_;