From eb60fec8e2c0b081c5347a6ad1ad6d373886b5e3 Mon Sep 17 00:00:00 2001 From: Rob Blanckaert Date: Fri, 1 Mar 2019 11:30:32 -0800 Subject: [PATCH] Filter Strings for Deck Editor search (#3582) * Add MagicCards.info like fitler parser. * Use FilterString whenever one of [:=<>] is in the edit box. * Opts * Opt * - Capture errors - Allow querying any property by full name * clang format * Update cockatrice/src/filter_string.cpp Co-Authored-By: basicer * - Some refactoring for clarity - More filters - Add filter help * Clangify * Add icon * Fix test name * Remove stay debug * - Add Rarity filter - Make " trigger filter string mode * You have to pass both filter types * clangify * - Allow filtering by legality - Import legality into card.xml * Add format filter to filtertree * More color search options * RIP extended * More fixes * Fix c:m * set syntax help parent * Fix warning * add additional explanations to syntax help * Allow multiple ands/ors to be chained * Cleanup and refactor Signed-off-by: Zach Halpern * Move utility into guards Signed-off-by: Zach Halpern * I heard you like refactors so I put a refactor inside your refactor (#3594) * I heard you like refactors so I put a refactor inside your refactor so you can refactor while you refactor * clangify * Update tab_deck_editor.h --- cockatrice/CMakeLists.txt | 1 + cockatrice/cockatrice.qrc | 2 + cockatrice/resources/help/search.md | 60 +++ cockatrice/resources/icons/info.svg | 43 +++ cockatrice/src/carddatabase.h | 5 +- cockatrice/src/carddatabasemodel.cpp | 13 +- cockatrice/src/carddatabasemodel.h | 49 ++- cockatrice/src/cardfilter.cpp | 2 + cockatrice/src/cardfilter.h | 6 +- cockatrice/src/cardinfotext.cpp | 14 +- cockatrice/src/cardinfotext.h | 2 +- cockatrice/src/filter_string.cpp | 352 ++++++++++++++++++ cockatrice/src/filter_string.h | 48 +++ cockatrice/src/filtertree.cpp | 22 +- cockatrice/src/filtertree.h | 6 +- cockatrice/src/game_specific_terms.h | 3 + cockatrice/src/tab_deck_editor.cpp | 38 +- cockatrice/src/tab_deck_editor.h | 19 +- oracle/src/oracleimporter.cpp | 18 +- tests/carddatabase/CMakeLists.txt | 23 +- tests/carddatabase/carddatabase_test.cpp | 53 +-- tests/carddatabase/filter_string_test.cpp | 66 ++++ tests/carddatabase/mocks.cpp | 54 +++ .../{carddatabase_test.h => mocks.h} | 3 + 24 files changed, 780 insertions(+), 122 deletions(-) create mode 100644 cockatrice/resources/help/search.md create mode 100644 cockatrice/resources/icons/info.svg create mode 100644 cockatrice/src/filter_string.cpp create mode 100644 cockatrice/src/filter_string.h create mode 100644 tests/carddatabase/filter_string_test.cpp create mode 100644 tests/carddatabase/mocks.cpp rename tests/carddatabase/{carddatabase_test.h => mocks.h} (96%) diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 8b4bd921..fb24c46a 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -123,6 +123,7 @@ SET(cockatrice_SOURCES src/carddbparser/carddatabaseparser.cpp src/carddbparser/cockatricexml3.cpp src/carddbparser/cockatricexml4.cpp + src/filter_string.cpp ${VERSION_STRING_CPP} ) diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index a5cded2c..8fd40bb4 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -18,6 +18,7 @@ resources/icons/delete.svg resources/icons/forgot_password.svg resources/icons/increment.svg + resources/icons/info.svg resources/icons/lock.svg resources/icons/not_ready_start.svg resources/icons/pencil.svg @@ -360,5 +361,6 @@ resources/tips/images/themes.png resources/tips/images/tip_of_the_day.png + resources/help/search.md diff --git a/cockatrice/resources/help/search.md b/cockatrice/resources/help/search.md new file mode 100644 index 00000000..bafad442 --- /dev/null +++ b/cockatrice/resources/help/search.md @@ -0,0 +1,60 @@ +## Syntax Help +----- +The search bar recognizes a set of special commands similar to some other card databases. Here is a list with examples. Each entry can be clicked to test the query and has a small explanation. Note that all searches are case insensitive. +
+
Name:
+
[birds of paradise](#birds of paradise) (Any card name containing the words birds, of, and paradise)
+
["birds of paradise"](#%22birds of paradise%22) (Any card name containing the exact phrase "birds of paradise")
+ +
Rules Text (Oracle):
+
[o:flying](#o:flying) (Any card text that has the word flying)
+
[o:"first strike"](#o:%22first strike%22) (Any card text that has the exact phrase "first strike")
+
[o:"{T}" o:"add one mana of any color"](#o:%22{T}%22 o:%22add one mana of any color%22) (Any card text that has a tap symbol and the phrase "add one mana of any color")
+ +
Types:
+
[t:angel](#t:angel) (Any card with the type angel)
+
[t:angel t:legendary](#t:angel t:legendary) (Any angel that's also legendary)
+
[t:basic](#t:basic) (Any card with the type basic)
+
[t:arcane t:instant](#t:arcane t:instant) (Any card with the types arcane and instant)
+ +
Colors:
+
[c:w](#c:w) (Any card that is white)
+
[c:wu](#c:wu) (Any card that is white or blue)
+
[c:wum](#c:wum) (Any card that is white or blue, and multicolored)
+ +
[c:c](#c:c) (Any colorless card)
+ +
Power, Toughness, Converted Mana Cost:
+
[tou:1](#tou:1) (Any card with a toughness of 1)
+
[pow>=8](#pow>=8) (Any card with a power greater than or equal to 8)
+
[cmc=7](#cmc=7) (Any card with a converted mana cost equal to 7)
+ +
Rarity:
+
[r:mythic](#r:mythic) (Any card that has the mythic-rare rarity)
+ +
Format:
+
[f:standard](#f:standard) (Any card that can be played in standard)
+
[banned:modern](#banned:modern) (Any card that is banned in modern)
+
[restricted:vintage](#restricted:vintage) (Any card that is restricted in vintage)
+
[legal:pauper](#legal:pauper) (Any card that is legal in pauper)
+ +
Edition:
+
[set:lea](#set:lea) (Cards that appear in Alpha, which has the set code LEA)
+
[e:lea,leb](#e:lea,leb) (Cards that appear in Alpha or Beta)
+
e:lea,leb -(e:lea e:leb) (Cards that appear in Alpha or Beta but not in both editions)
+ +
Inverse:
+
[c:wu -c:m](#c:wu -c:m) (Any card that is white or blue, but not multicolored)
+ +
Branching:
+
[t:sliver or o:changeling](#t:sliver or o:changeling) (Any card that is either a sliver or has changeling)
+ +
Grouping:
+
t:angel -(angel or c:w) (Any angel that doesn't have angel in its name and isn't white)
+ +
diff --git a/cockatrice/resources/icons/info.svg b/cockatrice/resources/icons/info.svg new file mode 100644 index 00000000..00b23cc2 --- /dev/null +++ b/cockatrice/resources/icons/info.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/cockatrice/src/carddatabase.h b/cockatrice/src/carddatabase.h index c5cf0ea2..9d137179 100644 --- a/cockatrice/src/carddatabase.h +++ b/cockatrice/src/carddatabase.h @@ -21,7 +21,6 @@ class CardRelation; class ICardDatabaseParser; typedef QMap QStringMap; -typedef QMap MuidMap; typedef QSharedPointer CardInfoPtr; typedef QSharedPointer CardSetPtr; typedef QMap CardInfoPerSetMap; @@ -248,6 +247,10 @@ public: properties.insert(_name, _value); emit cardInfoChanged(smartThis); } + bool hasProperty(const QString &propertyName) const + { + return properties.contains(propertyName); + } const CardInfoPerSetMap &getSets() const { return sets; diff --git a/cockatrice/src/carddatabasemodel.cpp b/cockatrice/src/carddatabasemodel.cpp index 9ae85578..6a4b4055 100644 --- a/cockatrice/src/carddatabasemodel.cpp +++ b/cockatrice/src/carddatabasemodel.cpp @@ -143,12 +143,16 @@ void CardDatabaseModel::cardRemoved(CardInfoPtr card) endRemoveRows(); } -CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent) : QSortFilterProxyModel(parent), isToken(ShowAll) +CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent) + : QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr) { filterTree = nullptr; setFilterCaseSensitivity(Qt::CaseInsensitive); setSortCaseSensitivity(Qt::CaseInsensitive); + dirtyTimer.setSingleShot(true); + connect(&dirtyTimer, &QTimer::timeout, this, &CardDatabaseDisplayModel::invalidate); + loadedRowCount = 0; } @@ -285,6 +289,13 @@ bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex if (((isToken == ShowTrue) && !info->getIsToken()) || ((isToken == ShowFalse) && info->getIsToken())) return false; + if (filterString != nullptr) { + if (filterTree != nullptr && !filterTree->acceptsCard(info)) { + return false; + } + return filterString->check(info); + } + return rowMatchesCardName(info); } diff --git a/cockatrice/src/carddatabasemodel.h b/cockatrice/src/carddatabasemodel.h index 8e8cb67c..a2f90118 100644 --- a/cockatrice/src/carddatabasemodel.h +++ b/cockatrice/src/carddatabasemodel.h @@ -2,10 +2,12 @@ #define CARDDATABASEMODEL_H #include "carddatabase.h" +#include "filter_string.h" #include #include #include #include +#include class FilterTree; @@ -67,11 +69,12 @@ public: private: FilterBool isToken; - QString cardNameBeginning, cardName, cardText; - QString searchTerm; + QString cardName, cardText; QSet cardNameSet, cardTypes, cardColors; FilterTree *filterTree; + FilterString *filterString; int loadedRowCount; + QTimer dirtyTimer; /** The translation table that will be used for sanitizeCardName. */ static QMap characterTranslation; @@ -82,41 +85,33 @@ public: void setIsToken(FilterBool _isToken) { isToken = _isToken; - invalidate(); - } - void setCardNameBeginning(const QString &_beginning) - { - cardNameBeginning = _beginning; - invalidate(); + dirty(); } + void setCardName(const QString &_cardName) { + if (filterString != nullptr) { + delete filterString; + filterString = nullptr; + } cardName = sanitizeCardName(_cardName, characterTranslation); - invalidate(); + dirty(); + } + void setStringFilter(const QString &_src) + { + delete filterString; + filterString = new FilterString(_src); + dirty(); } void setCardNameSet(const QSet &_cardNameSet) { cardNameSet = _cardNameSet; - invalidate(); + dirty(); } - void setSearchTerm(const QString &_searchTerm) + + void dirty() { - searchTerm = _searchTerm; - } - void setCardText(const QString &_cardText) - { - cardText = _cardText; - invalidate(); - } - void setCardTypes(const QSet &_cardTypes) - { - cardTypes = _cardTypes; - invalidate(); - } - void setCardColors(const QSet &_cardColors) - { - cardColors = _cardColors; - invalidate(); + dirtyTimer.start(20); } void clearFilterAll(); int rowCount(const QModelIndex &parent = QModelIndex()) const override; diff --git a/cockatrice/src/cardfilter.cpp b/cockatrice/src/cardfilter.cpp index ce72d794..05fee44f 100644 --- a/cockatrice/src/cardfilter.cpp +++ b/cockatrice/src/cardfilter.cpp @@ -41,6 +41,8 @@ const QString CardFilter::attrName(Attr a) return tr("Toughness"); case AttrLoyalty: return tr("Loyalty"); + case AttrFormat: + return tr("Format"); default: return QString(""); } diff --git a/cockatrice/src/cardfilter.h b/cockatrice/src/cardfilter.h index 514ebd3e..7412d0f8 100644 --- a/cockatrice/src/cardfilter.h +++ b/cockatrice/src/cardfilter.h @@ -3,6 +3,7 @@ #include #include +#include class CardFilter : public QObject { @@ -18,7 +19,7 @@ public: TypeEnd }; - /* if you add an atribute here you also need to + /* if you add an attribute here you also need to * add its string representation in attrName */ enum Attr { @@ -33,6 +34,7 @@ public: AttrText, AttrTough, AttrType, + AttrFormat, AttrEnd }; @@ -42,7 +44,7 @@ private: enum Attr a; public: - CardFilter(QString term, Type type, Attr attr) : trm(term), t(type), a(attr){}; + CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr){}; Type type() const { diff --git a/cockatrice/src/cardinfotext.cpp b/cockatrice/src/cardinfotext.cpp index 448b276b..a28e13c9 100644 --- a/cockatrice/src/cardinfotext.cpp +++ b/cockatrice/src/cardinfotext.cpp @@ -16,7 +16,7 @@ CardInfoText::CardInfoText(QWidget *parent) : QFrame(parent), info(nullptr) textLabel = new QTextEdit(); textLabel->setReadOnly(true); - QGridLayout *grid = new QGridLayout(this); + auto *grid = new QGridLayout(this); grid->addWidget(nameLabel, 0, 0); grid->addWidget(textLabel, 1, 0, -1, 2); grid->setRowStretch(1, 1); @@ -39,6 +39,8 @@ void CardInfoText::setCard(CardInfoPtr card) QStringList cardProps = card->getProperties(); foreach (QString key, cardProps) { + if (key.contains("-")) + continue; QString keyText = Mtg::getNicePropertyName(key).toHtmlEscaped() + ":"; text += QString("%1%2").arg(keyText, card->getProperty(key).toHtmlEscaped()); @@ -46,16 +48,16 @@ void CardInfoText::setCard(CardInfoPtr card) auto relatedCards = card->getRelatedCards(); auto reverserelatedCards2Me = card->getReverseRelatedCards2Me(); - if (relatedCards.size() || reverserelatedCards2Me.size()) { + if (!relatedCards.empty() || !reverserelatedCards2Me.empty()) { text += QString("%1").arg(tr("Related cards:")); - for (int i = 0; i < relatedCards.size(); ++i) { - QString tmp = relatedCards.at(i)->getName().toHtmlEscaped(); + for (auto *relatedCard : relatedCards) { + QString tmp = relatedCard->getName().toHtmlEscaped(); text += "" + tmp + "
"; } - for (int i = 0; i < reverserelatedCards2Me.size(); ++i) { - QString tmp = reverserelatedCards2Me.at(i)->getName().toHtmlEscaped(); + for (auto *i : reverserelatedCards2Me) { + QString tmp = i->getName().toHtmlEscaped(); text += "" + tmp + "
"; } diff --git a/cockatrice/src/cardinfotext.h b/cockatrice/src/cardinfotext.h index 7ee0a890..882e542a 100644 --- a/cockatrice/src/cardinfotext.h +++ b/cockatrice/src/cardinfotext.h @@ -17,7 +17,7 @@ private: CardInfoPtr info; public: - CardInfoText(QWidget *parent = 0); + explicit CardInfoText(QWidget *parent = nullptr); void retranslateUi(); void setInvalidCardName(const QString &cardName); diff --git a/cockatrice/src/filter_string.cpp b/cockatrice/src/filter_string.cpp new file mode 100644 index 00000000..b514789b --- /dev/null +++ b/cockatrice/src/filter_string.cpp @@ -0,0 +1,352 @@ +#include "filter_string.h" +#include "../../common/lib/peglib.h" + +#include +#include +#include +#include + +peg::parser search(R"( +Start <- QueryPartList +~ws <- [ ]+ +QueryPartList <- ComplexQueryPart ( ws ("and" ws)? ComplexQueryPart)* ws* + +ComplexQueryPart <- SomewhatComplexQueryPart ws $or<[oO][rR]> 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' [:] RegexString + + +CMCQuery <- 'cmc' ws? NumericExpression +PowerQuery <- [Pp] 'ow' 'er'? ws? NumericExpression +ToughnessQuery <- [Tt] 'ou' 'ghness'? ws? NumericExpression +RarityQuery <- [rR] ':' RegexString + +FormatQuery <- 'f' ':' Format / Legality ':' Format +Format <- [Mm] 'odern'? / [Ss] 'tandard'? / [Vv] 'intage'? / [Ll] 'egacy'? / [Cc] 'ommander'? +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 [:] RegexString / String ws? NumericExpression + +NonQuote <- !["]. +UnescapedStringListPart <- [a-zA-Z0-9']+ +String <- UnescapedStringListPart / ["] ["] +StringValue <- String / [(] StringList [)] +StringList <- StringListString (ws? [,] ws? StringListString)* +StringListString <- UnescapedStringListPart +GenericQuery <- RegexString +RegexString <- String + +FlexStringValue <- CompactStringSet / String / [(] StringList [)] +CompactStringSet <- StringListString ([,+] StringListString)+ + +NumericExpression <- NumericOperator ws? NumericValue +NumericOperator <- [=:] / <[> +NumericValue <- [0-9]+ + +)"); + +std::once_flag init; + +static void setupParserRules() +{ + auto passthru = [](const peg::SemanticValues &sv) -> Filter { return !sv.empty() ? sv[0].get() : nullptr; }; + + search["Start"] = passthru; + search["QueryPartList"] = [](const peg::SemanticValues &sv) -> Filter { + return [=](CardData x) { + for (int i = 0; i < sv.size(); ++i) { + if (!sv[i].get()(x)) + return false; + } + return true; + }; + }; + search["ComplexQueryPart"] = [](const peg::SemanticValues &sv) -> Filter { + return [=](CardData x) { + for (int i = 0; i < sv.size(); ++i) { + if (sv[i].get()(x)) + return true; + } + return false; + }; + }; + search["SomewhatComplexQueryPart"] = passthru; + search["QueryPart"] = passthru; + search["NotQuery"] = [](const peg::SemanticValues &sv) -> Filter { + Filter dependent = sv[0].get(); + return [=](CardData x) -> bool { return !dependent(x); }; + }; + search["TypeQuery"] = [](const peg::SemanticValues &sv) -> Filter { + StringMatcher matcher = sv[0].get(); + return [=](CardData x) -> bool { return matcher(x->getCardType()); }; + }; + search["SetQuery"] = [](const peg::SemanticValues &sv) -> Filter { + StringMatcher matcher = sv[0].get(); + return [=](CardData x) -> bool { + for (const auto &set : x->getSets().keys()) { + if (matcher(set)) + return true; + } + return false; + }; + }; + search["RarityQuery"] = [](const peg::SemanticValues &sv) -> Filter { + StringMatcher matcher = sv[0].get(); + return [=](CardData x) -> bool { + for (const auto &set : x->getSets().values()) { + if (matcher(set.getProperty("rarity"))) + return true; + } + return false; + }; + }; + search["FormatQuery"] = [](const peg::SemanticValues &sv) -> Filter { + if (sv.choice() == 0) { + QString format = sv[0].get(); + return [=](CardData x) -> bool { return x->getProperty(QString("format-%1").arg(format)) == "legal"; }; + } else { + QString format = sv[1].get(); + QString legality = sv[0].get(); + return [=](CardData x) -> bool { return x->getProperty(QString("format-%1").arg(format)) == legality; }; + } + }; + search["Legality"] = [](const peg::SemanticValues &sv) -> QString { + switch (tolower(sv.str()[0])) { + case 'l': + return "legal"; + case 'b': + return "banned"; + case 'r': + return "restricted"; + default: + return ""; + } + }; + + search["Format"] = [](const peg::SemanticValues &sv) -> QString { + switch (tolower(sv.str()[0])) { + case 'm': + return "modern"; + case 's': + return "standard"; + case 'v': + return "vintage"; + case 'l': + return "legacy"; + case 'c': + return "commander"; + default: + return ""; + } + }; + search["StringValue"] = [](const peg::SemanticValues &sv) -> StringMatcher { + if (sv.choice() == 0) { + auto target = sv[0].get(); + return [=](const QString &s) { return s.split(" ").contains(target, Qt::CaseInsensitive); }; + } else { + auto target = sv[0].get(); + return [=](const QString &s) { + for (const QString &str : target) { + if (s.split(" ").contains(str, Qt::CaseInsensitive)) { + return true; + } + } + return false; + }; + } + }; + + search["String"] = [](const peg::SemanticValues &sv) -> QString { + if (sv.choice() == 0) { + return QString::fromStdString(sv.str()); + } else { + return QString::fromStdString(sv.token(0)); + } + }; + search["FlexStringValue"] = [](const peg::SemanticValues &sv) -> StringMatcher { + if (sv.choice() != 1) { + auto target = sv[0].get(); + return [=](const QString &s) { + for (const QString &str : target) { + if (s.split(" ").contains(str, Qt::CaseInsensitive)) { + return true; + } + } + return false; + }; + } else { + auto target = sv[0].get(); + return [=](const QString &s) { return s.split(" ").contains(target, Qt::CaseInsensitive); }; + } + }; + search["CompactStringSet"] = search["StringList"] = [](const peg::SemanticValues &sv) -> QStringList { + QStringList result; + for (int i = 0; i < sv.size(); ++i) { + result.append(sv[i].get()); + } + return result; + }; + search["StringListString"] = [](const peg::SemanticValues &sv) -> QString { + return QString::fromStdString(sv.str()); + }; + + search["NumericExpression"] = [](const peg::SemanticValues &sv) -> NumberMatcher { + auto arg = sv[1].get(); + auto op = sv[0].get(); + + if (op == ">") + return [=](int s) { return s > arg; }; + if (op == ">=") + return [=](int s) { return s >= arg; }; + if (op == "<") + return [=](int s) { return s < arg; }; + if (op == "<=") + return [=](int s) { return s <= arg; }; + if (op == "=") + return [=](int s) { return s == arg; }; + if (op == ":") + return [=](int s) { return s == arg; }; + if (op == "!=") + return [=](int s) { return s != arg; }; + return [](int) { return false; }; + }; + + search["NumericValue"] = [](const peg::SemanticValues &sv) -> int { + return QString::fromStdString(sv.str()).toInt(); + }; + + search["NumericOperator"] = [](const peg::SemanticValues &sv) -> QString { + return QString::fromStdString(sv.str()); + }; + + search["RegexString"] = [](const peg::SemanticValues &sv) -> StringMatcher { + auto target = sv[0].get(); + return [=](const QString &s) { return s.QString::contains(target, Qt::CaseInsensitive); }; + }; + + search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter { + StringMatcher matcher = sv[0].get(); + return [=](CardData x) { return matcher(x->getText()); }; + }; + + search["ColorQuery"] = [](const peg::SemanticValues &sv) -> Filter { + QString parts; + for (int i = 0; i < sv.size(); ++i) { + parts += sv[i].get(); + } + bool idenity = sv.tokens[0].first[0] != 'i'; + if (sv.tokens[1].first[0] == ':') { + return [=](CardData x) { + QString match = idenity ? x->getColors() : x->getProperty("coloridentity"); + if (parts.contains("m") && match.length() < 2) { + return false; + } else if (parts == "m") { + return true; + } + + if (parts.contains("c") && match.length() == 0) + return true; + + for (const auto &i : match) { + if (parts.contains(i)) + return true; + } + return false; + }; + } else { + return [=](CardData x) { + QString match = idenity ? 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; + } + + for (const auto &i : match) { + if (!parts.contains(i)) + return false; + } + return true; + }; + } + }; + + search["CMCQuery"] = [](const peg::SemanticValues &sv) -> Filter { + NumberMatcher matcher = sv[0].get(); + return [=](CardData x) -> bool { return matcher(x->getProperty("cmc").toInt()); }; + }; + search["PowerQuery"] = [](const peg::SemanticValues &sv) -> Filter { + NumberMatcher matcher = sv[0].get(); + return [=](CardData x) -> bool { return matcher(x->getPowTough().split("/")[0].toInt()); }; + }; + search["ToughnessQuery"] = [](const peg::SemanticValues &sv) -> Filter { + NumberMatcher matcher = sv[0].get(); + return [=](CardData x) -> bool { + auto parts = x->getPowTough().split("/"); + return matcher(parts.length() == 2 ? parts[1].toInt() : 0); + }; + }; + search["FieldQuery"] = [](const peg::SemanticValues &sv) -> Filter { + QString field = sv[0].get(); + if (sv.choice() == 0) { + StringMatcher matcher = sv[1].get(); + return [=](CardData x) -> bool { return x->hasProperty(field) ? matcher(x->getProperty(field)) : false; }; + } else { + NumberMatcher matcher = sv[1].get(); + return [=](CardData x) -> bool { + return x->hasProperty(field) ? matcher(x->getProperty(field).toInt()) : false; + }; + } + }; + search["GenericQuery"] = [](const peg::SemanticValues &sv) -> Filter { + StringMatcher matcher = sv[0].get(); + return [=](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 ? sv[0].get() : *sv.c_str(); + }; +} + +FilterString::FilterString(const QString &expr) +{ + QByteArray ba = expr.toLocal8Bit(); + + std::call_once(init, setupParserRules); + + _error = QString(); + + if (ba.isEmpty()) { + result = [](CardData) -> bool { return true; }; + return; + } + + search.log = [&](size_t ln, size_t col, const std::string &msg) { + _error = QString("%1:%2: %3").arg(ln).arg(col).arg(QString::fromStdString(msg)); + }; + + if (!search.parse(ba.data(), result)) { + std::cout << "Error!" << _error.toStdString() << std::endl; + result = [](CardData) -> bool { return false; }; + } +} diff --git a/cockatrice/src/filter_string.h b/cockatrice/src/filter_string.h new file mode 100644 index 00000000..690ff2d7 --- /dev/null +++ b/cockatrice/src/filter_string.h @@ -0,0 +1,48 @@ +#ifndef FILTER_STRING_H +#define FILTER_STRING_H + +#include "carddatabase.h" +#include "filtertree.h" + +#include +#include +#include +#include + +typedef CardInfoPtr CardData; +typedef std::function Filter; +typedef std::function StringMatcher; +typedef std::function NumberMatcher; + +namespace peg +{ +template struct AstBase; +struct EmptyType; +typedef AstBase Ast; +} // namespace peg + +class FilterString +{ +public: + explicit FilterString(const QString &exp); + bool check(const CardData &card) + { + return result(card); + } + + bool valid() + { + return _error.isEmpty(); + } + + QString error() + { + return _error; + } + +private: + QString _error; + Filter result; +}; + +#endif diff --git a/cockatrice/src/filtertree.cpp b/cockatrice/src/filtertree.cpp index 1dcb37cf..7bb76b2b 100644 --- a/cockatrice/src/filtertree.cpp +++ b/cockatrice/src/filtertree.cpp @@ -253,6 +253,11 @@ bool FilterItem::acceptCmc(const CardInfoPtr info) const } } +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()) { @@ -400,6 +405,8 @@ bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) c return acceptPowerToughness(info, attr); case CardFilter::AttrLoyalty: return acceptLoyalty(info); + case CardFilter::AttrFormat: + return acceptFormat(info); default: return true; /* ignore this attribute */ } @@ -439,16 +446,6 @@ FilterItemList *FilterTree::attrTypeList(CardFilter::Attr attr, CardFilter::Type return attrLogicMap(attr)->typeList(type); } -int FilterTree::findTermIndex(CardFilter::Attr attr, CardFilter::Type type, const QString &term) -{ - return attrTypeList(attr, type)->termIndex(term); -} - -int FilterTree::findTermIndex(const CardFilter *f) -{ - return findTermIndex(f->attr(), f->type(), f->term()); -} - FilterTreeNode *FilterTree::termNode(CardFilter::Attr attr, CardFilter::Type type, const QString &term) { return attrTypeList(attr, type)->termNode(term); @@ -459,11 +456,6 @@ FilterTreeNode *FilterTree::termNode(const CardFilter *f) return termNode(f->attr(), f->type(), f->term()); } -FilterTreeNode *FilterTree::attrTypeNode(CardFilter::Attr attr, CardFilter::Type type) -{ - return attrTypeList(attr, type); -} - bool FilterTree::testAttr(const CardInfoPtr info, const LogicMap *lm) const { const FilterItemList *fil; diff --git a/cockatrice/src/filtertree.h b/cockatrice/src/filtertree.h index 7227f944..a837c221 100644 --- a/cockatrice/src/filtertree.h +++ b/cockatrice/src/filtertree.h @@ -208,6 +208,7 @@ public: 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; }; @@ -252,11 +253,10 @@ private: public: FilterTree(); ~FilterTree() override; - int findTermIndex(CardFilter::Attr attr, CardFilter::Type type, const QString &term); - int findTermIndex(const CardFilter *f); + FilterTreeNode *termNode(CardFilter::Attr attr, CardFilter::Type type, const QString &term); FilterTreeNode *termNode(const CardFilter *f); - FilterTreeNode *attrTypeNode(CardFilter::Attr attr, CardFilter::Type type); + const QString text() const override { return QString("root"); diff --git a/cockatrice/src/game_specific_terms.h b/cockatrice/src/game_specific_terms.h index 9e3d0386..9bae8fd7 100644 --- a/cockatrice/src/game_specific_terms.h +++ b/cockatrice/src/game_specific_terms.h @@ -21,6 +21,7 @@ QString const ManaCost("manacost"); QString const PowTough("pt"); QString const Side("side"); QString const Layout("layout"); +QString const ColorIdentity("coloridentity"); inline static const QString getNicePropertyName(QString key) { @@ -42,6 +43,8 @@ inline static const QString getNicePropertyName(QString key) return QCoreApplication::translate("Mtg", "Side"); if (key == Layout) return QCoreApplication::translate("Mtg", "Layout"); + if (key == ColorIdentity) + return QCoreApplication::translate("Mtg", "Color Identity"); return key; } }; // namespace Mtg diff --git a/cockatrice/src/tab_deck_editor.cpp b/cockatrice/src/tab_deck_editor.cpp index 79556bda..743ef948 100644 --- a/cockatrice/src/tab_deck_editor.cpp +++ b/cockatrice/src/tab_deck_editor.cpp @@ -33,8 +33,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -349,6 +351,7 @@ void TabDeckEditor::createCentralFrame() searchEdit->setPlaceholderText(tr("Search by card name")); searchEdit->setClearButtonEnabled(true); searchEdit->addAction(QPixmap("theme:icons/search"), QLineEdit::LeadingPosition); + auto help = searchEdit->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition); searchEdit->installEventFilter(&searchKeySignals); setFocusProxy(searchEdit); @@ -363,6 +366,7 @@ void TabDeckEditor::createCentralFrame() connect(&searchKeySignals, SIGNAL(onCtrlAltLBracket()), this, SLOT(actDecrementCardFromSideboard())); connect(&searchKeySignals, SIGNAL(onCtrlAltEnter()), this, SLOT(actAddCardToSideboard())); connect(&searchKeySignals, SIGNAL(onCtrlEnter()), this, SLOT(actAddCardToSideboard())); + connect(help, &QAction::triggered, this, &TabDeckEditor::showSearchSyntaxHelp); databaseModel = new CardDatabaseModel(db, true, this); databaseModel->setObjectName("databaseModel"); @@ -700,7 +704,7 @@ void TabDeckEditor::updateCardInfoRight(const QModelIndex ¤t, const QModel void TabDeckEditor::updateSearch(const QString &search) { - databaseDisplayModel->setCardName(search); + databaseDisplayModel->setStringFilter(search); QModelIndexList sel = databaseView->selectionModel()->selectedRows(); if (sel.isEmpty() && databaseDisplayModel->rowCount()) databaseView->selectionModel()->setCurrentIndex(databaseDisplayModel->index(0, 0), @@ -1212,3 +1216,35 @@ void TabDeckEditor::setSaveStatus(bool newStatus) aPrintDeck->setEnabled(newStatus); analyzeDeckMenu->setEnabled(newStatus); } + +void TabDeckEditor::showSearchSyntaxHelp() +{ + + QFile file("theme:help/search.md"); + + if (!file.open(QFile::ReadOnly | QFile::Text)) { + return; + } + + QTextStream in(&file); + QString text = in.readAll(); + file.close(); + + // Poor Markdown Converter + auto opts = QRegularExpression::MultilineOption; + text = text.replace(QRegularExpression("^(###)(.*)", opts), "

\\2

") + .replace(QRegularExpression("^(##)(.*)", opts), "

\\2

") + .replace(QRegularExpression("^(#)(.*)", opts), "

\\2

") + .replace(QRegularExpression("^------*", opts), "
") + .replace(QRegularExpression("\\[([^\[]+)\\]\\(([^\\)]+)\\)", opts), "\\1"); + + auto browser = new QTextBrowser; + browser->setParent(this, Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | + Qt::WindowCloseButtonHint | Qt::WindowFullscreenButtonHint); + browser->setWindowTitle("Search Help"); + browser->setReadOnly(true); + browser->setMinimumSize({500, 600}); + browser->setHtml(text); + connect(browser, &QTextBrowser::anchorClicked, [=](QUrl link) { searchEdit->setText(link.fragment()); }); + browser->show(); +} diff --git a/cockatrice/src/tab_deck_editor.h b/cockatrice/src/tab_deck_editor.h index 5aec4f89..b64684f6 100644 --- a/cockatrice/src/tab_deck_editor.h +++ b/cockatrice/src/tab_deck_editor.h @@ -13,7 +13,7 @@ class CardDatabaseModel; class CardDatabaseDisplayModel; class DeckListModel; class QTreeView; -class QTableView; + class CardFrame; class QTextEdit; class QLabel; @@ -33,10 +33,10 @@ private: QTreeView *treeView; protected: - void keyPressEvent(QKeyEvent *event); + void keyPressEvent(QKeyEvent *event) override; public: - SearchLineEdit() : QLineEdit(), treeView(0) + SearchLineEdit() : QLineEdit(), treeView(nullptr) { } void setTreeView(QTreeView *_treeView) @@ -90,12 +90,13 @@ private slots: void freeDocksSize(); void refreshShortcuts(); - bool eventFilter(QObject *o, QEvent *e); + bool eventFilter(QObject *o, QEvent *e) override; void dockVisibleTriggered(); void dockFloatingTriggered(); void dockTopLevelChanged(bool topLevel); void saveDbHeaderState(); void setSaveStatus(bool newStatus); + void showSearchSyntaxHelp(); private: CardInfoPtr currentCardInfo() const; @@ -146,10 +147,10 @@ private: QWidget *centralWidget; public: - TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent = 0); - ~TabDeckEditor(); - void retranslateUi(); - QString getTabText() const; + explicit TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr); + ~TabDeckEditor() override; + void retranslateUi() override; + QString getTabText() const override; void setDeck(DeckLoader *_deckLoader); void setModified(bool _windowModified); bool confirmClose(); @@ -160,7 +161,7 @@ public: void createCentralFrame(); public slots: - void closeRequest(); + void closeRequest() override; signals: void deckEditorClosing(TabDeckEditor *tab); }; diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index e080a915..8015e41f 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -98,6 +98,11 @@ CardInfoPtr OracleImporter::addCard(QString name, sortAndReduceColors(allColors); properties.insert("colors", allColors); } + QString allColorIdent = properties.value("colorIdenity").toString(); + if (allColorIdent.size() > 1) { + sortAndReduceColors(allColorIdent); + properties.insert("coloridentity", allColorIdent); + } // DETECT CARD POSITIONING INFO @@ -178,7 +183,7 @@ int OracleImporter::importCardsFromSet(CardSetPtr currentSet, const QList splitCards; QString ptSeparator("/"); QVariantMap card; - QString layout, name, text, colors, maintype, power, toughness; + QString layout, name, text, colors, colorIdentity, maintype, power, toughness; bool isToken; QStringList additionalNames; QVariantHash properties; @@ -232,6 +237,11 @@ int OracleImporter::importCardsFromSet(CardSetPtr currentSet, const QList + +CardDatabase *db; + +#define Query(name, card, query, match) \ +TEST_F(CardQuery, name) {\ + ASSERT_EQ(FilterString(query).check(card), match);\ +} + + +namespace +{ + +class CardQuery : public ::testing::Test { + protected: + void SetUp() override { + cat = db->getCardBySimpleName("Cat"); + } + + // void TearDown() override {} + + CardData cat; +}; + + Query(Empty, cat, "", true) + Query(Typing, cat, "t", true) + + Query(NonMatchingType, cat, "t:kithkin", false) + Query(MatchingType, cat, "t:creature", true) + Query(Not1, cat, "not t:kithkin", true) + Query(Not2, cat, "not t:creature", false) + Query(Case, cat, "t:cReAtUrE", true) + + Query(And, cat, "t:creature t:creature", true) + Query(And2, cat, "t:creature t:sorcery", false) + + Query(Or, cat, "t:bat or t:creature", true) + + Query(Cmc1, cat, "cmc=2", true) + Query(Cmc2, cat, "cmc>3", false) + Query(Cmc3, cat, "cmc>1", true) + + Query(Quotes, cat, "t:\"creature\"", true); + + Query(Field, cat, "pt:\"3/3\"", true) + + Query(Color1, cat, "c:g", true); + Query(Color2, cat, "c:gw", true); + Query(Color3, cat, "c!g", true); + Query(Color4, cat, "c!gw", false); + +} // namespace + +int main(int argc, char **argv) +{ + settingsCache = new SettingsCache; + db = new CardDatabase; + db->loadCardDatabases(); + + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp new file mode 100644 index 00000000..64c8be32 --- /dev/null +++ b/tests/carddatabase/mocks.cpp @@ -0,0 +1,54 @@ + +#include "mocks.h" + +void CardDatabaseSettings::setSortKey(QString /* shortName */, unsigned int /* sortKey */){}; +void CardDatabaseSettings::setEnabled(QString /* shortName */, bool /* enabled */){}; +void CardDatabaseSettings::setIsKnown(QString /* shortName */, bool /* isknown */){}; +unsigned int CardDatabaseSettings::getSortKey(QString /* shortName */) +{ + return 0; +}; +bool CardDatabaseSettings::isEnabled(QString /* shortName */) +{ + return true; +}; +bool CardDatabaseSettings::isKnown(QString /* shortName */) +{ + return true; +}; + +SettingsCache::SettingsCache() +{ + cardDatabaseSettings = new CardDatabaseSettings(); +}; +SettingsCache::~SettingsCache() +{ + delete cardDatabaseSettings; +}; +QString SettingsCache::getCustomCardDatabasePath() const +{ + return QString("%1/customsets/").arg(CARDDB_DATADIR); +} +QString SettingsCache::getCardDatabasePath() const +{ + return QString("%1/cards.xml").arg(CARDDB_DATADIR); +} +QString SettingsCache::getTokenDatabasePath() const +{ + return QString("%1/tokens.xml").arg(CARDDB_DATADIR); +} +QString SettingsCache::getSpoilerCardDatabasePath() const +{ + return QString("%1/spoiler.xml").arg(CARDDB_DATADIR); +} +CardDatabaseSettings &SettingsCache::cardDatabase() const +{ + return *cardDatabaseSettings; +} + + +void PictureLoader::clearPixmapCache(CardInfoPtr /* card */) +{ +} + +SettingsCache *settingsCache; diff --git a/tests/carddatabase/carddatabase_test.h b/tests/carddatabase/mocks.h similarity index 96% rename from tests/carddatabase/carddatabase_test.h rename to tests/carddatabase/mocks.h index 07793b85..c6286fc1 100644 --- a/tests/carddatabase/carddatabase_test.h +++ b/tests/carddatabase/mocks.h @@ -10,6 +10,7 @@ #define SETTINGSCACHE_H + class CardDatabaseSettings { public: @@ -40,6 +41,8 @@ signals: void cardDatabasePathChanged(); }; +extern SettingsCache *settingsCache; + #define PICTURELOADER_H class PictureLoader