Move cards and filters folder out of game (#6145)

* big move

* also move game_specific_terms

* fix imports

* alphabetize cmake

* fix build failure

* create database folder and move files into it

* fix includes

* run formatter
This commit is contained in:
RickyRister 2025-09-16 03:02:57 -07:00 committed by GitHub
parent bed79ef89e
commit 7ac22a6ce8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
149 changed files with 218 additions and 218 deletions

View file

@ -0,0 +1,193 @@
#include "deck_filter_string.h"
#include "../database/card_database_manager.h"
#include "filter_string.h"
#include "lib/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 / DeckContentQuery / DeckNameQuery / FileNameQuery / PathQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
DeckContentQuery <- CardSearch NumericExpression?
CardSearch <- '[[' CardFilterString ']]'
CardFilterString <- (!']]'.)*
DeckNameQuery <- ([Dd] 'eck')? [Nn] 'ame'? [:] String
FileNameQuery <- [Ff] ('ile' 'name'?)? [:] String
PathQuery <- [Pp] 'ath'? [:] String
GenericQuery <- String
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].
UnescapedStringListPart <- !['":<>=! ].
SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)*
String <- SingleApostropheString / UnescapedStringListPart+ / ["] <NonDoubleQuoteUnlessEscaped*> ["] / ['] <NonSingleQuoteUnlessEscaped*> [']
NumericExpression <- NumericOperator ws? NumericValue
NumericOperator <- [=:] / <[><!][=]?>
NumericValue <- [0-9]+
)");
static std::once_flag init;
static void setupParserRules()
{
// plumbing
auto passthru = [](const peg::SemanticValues &sv) -> DeckFilter {
return !sv.empty() ? std::any_cast<DeckFilter>(sv[0]) : nullptr;
};
search["Start"] = passthru;
search["QueryPartList"] = [](const peg::SemanticValues &sv) -> DeckFilter {
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
auto matchesFilter = [&deck, &info](const std::any &query) {
return std::any_cast<DeckFilter>(query)(deck, info);
};
return std::all_of(sv.begin(), sv.end(), matchesFilter);
};
};
search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> DeckFilter {
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) {
auto matchesFilter = [&deck, &info](const std::any &query) {
return std::any_cast<DeckFilter>(query)(deck, info);
};
return std::any_of(sv.begin(), sv.end(), matchesFilter);
};
};
search["SomewhatComplexQueryPart"] = passthru;
search["QueryPart"] = passthru;
search["NotQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
const auto dependent = std::any_cast<DeckFilter>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) -> bool {
return !dependent(deck, info);
};
};
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["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()));
};
// actual functionality
search["DeckContentQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto cardFilter = FilterString(std::any_cast<QString>(sv[0]));
auto numberMatcher = sv.size() > 1 ? std::any_cast<NumberMatcher>(sv[1]) : [](int count) { return count > 0; };
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) -> bool {
int count = 0;
deck->deckLoader->forEachCard([&](InnerDecklistNode *, const DecklistCardNode *node) {
auto cardInfoPtr = CardDatabaseManager::getInstance()->getCardInfo(node->getName());
if (!cardInfoPtr.isNull() && cardFilter.check(cardInfoPtr)) {
count += node->getNumber();
}
});
return numberMatcher(count);
};
};
search["CardSearch"] = [](const peg::SemanticValues &sv) -> QString { return std::any_cast<QString>(sv[0]); };
search["CardFilterString"] = [](const peg::SemanticValues &sv) -> QString {
return QString::fromStdString(std::string(sv.sv()));
};
search["DeckNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
return deck->deckLoader->getName().contains(name, Qt::CaseInsensitive);
};
};
search["FileNameQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
auto filename = QFileInfo(deck->filePath).fileName();
return filename.contains(name, Qt::CaseInsensitive);
};
};
search["PathQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) {
return info.relativeFilePath.contains(name, Qt::CaseInsensitive);
};
};
search["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
auto name = std::any_cast<QString>(sv[0]);
return [=](const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &) {
return deck->getDisplayName().contains(name, Qt::CaseInsensitive);
};
};
}
DeckFilterString::DeckFilterString()
{
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
_error = "Not initialized";
}
DeckFilterString::DeckFilterString(const QString &expr)
{
QByteArray ba = expr.simplified().toUtf8();
std::call_once(init, setupParserRules);
_error = QString();
if (ba.isEmpty()) {
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { 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(), filter)) {
qCInfo(DeckFilterStringLog).nospace() << "DeckFilterString error for " << expr << "; " << qPrintable(_error);
filter = [](const DeckPreviewWidget *, const ExtraDeckSearchInfo &) { return false; };
}
}

