Split filters into libraries where applicable. (#6293)

* Split filters into libraries where applicable.

Took 23 minutes

Took 2 minutes

* Include filter string.

Took 5 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL
2025-11-09 12:19:27 +01:00
committed by GitHub
parent 484e8e64a6
commit 9f2ac78609
28 changed files with 109 additions and 68 deletions

View File

@@ -0,0 +1,24 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h
libcockatrice/filters/filter_tree.h
)
if(Qt6_FOUND)
qt6_wrap_cpp(MOC_SOURCES ${HEADERS})
elseif(Qt5_FOUND)
qt5_wrap_cpp(MOC_SOURCES ${HEADERS})
endif()
add_library(
libcockatrice_filters STATIC ${MOC_SOURCES} libcockatrice/filters/filter_card.cpp
libcockatrice/filters/filter_string.cpp libcockatrice/filters/filter_tree.cpp
)
add_dependencies(libcockatrice_filters libcockatrice_card)
target_include_directories(libcockatrice_filters PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(libcockatrice_filters PUBLIC libcockatrice_card ${QT_CORE_MODULE})

View File

@@ -0,0 +1,92 @@
#include "filter_card.h"
#include <QJsonObject>
QJsonObject CardFilter::toJson() const
{
QJsonObject obj;
obj["term"] = trm;
obj["type"] = typeName(t);
obj["attr"] = attrName(a);
return obj;
}
CardFilter *CardFilter::fromJson(const QJsonObject &obj)
{
QString term = obj["term"].toString();
QString typeStr = obj["type"].toString();
QString attrStr = obj["attr"].toString();
Type type = TypeEnd;
Attr attr = AttrEnd;
// Convert type string back to enum
for (int i = 0; i < TypeEnd; i++) {
if (typeName(static_cast<Type>(i)) == typeStr) {
type = static_cast<Type>(i);
break;
}
}
// Convert attr string back to enum
for (int i = 0; i < AttrEnd; i++) {
if (attrName(static_cast<Attr>(i)) == attrStr) {
attr = static_cast<Attr>(i);
break;
}
}
return new CardFilter(term, type, attr);
}
const QString CardFilter::typeName(Type t)
{
switch (t) {
case TypeAnd:
return tr("AND", "Logical conjunction operator used in card filter");
case TypeOr:
return tr("OR", "Logical disjunction operator used in card filter");
case TypeAndNot:
return tr("AND NOT", "Negated logical conjunction operator used in card filter");
case TypeOrNot:
return tr("OR NOT", "Negated logical disjunction operator used in card filter");
default:
return QString("");
}
}
const QString CardFilter::attrName(Attr a)
{
switch (a) {
case AttrName:
return tr("Name");
case AttrType:
return tr("Type");
case AttrColor:
return tr("Color");
case AttrText:
return tr("Text");
case AttrSet:
return tr("Set");
case AttrManaCost:
return tr("Mana Cost");
case AttrCmc:
return tr("Mana Value");
case AttrRarity:
return tr("Rarity");
case AttrPow:
return tr("Power");
case AttrTough:
return tr("Toughness");
case AttrLoyalty:
return tr("Loyalty");
case AttrFormat:
return tr("Format");
case AttrMainType:
return tr("Main Type");
case AttrSubType:
return tr("Sub Type");
default:
return QString("");
}
}

View File

@@ -0,0 +1,78 @@
/**
* @file filter_card.h
* @ingroup CardDatabaseModelFilters
* @brief TODO: Document this.
*/
#ifndef CARDFILTER_H
#define CARDFILTER_H
#include <QObject>
#include <QString>
#include <utility>
class CardFilter : public QObject
{
Q_OBJECT
public:
enum Type
{
TypeAnd = 0,
TypeOr,
TypeAndNot,
TypeOrNot,
TypeEnd
};
/* if you add an attribute here you also need to
* add its string representation in attrName */
enum Attr
{
AttrCmc = 0,
AttrColor,
AttrLoyalty,
AttrManaCost,
AttrName,
AttrPow,
AttrRarity,
AttrSet,
AttrText,
AttrTough,
AttrType,
AttrMainType,
AttrSubType,
AttrFormat,
AttrEnd,
};
private:
QString trm;
enum Type t;
enum Attr a;
public:
CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr)
{
}
Type type() const
{
return t;
}
const QString &term() const
{
return trm;
}
Attr attr() const
{
return a;
}
QJsonObject toJson() const;
static CardFilter *fromJson(const QJsonObject &json);
static const QString typeName(Type t);
static const QString attrName(Attr a);
};
#endif

View File

