[VisualDatabase] Draw the card grid with a model/view (#7701)

* [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>
This commit is contained in:
BruebachL 2026-11-02 08:35:58 +01:00 • committed by GitHub
parent 94e6281d87
commit a1a8a59f4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1019 additions and 277 deletions

View file

@ -1,6 +1,7 @@
set(HEADERS
card_database_model.h
card_database_display_model.h
card_database_visual_display_model.h
card/card_completer_proxy_model.h
card/card_search_model.h
card_set/card_sets_model.h
@ -13,6 +14,7 @@ add_library(
${HEADERS}
card_database_model.cpp
card_database_display_model.cpp
card_database_visual_display_model.cpp
card/card_completer_proxy_model.cpp
card/card_search_model.cpp
card_set/card_sets_model.cpp

View file

@ -21,7 +21,7 @@
class QObject;
CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr),
: QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr), cardModel(nullptr),
collator(NaturalSort::makeNaturalSortCollator())
{
filterTree = nullptr;
@ -37,6 +37,7 @@ CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model)
{
cardModel = qobject_cast<CardDatabaseModel *>(model);
QSortFilterProxyModel::setSourceModel(model);
connect(model, &QAbstractItemModel::rowsInserted, this, [this]() { dirty(); });
@ -46,6 +47,25 @@ void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *model)
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'\''},

View file

@ -21,6 +21,7 @@
#include <libcockatrice/filters/filter_string.h>
#include <qtmetamacros.h>
class CardDatabaseModel;
class FilterTree;
class QObject;
template <class Key, class T> class QMap;
@ -36,12 +37,25 @@ public:
ShowAll
};
enum Role
{
/**
* @brief The CardInfoPtr behind a proxy row.
*
* Saves views from looking the card up again through its name, and keeps
* them from depending on the source model's column layout.
*/
CardInfoRole = Qt::UserRole + 1
};
private:
FilterBool isToken;
QString cardName, cardText;
QSet<QString> cardNameSet, cardTypes, cardColors;
FilterTree *filterTree;
FilterString *filterString;
/// The source model, kept typed so CardInfoRole does not cast per row.
CardDatabaseModel *cardModel;
QTimer dirtyTimer;
CardSearchLanguage searchLanguage;
QString searchText;
@ -84,6 +98,16 @@ public:
dirtyTimer.start(20);
}
void clearFilterAll();
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Returns the card behind a proxy row, without the QVariant wrapper of CardInfoRole.
*
* The visual grid calls this for every row on each rebuild; CardInfoRole stays the
* generic way in for consumers that only see the model through data().
*/
[[nodiscard]] CardInfoPtr cardInfoAt(const QModelIndex &index) const;
signals:
void modelDirty();

View file