View file

@ -0,0 +1,51 @@
#ifndef DECK_FILTER_STRING_H
#define DECK_FILTER_STRING_H
#include "../client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.h"
#include <QLoggingCategory>
#include <QMap>
#include <QString>
#include <functional>
#include <utility>
inline Q_LOGGING_CATEGORY(DeckFilterStringLog, "deck_filter_string");
/**
* Extra info relevant to filtering that isn't present in the DeckPreviewWidget
*/
struct ExtraDeckSearchInfo
{
/**
* The relative filepath starting from the deck folder
*/
QString relativeFilePath;
};
typedef std::function<bool(const DeckPreviewWidget *, const ExtraDeckSearchInfo &)> DeckFilter;
class DeckFilterString
{
public:
DeckFilterString();
explicit DeckFilterString(const QString &expr);
bool check(const DeckPreviewWidget *deck, const ExtraDeckSearchInfo &info) const
{
return filter(deck, info);
}
bool valid() const
{
return _error.isEmpty();
}
QString error()
{
return _error;
}
private:
QString _error;
DeckFilter filter;
};
#endif // DECK_FILTER_STRING_H

View file

@ -0,0 +1,78 @@
#include "filter_builder.h"
#include "../deck/custom_line_edit.h"
#include "filter_card.h"
#include <QComboBox>
#include <QGridLayout>
#include <QPushButton>
FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
{
filterCombo = new QComboBox;
filterCombo->setObjectName("filterCombo");
for (int i = 0; i < CardFilter::AttrEnd; i++)
filterCombo->addItem(CardFilter::attrName(static_cast<CardFilter::Attr>(i)), QVariant(i));
typeCombo = new QComboBox;
typeCombo->setObjectName("typeCombo");
for (int i = 0; i < CardFilter::TypeEnd; i++)
typeCombo->addItem(CardFilter::typeName(static_cast<CardFilter::Type>(i)), QVariant(i));
QPushButton *ok = new QPushButton(QPixmap("theme:icons/increment"), QString());
ok->setObjectName("ok");
ok->setMaximumSize(20, 20);
edit = new LineEditUnfocusable;
edit->setObjectName("edit");
edit->setPlaceholderText(tr("Type your filter here"));
edit->setClearButtonEnabled(true);
edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
QGridLayout *layout = new QGridLayout;
layout->setObjectName("layout");
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(typeCombo, 0, 0, 1, 2);
layout->addWidget(filterCombo, 0, 2, 1, 2);
layout->addWidget(edit, 1, 0, 1, 3);
layout->addWidget(ok, 1, 3);
setLayout(layout);
connect(filterCombo, qOverload<int>(&QComboBox::activated), edit, [this](int) { setFocus(); });
connect(edit, &LineEditUnfocusable::returnPressed, this, &FilterBuilder::emit_add);
connect(ok, &QPushButton::released, this, &FilterBuilder::emit_add);
fltr = NULL;
}
FilterBuilder::~FilterBuilder()
{
destroyFilter();
}
void FilterBuilder::destroyFilter()
{
if (fltr)
delete fltr;
}
static int comboCurrentIntData(const QComboBox *combo)
{
return combo->itemData(combo->currentIndex()).toInt();
}
void FilterBuilder::emit_add()
{
QString txt;
txt = edit->text();
if (txt.length() < 1)
return;
destroyFilter();
fltr = new CardFilter(txt, static_cast<CardFilter::Type>(comboCurrentIntData(typeCombo)),
static_cast<CardFilter::Attr>(comboCurrentIntData(filterCombo)));
emit add(fltr);
edit->clear();
}

View file

@ -0,0 +1,37 @@
#ifndef FILTERBUILDER_H
#define FILTERBUILDER_H
#include <QWidget>
class QCheckBox;
class QComboBox;
class LineEditUnfocusable;
class CardFilter;
class FilterBuilder : public QWidget
{
Q_OBJECT
private:
QComboBox *typeCombo;
QComboBox *filterCombo;
LineEditUnfocusable *edit;
CardFilter *fltr;
void destroyFilter();
public:
explicit FilterBuilder(QWidget *parent = nullptr);
~FilterBuilder() override;
signals:
void add(const CardFilter *f);
public slots:
private slots:
void emit_add();
protected:
};
#endif

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,70 @@
#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,413 @@
#include "filter_string.h"
#include "../../../common/lib/peglib.h"
#include <QByteArray>
#include <QDebug>
#include <QRegularExpression>
#include <QString>
#include <functional>
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,56 @@
#ifndef FILTER_STRING_H
#define FILTER_STRING_H
#include "../card/card_info.h"
#include "filter_tree.h"
#include <QLoggingCategory>
#include <QMap>
#include <QString>
#include <functional>
#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,277 @@
#ifndef FILTERTREE_H
#define FILTERTREE_H
#include "../database/card_database.h"
#include "filter_card.h"
#include <QList>
#include <QMap>
#include <QObject>
#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

View file

@ -0,0 +1,329 @@
#include "filter_tree_model.h"
#include "filter_card.h"
#include "filter_tree.h"
#include <QFont>
FilterTreeModel::FilterTreeModel(QObject *parent) : QAbstractItemModel(parent)
{
fTree = new FilterTree;
connect(fTree, &FilterTree::preInsertRow, this, &FilterTreeModel::proxyBeginInsertRow);
connect(fTree, &FilterTree::postInsertRow, this, &FilterTreeModel::proxyEndInsertRow);
connect(fTree, &FilterTree::preRemoveRow, this, &FilterTreeModel::proxyBeginRemoveRow);
connect(fTree, &FilterTree::postRemoveRow, this, &FilterTreeModel::proxyEndRemoveRow);
}
FilterTreeModel::~FilterTreeModel()
{
delete fTree;
}
void FilterTreeModel::proxyBeginInsertRow(const FilterTreeNode *node, int i)
{
int idx;
idx = node->index();
if (idx >= 0)
beginInsertRows(createIndex(idx, 0, (void *)node), i, i);
}
void FilterTreeModel::proxyEndInsertRow(const FilterTreeNode *node, int)
{
int idx;
idx = node->index();
if (idx >= 0)
endInsertRows();
}
void FilterTreeModel::proxyBeginRemoveRow(const FilterTreeNode *node, int i)
{
int idx;
idx = node->index();
if (idx >= 0)
beginRemoveRows(createIndex(idx, 0, (void *)node), i, i);
}
void FilterTreeModel::proxyEndRemoveRow(const FilterTreeNode *node, int)
{
int idx;
idx = node->index();
if (idx >= 0)
endRemoveRows();
}
FilterTreeNode *FilterTreeModel::indexToNode(const QModelIndex &idx) const
{
void *ip;
FilterTreeNode *node;
if (!idx.isValid())
return fTree;
ip = idx.internalPointer();
if (ip == NULL)
return fTree;
node = static_cast<FilterTreeNode *>(ip);
return node;
}
void FilterTreeModel::addFilter(const CardFilter *f)
{
emit layoutAboutToBeChanged();
fTree->termNode(f);
emit layoutChanged();
}
void FilterTreeModel::removeFilter(const CardFilter *f)
{
emit layoutAboutToBeChanged();
fTree->removeFilter(f);
emit layoutChanged();
}
void FilterTreeModel::clearFiltersOfType(CardFilter::Attr filterType)
{
emit layoutAboutToBeChanged();
// Recursively remove all nodes with the given filter type
fTree->removeFiltersByAttr(filterType);
emit layoutChanged();
}
QList<const CardFilter *> FilterTreeModel::getFiltersOfType(CardFilter::Attr filterType) const
{
QList<const CardFilter *> filters;
if (!fTree) {
return filters;
}
std::function<void(const FilterTreeNode *)> traverse;
traverse = [&](const FilterTreeNode *node) {
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
if (filterItem->attr() == filterType) {
QString text = filterItem->text();
filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
}
}
for (int i = 0; i < node->childCount(); ++i) {
traverse(node->nodeAt(i));
}
};
traverse(fTree);
return filters;
}
QList<const CardFilter *> FilterTreeModel::allFilters() const
{
QList<const CardFilter *> filters;
if (!fTree) {
return filters;
}
std::function<void(const FilterTreeNode *)> traverse;
traverse = [&](const FilterTreeNode *node) {
if (const auto *filterItem = dynamic_cast<const FilterItem *>(node)) {
QString text = filterItem->text();
filters.append(new CardFilter(text, filterItem->type(), filterItem->attr()));
}
for (int i = 0; i < node->childCount(); ++i) {
traverse(node->nodeAt(i));
}
};
traverse(fTree);
return filters;
}
int FilterTreeModel::rowCount(const QModelIndex &parent) const
{
const FilterTreeNode *node;
int result;
if (parent.column() > 0)
return 0;
node = indexToNode(parent);
if (node)
result = node->childCount();
else
result = 0;
return result;
}
int FilterTreeModel::columnCount(const QModelIndex & /*parent*/) const
{
return 1;
}
QVariant FilterTreeModel::data(const QModelIndex &index, int role) const
{
const FilterTreeNode *node;
if (!index.isValid())
return QVariant();
if (index.column() >= columnCount())
return QVariant();
node = indexToNode(index);
if (node == NULL)
return QVariant();
switch (role) {
case Qt::FontRole:
if (!node->isLeaf()) {
QFont f;
f.setBold(true);
return f;
}
break;
case Qt::DisplayRole:
case Qt::EditRole:
case Qt::ToolTipRole:
case Qt::StatusTipRole:
case Qt::WhatsThisRole:
return node->text();
case Qt::CheckStateRole:
if (node->isEnabled())
return Qt::Checked;
else
return Qt::Unchecked;
default:
return QVariant();
}
return QVariant();
}
bool FilterTreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
FilterTreeNode *node;
if (!index.isValid())
return false;
if (index.column() >= columnCount())
return false;
if (role != Qt::CheckStateRole)
return false;
node = indexToNode(index);
if (node == NULL || node == fTree)
return false;
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
if (state == Qt::Checked)
node->enable();
else
node->disable();
emit dataChanged(index, index);
return true;
}
Qt::ItemFlags FilterTreeModel::flags(const QModelIndex &index) const
{
const FilterTreeNode *node;
Qt::ItemFlags result;
if (!index.isValid())
return Qt::NoItemFlags;
node = indexToNode(index);
if (node == NULL)
return Qt::NoItemFlags;
result = Qt::ItemIsEnabled;
if (node == fTree)
return result;
result |= Qt::ItemIsSelectable;
result |= Qt::ItemIsUserCheckable;
return result;
}
QModelIndex FilterTreeModel::nodeIndex(const FilterTreeNode *node, int row, int column) const
{
FilterTreeNode *child;
if (column > 0 || row >= node->childCount())
return QModelIndex();
child = node->nodeAt(row);
return createIndex(row, column, child);
}
QModelIndex FilterTreeModel::index(int row, int column, const QModelIndex &parent) const
{
const FilterTreeNode *node;
if (!hasIndex(row, column, parent))
return QModelIndex();
node = indexToNode(parent);
if (node == NULL)
return QModelIndex();
return nodeIndex(node, row, column);
}
QModelIndex FilterTreeModel::parent(const QModelIndex &ind) const
{
const FilterTreeNode *node;
FilterTreeNode *parent;
QModelIndex idx;
if (!ind.isValid())
return QModelIndex();
node = indexToNode(ind);
if (node == NULL || node == fTree)
return QModelIndex();
parent = node->parent();
if (parent) {
int row = parent->index();
if (row < 0)
return QModelIndex();
idx = createIndex(row, 0, parent);
return idx;
}
return QModelIndex();
}
bool FilterTreeModel::removeRows(int row, int count, const QModelIndex &parent)
{
FilterTreeNode *node;
int i, last;
last = row + count - 1;
if (!parent.isValid() || count < 1 || row < 0)
return false;
node = indexToNode(parent);
if (node == NULL || last >= node->childCount())
return false;
for (i = 0; i < count; i++)
node->deleteAt(row);
if (node != fTree && node->childCount() < 1)
return removeRow(parent.row(), parent.parent());
return true;
}
void FilterTreeModel::clear()
{
emit layoutAboutToBeChanged();
fTree->clear();
emit layoutChanged();
}

View file

