mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-25 08:03:58 -05:00
[VDS] Add ability to search by deck contents (#5943)
* [VDS] Add ability to search by deck contents * add deck search syntax help * fix build failure
This commit is contained in:
parent
8cc64bf44e
commit
1eee314d17
|
|
@ -190,6 +190,7 @@ set(cockatrice_SOURCES
|
|||
src/game/cards/card_search_model.cpp
|
||||
src/game/deckview/deck_view.cpp
|
||||
src/game/deckview/deck_view_container.cpp
|
||||
src/game/filters/deck_filter_string.cpp
|
||||
src/game/filters/filter_builder.cpp
|
||||
src/game/filters/filter_card.cpp
|
||||
src/game/filters/filter_string.cpp
|
||||
|
|
|
|||
|
|
@ -379,5 +379,6 @@
|
|||
<file>resources/tips/tips_of_the_day.xml</file>
|
||||
|
||||
<file>resources/help/search.md</file>
|
||||
<file>resources/help/deck_search.md</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
|||
|
|
@ -60,4 +60,6 @@
|
|||
|
||||
# pixel_map_generator = false
|
||||
|
||||
# deck_filter_string = false
|
||||
# filter_string = false
|
||||
# syntax_help = false
|
||||
|
|
|
|||
26
cockatrice/resources/help/deck_search.md
Normal file
26
cockatrice/resources/help/deck_search.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
## Deck Search Syntax Help
|
||||
-----
|
||||
The search bar recognizes a set of special commands.<br>
|
||||
In this list of examples below, each entry has an explanation and can be clicked to test the query. Note that all
|
||||
searches are case insensitive.
|
||||
<dl>
|
||||
<dt>Filename:</dt>
|
||||
<dd>[red deck wins](#red deck wins) <small>(Any deck filename containing the words red, deck, and wins)</small></dd>
|
||||
<dd>["red deck wins"](#%22red deck wins%22) <small>(Any deck filename containing the exact phrase "red deck wins")</small></dd>
|
||||
|
||||
<dt>Deck Contents (Uses [card search expressions](#cardSearchSyntaxHelp)):</dt>
|
||||
<dd><a href="#[[plains]]">[[plains]]</a> <small>(Any deck that contains at least one card with "plains" in its name)</small></dd>
|
||||
<dd><a href="#[[t:legendary]]">[[t:legendary]]</a> <small>(Any deck that contains at least one legendary)</small></dd>
|
||||
<dd><a href="#[[t:legendary]]>5">[[t:legendary]]>5</a> <small>(Any card that contains at least 5 legendaries)</small></dd>
|
||||
<dd><a href="#[[]]:100">[[]]:100</a> <small>(Any deck that contains exactly 100 cards)</small></dd>
|
||||
|
||||
<dt>Negate:</dt>
|
||||
<dd>[soldier -aggro](#soldier -aggro) <small>(Any deck filename that contains "soldier", but not "aggro")</small></dd>
|
||||
|
||||
<dt>Branching:</dt>
|
||||
<dd>[t:aggro OR o:control](#t:aggro OR o:control) <small>(Any deck filename that contains either aggro or control)</small></dd>
|
||||
|
||||
<dt>Grouping:</dt>
|
||||
<dd><a href="#red -([[]]:100 or aggro)">red -([[]]:100 or aggro)</a> <small>(Any deck that has red in its filename but is not 100 cards or has aggro in its filename)</small></dd>
|
||||
|
||||
</dl>
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
#include "visual_deck_storage_search_widget.h"
|
||||
|
||||
#include "../../../../game/filters/deck_filter_string.h"
|
||||
#include "../../../../game/filters/syntax_help.h"
|
||||
#include "../../../../settings/cache_settings.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
|
||||
#include <QAction>
|
||||
|
||||
/**
|
||||
* @brief Constructs a PrintingSelectorCardSearchWidget for searching cards by set name or set code.
|
||||
|
|
@ -17,7 +22,13 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(VisualDeckStorageWi
|
|||
setLayout(layout);
|
||||
|
||||
searchBar = new QLineEdit(this);
|
||||
searchBar->setPlaceholderText(tr("Search by filename"));
|
||||
searchBar->setPlaceholderText(tr("Search by filename (or search expression)"));
|
||||
searchBar->setClearButtonEnabled(true);
|
||||
searchBar->addAction(loadColorAdjustedPixmap("theme:icons/search"), QLineEdit::LeadingPosition);
|
||||
|
||||
auto help = searchBar->addAction(QPixmap("theme:icons/info"), QLineEdit::TrailingPosition);
|
||||
connect(help, &QAction::triggered, this, [this] { createDeckSearchSyntaxHelpWindow(searchBar); });
|
||||
|
||||
layout->addWidget(searchBar);
|
||||
|
||||
// Add a debounce timer for the search bar to limit frequent updates
|
||||
|
|
@ -52,11 +63,11 @@ static QString getFileSearchName(const QString &filePath, bool includeFolderName
|
|||
{
|
||||
QString deckPath = SettingsCache::instance().getDeckPath();
|
||||
if (includeFolderName && filePath.startsWith(deckPath)) {
|
||||
return filePath.mid(deckPath.length()).toLower();
|
||||
return filePath.mid(deckPath.length());
|
||||
}
|
||||
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString fileName = fileInfo.fileName().toLower();
|
||||
QString fileName = fileInfo.fileName();
|
||||
return fileName;
|
||||
}
|
||||
|
||||
|
|
@ -64,14 +75,10 @@ void VisualDeckStorageSearchWidget::filterWidgets(QList<DeckPreviewWidget *> wid
|
|||
const QString &searchText,
|
||||
bool includeFolderName)
|
||||
{
|
||||
if (searchText.isEmpty() || searchText.isNull()) {
|
||||
for (auto widget : widgets) {
|
||||
widget->filteredBySearch = false;
|
||||
}
|
||||
}
|
||||
auto filterString = DeckFilterString(searchText);
|
||||
|
||||
for (auto file : widgets) {
|
||||
QString fileSearchName = getFileSearchName(file->filePath, includeFolderName);
|
||||
file->filteredBySearch = !fileSearchName.contains(searchText.toLower());
|
||||
for (auto widget : widgets) {
|
||||
QString fileSearchName = getFileSearchName(widget->filePath, includeFolderName);
|
||||
widget->filteredBySearch = !filterString.check(widget, {fileSearchName});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
167
cockatrice/src/game/filters/deck_filter_string.cpp
Normal file
167
cockatrice/src/game/filters/deck_filter_string.cpp
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
#include "deck_filter_string.h"
|
||||
|
||||
#include "../cards/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 / GenericQuery
|
||||
|
||||
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
|
||||
|
||||
DeckContentQuery <- CardSearch NumericExpression?
|
||||
CardSearch <- '[[' CardFilterString ']]'
|
||||
CardFilterString <- (!']]'.)*
|
||||
|
||||
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()->getCard(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["GenericQuery"] = [](const peg::SemanticValues &sv) -> DeckFilter {
|
||||
auto name = std::any_cast<QString>(sv[0]);
|
||||
return [=](const DeckPreviewWidget *, const ExtraDeckSearchInfo &info) {
|
||||
return info.fileSearchName.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; };
|
||||
}
|
||||
}
|
||||
51
cockatrice/src/game/filters/deck_filter_string.h
Normal file
51
cockatrice/src/game/filters/deck_filter_string.h
Normal 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 filename used for filtering. Varies based on settings.
|
||||
*/
|
||||
QString fileSearchName;
|
||||
};
|
||||
|
||||
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
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
#include <QString>
|
||||
#include <functional>
|
||||
|
||||
peg::parser search(R"(
|
||||
static peg::parser search(R"(
|
||||
Start <- QueryPartList
|
||||
~ws <- [ ]+
|
||||
QueryPartList <- ComplexQueryPart ( ws ("AND" ws)? ComplexQueryPart)* ws*
|
||||
|
|
@ -63,7 +63,7 @@ NumericOperator <- [=:] / <[><!][=]?>
|
|||
NumericValue <- [0-9]+
|
||||
)");
|
||||
|
||||
std::once_flag init;
|
||||
static std::once_flag init;
|
||||
|
||||
static void setupParserRules()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@
|
|||
*
|
||||
* @return the QTextBrowser
|
||||
*/
|
||||
static QTextBrowser *createBrowser()
|
||||
static QTextBrowser *createBrowser(const QString &helpFile)
|
||||
{
|
||||
QFile file("theme:help/search.md");
|
||||
QFile file(helpFile);
|
||||
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||
qCWarning(SyntaxHelpLog) << "Could not open syntax help file: " << helpFile;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
@ -54,9 +55,29 @@ static QTextBrowser *createBrowser()
|
|||
*/
|
||||
QTextBrowser *createSearchSyntaxHelpWindow(QLineEdit *lineEdit)
|
||||
{
|
||||
auto browser = createBrowser();
|
||||
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;
|
||||
}
|
||||
|
|
@ -2,8 +2,13 @@
|
|||
#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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user