mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
* [VisualDatabase] Draw the card grid with a model/view instead of a widget per card The visual card database built one CardInfoPictureWithTextOverlayWidget per card, 100 per page, and tore them down on every search, resize and scroll page. It also looked each card up again by name from the model row, kept a shadow QList<ExactCard>, and carried paging state plus a 5 s timer that only existed to defer the first page. Replace that with a real view on a flattened model: - CardDatabaseDisplayModel gains CardInfoRole and cardInfoAt(), so a view can get the CardInfoPtr behind a proxy row without a name round trip. - New CardDatabaseVisualDisplayModel (libcockatrice_models) flattens the filtered and sorted proxy into one entry per displayed card picture: the preferred printing normally, every printing of the selected sets while set filters are active. Proxy changes rebuild it on a 30 ms coalescing timer. - New CardDatabaseVisualView (QListView IconMode) paints only visible rows through the new CardPictureDelegate, keeps the old interaction contract (left/right/middle press -> cardClicked, hover -> cardHovered, Ctrl+wheel resize, no selection, no focus stealing, scroll to top on a new result) and repaints when CardPictureLoader reports an async image. - CardSizeWidget::enableCtrlScrollResize() also watches scroll-area viewports, which is what makes Ctrl+wheel work over a QListView. - VisualDatabaseDisplayWidget drops the paging, the shadow list, the dead members and the 5 s timer. Behaviour-preserving: no gameplay change, no new user-facing strings. * [VisualDatabase] Fix the card grid wheel step QListView hands the delegate's item height to the scroll bar as its single step under ScrollPerPixel, so QAbstractSlider moved wheelScrollLines() whole card rows per notch. Handle the wheel in the view instead: one line is an eighth of the grid cell, so a notch moves about a third of a card row at any card size and still follows the OS line count. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
293 lines
9.5 KiB
C++
293 lines
9.5 KiB
C++
#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 (right.column() == CardDatabaseModel::PTColumn && left.column() == CardDatabaseModel::PTColumn) {
|
||
// The comparator runs O(n log n) times while the view is sorted, so
|
||
// splitting every power/toughness string into a QStringList allocates
|
||
// a lot for data that has exactly one separator. Work on views and
|
||
// only split the two halves out when both strings are well formed.
|
||
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 powerComparison = lessThanNumerically(leftPT.left(leftSlash), rightPT.left(rightSlash));
|
||
if (powerComparison != 0) {
|
||
return powerComparison < 0;
|
||
}
|
||
// power equal, check toughness
|
||
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);
|
||
}
|