Use common filter parser for collection and playlist

This commit is contained in:
Jonas Kvinge
2024-07-11 00:49:49 +02:00
parent dd904fe3c2
commit 6543e4c5da
17 changed files with 1102 additions and 1246 deletions

View File

@@ -49,6 +49,7 @@
#include "core/shared_ptr.h"
#include "core/iconloader.h"
#include "core/settings.h"
#include "filterparser/filterparser.h"
#include "playlist.h"
#include "playlisttabbar.h"
#include "playlistview.h"
@@ -124,37 +125,7 @@ PlaylistContainer::PlaylistContainer(QWidget *parent)
QObject::connect(ui_->playlist, &PlaylistView::FocusOnFilterSignal, this, &PlaylistContainer::FocusOnFilter);
ui_->search_field->installEventFilter(this);
QString available_fields = PlaylistFilter().column_names().keys().join(QLatin1String(", "));
ui_->search_field->setToolTip(
QLatin1String("<html><head/><body><p>") +
tr("Prefix a search term with a field name to limit the search to that field, e.g.:") +
QLatin1Char(' ') +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("artist") +
QLatin1String(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
tr("searches the playlist for all artists that contain the word %1. ").arg(QLatin1String("Strawbs")) +
QLatin1String("</p><p>") +
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
.arg(QLatin1String(" =, !=, &lt;, &gt;, &lt;="), QLatin1String("&gt;=")) +
QLatin1String("<span style=\"font-weight:600;\">") +
tr("rating") +
QLatin1String("</span>") +
QLatin1String(":>=") +
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
QLatin1String("</p><p>") +
tr("Multiple search terms can also be combined with \"%1\" (default) and \"%2\", as well as grouped with parentheses. ")
.arg(QLatin1String("AND"), QLatin1String("OR")) +
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
tr("Available fields") +
QLatin1String(": ") + QLatin1String("</span><span style=\"font-style:italic;\">") +
available_fields +
QLatin1String("</span>.") +
QLatin1String("</p></body></html>")
);
ui_->search_field->setToolTip(FilterParser::ToolTip());
ReloadSettings();
@@ -234,7 +205,7 @@ void PlaylistContainer::SetViewModel(Playlist *playlist, const int scroll_positi
emit ViewSelectionModelChanged();
// Update filter
ui_->search_field->setText(playlist->filter()->filter_text());
ui_->search_field->setText(playlist->filter()->filter_string());
// Update the no matches label
QObject::connect(playlist_->filter(), &QSortFilterProxyModel::modelReset, this, &PlaylistContainer::UpdateNoMatchesLabel);
@@ -452,7 +423,7 @@ void PlaylistContainer::UpdateFilter() {
if (!ui_->toolbar->isVisible()) return;
manager_->current()->filter()->SetFilterText(ui_->search_field->text());
manager_->current()->filter()->SetFilterString(ui_->search_field->text());
ui_->playlist->JumpToCurrentlyPlayingTrack();
UpdateNoMatchesLabel();

View File

@@ -23,54 +23,20 @@
#include <QObject>
#include <QString>
#include <QAbstractItemModel>
#include <QSortFilterProxyModel>
#include "playlist/playlist.h"
#include "playlist/playlistitem.h"
#include "filterparser/filterparser.h"
#include "filterparser/filtertree.h"
#include "playlistfilter.h"
#include "playlistfilterparser.h"
PlaylistFilter::PlaylistFilter(QObject *parent)
: QSortFilterProxyModel(parent),
filter_tree_(new PlaylistNopFilter),
filter_tree_(new NopFilter),
query_hash_(0) {
setDynamicSortFilter(true);
column_names_[QStringLiteral("title")] = static_cast<int>(Playlist::Column::Title);
column_names_[QStringLiteral("name")] = static_cast<int>(Playlist::Column::Title);
column_names_[QStringLiteral("artist")] = static_cast<int>(Playlist::Column::Artist);
column_names_[QStringLiteral("album")] = static_cast<int>(Playlist::Column::Album);
column_names_[QStringLiteral("albumartist")] = static_cast<int>(Playlist::Column::AlbumArtist);
column_names_[QStringLiteral("performer")] = static_cast<int>(Playlist::Column::Performer);
column_names_[QStringLiteral("composer")] = static_cast<int>(Playlist::Column::Composer);
column_names_[QStringLiteral("year")] = static_cast<int>(Playlist::Column::Year);
column_names_[QStringLiteral("originalyear")] = static_cast<int>(Playlist::Column::OriginalYear);
column_names_[QStringLiteral("track")] = static_cast<int>(Playlist::Column::Track);
column_names_[QStringLiteral("disc")] = static_cast<int>(Playlist::Column::Disc);
column_names_[QStringLiteral("length")] = static_cast<int>(Playlist::Column::Length);
column_names_[QStringLiteral("genre")] = static_cast<int>(Playlist::Column::Genre);
column_names_[QStringLiteral("samplerate")] = static_cast<int>(Playlist::Column::Samplerate);
column_names_[QStringLiteral("bitdepth")] = static_cast<int>(Playlist::Column::Bitdepth);
column_names_[QStringLiteral("bitrate")] = static_cast<int>(Playlist::Column::Bitrate);
column_names_[QStringLiteral("filename")] = static_cast<int>(Playlist::Column::Filename);
column_names_[QStringLiteral("grouping")] = static_cast<int>(Playlist::Column::Grouping);
column_names_[QStringLiteral("comment")] = static_cast<int>(Playlist::Column::Comment);
column_names_[QStringLiteral("rating")] = static_cast<int>(Playlist::Column::Rating);
column_names_[QStringLiteral("playcount")] = static_cast<int>(Playlist::Column::PlayCount);
column_names_[QStringLiteral("skipcount")] = static_cast<int>(Playlist::Column::SkipCount);
numerical_columns_ << static_cast<int>(Playlist::Column::Year)
<< static_cast<int>(Playlist::Column::OriginalYear)
<< static_cast<int>(Playlist::Column::Track)
<< static_cast<int>(Playlist::Column::Disc)
<< static_cast<int>(Playlist::Column::Length)
<< static_cast<int>(Playlist::Column::Samplerate)
<< static_cast<int>(Playlist::Column::Bitdepth)
<< static_cast<int>(Playlist::Column::Bitrate)
<< static_cast<int>(Playlist::Column::PlayCount)
<< static_cast<int>(Playlist::Column::SkipCount);
}
PlaylistFilter::~PlaylistFilter() = default;
@@ -80,29 +46,35 @@ void PlaylistFilter::sort(int column, Qt::SortOrder order) {
sourceModel()->sort(column, order);
}
bool PlaylistFilter::filterAcceptsRow(const int row, const QModelIndex &parent) const {
bool PlaylistFilter::filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const {
Playlist *playlist = qobject_cast<Playlist*>(sourceModel());
if (!playlist) return false;
const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
if (!idx.isValid()) return false;
PlaylistItemPtr item = playlist->item_at(idx.row());
if (!item) return false;
if (filter_string_.isEmpty()) return true;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
size_t hash = qHash(filter_text_);
const size_t hash = qHash(filter_string_);
#else
uint hash = qHash(filter_text_);
const uint hash = qHash(filter_string_);
#endif
if (hash != query_hash_) {
// Parse the query
PlaylistFilterParser p(filter_text_, column_names_, numerical_columns_);
FilterParser p(filter_string_);
filter_tree_.reset(p.parse());
query_hash_ = hash;
}
// Test the row
return filter_tree_->accept(row, parent, sourceModel());
return filter_tree_->accept(item->Metadata());
}
void PlaylistFilter::SetFilterText(const QString &filter_text) {
void PlaylistFilter::SetFilterString(const QString &filter_string) {
filter_text_ = filter_text;
setFilterFixedString(filter_text);
filter_string_ = filter_string;
setFilterFixedString(filter_string);
}

View File

@@ -24,15 +24,11 @@
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QMap>
#include <QSet>
#include <QSortFilterProxyModel>
#include <QScopedPointer>
#include <QString>
#include <QSortFilterProxyModel>
class PlaylistFilterTree;
#include "filterparser/filtertree.h"
class PlaylistFilter : public QSortFilterProxyModel {
Q_OBJECT
@@ -48,23 +44,18 @@ class PlaylistFilter : public QSortFilterProxyModel {
// public so Playlist::NextVirtualIndex and friends can get at it
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
void SetFilterText(const QString &filter_text);
QString filter_text() const { return filter_text_; }
QMap<QString, int> column_names() const { return column_names_; }
void SetFilterString(const QString &filter_string);
QString filter_string() const { return filter_string_; }
private:
// Mutable because they're modified from filterAcceptsRow() const
mutable QScopedPointer<PlaylistFilterTree> filter_tree_;
mutable QScopedPointer<FilterTree> filter_tree_;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
mutable size_t query_hash_;
#else
mutable uint query_hash_;
#endif
QMap<QString, int> column_names_;
QSet<int> numerical_columns_;
QString filter_text_;
QString filter_string_;
};
#endif // PLAYLISTFILTER_H

View File

@@ -1,610 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <cmath>
#include <QList>
#include <QMap>
#include <QSet>
#include <QChar>
#include <QScopedPointer>
#include <QString>
#include <QtAlgorithms>
#include <QAbstractItemModel>
#include "playlist.h"
#include "playlistfilterparser.h"
#include "utilities/searchparserutils.h"
class PlaylistSearchTermComparator {
public:
PlaylistSearchTermComparator() = default;
virtual ~PlaylistSearchTermComparator() = default;
virtual bool Matches(const QString &element) const = 0;
private:
Q_DISABLE_COPY(PlaylistSearchTermComparator)
};
// "compares" by checking if the field contains the search term
class PlaylistDefaultComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistDefaultComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.contains(search_term_);
}
private:
QString search_term_;
Q_DISABLE_COPY(PlaylistDefaultComparator)
};
class PlaylistEqComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistEqComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ == element;
}
private:
QString search_term_;
};
class PlaylistNeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistNeComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ != element;
}
private:
QString search_term_;
};
class PlaylistLexicalGtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLexicalGtComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element > search_term_;
}
private:
QString search_term_;
};
class PlaylistLexicalGeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLexicalGeComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element >= search_term_;
}
private:
QString search_term_;
};
class PlaylistLexicalLtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLexicalLtComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element < search_term_;
}
private:
QString search_term_;
};
class PlaylistLexicalLeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLexicalLeComparator(const QString &value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element <= search_term_;
}
private:
QString search_term_;
};
// Float Comparators are for the rating
class PlaylistFloatEqComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatEqComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ == element.toFloat();
}
private:
float search_term_;
};
class PlaylistFloatNeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatNeComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return search_term_ != element.toFloat();
}
private:
float search_term_;
};
class PlaylistFloatGtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatGtComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() > search_term_;
}
private:
float search_term_;
};
class PlaylistFloatGeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatGeComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() >= search_term_;
}
private:
float search_term_;
};
class PlaylistFloatLtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatLtComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() < search_term_;
}
private:
float search_term_;
};
class PlaylistFloatLeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistFloatLeComparator(const float value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toFloat() <= search_term_;
}
private:
float search_term_;
};
class PlaylistGtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistGtComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() > search_term_;
}
private:
int search_term_;
};
class PlaylistGeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistGeComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() >= search_term_;
}
private:
int search_term_;
};
class PlaylistLtComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLtComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() < search_term_;
}
private:
int search_term_;
};
class PlaylistLeComparator : public PlaylistSearchTermComparator {
public:
explicit PlaylistLeComparator(const int value) : search_term_(value) {}
bool Matches(const QString &element) const override {
return element.toInt() <= search_term_;
}
private:
int search_term_;
};
// The length field of the playlist (entries) contains a song's running time in nanoseconds.
// However, We don't really care about nanoseconds, just seconds.
// Thus, with this decorator we drop the last 9 digits, if that many are present.
class PlaylistDropTailComparatorDecorator : public PlaylistSearchTermComparator {
public:
explicit PlaylistDropTailComparatorDecorator(PlaylistSearchTermComparator *cmp) : cmp_(cmp) {}
bool Matches(const QString &element) const override {
if (element.length() > 9) {
return cmp_->Matches(element.left(element.length() - 9));
}
else {
return cmp_->Matches(element);
}
}
private:
QScopedPointer<PlaylistSearchTermComparator> cmp_;
};
class PlaylistRatingComparatorDecorator : public PlaylistSearchTermComparator {
public:
explicit PlaylistRatingComparatorDecorator(PlaylistSearchTermComparator *cmp) : cmp_(cmp) {}
bool Matches(const QString &element) const override {
return cmp_->Matches(QString::number(lround(element.toDouble() * 10.0)));
}
private:
QScopedPointer<PlaylistSearchTermComparator> cmp_;
};
// Filter that applies a SearchTermComparator to all fields of a playlist entry
class PlaylistFilterTerm : public PlaylistFilterTree {
public:
explicit PlaylistFilterTerm(PlaylistSearchTermComparator *comparator, const QList<int> &columns) : cmp_(comparator), columns_(columns) {}
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
for (const int i : columns_) {
const QModelIndex idx = model->index(row, i, parent);
if (cmp_->Matches(idx.data().toString().toLower())) return true;
}
return false;
}
FilterType type() override { return FilterType::Term; }
private:
QScopedPointer<PlaylistSearchTermComparator> cmp_;
QList<int> columns_;
};
// Filter that applies a SearchTermComparator to one specific field of a playlist entry
class PlaylistFilterColumnTerm : public PlaylistFilterTree {
public:
PlaylistFilterColumnTerm(const int column, PlaylistSearchTermComparator *comparator) : col(column), cmp_(comparator) {}
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
const QModelIndex idx = model->index(row, col, parent);
return cmp_->Matches(idx.data().toString().toLower());
}
FilterType type() override { return FilterType::Column; }
private:
int col;
QScopedPointer<PlaylistSearchTermComparator> cmp_;
};
class PlaylistNotFilter : public PlaylistFilterTree {
public:
explicit PlaylistNotFilter(const PlaylistFilterTree *inv) : child_(inv) {}
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
return !child_->accept(row, parent, model);
}
FilterType type() override { return FilterType::Not; }
private:
QScopedPointer<const PlaylistFilterTree> child_;
};
class PlaylistOrFilter : public PlaylistFilterTree {
public:
~PlaylistOrFilter() override { qDeleteAll(children_); }
virtual void add(PlaylistFilterTree *child) { children_.append(child); }
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
return std::any_of(children_.begin(), children_.end(), [row, parent, model](PlaylistFilterTree *child) { return child->accept(row, parent, model); });
}
FilterType type() override { return FilterType::Or; }
private:
QList<PlaylistFilterTree*> children_;
};
class PlaylistAndFilter : public PlaylistFilterTree {
public:
~PlaylistAndFilter() override { qDeleteAll(children_); }
virtual void add(PlaylistFilterTree *child) { children_.append(child); }
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
return !std::any_of(children_.begin(), children_.end(), [row, parent, model](PlaylistFilterTree *child) { return !child->accept(row, parent, model); });
}
FilterType type() override { return FilterType::And; }
private:
QList<PlaylistFilterTree*> children_;
};
PlaylistFilterParser::PlaylistFilterParser(const QString &filter, const QMap<QString, int> &columns, const QSet<int> &numerical_cols) : iter_{}, end_{}, filterstring_(filter), columns_(columns), numerical_columns_(numerical_cols) {}
PlaylistFilterTree *PlaylistFilterParser::parse() {
iter_ = filterstring_.constBegin();
end_ = filterstring_.constEnd();
return parseOrGroup();
}
void PlaylistFilterParser::advance() {
while (iter_ != end_ && iter_->isSpace()) {
++iter_;
}
}
PlaylistFilterTree *PlaylistFilterParser::parseOrGroup() {
advance();
if (iter_ == end_) return new PlaylistNopFilter;
PlaylistOrFilter *group = new PlaylistOrFilter;
group->add(parseAndGroup());
advance();
while (checkOr()) {
group->add(parseAndGroup());
advance();
}
return group;
}
PlaylistFilterTree *PlaylistFilterParser::parseAndGroup() {
advance();
if (iter_ == end_) return new PlaylistNopFilter;
PlaylistAndFilter *group = new PlaylistAndFilter();
do {
group->add(parseSearchExpression());
advance();
if (iter_ != end_ && *iter_ == QLatin1Char(')')) break;
if (checkOr(false)) {
break;
}
checkAnd(); // if there's no 'AND', we'll add the term anyway...
} while (iter_ != end_);
return group;
}
bool PlaylistFilterParser::checkAnd() {
if (iter_ != end_) {
if (*iter_ == QLatin1Char('A')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && *iter_ == QLatin1Char('N')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && *iter_ == QLatin1Char('D')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
advance();
buf_.clear();
return true;
}
}
}
}
}
return false;
}
bool PlaylistFilterParser::checkOr(const bool step_over) {
if (!buf_.isEmpty()) {
if (buf_ == QLatin1String("OR")) {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
}
else {
if (iter_ != end_) {
if (*iter_ == QLatin1Char('O')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && *iter_ == QLatin1Char('R')) {
buf_ += *iter_;
++iter_;
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
}
}
}
}
return false;
}
PlaylistFilterTree *PlaylistFilterParser::parseSearchExpression() {
advance();
if (iter_ == end_) return new PlaylistNopFilter;
if (*iter_ == QLatin1Char('(')) {
++iter_;
advance();
PlaylistFilterTree *tree = parseOrGroup();
advance();
if (iter_ != end_) {
if (*iter_ == QLatin1Char(')')) {
++iter_;
}
}
return tree;
}
else if (*iter_ == QLatin1Char('-')) {
++iter_;
PlaylistFilterTree *tree = parseSearchExpression();
if (tree->type() != PlaylistFilterTree::FilterType::Nop) return new PlaylistNotFilter(tree);
return tree;
}
else {
return parseSearchTerm();
}
}
PlaylistFilterTree *PlaylistFilterParser::parseSearchTerm() {
QString col;
QString search;
QString prefix;
bool inQuotes = false;
for (; iter_ != end_; ++iter_) {
if (inQuotes) {
if (*iter_ == QLatin1Char('"')) {
inQuotes = false;
}
else {
buf_ += *iter_;
}
}
else {
if (*iter_ == QLatin1Char('"')) {
inQuotes = true;
}
else if (col.isEmpty() && *iter_ == QLatin1Char(':')) {
col = buf_.toLower();
buf_.clear();
prefix.clear(); // prefix isn't allowed here - let's ignore it
}
else if (iter_->isSpace() || *iter_ == QLatin1Char('(') || *iter_ == QLatin1Char(')') || *iter_ == QLatin1Char('-')) {
break;
}
else if (buf_.isEmpty()) {
// we don't know whether there is a column part in this search term thus we assume the latter and just try and read a prefix
if (prefix.isEmpty() && (*iter_ == QLatin1Char('>') || *iter_ == QLatin1Char('<') || *iter_ == QLatin1Char('=') || *iter_ == QLatin1Char('!'))) {
prefix += *iter_;
}
else if (prefix != QLatin1Char('=') && *iter_ == QLatin1Char('=')) {
prefix += *iter_;
}
else {
buf_ += *iter_;
}
}
else {
buf_ += *iter_;
}
}
}
search = buf_.toLower();
buf_.clear();
return createSearchTermTreeNode(col, prefix, search);
}
PlaylistFilterTree *PlaylistFilterParser::createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const {
if (search.isEmpty() && prefix != QLatin1Char('=')) {
return new PlaylistNopFilter;
}
PlaylistSearchTermComparator *cmp = nullptr;
// Handle the float based Rating Column
if (columns_[col] == static_cast<int>(Playlist::Column::Rating)) {
float parsed_search = Utilities::ParseSearchRating(search);
if (prefix == QLatin1Char('=')) {
cmp = new PlaylistFloatEqComparator(parsed_search);
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new PlaylistFloatNeComparator(parsed_search);
}
else if (prefix == QLatin1Char('>')) {
cmp = new PlaylistFloatGtComparator(parsed_search);
}
else if (prefix == QLatin1String(">=")) {
cmp = new PlaylistFloatGeComparator(parsed_search);
}
else if (prefix == QLatin1Char('<')) {
cmp = new PlaylistFloatLtComparator(parsed_search);
}
else if (prefix == QLatin1String("<=")) {
cmp = new PlaylistFloatLeComparator(parsed_search);
}
else {
cmp = new PlaylistFloatEqComparator(parsed_search);
}
}
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
cmp = new PlaylistNeComparator(search);
}
else if (!col.isEmpty() && columns_.contains(col) && numerical_columns_.contains(columns_[col])) {
// The length column contains the time in seconds (nanoseconds, actually - the "nano" part is handled by the DropTailComparatorDecorator, though).
int search_value = 0;
if (columns_[col] == static_cast<int>(Playlist::Column::Length)) {
search_value = Utilities::ParseSearchTime(search);
}
else {
search_value = search.toInt();
}
// Alright, back to deciding which comparator we'll use
if (prefix == QLatin1Char('>')) {
cmp = new PlaylistGtComparator(search_value);
}
else if (prefix == QLatin1String(">=")) {
cmp = new PlaylistGeComparator(search_value);
}
else if (prefix == QLatin1Char('<')) {
cmp = new PlaylistLtComparator(search_value);
}
else if (prefix == QLatin1String("<=")) {
cmp = new PlaylistLeComparator(search_value);
}
else {
// Convert back because for time/rating
cmp = new PlaylistEqComparator(QString::number(search_value));
}
}
else {
if (prefix == QLatin1Char('=')) {
cmp = new PlaylistEqComparator(search);
}
else if (prefix == QLatin1Char('>')) {
cmp = new PlaylistLexicalGtComparator(search);
}
else if (prefix == QLatin1String(">=")) {
cmp = new PlaylistLexicalGeComparator(search);
}
else if (prefix == QLatin1Char('<')) {
cmp = new PlaylistLexicalLtComparator(search);
}
else if (prefix == QLatin1String("<=")) {
cmp = new PlaylistLexicalLeComparator(search);
}
else {
cmp = new PlaylistDefaultComparator(search);
}
}
if (columns_.contains(col)) {
if (columns_[col] == static_cast<int>(Playlist::Column::Length)) {
cmp = new PlaylistDropTailComparatorDecorator(cmp);
}
return new PlaylistFilterColumnTerm(columns_[col], cmp);
}
else {
return new PlaylistFilterTerm(cmp, columns_.values());
}
}

View File

@@ -1,101 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTFILTERPARSER_H
#define PLAYLISTFILTERPARSER_H
#include "config.h"
#include <QSet>
#include <QMap>
#include <QString>
class QAbstractItemModel;
class QModelIndex;
// Structure for filter parse tree
class PlaylistFilterTree {
public:
PlaylistFilterTree() = default;
virtual ~PlaylistFilterTree() {}
virtual bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const = 0;
enum class FilterType {
Nop = 0,
Or,
And,
Not,
Column,
Term
};
virtual FilterType type() = 0;
private:
Q_DISABLE_COPY(PlaylistFilterTree)
};
// Trivial filter that accepts *anything*
class PlaylistNopFilter : public PlaylistFilterTree {
public:
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override { Q_UNUSED(row); Q_UNUSED(parent); Q_UNUSED(model); return true; }
FilterType type() override { return FilterType::Nop; }
};
// A utility class to parse search filter strings into a decision tree
// that can decide whether a playlist entry matches the filter.
//
// Here's a grammar describing the filters we expect:
//  expr ::= or-group
// or-group ::= and-group ('OR' and-group)*
// and-group ::= sexpr ('AND' sexpr)*
// sexpr ::= sterm | '-' sexpr | '(' or-group ')'
// sterm ::= col ':' sstring | sstring
// sstring ::= prefix? string
// string ::= [^:-()" ]+ | '"' [^"]+ '"'
// prefix ::= '=' | '<' | '>' | '<=' | '>='
// col ::= "title" | "artist" | ...
class PlaylistFilterParser {
public:
explicit PlaylistFilterParser(const QString &filter, const QMap<QString, int> &columns, const QSet<int> &numerical_cols);
PlaylistFilterTree *parse();
private:
void advance();
PlaylistFilterTree *parseOrGroup();
PlaylistFilterTree *parseAndGroup();
// Check if iter is at the start of 'AND' if so, step over it and return true if not, return false and leave iter where it was
bool checkAnd();
// Check if iter is at the start of 'OR'
bool checkOr(const bool step_over = true);
PlaylistFilterTree *parseSearchExpression();
PlaylistFilterTree *parseSearchTerm();
PlaylistFilterTree *createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const;
QString::const_iterator iter_;
QString::const_iterator end_;
QString buf_;
const QString filterstring_;
const QMap<QString, int> columns_;
const QSet<int> numerical_columns_;
};
#endif // PLAYLISTFILTERPARSER_H