Cockatrice/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp
2026-11-03 18:45:57 -07:00

300 lines
9.9 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "card_database_display_model.h"
#include "../natural_sort_collator.h"
#include "card_database_model.h"
#include "libcockatrice/card/card_localization.h"
#include "libcockatrice/filters/filter_string.h"
#include "libcockatrice/filters/filter_tree.h"
#include <QAbstractItemModel>
#include <QChar>
#include <QList>
#include <QMap>
#include <QSharedPointer>
#include <QStringList>
#include <QVariant>
#include <QtGlobal>
#include <qnamespace.h>
#include <string>
#include <utility>
class QObject;
CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr), cardModel(nullptr),
collator(NaturalSort::makeNaturalSortCollator())
{
filterTree = nullptr;
setFilterCaseSensitivity(Qt::CaseInsensitive);
setSortCaseSensitivity(Qt::CaseInsensitive);
dirtyTimer.setSingleShot(true);
connect(&dirtyTimer, &QTimer::timeout, this, [this]() {
invalidate();
emit modelDirty();
});
}
void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model)
{
cardModel = qobject_cast<CardDatabaseModel *>(model);
QSortFilterProxyModel::setSourceModel(model);
connect(model, &QAbstractItemModel::rowsInserted, this, [this]() { dirty(); });
connect(model, &QAbstractItemModel::rowsRemoved, this, [this]() { dirty(); });
connect(model, &QAbstractItemModel::modelReset, this, [this]() { dirty(); });
}
QVariant CardDatabaseDisplayModel::data(const QModelIndex &index, int role) const
{
if (role == CardInfoRole) {
return QVariant::fromValue(cardInfoAt(index));
}
return QSortFilterProxyModel::data(index, role);
}
CardInfoPtr CardDatabaseDisplayModel::cardInfoAt(const QModelIndex &index) const
{
if (!index.isValid() || cardModel == nullptr) {
return {};
}
const QModelIndex sourceIndex = mapToSource(index);
return sourceIndex.isValid() ? cardModel->getCard(sourceIndex.row()) : CardInfoPtr();
}
QMap<wchar_t, wchar_t> CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'},
{L'”', L'\"'},
{L'‘', L'\''},
{L'’', L'\''}};
bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
const QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString();
const QString rightString = sourceModel()->data(right, CardDatabaseModel::SortRole).toString();
if (!cardName.isEmpty() && left.column() == CardDatabaseModel::NameColumn) {
const bool isLeftType = leftString.startsWith(cardName, Qt::CaseInsensitive);
const bool isRightType = rightString.startsWith(cardName, Qt::CaseInsensitive);
// test for an exact match: isLeftType && leftString.size() == cardName.size()
// or an exclusive start match: isLeftType && !isRightType
if (isLeftType && (!isRightType || leftString.size() == cardName.size())) {
return true;
}
// same checks for the right string
if (isRightType && (!isLeftType || rightString.size() == cardName.size())) {
return false;
}
} else if (left.column() == right.column() &&
(left.column() == CardDatabaseModel::AttackColumn ||
left.column() == CardDatabaseModel::ArmourColumn ||
left.column() == CardDatabaseModel::HealthColumn)) {
// Compare numeric card statistics as numbers: 9 sorts before 10.
// Values containing expressions retain the existing numeric-prefix
// comparator instead of being forced into an integer type.
return lessThanNumerically(leftString, rightString) < 0;
} else if (right.column() == CardDatabaseModel::AtkHpColumn &&
left.column() == CardDatabaseModel::AtkHpColumn) {
// ATK/HP sorts by Attack first, then Health, both numerically.
// Operate on views to avoid allocations during large database sorts.
const QStringView leftPT = leftString;
const QStringView rightPT = rightString;
const qsizetype leftSlash = leftPT.indexOf(u'/');
const qsizetype rightSlash = rightPT.indexOf(u'/');
const bool leftIsWellFormed = leftSlash >= 0 && leftPT.indexOf(u'/', leftSlash + 1) < 0;
const bool rightIsWellFormed = rightSlash >= 0 && rightPT.indexOf(u'/', rightSlash + 1) < 0;
if (leftIsWellFormed && rightIsWellFormed) {
const int attackComparison = lessThanNumerically(leftPT.left(leftSlash), rightPT.left(rightSlash));
if (attackComparison != 0) {
return attackComparison < 0;
}
// Attack equal, compare Health
return lessThanNumerically(leftPT.mid(leftSlash + 1), rightPT.mid(rightSlash + 1)) < 0;
}
}
return collator.compare(leftString, rightString) < 0;
}
int CardDatabaseDisplayModel::lessThanNumerically(QStringView left, QStringView right) const
{
if (left == right) {
return 0;
}
bool okLeft = false;
bool okRight = false;
float leftNum = left.toFloat(&okLeft);
float rightNum = right.toFloat(&okRight);
if (okLeft && okRight) {
if (leftNum < rightNum) {
return -1;
} else if (leftNum > rightNum) {
return 1;
} else {
return 0;
}
}
// try and parsing again, for weird ones like "1+*"
QStringView leftAfterNum;
QStringView rightAfterNum;
if (!okLeft) {
int leftNumIndex = 0;
for (; leftNumIndex < left.size(); leftNumIndex++) {
if (!left.at(leftNumIndex).isDigit()) {
break;
}
}
if (leftNumIndex != 0) {
leftNum = left.left(leftNumIndex).toFloat(&okLeft);
leftAfterNum = left.right(leftNumIndex);
}
}
if (!okRight) {
int rightNumIndex = 0;
for (; rightNumIndex < right.size(); rightNumIndex++) {
if (!right.at(rightNumIndex).isDigit()) {
break;
}
}
if (rightNumIndex != 0) {
rightNum = right.left(rightNumIndex).toFloat(&okRight);
rightAfterNum = right.right(rightNumIndex);
}
}
if (okLeft && okRight) {
if (leftNum != rightNum) {
// both parsed as numbers, but different number
if (leftNum < rightNum) {
return -1;
} else {
return 1;
}
} else {
// both parsed, same number, but at least one has something else
// so compare the part after the number - prefer nothing
return collator.compare(leftAfterNum, rightAfterNum);
}
} else if (okLeft) {
return -1;
} else if (okRight) {
return 1;
}
// couldn't parse it, just return String comparison
return collator.compare(left, right);
}
bool CardDatabaseDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const
{
CardInfoPtr info = static_cast<CardDatabaseModel *>(sourceModel())->getCard(sourceRow);
if (((isToken == ShowTrue) && !info->getIsToken()) || ((isToken == ShowFalse) && info->getIsToken())) {
return false;
}
if (filterString != nullptr) {
if (filterTree != nullptr && !filterTree->acceptsCard(info, searchLanguage)) {
return false;
}
return filterString->check(info);
}
return rowMatchesCardName(info);
}
bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const
{
if (!cardName.isEmpty()) {
const bool matchesEnglish = info->getName().contains(cardName, Qt::CaseInsensitive);
const bool matchesLocalized =
!searchLanguage.isEnglishOnly() &&
info->getLocalizedName(searchLanguage.language).contains(cardName, Qt::CaseInsensitive);
if (!matchesEnglish && !matchesLocalized) {
return false;
}
}
if (!cardNameSet.isEmpty() && !cardNameSet.contains(info->getName())) {
return false;
}
if (filterTree != nullptr) {
return filterTree->acceptsCard(info, searchLanguage);
}
return true;
}
void CardDatabaseDisplayModel::clearFilterAll()
{
#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
beginFilterChange();
#endif
cardName.clear();
cardText.clear();
cardTypes.clear();
cardColors.clear();
if (filterTree != nullptr) {
filterTree->clear();
}
#if (QT_VERSION >= QT_VERSION_CHECK(6, 10, 0))
endFilterChange(QSortFilterProxyModel::Direction::Rows);
#else
invalidateFilter();
#endif
}
void CardDatabaseDisplayModel::setFilterTree(FilterTree *_filterTree)
{
if (this->filterTree != nullptr) {
disconnect(this->filterTree, nullptr, this, nullptr);
}
this->filterTree = _filterTree;
connect(this->filterTree, &FilterTree::changed, this, &CardDatabaseDisplayModel::filterTreeChanged);
invalidate();
}
void CardDatabaseDisplayModel::setStringFilter(const QString &_src)
{
searchText = _src;
delete filterString;
filterString = new FilterString(_src, searchLanguage);
dirty();
}
void CardDatabaseDisplayModel::setSearchLanguage(const CardSearchLanguage &searchLang)
{
if (searchLanguage == searchLang) {
return;
}
searchLanguage = searchLang;
if (filterString != nullptr) {
setStringFilter(searchText);
}
dirty();
}
void CardDatabaseDisplayModel::filterTreeChanged()
{
invalidate();
}
const QString CardDatabaseDisplayModel::sanitizeCardName(const QString &dirtyName, const QMap<wchar_t, wchar_t> &table)
{
std::wstring toReturn = dirtyName.toStdWString();
for (wchar_t &ch : toReturn) {
if (table.contains(ch)) {
ch = table.value(ch);
}
}
return QString::fromStdWString(toReturn);
}