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 <basicer@basicer.com>

* - 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 <ZaHalpern+github@gmail.com>

* Move utility into guards

Signed-off-by: Zach Halpern <ZaHalpern+github@gmail.com>

* 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
This commit is contained in:
Rob Blanckaert 2019-03-01 11:30:32 -08:00 committed by Zach H
parent 4427ad1451
commit eb60fec8e2
24 changed files with 780 additions and 122 deletions

View file

@ -21,7 +21,6 @@ class CardRelation;
class ICardDatabaseParser;
typedef QMap<QString, QString> QStringMap;
typedef QMap<QString, int> MuidMap;
typedef QSharedPointer<CardInfo> CardInfoPtr;
typedef QSharedPointer<CardSet> CardSetPtr;
typedef QMap<QString, CardInfoPerSet> 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;

View file

@ -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);
}

View file

@ -2,10 +2,12 @@
#define CARDDATABASEMODEL_H
#include "carddatabase.h"
#include "filter_string.h"
#include <QAbstractListModel>
#include <QList>
#include <QSet>
#include <QSortFilterProxyModel>
#include <QTimer>
class FilterTree;
@ -67,11 +69,12 @@ public:
private:
FilterBool isToken;
QString cardNameBeginning, cardName, cardText;
QString searchTerm;
QString cardName, cardText;
QSet<QString> cardNameSet, cardTypes, cardColors;
FilterTree *filterTree;
FilterString *filterString;
int loadedRowCount;
QTimer dirtyTimer;
/** The translation table that will be used for sanitizeCardName. */
static QMap<wchar_t, wchar_t> 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<QString> &_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<QString> &_cardTypes)
{
cardTypes = _cardTypes;
invalidate();
}
void setCardColors(const QSet<QString> &_cardColors)
{
cardColors = _cardColors;
invalidate();
dirtyTimer.start(20);
}
void clearFilterAll();
int rowCount(const QModelIndex &parent = QModelIndex()) const override;

View file

@ -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("");
}

View file

@ -3,6 +3,7 @@
#include <QObject>
#include <QString>
#include <utility>
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
{

View file

@ -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("<tr><td>%1</td><td></td><td>%2</td></tr>").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("<tr><td>%1</td><td width=\"5\"></td><td>").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 += "<a href=\"" + tmp + "\">" + tmp + "</a><br>";
}
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 += "<a href=\"" + tmp + "\">" + tmp + "</a><br>";
}

View file

@ -17,7 +17,7 @@ private:
CardInfoPtr info;
public:
CardInfoText(QWidget *parent = 0);
explicit CardInfoText(QWidget *parent = nullptr);
void retranslateUi();
void setInvalidCardName(const QString &cardName);

View file

@ -0,0 +1,352 @@
#include "filter_string.h"
#include "../../common/lib/peglib.h"
#include <QByteArray>
#include <QString>
#include <cmath>
#include <functional>
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 / ["] <NonQuote*> ["]
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<Filter>() : 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<Filter>()(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<Filter>()(x))
return true;
}
return false;
};
};
search["SomewhatComplexQueryPart"] = passthru;
search["QueryPart"] = passthru;
search["NotQuery"] = [](const peg::SemanticValues &sv) -> Filter {
Filter dependent = sv[0].get<Filter>();
return [=](CardData x) -> bool { return !dependent(x); };
};
search["TypeQuery"] = [](const peg::SemanticValues &sv) -> Filter {
StringMatcher matcher = sv[0].get<StringMatcher>();
return [=](CardData x) -> bool { return matcher(x->getCardType()); };
};
search["SetQuery"] = [](const peg::SemanticValues &sv) -> Filter {
StringMatcher matcher = sv[0].get<StringMatcher>();
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<StringMatcher>();
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<QString>();
return [=](CardData x) -> bool { return x->getProperty(QString("format-%1").arg(format)) == "legal"; };
} else {
QString format = sv[1].get<QString>();
QString legality = sv[0].get<QString>();
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<QString>();
return [=](const QString &s) { return s.split(" ").contains(target, Qt::CaseInsensitive); };
} else {
auto target = sv[0].get<QStringList>();
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<QStringList>();
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<QString>();
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<QString>());
}
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<int>();
auto op = sv[0].get<QString>();
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<QString>();
return [=](const QString &s) { return s.QString::contains(target, Qt::CaseInsensitive); };
};
search["OracleQuery"] = [](const peg::SemanticValues &sv) -> Filter {
StringMatcher matcher = sv[0].get<StringMatcher>();
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<char>();
}
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<NumberMatcher>();
return [=](CardData x) -> bool { return matcher(x->getProperty("cmc").toInt()); };
};
search["PowerQuery"] = [](const peg::SemanticValues &sv) -> Filter {
NumberMatcher matcher = sv[0].get<NumberMatcher>();
return [=](CardData x) -> bool { return matcher(x->getPowTough().split("/")[0].toInt()); };
};
search["ToughnessQuery"] = [](const peg::SemanticValues &sv) -> Filter {
NumberMatcher matcher = sv[0].get<NumberMatcher>();
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<QString>();
if (sv.choice() == 0) {
StringMatcher matcher = sv[1].get<StringMatcher>();
return [=](CardData x) -> bool { return x->hasProperty(field) ? matcher(x->getProperty(field)) : false; };
} else {
NumberMatcher matcher = sv[1].get<NumberMatcher>();
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<StringMatcher>();
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<char>() : *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; };
}
}

View file

@ -0,0 +1,48 @@
#ifndef FILTER_STRING_H
#define FILTER_STRING_H
#include "carddatabase.h"
#include "filtertree.h"
#include <QMap>
#include <QString>
#include <functional>
#include <utility>
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:
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

View file

@ -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;

View file

@ -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");

View file

@ -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

View file

@ -33,8 +33,10 @@
#include <QPrintPreviewDialog>
#include <QProcessEnvironment>
#include <QPushButton>
#include <QRegularExpression>
#include <QSignalMapper>
#include <QSplitter>
#include <QTextBrowser>
#include <QTextEdit>
#include <QTextStream>
#include <QTimer>
@ -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 &current, 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), "<h3>\\2</h3>")
.replace(QRegularExpression("^(##)(.*)", opts), "<h2>\\2</h2>")
.replace(QRegularExpression("^(#)(.*)", opts), "<h1>\\2</h1>")
.replace(QRegularExpression("^------*", opts), "<hr />")
.replace(QRegularExpression("\\[([^\[]+)\\]\\(([^\\)]+)\\)", opts), "<a href=\'\\2\'>\\1</a>");
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();
}

View file

@ -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);
};