@ -0,0 +1,53 @@
#ifndef FILTERTREEMODEL_H
#define FILTERTREEMODEL_H
#include "filter_card.h"
#include <QAbstractItemModel>
class FilterTree;
class CardFilter;
class FilterTreeNode;
class FilterTreeModel : public QAbstractItemModel
{
Q_OBJECT
private:
FilterTree *fTree;
public slots:
void addFilter(const CardFilter *f);
void removeFilter(const CardFilter *f);
void clearFiltersOfType(CardFilter::Attr filterType);
QList<const CardFilter *> getFiltersOfType(CardFilter::Attr filterType) const;
QList<const CardFilter *> allFilters() const;
private slots:
void proxyBeginInsertRow(const FilterTreeNode *, int);
void proxyEndInsertRow(const FilterTreeNode *, int);
void proxyBeginRemoveRow(const FilterTreeNode *, int);
void proxyEndRemoveRow(const FilterTreeNode *, int);
private:
FilterTreeNode *indexToNode(const QModelIndex &idx) const;
QModelIndex nodeIndex(const FilterTreeNode *node, int row, int column) const;
public:
FilterTreeModel(QObject *parent = nullptr);
~FilterTreeModel() override;
FilterTree *filterTree() const
{
return fTree;
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QModelIndex parent(const QModelIndex &ind) const override;
QModelIndex index(int row, int column, const QModelIndex &parent) const override;
bool removeRows(int row, int count, const QModelIndex &parent) override;
void clear();
};
#endif

View file

@ -0,0 +1,83 @@
#include "syntax_help.h"
#include <QFile>
#include <QRegularExpression>
#include <QTextStream>
/**
* Creates the card search syntax help window
*
* @return the QTextBrowser
*/
static QTextBrowser *createBrowser(const QString &helpFile)
{
QFile file(helpFile);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
qCWarning(SyntaxHelpLog) << "Could not open syntax help file: " << helpFile;
return nullptr;
}
QTextStream in(&file);
QString text = in.readAll();
file.close();
// Poor Markdown Converter
auto opts = QRegularExpression::MultilineOption;
text = text.replace(QRegularExpression("^(###)(.*)", opts), "<h3>\\2</h3>")
.replace(QRegularExpression("^(##)(.*)", opts), "<h2>\\2</h2>")
.replace(QRegularExpression("^(#)(.*)", opts), "<h1>\\2</h1>")
.replace(QRegularExpression("^------*", opts), "<hr />")
.replace(QRegularExpression(R"(\[([^[]+)\]\(([^\)]+)\))", opts), R"(<a href='\2'>\1</a>)");
auto browser = new QTextBrowser();
browser->setParent(nullptr, Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint |
Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint |
Qt::WindowFullscreenButtonHint);
browser->setWindowTitle("Search Help");
browser->setReadOnly(true);
browser->setMinimumSize({500, 600});
QString sheet = QString("a { text-decoration: underline; color: rgb(71,158,252) };");
browser->document()->setDefaultStyleSheet(sheet);
browser->setHtml(text);
browser->show();
return browser;
}
/**
* Creates the card search syntax help window and connects its anchorClicked signal to the given QLineEdit.
* The window will automatically close when the QLineEdit is destroyed.
*
* @return the QTextBrowser
*/
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit)
{
auto browser = createBrowser("theme:help/search.md");
QObject::connect(browser, &QTextBrowser::anchorClicked,
[lineEdit](const QUrl &link) { lineEdit->setText(link.fragment()); });
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
return browser;
}
/**
* Creates the deck search syntax help window and connects its anchorClicked signal to the given QLineEdit.
* The window will automatically close when the QLineEdit is destroyed.
*
* @return the QTextBrowser
*/
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit)
{
auto browser = createBrowser("theme:help/deck_search.md");
QObject::connect(browser, &QTextBrowser::anchorClicked, [lineEdit](const QUrl &link) {
if (link.fragment() == "cardSearchSyntaxHelp") {
createSearchSyntaxHelpWindow(lineEdit);
} else {
lineEdit->setText(link.fragment());
}
});
QObject::connect(lineEdit, &QObject::destroyed, browser, &QTextBrowser::close);
return browser;
}

View file

@ -0,0 +1,14 @@
#ifndef SEARCH_SYNTAX_HELP_H
#define SEARCH_SYNTAX_HELP_H
#include <QLineEdit>
#include <QLoggingCategory>
#include <QTextBrowser>
inline Q_LOGGING_CATEGORY(SyntaxHelpLog, "syntax_help");
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit);
QTextBrowser *createDeckSearchSyntaxHelpWindow(QLineEdit *lineEdit);
#endif // SEARCH_SYNTAX_HELP_H