Merge pull request #5960 from vikramambrose/feature-playlist-page-search-filter

Add search filter to playlist list page
This commit is contained in:
John Maguire 2018-02-03 17:19:10 +00:00 committed by GitHub
commit eafc1713ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 160 additions and 27 deletions

View File

@ -33,9 +33,17 @@
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
class PlaylistListSortFilterModel : public QSortFilterProxyModel {
#include <iostream>
/* 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<QModelIndex> 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<PlaylistListFilterProxyModel*>(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;
}
}
}
@ -158,9 +250,7 @@ void PlaylistListContainer::SetApplication(Application* app) {
// Get all playlists, even ones that are hidden in the UI.
for (const PlaylistBackend::Playlist& p :
app->playlist_backend()->GetAllFavoritePlaylists()) {
QStandardItem* playlist_item = model_->NewPlaylist(p.name, p.id);
QStandardItem* parent_folder = model_->FolderByPath(p.ui_path);
parent_folder->appendRow(playlist_item);
AddPlaylist(p.id,p.name,true,&p.ui_path);
}
}
@ -177,7 +267,7 @@ void PlaylistListContainer::NewFolderClicked() {
}
void PlaylistListContainer::AddPlaylist(int id, const QString& name,
bool favorite) {
bool favorite, const QString *ui_path) {
if (!favorite) {
return;
}
@ -188,11 +278,18 @@ void PlaylistListContainer::AddPlaylist(int id, const QString& name,
return;
}
const QString& ui_path = app_->playlist_manager()->playlist(id)->ui_path();
if(ui_path == nullptr)
ui_path = &app_->playlist_manager()->playlist(id)->ui_path();
QStandardItem* playlist_item = model_->NewPlaylist(name, id);
QStandardItem* parent_folder = model_->FolderByPath(ui_path);
QStandardItem* parent_folder = model_->FolderByPath(*ui_path);
parent_folder->appendRow(playlist_item);
for (const Song s : app_->playlist_backend()->GetPlaylistSongs(id)) {
QStandardItem* track_item = model_->NewTrack(s);
track_item->setDragEnabled(false);
playlist_item->appendRow(track_item);
}
}
void PlaylistListContainer::PlaylistRenamed(int id, const QString& new_name) {
@ -267,6 +364,19 @@ void PlaylistListContainer::CurrentChanged(Playlist* new_playlist) {
ui_->tree->scrollTo(index);
}
void PlaylistListContainer::SearchTextEdited(const QString& text) {
QRegExp regexp(text);
regexp.setCaseSensitivity(Qt::CaseInsensitive);
proxy_->setFilterRegExp(regexp);
if(regexp.isEmpty()) {
ui_->tree->collapseAll();
} else {
proxy_->refreshExpanded(ui_->tree);
}
}
void PlaylistListContainer::PlaylistPathChanged(int id,
const QString& new_path) {
// Update the path in the database

View File

@ -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);
@ -56,7 +59,7 @@ class PlaylistListContainer : public QWidget {
// From the PlaylistManager
void PlaylistRenamed(int id, const QString& new_name);
// Add playlist if favorite == true
void AddPlaylist(int id, const QString& name, bool favorite);
void AddPlaylist(int id, const QString& name, bool favorite, const QString *ui_path = nullptr);
void RemovePlaylist(int id);
void SavePlaylist();
void PlaylistFavoriteStateChanged(int id, bool favorite);
@ -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_;

View File

@ -41,6 +41,13 @@
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QSearchField" name="search" native="true">
<property name="placeholderText" stdset="0">
<string>Search for anything</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="new_folder">
<property name="toolTip">
@ -72,19 +79,6 @@
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>70</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
@ -125,6 +119,11 @@
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QSearchField</class>
<extends>QWidget</extends>
<header>3rdparty/qocoa/qsearchfield.h</header>
</customwidget>
<customwidget>
<class>AutoExpandingTreeView</class>
<extends>QTreeView</extends>

View File

@ -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)) {

View File

@ -2,16 +2,16 @@
#define PLAYLISTLISTMODEL_H
#include <QStandardItemModel>
#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_;