Add search filter to playlist list page
This allows power users who keep 100s of playlists to easily find a playlist either by directory name, playlist name or by searching for a song artist/title a playlist might contain
This commit is contained in:
parent
790fc2ff28
commit
3da938eac2
|
@ -33,9 +33,17 @@
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
#include <QStandardItemModel>
|
#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:
|
public:
|
||||||
explicit PlaylistListSortFilterModel(QObject* parent)
|
explicit PlaylistListFilterProxyModel(QObject* parent)
|
||||||
: QSortFilterProxyModel(parent) {}
|
: QSortFilterProxyModel(parent) {}
|
||||||
|
|
||||||
bool lessThan(const QModelIndex& left, const QModelIndex& right) const {
|
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.
|
// deterministic sorting even when two items are named the same.
|
||||||
return left.row() < right.row();
|
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)
|
PlaylistListContainer::PlaylistListContainer(QWidget* parent)
|
||||||
|
@ -60,11 +145,12 @@ PlaylistListContainer::PlaylistListContainer(QWidget* parent)
|
||||||
action_remove_(new QAction(this)),
|
action_remove_(new QAction(this)),
|
||||||
action_save_playlist_(new QAction(this)),
|
action_save_playlist_(new QAction(this)),
|
||||||
model_(new PlaylistListModel(this)),
|
model_(new PlaylistListModel(this)),
|
||||||
proxy_(new PlaylistListSortFilterModel(this)),
|
proxy_(new PlaylistListFilterProxyModel(this)),
|
||||||
loaded_icons_(false),
|
loaded_icons_(false),
|
||||||
active_playlist_id_(-1) {
|
active_playlist_id_(-1) {
|
||||||
ui_->setupUi(this);
|
ui_->setupUi(this);
|
||||||
ui_->tree->setAttribute(Qt::WA_MacShowFocusRect, false);
|
ui_->tree->setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||||
|
ui_->tree->SetAutoOpen(false);
|
||||||
|
|
||||||
action_new_folder_->setText(tr("New folder"));
|
action_new_folder_->setText(tr("New folder"));
|
||||||
action_remove_->setText(tr("Delete"));
|
action_remove_->setText(tr("Delete"));
|
||||||
|
@ -91,6 +177,8 @@ PlaylistListContainer::PlaylistListContainer(QWidget* parent)
|
||||||
|
|
||||||
model_->invisibleRootItem()->setData(PlaylistListModel::Type_Folder,
|
model_->invisibleRootItem()->setData(PlaylistListModel::Type_Folder,
|
||||||
PlaylistListModel::Role_Type);
|
PlaylistListModel::Role_Type);
|
||||||
|
|
||||||
|
connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(SearchTextEdited(QString)));
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaylistListContainer::~PlaylistListContainer() { delete ui_; }
|
PlaylistListContainer::~PlaylistListContainer() { delete ui_; }
|
||||||
|
@ -128,6 +216,10 @@ void PlaylistListContainer::RecursivelySetIcons(QStandardItem* parent) const {
|
||||||
case PlaylistListModel::Type_Playlist:
|
case PlaylistListModel::Type_Playlist:
|
||||||
child->setIcon(model_->playlist_icon());
|
child->setIcon(model_->playlist_icon());
|
||||||
break;
|
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* playlist_item = model_->NewPlaylist(p.name, p.id);
|
||||||
QStandardItem* parent_folder = model_->FolderByPath(p.ui_path);
|
QStandardItem* parent_folder = model_->FolderByPath(p.ui_path);
|
||||||
parent_folder->appendRow(playlist_item);
|
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);
|
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,
|
void PlaylistListContainer::PlaylistPathChanged(int id,
|
||||||
const QString& new_path) {
|
const QString& new_path) {
|
||||||
// Update the path in the database
|
// Update the path in the database
|
||||||
|
|
|
@ -31,6 +31,8 @@ class Playlist;
|
||||||
class PlaylistListModel;
|
class PlaylistListModel;
|
||||||
class Ui_PlaylistListContainer;
|
class Ui_PlaylistListContainer;
|
||||||
|
|
||||||
|
class PlaylistListFilterProxyModel;
|
||||||
|
|
||||||
class PlaylistListContainer : public QWidget {
|
class PlaylistListContainer : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
@ -49,6 +51,7 @@ class PlaylistListContainer : public QWidget {
|
||||||
void NewFolderClicked();
|
void NewFolderClicked();
|
||||||
void DeleteClicked();
|
void DeleteClicked();
|
||||||
void ItemDoubleClicked(const QModelIndex& index);
|
void ItemDoubleClicked(const QModelIndex& index);
|
||||||
|
void SearchTextEdited(const QString& text);
|
||||||
|
|
||||||
// From the model
|
// From the model
|
||||||
void PlaylistPathChanged(int id, const QString& new_path);
|
void PlaylistPathChanged(int id, const QString& new_path);
|
||||||
|
@ -87,7 +90,7 @@ class PlaylistListContainer : public QWidget {
|
||||||
QAction* action_save_playlist_;
|
QAction* action_save_playlist_;
|
||||||
|
|
||||||
PlaylistListModel* model_;
|
PlaylistListModel* model_;
|
||||||
QSortFilterProxyModel* proxy_;
|
PlaylistListFilterProxyModel* proxy_;
|
||||||
|
|
||||||
bool loaded_icons_;
|
bool loaded_icons_;
|
||||||
QIcon padded_play_icon_;
|
QIcon padded_play_icon_;
|
||||||
|
|
|
@ -41,6 +41,13 @@
|
||||||
<property name="margin">
|
<property name="margin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QSearchField" name="search" native="true">
|
||||||
|
<property name="placeholderText" stdset="0">
|
||||||
|
<string>Search for anything</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="new_folder">
|
<widget class="QToolButton" name="new_folder">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
|
@ -72,19 +79,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -125,6 +119,11 @@
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>QSearchField</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>3rdparty/qocoa/qsearchfield.h</header>
|
||||||
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>AutoExpandingTreeView</class>
|
<class>AutoExpandingTreeView</class>
|
||||||
<extends>QTreeView</extends>
|
<extends>QTreeView</extends>
|
||||||
|
|
|
@ -71,6 +71,12 @@ void PlaylistListModel::AddRowMappings(const QModelIndex& begin,
|
||||||
void PlaylistListModel::AddRowItem(QStandardItem* item,
|
void PlaylistListModel::AddRowItem(QStandardItem* item,
|
||||||
const QString& parent_path) {
|
const QString& parent_path) {
|
||||||
switch (item->data(Role_Type).toInt()) {
|
switch (item->data(Role_Type).toInt()) {
|
||||||
|
case Type_Track: {
|
||||||
|
// const int id = item->data(Role_TrackId).toInt();
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case Type_Playlist: {
|
case Type_Playlist: {
|
||||||
const int id = item->data(Role_PlaylistId).toInt();
|
const int id = item->data(Role_PlaylistId).toInt();
|
||||||
playlists_by_id_[id] = item;
|
playlists_by_id_[id] = item;
|
||||||
|
@ -172,6 +178,16 @@ QStandardItem* PlaylistListModel::NewPlaylist(const QString& name,
|
||||||
return ret;
|
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,
|
bool PlaylistListModel::setData(const QModelIndex& index, const QVariant& value,
|
||||||
int role) {
|
int role) {
|
||||||
if (!QStandardItemModel::setData(index, value, role)) {
|
if (!QStandardItemModel::setData(index, value, role)) {
|
||||||
|
|
|
@ -2,16 +2,16 @@
|
||||||
#define PLAYLISTLISTMODEL_H
|
#define PLAYLISTLISTMODEL_H
|
||||||
|
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
|
#include "core/song.h"
|
||||||
class PlaylistListModel : public QStandardItemModel {
|
class PlaylistListModel : public QStandardItemModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PlaylistListModel(QObject* parent = nullptr);
|
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,
|
bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row,
|
||||||
int column, const QModelIndex& parent);
|
int column, const QModelIndex& parent);
|
||||||
|
@ -19,6 +19,7 @@ class PlaylistListModel : public QStandardItemModel {
|
||||||
// These icons will be used for newly created playlists and folders.
|
// 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.
|
// 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);
|
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& playlist_icon() const { return playlist_icon_; }
|
||||||
const QIcon& folder_icon() const { return folder_icon_; }
|
const QIcon& folder_icon() const { return folder_icon_; }
|
||||||
|
|
||||||
|
@ -41,6 +42,9 @@ class PlaylistListModel : public QStandardItemModel {
|
||||||
// added to the model yet.
|
// added to the model yet.
|
||||||
QStandardItem* NewPlaylist(const QString& name, int id) const;
|
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
|
// QStandardItemModel
|
||||||
bool setData(const QModelIndex& index, const QVariant& value, int role);
|
bool setData(const QModelIndex& index, const QVariant& value, int role);
|
||||||
|
|
||||||
|
@ -61,6 +65,7 @@ signals:
|
||||||
private:
|
private:
|
||||||
bool dropping_rows_;
|
bool dropping_rows_;
|
||||||
|
|
||||||
|
QIcon track_icon_;
|
||||||
QIcon playlist_icon_;
|
QIcon playlist_icon_;
|
||||||
QIcon folder_icon_;
|
QIcon folder_icon_;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue