mirror of
https://github.com/clementine-player/Clementine
synced 2024-12-16 19:31:02 +01:00
Support logical operators in the playlist filter box. Fixes issue 700
This commit is contained in:
parent
b63d1cf9f1
commit
cd6b1a2d7c
@ -213,6 +213,7 @@ set(SOURCES
|
||||
playlist/playlistbackend.cpp
|
||||
playlist/playlistcontainer.cpp
|
||||
playlist/playlistdelegates.cpp
|
||||
playlist/playlistfilterparser.cpp
|
||||
playlist/playlistfilter.cpp
|
||||
playlist/playlistheader.cpp
|
||||
playlist/playlistitem.cpp
|
||||
|
@ -16,11 +16,13 @@
|
||||
*/
|
||||
|
||||
#include "playlistfilter.h"
|
||||
#include "playlistfilterparser.h"
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
PlaylistFilter::PlaylistFilter(QObject *parent)
|
||||
: QSortFilterProxyModel(parent),
|
||||
filter_tree_(new NopFilter),
|
||||
query_hash_(0)
|
||||
{
|
||||
setDynamicSortFilter(true);
|
||||
@ -38,11 +40,20 @@ PlaylistFilter::PlaylistFilter(QObject *parent)
|
||||
column_names_["genre"] = Playlist::Column_Genre;
|
||||
column_names_["score"] = Playlist::Column_Score;
|
||||
column_names_["comment"] = Playlist::Column_Comment;
|
||||
column_names_["bpm"] = Playlist::Column_BPM;
|
||||
column_names_["bitrate"] = Playlist::Column_Bitrate;
|
||||
column_names_["filename"] = Playlist::Column_Filename;
|
||||
|
||||
exact_columns_ << Playlist::Column_Length
|
||||
<< Playlist::Column_Track
|
||||
<< Playlist::Column_Disc
|
||||
<< Playlist::Column_Year;
|
||||
<< Playlist::Column_Year
|
||||
<< Playlist::Column_Score
|
||||
<< Playlist::Column_BPM
|
||||
<< Playlist::Column_Bitrate;
|
||||
}
|
||||
|
||||
PlaylistFilter::~PlaylistFilter() {
|
||||
}
|
||||
|
||||
void PlaylistFilter::sort(int column, Qt::SortOrder order) {
|
||||
@ -51,56 +62,17 @@ void PlaylistFilter::sort(int column, Qt::SortOrder order) {
|
||||
}
|
||||
|
||||
bool PlaylistFilter::filterAcceptsRow(int row, const QModelIndex &parent) const {
|
||||
QString filter = filterRegExp().pattern().toLower();
|
||||
QString filter = filterRegExp().pattern();
|
||||
|
||||
uint hash = qHash(filter);
|
||||
if (hash != query_hash_) {
|
||||
// Parse the query
|
||||
query_cache_.clear();
|
||||
|
||||
QStringList sections = filter.simplified().split(' ');
|
||||
foreach (const QString& section, sections) {
|
||||
QString key = section.section(':', 0, 0).toLower();
|
||||
if (section.contains(':') && column_names_.contains(key)) {
|
||||
// Specific column
|
||||
query_cache_ << SearchTerm(
|
||||
section.section(':', 1, -1).toLower(),
|
||||
column_names_[key],
|
||||
exact_columns_.contains(column_names_[key]));
|
||||
} else {
|
||||
query_cache_ << SearchTerm(section);
|
||||
}
|
||||
}
|
||||
FilterParser p(filter,column_names_,exact_columns_);
|
||||
filter_tree_.reset(p.parse());
|
||||
|
||||
query_hash_ = hash;
|
||||
}
|
||||
|
||||
// Test the row
|
||||
QString all_columns;
|
||||
|
||||
foreach (const SearchTerm& term, query_cache_) {
|
||||
if (term.column_ != -1) {
|
||||
// Specific column
|
||||
QModelIndex index(sourceModel()->index(row, term.column_, parent));
|
||||
QString value(index.data().toString().toLower());
|
||||
|
||||
if (term.exact_ && value != term.value_)
|
||||
return false;
|
||||
else if (!term.exact_ && !value.contains(term.value_))
|
||||
return false;
|
||||
} else {
|
||||
// All columns
|
||||
if (all_columns.isNull()) {
|
||||
// Cache the concatenated value of all columns
|
||||
foreach (int column, column_names_.values()) {
|
||||
QModelIndex index(sourceModel()->index(row, column, parent));
|
||||
all_columns += index.data().toString().toLower() + ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if (!all_columns.contains(term.value_))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return filter_tree_->accept(row,parent,sourceModel());
|
||||
}
|
||||
|
@ -18,17 +18,21 @@
|
||||
#ifndef PLAYLISTFILTER_H
|
||||
#define PLAYLISTFILTER_H
|
||||
|
||||
#include <QScopedPointer>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "playlist.h"
|
||||
|
||||
#include <QSet>
|
||||
|
||||
class FilterTree;
|
||||
|
||||
class PlaylistFilter : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PlaylistFilter(QObject* parent = 0);
|
||||
~PlaylistFilter();
|
||||
|
||||
// QAbstractItemModel
|
||||
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder);
|
||||
@ -38,17 +42,8 @@ public:
|
||||
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const;
|
||||
|
||||
private:
|
||||
struct SearchTerm {
|
||||
SearchTerm(const QString& value = QString(), int column = -1, bool exact = false)
|
||||
: value_(value), column_(column), exact_(exact) {}
|
||||
|
||||
QString value_;
|
||||
int column_;
|
||||
bool exact_;
|
||||
};
|
||||
|
||||
// Mutable because they're modified from filterAcceptsRow() const
|
||||
mutable QList<SearchTerm> query_cache_;
|
||||
mutable QScopedPointer<FilterTree> filter_tree_;
|
||||
mutable uint query_hash_;
|
||||
|
||||
QMap<QString, int> column_names_;
|
||||
|
493
src/playlist/playlistfilterparser.cpp
Normal file
493
src/playlist/playlistfilterparser.cpp
Normal file
@ -0,0 +1,493 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine 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.
|
||||
|
||||
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "playlistfilterparser.h"
|
||||
#include "playlist.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
|
||||
class SearchTermComparator {
|
||||
public:
|
||||
virtual ~SearchTermComparator() {}
|
||||
virtual bool Matches(const QString& element) const = 0;
|
||||
};
|
||||
|
||||
// "compares" by checking if the field contains the search term
|
||||
class DefaultComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit DefaultComparator(const QString& value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return element.contains(search_term_);
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class EqComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit EqComparator(const QString& value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return search_term_ == element;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class LexicalGtComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit LexicalGtComparator(const QString& value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return element > search_term_;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class LexicalGeComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit LexicalGeComparator(const QString& value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return element >= search_term_;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class LexicalLtComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit LexicalLtComparator(const QString& value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return element < search_term_;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class LexicalLeComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit LexicalLeComparator(const QString& value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return element <= search_term_;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class GtComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit GtComparator(int value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return element.toInt() > search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class GeComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit GeComparator(int value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return element.toInt() >= search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class LtComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit LtComparator(int value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return element.toInt() < search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class LeComparator : public SearchTermComparator {
|
||||
public:
|
||||
explicit LeComparator(int value) : search_term_(value) {}
|
||||
virtual bool Matches(const QString& element) const {
|
||||
return element.toInt() <= search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
// The length field of the playlist (entries) contains a
|
||||
// song's running time in nano seconds. However, We don't
|
||||
// really care about nano seconds, just seconds. Thus, with
|
||||
// this decorator we drop the last 9 digits, if that many
|
||||
// are present.
|
||||
class DropTailComparatorDecorator : public SearchTermComparator {
|
||||
public:
|
||||
explicit DropTailComparatorDecorator(SearchTermComparator* cmp) : cmp_(cmp) {}
|
||||
|
||||
virtual bool Matches(const QString& element) const {
|
||||
if (element.length() > 9)
|
||||
return cmp_->Matches(element.left(element.length()-9));
|
||||
else
|
||||
return cmp_->Matches(element);
|
||||
}
|
||||
private:
|
||||
QScopedPointer<SearchTermComparator> cmp_;
|
||||
};
|
||||
|
||||
// filter that applies a SearchTermComparator to all fields of a playlist entry
|
||||
class FilterTerm : public FilterTree {
|
||||
public:
|
||||
explicit FilterTerm(SearchTermComparator* comparator, const QList<int>& columns) : cmp_(comparator), columns_(columns) {}
|
||||
|
||||
virtual bool accept(int row, const QModelIndex& parent, const QAbstractItemModel* const model) const {
|
||||
foreach (int i, columns_) {
|
||||
QModelIndex idx(model->index(row, i, parent));
|
||||
if (cmp_->Matches(idx.data().toString().toLower()))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
virtual FilterType type() { return Term; }
|
||||
private:
|
||||
QScopedPointer<SearchTermComparator> cmp_;
|
||||
QList<int> columns_;
|
||||
};
|
||||
|
||||
// filter that applies a SearchTermComparator to one specific field of a playlist entry
|
||||
class FilterColumnTerm : public FilterTree {
|
||||
public:
|
||||
FilterColumnTerm(int column, SearchTermComparator* comparator) : col(column), cmp_(comparator) {}
|
||||
|
||||
virtual bool accept(int row, const QModelIndex& parent, const QAbstractItemModel* const model) const {
|
||||
QModelIndex idx(model->index(row, col, parent));
|
||||
return cmp_->Matches(idx.data().toString().toLower());
|
||||
}
|
||||
virtual FilterType type() { return Column; }
|
||||
private:
|
||||
int col;
|
||||
QScopedPointer<SearchTermComparator> cmp_;
|
||||
};
|
||||
|
||||
class NotFilter : public FilterTree {
|
||||
public:
|
||||
explicit NotFilter(const FilterTree* inv) : child_(inv) {}
|
||||
virtual ~NotFilter() { delete child_; }
|
||||
virtual bool accept(int row, const QModelIndex& parent, const QAbstractItemModel* const model) const {
|
||||
return !child_->accept(row, parent, model);
|
||||
}
|
||||
virtual FilterType type() { return Not; }
|
||||
private:
|
||||
const FilterTree* child_;
|
||||
};
|
||||
|
||||
class OrFilter : public FilterTree {
|
||||
public:
|
||||
~OrFilter() { qDeleteAll(children_); }
|
||||
virtual void add(FilterTree* child) { children_.append(child); }
|
||||
virtual bool accept(int row, const QModelIndex& parent, const QAbstractItemModel* const model) const {
|
||||
foreach (FilterTree* child, children_) {
|
||||
if (child->accept(row, parent, model))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
FilterType type() { return Or; }
|
||||
private:
|
||||
QList<FilterTree*> children_;
|
||||
};
|
||||
|
||||
class AndFilter : public FilterTree {
|
||||
public:
|
||||
virtual ~AndFilter() { qDeleteAll(children_); }
|
||||
virtual void add(FilterTree* child) { children_.append(child); }
|
||||
virtual bool accept(int row, const QModelIndex& parent, const QAbstractItemModel* const model) const {
|
||||
foreach (FilterTree* child, children_) {
|
||||
if (!child->accept(row, parent, model))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
FilterType type() { return And; }
|
||||
private:
|
||||
QList<FilterTree*> children_;
|
||||
};
|
||||
|
||||
FilterParser::FilterParser(const QString& filter, const QMap< QString, int >& columns, QSet< int > exact_cols)
|
||||
: filterstring_(filter), columns_(columns), exact_columns_(exact_cols)
|
||||
{
|
||||
}
|
||||
|
||||
FilterTree* FilterParser::parse() {
|
||||
iter_ = filterstring_.constBegin();
|
||||
end_ = filterstring_.constEnd();
|
||||
return parseOrGroup();
|
||||
}
|
||||
|
||||
void FilterParser::advance() {
|
||||
while (iter_ != end_ && iter_->isSpace()) {
|
||||
++iter_;
|
||||
}
|
||||
}
|
||||
|
||||
FilterTree* FilterParser::parseOrGroup() {
|
||||
advance();
|
||||
if (iter_ == end_)
|
||||
return new NopFilter;
|
||||
|
||||
OrFilter* group = new OrFilter;
|
||||
group->add(parseAndGroup());
|
||||
advance();
|
||||
while (checkOr()) {
|
||||
group->add(parseAndGroup());
|
||||
advance();
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
FilterTree* FilterParser::parseAndGroup() {
|
||||
advance();
|
||||
if (iter_ == end_)
|
||||
return new NopFilter;
|
||||
|
||||
AndFilter* group = new AndFilter();
|
||||
do {
|
||||
group->add(parseSearchExpression());
|
||||
advance();
|
||||
if (iter_ != end_ && *iter_ == QChar(')'))
|
||||
break;
|
||||
if (checkOr(false)) {
|
||||
break;
|
||||
}
|
||||
checkAnd(); // if there's no 'AND', we'll add the term anyway...
|
||||
} while (iter_ != end_);
|
||||
return group;
|
||||
}
|
||||
|
||||
bool FilterParser::checkAnd() {
|
||||
if (iter_ != end_) {
|
||||
if (*iter_ == QChar('A')) {
|
||||
buf_ += *iter_;
|
||||
iter_++;
|
||||
if (iter_ != end_ && *iter_ == QChar('N')) {
|
||||
buf_ += *iter_;
|
||||
iter_++;
|
||||
if (iter_ != end_ && *iter_ == QChar('D')) {
|
||||
buf_ += *iter_;
|
||||
iter_++;
|
||||
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QChar('-') || *iter_ == '(')) {
|
||||
advance();
|
||||
buf_.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FilterParser::checkOr(bool step_over) {
|
||||
if (!buf_.isEmpty()) {
|
||||
if (buf_ == QString("OR")) {
|
||||
if (step_over) {
|
||||
buf_.clear();
|
||||
advance();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (iter_ != end_) {
|
||||
if (*iter_ == QChar('O')) {
|
||||
buf_ += *iter_;
|
||||
iter_++;
|
||||
if (iter_ != end_ && *iter_ == QChar('R')) {
|
||||
buf_ += *iter_;
|
||||
iter_++;
|
||||
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QChar('-') || *iter_ == '(')) {
|
||||
if (step_over) {
|
||||
buf_.clear();
|
||||
advance();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FilterTree* FilterParser::parseSearchExpression() {
|
||||
advance();
|
||||
if (iter_ == end_)
|
||||
return new NopFilter;
|
||||
if (*iter_ == QChar('(')) {
|
||||
iter_++;
|
||||
advance();
|
||||
FilterTree* tree = parseOrGroup();
|
||||
advance();
|
||||
if (iter_ != end_) {
|
||||
if (*iter_ == QChar(')')) {
|
||||
++iter_;
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
} else if (*iter_ == QChar('-')) {
|
||||
++iter_;
|
||||
FilterTree* tree = parseSearchExpression();
|
||||
if (tree->type() != FilterTree::Nop)
|
||||
return new NotFilter(tree);
|
||||
return tree;
|
||||
} else {
|
||||
return parseSearchTerm();
|
||||
}
|
||||
}
|
||||
|
||||
FilterTree* FilterParser::parseSearchTerm() {
|
||||
QString col;
|
||||
QString search;
|
||||
QString prefix;
|
||||
bool inQuotes = false;
|
||||
for (; iter_ != end_; ++iter_) {
|
||||
if (inQuotes) {
|
||||
if (*iter_ == QChar('"'))
|
||||
inQuotes = false;
|
||||
else
|
||||
buf_ += *iter_;
|
||||
} else {
|
||||
if (*iter_ == QChar('"')) {
|
||||
inQuotes = true;
|
||||
} else if (col.isEmpty() && *iter_ == QChar(':')) {
|
||||
col = buf_.toLower();
|
||||
buf_.clear();
|
||||
prefix.clear(); // prefix isn't allowed here - let's ignore it
|
||||
} else if (iter_->isSpace() ||
|
||||
*iter_ == QChar('(') ||
|
||||
*iter_ == QChar(')') ||
|
||||
*iter_ == QChar('-')) {
|
||||
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_ == QChar('>') || *iter_ == QChar('<') || *iter_ == QChar('='))) {
|
||||
prefix += *iter_;
|
||||
} else if (prefix != QString("=") && *iter_ == QChar('=')) {
|
||||
prefix += *iter_;
|
||||
} else {
|
||||
buf_ += *iter_;
|
||||
}
|
||||
} else {
|
||||
buf_ += *iter_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
search = buf_.toLower();
|
||||
buf_.clear();
|
||||
|
||||
return createSearchTermTreeNode(col, prefix, search);
|
||||
}
|
||||
|
||||
FilterTree* FilterParser::createSearchTermTreeNode(const QString& col, const QString& prefix, const QString& search) const {
|
||||
if (search.isEmpty())
|
||||
return new NopFilter;
|
||||
if (prefix.isEmpty()) {
|
||||
// easy case - no prefix :-)
|
||||
if (!col.isEmpty() && columns_.contains(col))
|
||||
return new FilterColumnTerm(columns_[col], exact_columns_.contains(columns_[col]) ?
|
||||
static_cast<SearchTermComparator*>(new EqComparator(search)) :
|
||||
static_cast<SearchTermComparator*>(new DefaultComparator(search)) );
|
||||
else
|
||||
return new FilterTerm(new DefaultComparator(search), columns_.values());
|
||||
} else {
|
||||
// here comes a mess :/
|
||||
// well, not that much of a mess, but so many options -_-
|
||||
SearchTermComparator* cmp = NULL;
|
||||
if (prefix == QString("=")) {
|
||||
cmp = new EqComparator(search);
|
||||
} else if (!col.isEmpty() && columns_.contains(col) && exact_columns_.contains(columns_[col])) {
|
||||
// the length column contains the time in seconds (nano seconds, actually -
|
||||
// the "nano" part is handled by the DropTailComparatorDecorator, though).
|
||||
QString _search = columns_[col] == Playlist::Column_Length ? parseTime(search) : search;
|
||||
|
||||
// alright, back to deciding which comparator we'll use
|
||||
if (prefix == QString(">")) {
|
||||
cmp = new GtComparator(_search.toInt());
|
||||
} else if (prefix == QString(">=")) {
|
||||
cmp = new GeComparator(_search.toInt());
|
||||
} else if (prefix == QString("<")) {
|
||||
cmp = new LtComparator(_search.toInt());
|
||||
} else if (prefix == QString("<=")) {
|
||||
cmp = new LeComparator(_search.toInt());
|
||||
} else {
|
||||
// just to be sure :-)
|
||||
cmp = new EqComparator(_search);
|
||||
}
|
||||
} else {
|
||||
if (prefix == QString(">")) {
|
||||
cmp = new LexicalGtComparator(search);
|
||||
} else if (prefix == QString(">=")) {
|
||||
cmp = new LexicalGeComparator(search);
|
||||
} else if (prefix == QString("<")) {
|
||||
cmp = new LexicalLtComparator(search);
|
||||
} else if (prefix == QString("<=")) {
|
||||
cmp = new LexicalLeComparator(search);
|
||||
} else {
|
||||
// just to be sure :-)
|
||||
cmp = new DefaultComparator(search);
|
||||
}
|
||||
}
|
||||
if (columns_.contains(col) && columns_[col] == Playlist::Column_Length)
|
||||
cmp = new DropTailComparatorDecorator(cmp);
|
||||
if (columns_.contains(col))
|
||||
return new FilterColumnTerm(columns_[col], cmp);
|
||||
else
|
||||
return new FilterTerm(cmp, columns_.values());
|
||||
}
|
||||
}
|
||||
|
||||
// Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
|
||||
// and return the number of seconds if it parses correctly.
|
||||
// If not, the original string is returned.
|
||||
// The 'h', 'm' and 's' components can have any length (including 0).
|
||||
//
|
||||
// A few examples:
|
||||
// "::" is parsed to "0"
|
||||
// "1::" is parsed to "3600"
|
||||
// "3:45" is parsed to "225"
|
||||
// "1:165" is parsed to "225"
|
||||
// "225" is parsed to "225" (srsly! ^.^)
|
||||
// "2:3:4:5" is parsed to "2:3:4:5"
|
||||
// "25m" is parsed to "25m"
|
||||
QString FilterParser::parseTime(const QString& timeStr) const {
|
||||
int seconds = 0, accum = 0, colonCount = 0;
|
||||
foreach (const QChar& c, timeStr) {
|
||||
if (c.isDigit()) {
|
||||
accum = accum*10+c.digitValue();
|
||||
} else if (c == ':') {
|
||||
seconds = seconds*60+accum;
|
||||
accum = 0;
|
||||
++colonCount;
|
||||
if (colonCount>2)
|
||||
return timeStr;
|
||||
} else if (!c.isSpace()) {
|
||||
return timeStr;
|
||||
}
|
||||
}
|
||||
seconds = seconds*60+accum;
|
||||
return QString::number(seconds);
|
||||
}
|
||||
|
94
src/playlist/playlistfilterparser.h
Normal file
94
src/playlist/playlistfilterparser.h
Normal file
@ -0,0 +1,94 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine 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.
|
||||
|
||||
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef PLAYLISTFILTERPARSER_H
|
||||
#define PLAYLISTFILTERPARSER_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QModelIndex>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
|
||||
class QAbstractItemModel;
|
||||
|
||||
// structure for filter parse tree
|
||||
class FilterTree {
|
||||
public:
|
||||
virtual ~FilterTree() {}
|
||||
virtual bool accept(int row, const QModelIndex& parent, const QAbstractItemModel* const model) const = 0;
|
||||
enum FilterType {
|
||||
Nop = 0,
|
||||
Or,
|
||||
And,
|
||||
Not,
|
||||
Column,
|
||||
Term
|
||||
};
|
||||
virtual FilterType type() = 0;
|
||||
};
|
||||
|
||||
// trivial filter that accepts *anything*
|
||||
class NopFilter : public FilterTree {
|
||||
public:
|
||||
virtual bool accept(int row, const QModelIndex& parent, const QAbstractItemModel* const model) const { return true; }
|
||||
virtual FilterType type() { return 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 FilterParser {
|
||||
public:
|
||||
FilterParser(const QString& filter, const QMap<QString, int>& columns, QSet<int> exact_cols);
|
||||
|
||||
FilterTree* parse();
|
||||
|
||||
private:
|
||||
void advance();
|
||||
FilterTree* parseOrGroup();
|
||||
FilterTree* parseAndGroup();
|
||||
// check if iter is at the start of 'AND'
|
||||
// if so, step over it and return true
|
||||
// it not, return false and leave iter where it was
|
||||
bool checkAnd();
|
||||
// check if iter is at the start of 'OR'
|
||||
bool checkOr(bool step_over = true);
|
||||
FilterTree* parseSearchExpression();
|
||||
FilterTree* parseSearchTerm();
|
||||
|
||||
FilterTree* createSearchTermTreeNode(const QString& col, const QString& prefix, const QString& search) const;
|
||||
QString parseTime(const QString& timeStr) const;
|
||||
|
||||
QString::const_iterator iter_, end_;
|
||||
QString buf_;
|
||||
const QString filterstring_;
|
||||
const QMap<QString, int> columns_;
|
||||
const QSet<int> exact_columns_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTFILTERPARSER_H
|
Loading…
Reference in New Issue
Block a user