@@ -0,0 +1,412 @@
#include "filter_string.h"
#include <QByteArray>
#include <QDebug>
#include <QRegularExpression>
#include <QString>
#include <functional>
#include <libcockatrice/utility/peglib.h>
static peg::parser search(R"(
Start <- QueryPartList
~ws <- [ ]+
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
ComplexQueryPart <- SomewhatComplexQueryPart ws "OR" ws ComplexQueryPart / SomewhatComplexQueryPart
SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQuery / ToughnessQuery / ColorQuery / TypeQuery / OracleQuery / FieldQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
SetQuery <- ('e'/'set') [:] FlexStringValue
OracleQuery <- 'o' [:] MatcherString
CMCQuery <- ('cmc'/'mv') ws? NumericExpression
PowerQuery <- [Pp] 'ow' 'er'? ws? NumericExpression
ToughnessQuery <- [Tt] 'ou' 'ghness'? ws? NumericExpression
RarityQuery <- [rR] ':' Rarity
Rarity <- [Cc] 'ommon'? / [Uu] 'ncommon'? / [Rr] 'are'? / [Mm] 'ythic'? / [Ss] 'pecial'? / [a-zA-Z] [a-z]*
FormatQuery <- 'f' ':' Format / Legality ':' Format
Format <- [a-zA-Z] [a-z]*
Legality <- [Ll] 'egal'? / [Bb] 'anned'? / [Rr] 'estricted'
TypeQuery <- [tT] 'ype'? [:] StringValue
Color <- < [Ww] 'hite'? / [Uu] / [Bb] 'lack'? / [Rr] 'ed'? / [Gg] 'reen'? / [Bb] 'lue'? >
ColorEx <- Color / [mc]
ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx*
FieldQuery <- String [:] MatcherString / String ws? NumericExpression
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
UnescapedStringListPart <- !['":<>=! ].
SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)*
String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuoteUnlessEscaped*> ["] / ['] <NonSingleQuoteUnlessEscaped*> [']
StringValue <- String / [(] StringList [)]
StringList <- StringListString (ws? [,] ws? StringListString)*
StringListString <- UnescapedStringListPart+
GenericQuery <- MatcherString
# A String that can either be a normal string or a regex search string
MatcherString <- RegexMatcher / NormalMatcher
NormalMatcher <- String
RegexMatcher <- '/' RegexMatcherString '/'
RegexMatcherString <- ('\\/' / !'/' .)+
FlexStringValue <- CompactStringSet / String / [(] StringList [)]
CompactStringSet <- StringListString ([,+] StringListString)+
NumericExpression <- NumericOperator ws? NumericValue
NumericOperator <- [=:] / <[><!][=]?>
NumericValue <- [0-9]+
)");
static std::once_flag init;
static void setupParserRules()
{
auto passthru = [](const peg::SemanticValues &sv) -> Filter {
return !sv.empty() ? std::any_cast<Filter>(sv[0]) : nullptr;
};
search["Start"] = passthru;
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> Filter {
return [=](const CardData &x) {
auto matchesFilter = [&x](const std::any &query) { return std::any_cast<Filter>(query)(x); };
return std::all_of(sv.begin(), sv.end(), matchesFilter);
};
};
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> Filter {
return [=](const CardData &x) {
auto matchesFilter = [&x](const std::any &query) { return std::any_cast<Filter>(query)(x); };
return std::any_of(sv.begin(), sv.end(), matchesFilter);
};
};
search["SomewhatComplexQueryPart"] = passthru;
search["QueryPart"] = passthru;
search["NotQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto dependent = std::any_cast<Filter>(sv[0]);
return [=](const CardData &x) -> bool { return !dependent(x); };
};
search["TypeQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]);
return [=](const CardData &x) -> bool { return matcher(x->getCardType()); };
};
search["SetQuery"] = [](const peg::SemanticValues &sv) -> Filter {
auto matcher = std::any_cast<StringMatcher>(sv[0]);
return [=](const CardData &x) -> bool {
QList<QString> sets = x->getSets().keys();
auto matchesSet = [&matcher](const QString &set) { return matcher(set); };
return std::any_of(sets.begin(), sets.end(), matchesSet);
};
};
search["Rarity"] = [](const peg::SemanticValues &sv) -> QString {
switch (tolower(std::string(sv.sv())[0])) {
case 'c':
return "common";
case 'u':
return "uncommon";
case 'r':
return "rare";
case 'm':
return "mythic";
case 's':
return "special";
default:
return QString::fromStdString(std::string(sv.sv()));
}
};
search["RarityQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto rarity = std::any_cast<QString>(sv[0]);
return [=](const CardData &x) -> bool {
QList<PrintingInfo> infos;
for (const auto &printings : x->getSets()) {
for (const auto &printing : printings) {
infos.append(printing);
}
}
auto matchesRarity = [&rarity](const PrintingInfo &info) { return rarity == info.getProperty("rarity"); };
return std::any_of(infos.begin(), infos.end(), matchesRarity);
};
};
search["FormatQuery"] = [](const peg::SemanticValues &sv) -> Filter {
if (sv.choice() == 0) {
const auto format = std::any_cast<QString>(sv[0]);
return
[=](const CardData &x) -> bool { return x->getProperty(QString("format-%1").arg(format)) == "legal"; };
}
const auto format = std::any_cast<QString>(sv[1]);
const auto legality = std::any_cast<QString>(sv[0]);
return [=](const CardData &x) -> bool { return x->getProperty(QString("format-%1").arg(format)) == legality; };
};
search["Legality"] = [](const peg::SemanticValues &sv) -> QString {
switch (tolower(std::string(sv.sv())[0])) {
case 'l':
return "legal";
case 'b':
return "banned";
case 'r':
return "restricted";
default:
return "";
}
};
search["Format"] = [](const peg::SemanticValues &sv) -> QString {
if (sv.size() == 1) {
switch (tolower(std::string(sv.sv())[0])) {
case 'm':
return "modern";
case 's':
return "standard";
case 'v':
return "vintage";
case 'l':
return "legacy";
case 'c':
return "commander";
case 'p':
return "pioneer";
default:
return "";
}
}
return QString::fromStdString(std::string(sv.sv())).toLower();
};
search["StringValue"] = [](const peg::SemanticValues &sv) -> StringMatcher {
// Helper function for word boundary matching
auto createWordBoundaryMatcher = [](const QString &target) {
QString pattern = QString("\\b%1\\b").arg(QRegularExpression::escape(target));
QRegularExpression regex(pattern, QRegularExpression::CaseInsensitiveOption);
return [regex](const QString &s) { return regex.match(s).hasMatch(); };
};
if (sv.choice() == 0) {
const auto target = std::any_cast<QString>(sv[0]);
return createWordBoundaryMatcher(target);
}
const auto target = std::any_cast<QStringList>(sv[0]);
return [=](const QString &s) {
auto containsString = [&s, &createWordBoundaryMatcher](const QString &str) {
return createWordBoundaryMatcher(str)(s);
};
return std::any_of(target.begin(), target.end(), containsString);
};
};
search["String"] = [](const peg::SemanticValues &sv) -> QString {
if (sv.choice() == 0) {
return QString::fromStdString(std::string(sv.sv()));
}
return QString::fromStdString(std::string(sv.token(0)));
};
search["FlexStringValue"] = [](const peg::SemanticValues &sv) -> StringMatcher {
if (sv.choice() != 1) {
const auto target = std::any_cast<QStringList>(sv[0]);
return [=](const QString &s) {
auto containsString = [&s](const QString &str) {
return s.split(" ").contains(str, Qt::CaseInsensitive);
};
return std::any_of(target.begin(), target.end(), containsString);
};
}
const auto target = std::any_cast<QString>(sv[0]);
return [=](const QString &s) { return s.split(" ").contains(target, Qt::CaseInsensitive); };
};
search["CompactStringSet"] = [](const peg::SemanticValues &sv) -> QStringList {
QStringList result;
for (const auto &i : sv) {
result.append(std::any_cast<QString>(i));
}
return result;
};
search["StringList"] = [](const peg::SemanticValues &sv) -> QStringList {
QStringList result;
for (const auto &i : sv) {
result.append(std::any_cast<QString>(i));
}
return result;
};
search["StringListString"] = [](const peg::SemanticValues &sv) -> QString {
return QString::fromStdString(std::string(sv.sv()));
};
search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher {
const auto arg = std::any_cast<int>(sv[1]);
const auto op = std::any_cast<QString>(sv[0]);
if (op == ">")
return [=](const int s) { return s > arg; };
if (op == ">=")
return [=](const int s) { return s >= arg; };
if (op == "<")
return [=](const int s) { return s < arg; };
if (op == "<=")
return [=](const int s) { return s <= arg; };
if (op == "=")
return [=](const int s) { return s == arg; };
if (op == ":")
return [=](const int s) { return s == arg; };
if (op == "!=")
return [=](const int s) { return s != arg; };
return [](int) { return false; };
};
search["NumericValue"] = [](const peg::SemanticValues &sv) -> int {
return QString::fromStdString(std::string(sv.sv())).toInt();
};
search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString {
return QString::fromStdString(std::string(sv.sv()));
};
search["NormalMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher {
auto target = std::any_cast<QString>(sv[0]);
auto sanitizedTarget = QString(target);
sanitizedTarget.replace("\\\"", "\"");
sanitizedTarget.replace("\\'", "'");
return [=](const QString &s) { return s.contains(sanitizedTarget, Qt::CaseInsensitive); };
};
search["RegexMatcher"] = [](const peg::SemanticValues &sv) -> StringMatcher {
auto target = std::any_cast<QString>(sv[0]);
auto regex = QRegularExpression(target, QRegularExpression::CaseInsensitiveOption);
return [=](const QString &s) { return regex.match(s).hasMatch(); };
};
search["RegexMatcherString"] = [](const peg::SemanticValues &sv) -> QString {
return QString::fromStdString(sv.token_to_string());
};
search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]);
return [=](const CardData &x) { return matcher(x->getText()); };
};
search["ColorQuery"] = [](const peg::SemanticValues &sv) -> Filter {
QString parts;
for (const auto &i : sv) {
parts += std::any_cast<char>(i);
}
const bool identity = sv.tokens[0].empty() || sv.tokens[0][0] != 'i';
if (sv.tokens[1][0] == ':') {
return [=](const CardData &x) {
QString match = identity ? x->getColors() : x->getProperty("coloridentity");
if (parts.contains("m") && match.length() < 2) {
return false;
}
if (parts == "m") {
return true;
}
if (parts.contains("c") && match.length() == 0)
return true;
auto containsColor = [&parts](const QString &s) { return parts.contains(s); };
return std::any_of(match.begin(), match.end(), containsColor);
};
}
return [=](const CardData &x) {
QString match = identity ? x->getColors() : x->getProperty("colorIdentity");
if (parts.contains("m") && match.length() < 2) {
return false;
}
if (parts.contains("c") && match.length() != 0) {
return false;
}
for (const auto &part : parts) {
if (!match.contains(part)) {
return false;
}
}
auto containsColor = [&parts](const QString &s) { return parts.contains(s); };
return std::all_of(match.begin(), match.end(), containsColor);
};
};
search["CMCQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<NumberMatcher>(sv[0]);
return [=](const CardData &x) -> bool { return matcher(x->getProperty("cmc").toInt()); };
};
search["PowerQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<NumberMatcher>(sv[0]);
return [=](const CardData &x) -> bool { return matcher(x->getPowTough().split("/")[0].toInt()); };
};
search["ToughnessQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<NumberMatcher>(sv[0]);
return [=](const CardData &x) -> bool {
auto parts = x->getPowTough().split("/");
return matcher(parts.length() == 2 ? parts[1].toInt() : 0);
};
};
search["FieldQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto field = std::any_cast<QString>(sv[0]);
if (sv.choice() == 0) {
const auto matcher = std::any_cast<StringMatcher>(sv[1]);
return [=](const CardData &x) -> bool { return x->hasProperty(field) && matcher(x->getProperty(field)); };
}
const auto matcher = std::any_cast<NumberMatcher>(sv[1]);
return
[=](const CardData &x) -> bool { return x->hasProperty(field) && matcher(x->getProperty(field).toInt()); };
};
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter {
const auto matcher = std::any_cast<StringMatcher>(sv[0]);
return [=](const CardData &x) { return matcher(x->getName()); };
};
search["Color"] = [](const peg::SemanticValues &sv) -> char { return "WUBRGU"[sv.choice()]; };
search["ColorEx"] = [](const peg::SemanticValues &sv) -> char {
return sv.choice() == 0 ? std::any_cast<char>(sv[0]) : *std::string(sv.sv()).c_str();
};
}
FilterString::FilterString()
{
result = [](const CardData &) -> bool { return false; };
_error = "Not initialized";
}
FilterString::FilterString(const QString &expr)
{
QByteArray ba = expr.simplified().toUtf8();
std::call_once(init, setupParserRules);
_error = QString();
if (ba.isEmpty()) {
result = [](const CardData &) -> bool { return true; };
return;
}
search.set_logger([&](size_t /*ln*/, size_t col, const std::string &msg) {
_error = QString("Error at position %1: %2").arg(col).arg(QString::fromStdString(msg));
});
if (!search.parse(ba.data(), result)) {
qCInfo(FilterStringLog).nospace() << "FilterString error for " << expr << "; " << qPrintable(_error);
result = [](const CardData &) -> bool { return false; };
}
}