@ -0,0 +1,123 @@
#include "card_database_visual_display_model.h"
#include "card_database_display_model.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include <QAbstractItemModel>
#include <QList>
#include <QStringList>
#include <QVariant>
/// Coalesces the bursts of proxy signals a filter, sort or database load produces.
static constexpr int REBUILD_COALESCE_MS = 30;
CardDatabaseVisualDisplayModel::CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel,
CardDatabaseQuerier *_querier,
QObject *parent)
: QAbstractListModel(parent), displayModel(_displayModel), querier(_querier)
{
rebuildTimer.setSingleShot(true);
rebuildTimer.setInterval(REBUILD_COALESCE_MS);
connect(&rebuildTimer, &QTimer::timeout, this, &CardDatabaseVisualDisplayModel::rebuild);
if (displayModel == nullptr) {
return;
}
connect(displayModel, &QAbstractItemModel::modelReset, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
connect(displayModel, &QAbstractItemModel::layoutChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
connect(displayModel, &QAbstractItemModel::rowsInserted, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
connect(displayModel, &QAbstractItemModel::rowsRemoved, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
connect(displayModel, &QAbstractItemModel::dataChanged, this, &CardDatabaseVisualDisplayModel::scheduleRebuild);
}
void CardDatabaseVisualDisplayModel::setSetFilterTerms(const QStringList &terms)
{
if (setFilterTerms == terms) {
return;
}
setFilterTerms = terms;
refresh();
}
void CardDatabaseVisualDisplayModel::refresh()
{
rebuildTimer.stop();
rebuild();
}
void CardDatabaseVisualDisplayModel::scheduleRebuild()
{
rebuildTimer.start();
}
void CardDatabaseVisualDisplayModel::rebuild()
{
beginResetModel();
entries.clear();
if (displayModel != nullptr) {
const int proxyRows = displayModel->rowCount();
entries.reserve(proxyRows);
for (int row = 0; row < proxyRows; ++row) {
const CardInfoPtr info = displayModel->cardInfoAt(displayModel->index(row, 0));
if (info) {
appendPrintingsFor(info);
}
}
}
endResetModel();
}
void CardDatabaseVisualDisplayModel::appendPrintingsFor(const CardInfoPtr &info)
{
if (setFilterTerms.isEmpty()) {
entries.append({info, PrintingInfo()});
return;
}
const auto &printingsBySet = info->getSets();
for (const QString &term : setFilterTerms) {
const auto printings = printingsBySet.constFind(term);
if (printings == printingsBySet.constEnd()) {
continue;
}
for (const PrintingInfo &printing : printings.value()) {
entries.append({info, printing});
}
}
}
int CardDatabaseVisualDisplayModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : static_cast<int>(entries.size());
}
QVariant CardDatabaseVisualDisplayModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= entries.size()) {
return {};
}
if (role == Qt::DisplayRole || role == Qt::AccessibleTextRole) {
return cardAt(index.row()).getName();
}
return {};
}
ExactCard CardDatabaseVisualDisplayModel::cardAt(int row) const
{
if (row < 0 || row >= entries.size()) {
return {};
}
const Entry &entry = entries.at(row);
if (entry.printing.isEmpty() && setFilterTerms.isEmpty()) {
return querier != nullptr ? querier->getPreferredCard(entry.info) : ExactCard(entry.info);
}
return ExactCard(entry.info, entry.printing);
}

View file

@ -0,0 +1,90 @@
/**
* @file card_database_visual_display_model.h
* @ingroup CardDatabaseModels
* @brief A list model of the exact cards a visual card grid paints.
*
* The filtered and sorted CardDatabaseDisplayModel holds one row per card name.
* A visual grid shows one picture per *printing* instead, so this model flattens
* the proxy rows into ExactCards: the preferred printing per card normally, and
* every printing of the selected sets while set filters are active.
*/
#ifndef COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H
#define COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H
#include "libcockatrice/card/card_info.h"
#include "libcockatrice/card/printing/exact_card.h"
#include "libcockatrice/card/printing/printing_info.h"
#include <QAbstractListModel>
#include <QList>
#include <QModelIndex>
#include <QPointer>
#include <QStringList>
#include <QTimer>
#include <qtmetamacros.h>
class CardDatabaseDisplayModel;
class CardDatabaseQuerier;
class CardDatabaseVisualDisplayModel : public QAbstractListModel
{
Q_OBJECT
public:
/**
* @brief Flattens _displayModel into the cards a visual grid displays.
* @param _displayModel The filtered and sorted proxy model of the card database.
* @param _querier Resolves the preferred printing of a card.
* @param parent Parent object.
*/
explicit CardDatabaseVisualDisplayModel(CardDatabaseDisplayModel *_displayModel,
CardDatabaseQuerier *_querier,
QObject *parent = nullptr);
/**
* @brief Sets the set shorthand names of the active set filters.
*
* With no terms every card is shown with its preferred printing. With terms
* every printing belonging to one of the named sets is shown instead, in term
* order. Rebuilds immediately when the terms change.
*/
void setSetFilterTerms(const QStringList &terms);
/**
* @brief Rebuilds the row list from the proxy model right away.
*
* Proxy model changes normally rebuild on a short coalescing timer; call this
* when the caller needs the new rows before returning.
*/
void refresh();
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/**
* @brief Returns the card painted at @p row, with its preferred printing resolved.
* @param row Row in this model, not in the proxy model.
*/
[[nodiscard]] ExactCard cardAt(int row) const;
private:
struct Entry
{
CardInfoPtr info;
/// Empty when the entry follows the preferred printing rather than a set filter.
PrintingInfo printing;
};
void scheduleRebuild();
void rebuild();
void appendPrintingsFor(const CardInfoPtr &info);
QPointer<CardDatabaseDisplayModel> displayModel;
CardDatabaseQuerier *querier;
QStringList setFilterTerms;
QList<Entry> entries;
QTimer rebuildTimer;
};
#endif // COCKATRICE_CARD_DATABASE_VISUAL_DISPLAY_MODEL_H