View File

@@ -0,0 +1,62 @@
/**
* @file filter_string.h
* @ingroup CardDatabaseModelFilters
* @brief TODO: Document this.
*/
#ifndef FILTER_STRING_H
#define FILTER_STRING_H
#include "filter_tree.h"
#include <QLoggingCategory>
#include <QMap>
#include <QString>
#include <functional>
#include <libcockatrice/card/card_info.h>
#include <utility>
inline Q_LOGGING_CATEGORY(FilterStringLog, "filter_string");
typedef CardInfoPtr CardData;
typedef std::function<bool(const CardData &)> Filter;
typedef std::function<bool(const QString &)> StringMatcher;
typedef std::function<bool(int)> NumberMatcher;
namespace peg
{
template <typename Annotation> struct AstBase;
struct EmptyType;
typedef AstBase<EmptyType> Ast;
} // namespace peg
class FilterString
{
public:
FilterString();
explicit FilterString(const QString &exp);
bool check(const CardData &card) const
{
if (card.isNull()) {
static CardInfoPtr blankCard = CardInfo::newInstance("");
return result(blankCard);
}
return result(card);
}
bool valid()
{
return _error.isEmpty();
}
QString error()
{
return _error;
}
private:
QString _error;
Filter result;
};
#endif

View File

@@ -0,0 +1,565 @@
#include "filter_tree.h"
#include "filter_card.h"
#include <QList>
template <class T> FilterTreeNode *FilterTreeBranch<T>::nodeAt(int i) const
{
return (childNodes.size() > i) ? childNodes.at(i) : nullptr;
}
template <class T> void FilterTreeBranch<T>::deleteAt(int i)
{
preRemoveChild(this, i);
delete childNodes.takeAt(i);
postRemoveChild(this, i);
nodeChanged();
}
template <class T> int FilterTreeBranch<T>::childIndex(const FilterTreeNode *node) const
{
auto *unconst = const_cast<FilterTreeNode *>(node);
auto downcasted = dynamic_cast<T>(unconst);
return (downcasted) ? childNodes.indexOf(downcasted) : -1;
}
template <class T> FilterTreeBranch<T>::~FilterTreeBranch()
{
while (!childNodes.isEmpty()) {
delete childNodes.takeFirst();
}
}
const FilterItemList *LogicMap::findTypeList(CardFilter::Type type) const
{
QList<FilterItemList *>::const_iterator i;
for (i = childNodes.constBegin(); i != childNodes.constEnd(); ++i) {
if ((*i)->type == type) {
return *i;
}
}
return nullptr;
}
FilterItemList *LogicMap::typeList(CardFilter::Type type)
{
QList<FilterItemList *>::iterator i;
int count = 0;
for (i = childNodes.begin(); i != childNodes.end(); ++i) {
if ((*i)->type == type) {
break;
}
count++;
}
if (i == childNodes.end()) {
preInsertChild(this, count);
i = childNodes.insert(i, new FilterItemList(type, this));
postInsertChild(this, count);
nodeChanged();
}
return *i;
}
FilterTreeNode *LogicMap::parent() const
{
return p;
}
int FilterItemList::termIndex(const QString &term) const
{
for (int i = 0; i < childNodes.count(); i++) {
if ((childNodes.at(i))->term == term) {
return i;
}
}
return -1;
}
FilterTreeNode *FilterItemList::termNode(const QString &term)
{
int i = termIndex(term);
if (i < 0) {
FilterItem *fi = new FilterItem(term, this);
int count = childNodes.count();
preInsertChild(this, count);
childNodes.append(fi);
postInsertChild(this, count);
nodeChanged();
return fi;
}
return childNodes.at(i);
}
bool FilterItemList::testTypeAnd(const CardInfoPtr info, CardFilter::Attr attr) const
{
for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) {
if (!(*i)->isEnabled()) {
continue;
}
if (!(*i)->acceptCardAttr(info, attr)) {
return false;
}
}
return true;
}
bool FilterItemList::testTypeAndNot(const CardInfoPtr info, CardFilter::Attr attr) const
{
// if any one in the list is true, return false
return !testTypeOr(info, attr);
}
bool FilterItemList::testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) const
{
bool noChildEnabledChild = true;
for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) {
if (!(*i)->isEnabled()) {
continue;
}
if (noChildEnabledChild) {
noChildEnabledChild = false;
}
if ((*i)->acceptCardAttr(info, attr)) {
return true;
}
}
return noChildEnabledChild;
}
bool FilterItemList::testTypeOrNot(const CardInfoPtr info, CardFilter::Attr attr) const
{
// if any one in the list is false, return true
return !testTypeAnd(info, attr);
}
bool FilterItem::acceptName(const CardInfoPtr info) const
{
return info->getName().contains(term, Qt::CaseInsensitive);
}
bool FilterItem::acceptType(const CardInfoPtr info) const
{
return info->getCardType().contains(term, Qt::CaseInsensitive);
}
bool FilterItem::acceptMainType(const CardInfoPtr info) const
{
const QStringList typeParts = info->getCardType().split("");
return !typeParts.isEmpty() && typeParts[0].contains(term, Qt::CaseInsensitive);
}
bool FilterItem::acceptSubType(const CardInfoPtr info) const
{
const QStringList typeParts = info->getCardType().split("");
return typeParts.size() > 1 && typeParts[1].contains(term, Qt::CaseInsensitive);
}
bool FilterItem::acceptColor(const CardInfoPtr info) const
{
QString converted_term = term.trimmed();
converted_term.replace("green", "g", Qt::CaseInsensitive);
converted_term.replace("grn", "g", Qt::CaseInsensitive);
converted_term.replace("blue", "u", Qt::CaseInsensitive);
converted_term.replace("blu", "u", Qt::CaseInsensitive);
converted_term.replace("black", "b", Qt::CaseInsensitive);
converted_term.replace("blk", "b", Qt::CaseInsensitive);
converted_term.replace("red", "r", Qt::CaseInsensitive);
converted_term.replace("white", "w", Qt::CaseInsensitive);
converted_term.replace("wht", "w", Qt::CaseInsensitive);
converted_term.replace("colorless", "c", Qt::CaseInsensitive);
converted_term.replace("colourless", "c", Qt::CaseInsensitive);
converted_term.replace("none", "c", Qt::CaseInsensitive);
converted_term.replace(QString(" "), QString(""), Qt::CaseInsensitive);
// Colorless card filter
if (converted_term.toLower() == "c" && info->getColors().length() < 1) {
return true;
}
/*
* This is a tricky part, if the filter has multiple colors in it, like UGW,
* then we should match all of them to the card's colors
*/
int match_count = 0;
for (auto &it : converted_term) {
if (info->getColors().contains(it, Qt::CaseInsensitive))
match_count++;
}
return match_count == converted_term.length();
}
bool FilterItem::acceptText(const CardInfoPtr info) const
{
return info->getText().contains(term, Qt::CaseInsensitive);
}
bool FilterItem::acceptSet(const CardInfoPtr info) const
{
bool status = false;
for (const auto &printings : info->getSets()) {
for (const auto &set : printings) {
if (set.getSet()->getShortName().compare(term, Qt::CaseInsensitive) == 0 ||
set.getSet()->getLongName().compare(term, Qt::CaseInsensitive) == 0) {
status = true;
break;
}
}
}
return status;
}
bool FilterItem::acceptManaCost(const CardInfoPtr info) const
{
QString partialCost = term.toUpper();
// Sort the mana cost so it will be easy to find
std::sort(partialCost.begin(), partialCost.end());
// Try to seperate the mana cost in case it's a split card
// if it's not a split card the loop will run only once
for (QString fullManaCost : info->getManaCost().split("//")) {
std::sort(fullManaCost.begin(), fullManaCost.end());
// If the partial is found in the full, return true
if (fullManaCost.contains(partialCost)) {
return true;
}
}
return false;
}
bool FilterItem::acceptCmc(const CardInfoPtr info) const
{
bool convertSuccess;
int cmcInt = info->getCmc().toInt(&convertSuccess);
// if conversion failed, check for the "//" separator used in split cards
if (!convertSuccess) {
int cmcSum = 0;
for (const QString &cmc : info->getCmc().split("//")) {
cmcInt = cmc.toInt();
cmcSum += cmcInt;
if (relationCheck(cmcInt)) {
return true;
}
}
return relationCheck(cmcSum);
} else {
return relationCheck(cmcInt);
}
}
bool FilterItem::acceptFormat(const CardInfoPtr info) const
{
return info->getProperty(QString("format-%1").arg(term.toLower())) == "legal";
}
bool FilterItem::acceptLoyalty(const CardInfoPtr info) const
{
if (info->getLoyalty().isEmpty()) {
return false;
} else {
bool success;
// if loyalty can't be converted to "int" it must be "X"
int loyalty = info->getLoyalty().toInt(&success);
if (success) {
return relationCheck(loyalty);
} else {
return term.trimmed().toUpper() == info->getLoyalty();
}
}
}
bool FilterItem::acceptPowerToughness(const CardInfoPtr info, CardFilter::Attr attr) const
{
int slash = info->getPowTough().indexOf("/");
if (slash == -1) {
return false;
}
QString valueString;
if (attr == CardFilter::AttrPow) {
valueString = info->getPowTough().mid(0, slash);
} else {
valueString = info->getPowTough().mid(slash + 1);
}
if (term == valueString) {
return true;
}
// advanced filtering should only happen after fast string comparison failed
bool conversion;
int value = valueString.toInt(&conversion);
return conversion ? relationCheck(value) : false;
}
bool FilterItem::acceptRarity(const CardInfoPtr info) const
{
QString converted_term = term.trimmed();
/*
* The purpose of this loop is to only apply one of the replacement
* policies and then escape. If we attempt to layer them on top of
* each other, we will get awkward results (i.e. comythic rare mythic rareon)
* Conditional statement will exit once a case is successful in
* replacement OR we go through all possible cases.
* Will also need to replace just "mythic"
*/
for (int i = 0; converted_term.length() <= 3 && i <= 6; i++) {
switch (i) {
case 0:
converted_term.replace("mr", "mythic", Qt::CaseInsensitive);
break;
case 1:
converted_term.replace("m r", "mythic", Qt::CaseInsensitive);
break;
case 2:
converted_term.replace("m", "mythic", Qt::CaseInsensitive);
break;
case 3:
converted_term.replace("c", "common", Qt::CaseInsensitive);
break;
case 4:
converted_term.replace("u", "uncommon", Qt::CaseInsensitive);
break;
case 5:
converted_term.replace("r", "rare", Qt::CaseInsensitive);
break;
case 6:
converted_term.replace("s", "special", Qt::CaseInsensitive);
break;
default:
break;
}
}
for (const auto &printings : info->getSets()) {
for (const auto &printing : printings) {
if (printing.getProperty("rarity").compare(converted_term, Qt::CaseInsensitive) == 0) {
return true;
}
}
}
return false;
}
bool FilterItem::relationCheck(int cardInfo) const
{
bool result, conversion;
// if int conversion fails, there's probably an operator at the start
result = (cardInfo == term.toInt(&conversion));
if (!conversion) {
// leading whitespaces could cause indexing to fail
QString trimmedTerm = term.trimmed();
// check whether it's a 2 char operator (<=, >=, or ==)
if (trimmedTerm[1] == '=') {
int termInt = trimmedTerm.mid(2).toInt();
if (trimmedTerm.startsWith('<')) {
result = (cardInfo <= termInt);
} else if (trimmedTerm.startsWith('>')) {
result = (cardInfo >= termInt);
} else {
result = (cardInfo == termInt);
}
} else {
int termInt = trimmedTerm.mid(1).toInt();
if (trimmedTerm.startsWith('<')) {
result = (cardInfo < termInt);
} else if (trimmedTerm.startsWith('>')) {
result = (cardInfo > termInt);
} else if (trimmedTerm.startsWith("=")) {
result = (cardInfo == termInt);
} else {
// the int conversion hasn't failed due to an operator at the start
result = false;
}
}
}
return result;
}
bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) const
{
switch (attr) {
case CardFilter::AttrName:
return acceptName(info);
case CardFilter::AttrType:
return acceptType(info);
case CardFilter::AttrColor:
return acceptColor(info);
case CardFilter::AttrText:
return acceptText(info);
case CardFilter::AttrSet:
return acceptSet(info);
case CardFilter::AttrManaCost:
return acceptManaCost(info);
case CardFilter::AttrCmc:
return acceptCmc(info);
case CardFilter::AttrRarity:
return acceptRarity(info);
case CardFilter::AttrPow:
case CardFilter::AttrTough:
// intentional fallthrough
return acceptPowerToughness(info, attr);
case CardFilter::AttrLoyalty:
return acceptLoyalty(info);
case CardFilter::AttrFormat:
return acceptFormat(info);
case CardFilter::AttrMainType:
return acceptMainType(info);
case CardFilter::AttrSubType:
return acceptSubType(info);
default:
return true; /* ignore this attribute */
}
}
/*
* Need to define these here to make QT happy, otherwise
* moc doesnt find some of the FilterTreeBranch symbols.
*/
FilterTree::FilterTree() = default;
FilterTree::~FilterTree() = default;
LogicMap *FilterTree::attrLogicMap(CardFilter::Attr attr)
{
QList<LogicMap *>::iterator i;
int count = 0;
for (i = childNodes.begin(); i != childNodes.end(); i++) {
if ((*i)->attr == attr) {
break;
}
count++;
}
if (i == childNodes.end()) {
preInsertChild(this, count);
i = childNodes.insert(i, new LogicMap(attr, this));
postInsertChild(this, count);
nodeChanged();
}
return *i;
}
FilterItemList *FilterTree::attrTypeList(CardFilter::Attr attr, CardFilter::Type type)
{
return attrLogicMap(attr)->typeList(type);
}
FilterTreeNode *FilterTree::termNode(CardFilter::Attr attr, CardFilter::Type type, const QString &term)
{
return attrTypeList(attr, type)->termNode(term);
}
FilterTreeNode *FilterTree::termNode(const CardFilter *f)
{
return termNode(f->attr(), f->type(), f->term());
}
bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm) const
{
const FilterItemList *fil;
bool status = true;
fil = lm->findTypeList(CardFilter::TypeAnd);
if (fil && fil->isEnabled() && !fil->testTypeAnd(info, lm->attr)) {
return false;
}
fil = lm->findTypeList(CardFilter::TypeAndNot);
if (fil && fil->isEnabled() && !fil->testTypeAndNot(info, lm->attr)) {
return false;
}
fil = lm->findTypeList(CardFilter::TypeOr);
if (fil && fil->isEnabled()) {
status = false;
// if this is true we can return because it is OR'd with the OrNot list
if (fil->testTypeOr(info, lm->attr)) {
return true;
}
}
fil = lm->findTypeList(CardFilter::TypeOrNot);
if (fil && fil->isEnabled() && fil->testTypeOrNot(info, lm->attr)) {
return true;
}
return status;
}
bool FilterTree::acceptsCard(const CardInfoPtr info) const
{
for (auto i = childNodes.constBegin(); i != childNodes.constEnd(); i++) {
if ((*i)->isEnabled() && !testAttr(info, *i)) {
return false;
}
}
return true;
}
void FilterTree::removeFiltersByAttr(CardFilter::Attr filterType)
{
for (int i = childNodes.size() - 1; i >= 0; --i) {
auto *child = dynamic_cast<LogicMap *>(childNodes.at(i));
if (child && child->attr == filterType) {
deleteAt(i);
}
}
}
void FilterTree::removeFilter(const CardFilter *toRemove)
{
for (int i = childNodes.size() - 1; i >= 0; --i) {
auto *logicMap = dynamic_cast<LogicMap *>(childNodes.at(i));
if (!logicMap || logicMap->attr != toRemove->attr())
continue;
FilterItemList *typeList = logicMap->typeList(toRemove->type());
if (!typeList)
continue;
int termIdx = typeList->termIndex(toRemove->term());
if (termIdx != -1) {
typeList->deleteAt(termIdx);
emit typeList->nodeChanged();
if (typeList->childCount() == 0) {
int logicIndex = logicMap->childIndex(typeList);
if (logicIndex != -1) {
logicMap->deleteAt(logicIndex);
}
}
}
}
}
void FilterTree::clear()
{
while (childCount() > 0) {
deleteAt(0);
}
emit changed();
}

View File

@@ -0,0 +1,283 @@
/**
* @file filter_tree.h
* @ingroup CardDatabaseModelFilters
* @brief TODO: Document this.
*/
#ifndef FILTERTREE_H
#define FILTERTREE_H
#include "filter_card.h"
#include <QList>
#include <QMap>
#include <QObject>
#include <libcockatrice/card/database/card_database.h>
#include <utility>
class FilterTreeNode
{
private:
bool enabled;
public:
FilterTreeNode() : enabled(true)
{
}
virtual bool isEnabled() const
{
return enabled;
}
virtual void enable()
{
enabled = true;
nodeChanged();
}
virtual void disable()
{
enabled = false;
nodeChanged();
}
virtual FilterTreeNode *parent() const
{
return nullptr;
}
virtual FilterTreeNode *nodeAt(int /* i */) const
{
return nullptr;
}
virtual void deleteAt(int /* i */)
{
}
virtual int childCount() const
{
return 0;
}
virtual int childIndex(const FilterTreeNode * /* node */) const
{
return -1;
}
virtual int index() const
{
return (parent() != nullptr) ? parent()->childIndex(this) : -1;
}
virtual const QString text() const
{
return QString("");
}
virtual bool isLeaf() const
{
return false;
}
virtual void nodeChanged() const
{
if (parent() != nullptr)
parent()->nodeChanged();
}
virtual void preInsertChild(const FilterTreeNode *p, int i) const
{
if (parent() != nullptr)
parent()->preInsertChild(p, i);
}
virtual void postInsertChild(const FilterTreeNode *p, int i) const
{
if (parent() != nullptr)
parent()->postInsertChild(p, i);
}
virtual void preRemoveChild(const FilterTreeNode *p, int i) const
{
if (parent() != nullptr)
parent()->preRemoveChild(p, i);
}
virtual void postRemoveChild(const FilterTreeNode *p, int i) const
{
if (parent() != nullptr)
parent()->postRemoveChild(p, i);
}
};
template <class T> class FilterTreeBranch : public FilterTreeNode
{
protected:
QList<T> childNodes;
public:
virtual ~FilterTreeBranch();
void removeFiltersByAttr(CardFilter::Attr filterType);
FilterTreeNode *nodeAt(int i) const override;
void deleteAt(int i) override;
int childCount() const override
{
return childNodes.size();
}
int childIndex(const FilterTreeNode *node) const override;
};
class FilterItemList;
class FilterTree;
class LogicMap : public FilterTreeBranch<FilterItemList *>
{
private:
FilterTree *const p;
public:
const CardFilter::Attr attr;
LogicMap(CardFilter::Attr a, FilterTree *parent) : p(parent), attr(a)
{
}
const FilterItemList *findTypeList(CardFilter::Type type) const;
FilterItemList *typeList(CardFilter::Type type);
FilterTreeNode *parent() const override;
const QString text() const override
{
return CardFilter::attrName(attr);
}
};
class FilterItem;
class FilterItemList : public FilterTreeBranch<FilterItem *>
{
private:
LogicMap *const p;
public:
const CardFilter::Type type;
FilterItemList(CardFilter::Type t, LogicMap *parent) : p(parent), type(t)
{
}
CardFilter::Attr attr() const
{
return p->attr;
}
FilterTreeNode *parent() const override
{
return p;
}
int termIndex(const QString &term) const;
FilterTreeNode *termNode(const QString &term);
const QString text() const override
{
return CardFilter::typeName(type);
}
bool testTypeAnd(CardInfoPtr info, CardFilter::Attr attr) const;
bool testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr) const;
bool testTypeOr(CardInfoPtr info, CardFilter::Attr attr) const;
bool testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr) const;
};
class FilterItem : public FilterTreeNode
{
private:
FilterItemList *const p;
public:
const QString term;
FilterItem(QString trm, FilterItemList *parent) : p(parent), term(std::move(trm))
{
}
virtual ~FilterItem() = default;
CardFilter::Attr attr() const
{
return p->attr();
}
CardFilter::Type type() const
{
return p->type;
}
FilterTreeNode *parent() const override
{
return p;
}
const QString text() const override
{
return term;
}
bool isLeaf() const override
{
return true;
}
bool acceptName(CardInfoPtr info) const;
bool acceptType(CardInfoPtr info) const;
bool acceptMainType(CardInfoPtr info) const;
bool acceptSubType(CardInfoPtr info) const;
bool acceptColor(CardInfoPtr info) const;
bool acceptText(CardInfoPtr info) const;
bool acceptSet(CardInfoPtr info) const;
bool acceptManaCost(CardInfoPtr info) const;
bool acceptCmc(CardInfoPtr info) const;
bool acceptPowerToughness(CardInfoPtr info, CardFilter::Attr attr) const;
bool acceptLoyalty(CardInfoPtr info) const;
bool acceptRarity(CardInfoPtr info) const;
bool acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr) const;
bool acceptFormat(CardInfoPtr info) const;
bool relationCheck(int cardInfo) const;
};
class FilterTree : public QObject, public FilterTreeBranch<LogicMap *>
{
Q_OBJECT
signals:
void preInsertRow(const FilterTreeNode *parent, int i) const;
void postInsertRow(const FilterTreeNode *parent, int i) const;
void preRemoveRow(const FilterTreeNode *parent, int i) const;
void postRemoveRow(const FilterTreeNode *parent, int i) const;
void changed() const;
private:
LogicMap *attrLogicMap(CardFilter::Attr attr);
FilterItemList *attrTypeList(CardFilter::Attr attr, CardFilter::Type type);
bool testAttr(CardInfoPtr info, const LogicMap *lm) const;
void nodeChanged() const override
{
emit changed();
}
void preInsertChild(const FilterTreeNode *p, int i) const override
{
emit preInsertRow(p, i);
}
void postInsertChild(const FilterTreeNode *p, int i) const override
{
emit postInsertRow(p, i);
}
void preRemoveChild(const FilterTreeNode *p, int i) const override
{
emit preRemoveRow(p, i);
}
void postRemoveChild(const FilterTreeNode *p, int i) const override
{
emit postRemoveRow(p, i);
}
public:
FilterTree();
~FilterTree() override;
FilterTreeNode *termNode(CardFilter::Attr attr, CardFilter::Type type, const QString &term);
FilterTreeNode *termNode(const CardFilter *f);
const QString text() const override
{
return QString("root");
}
int index() const override
{
return 0;
}
bool acceptsCard(CardInfoPtr info) const;
void removeFiltersByAttr(CardFilter::Attr filterType);
void removeFilter(const CardFilter *toRemove);
void clear();
};
#endif