Merge current upstream into finite-copy legality review

Preserve upstream master and the original nine-file copy-limit change. Retain QIODevice alongside upstream Oracle test includes. Invalidate derived format rules where the new upstream import reset and failure paths clear their cards.
This commit is contained in:
Bryce! 2026-11-04 16:01:48 -05:00
commit 3de8351b8b
443 changed files with 15637 additions and 11767 deletions

View file

@ -1,7 +1,3 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
add_subdirectory(libcockatrice/models/database)
add_subdirectory(libcockatrice/models/deck_list)

View file

@ -1,10 +1,7 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
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
@ -17,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;
@ -33,22 +33,37 @@ CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent)
invalidate();
emit modelDirty();
});
loadedRowCount = 0;
}
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]() {
loadedRowCount = 0;
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'\"'},
@ -56,32 +71,6 @@ QMap<wchar_t, wchar_t> CardDatabaseDisplayModel::characterTranslation = {{L'“'
{L'‘', L'\''},
{L'’', L'\''}};
bool CardDatabaseDisplayModel::canFetchMore(const QModelIndex &index) const
{
return loadedRowCount < sourceModel()->rowCount(index);
}
void CardDatabaseDisplayModel::fetchMore(const QModelIndex &index)
{
int remainder = sourceModel()->rowCount(index) - loadedRowCount;
int itemsToFetch = qMin(100, remainder);
if (itemsToFetch == 0) {
return;
}
const auto startIndex = qMin(rowCount(QModelIndex()), loadedRowCount);
beginInsertRows(QModelIndex(), startIndex, startIndex + itemsToFetch - 1);
loadedRowCount += itemsToFetch;
endInsertRows();
}
int CardDatabaseDisplayModel::rowCount(const QModelIndex &parent) const
{
return QSortFilterProxyModel::rowCount(parent);
}
bool CardDatabaseDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
const QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString();

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,13 +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;
int loadedRowCount;
/// The source model, kept typed so CardInfoRole does not cast per row.
CardDatabaseModel *cardModel;
QTimer dirtyTimer;
CardSearchLanguage searchLanguage;
QString searchText;
@ -85,9 +98,16 @@ public:
dirtyTimer.start(20);
}
void clearFilterAll();
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
[[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

@ -27,14 +27,18 @@ CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromE
CardDatabaseModel::~CardDatabaseModel() = default;
int CardDatabaseModel::rowCount(const QModelIndex & /*parent*/) const
int CardDatabaseModel::rowCount(const QModelIndex &parent) const
{
return cardList.size();
// A list model's card rows are leaves; returning the full card count for a
// valid parent makes every row claim all cards as children.
return parent.isValid() ? 0 : cardList.size();
}
int CardDatabaseModel::columnCount(const QModelIndex & /*parent*/) const
int CardDatabaseModel::columnCount(const QModelIndex &parent) const
{
return CARDDBMODEL_COLUMNS;
// Card rows are leaves, so a valid parent holds no columns. Reporting the
// row width for a card index would claim child cells that do not exist.
return parent.isValid() ? 0 : CARDDBMODEL_COLUMNS;
}
QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const

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

View file

@ -1,7 +1,3 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h)
add_library(

View file

@ -135,6 +135,13 @@ void DeckListModel::rebuildTree()
int DeckListModel::rowCount(const QModelIndex &parent) const
{
// Only column 0 carries children. Column 1 and later indexes are siblings
// used by views for the remaining cells of a row, and a model that hangs
// children off them breaks QAbstractItemModelTester and proxy models.
if (parent.isValid() && parent.column() != 0) {
return 0;
}
// debugIndexInfo("rowCount", parent);
auto *node = getNode<InnerDecklistNode *>(parent);
if (node) {
@ -283,7 +290,7 @@ QVariant DeckListModel::headerData(const int section, const Qt::Orientation orie
QModelIndex DeckListModel::index(int row, int column, const QModelIndex &parent) const
{
// debugIndexInfo("index", parent);
if (!hasIndex(row, column, parent)) {
if ((parent.isValid() && parent.column() != 0) || !hasIndex(row, column, parent)) {
return {};
}
@ -741,7 +748,8 @@ QModelIndex DeckListModel::nodeToIndex(AbstractDecklistNode *node) const
void DeckListModel::sortShadowTree(InnerDecklistNode *node, Qt::SortOrder order)
{
// The mapping is not needed: fresh shadow nodes have no persistent indices yet.
(void)DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order);
(void)DeckListModelCustomZones::sortWithCustomZonesLast(
root, node, order, activeGroupCriteria == DeckListModelGroupCriteria::MANA_COST && lastKnownColumn != 0);
for (int i = node->size() - 1; i >= 0; --i) {
if (auto *subNode = dynamic_cast<InnerDecklistNode *>(node->at(i))) {
@ -754,7 +762,8 @@ void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order)
{
// Sort children (custom zones always sorted after groups within a board) and
// use the movement mapping to update the list of persistent indices.
const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(root, node, order);
const auto mapping = DeckListModelCustomZones::sortWithCustomZonesLast(
root, node, order, activeGroupCriteria == DeckListModelGroupCriteria::MANA_COST && lastKnownColumn != 0);
QModelIndexList from, to;
int columns = columnCount();

View file

@ -8,6 +8,8 @@
#include <QHash>
#include <QSharedPointer>
#include <QVector>
#include <algorithm>
#include <cmath>
#include <utility>
namespace DeckListModelCustomZones
@ -111,7 +113,7 @@ QList<QPair<int, int>> plainSort(InnerDecklistNode *node, Qt::SortOrder order)
*
* @return The (preSortRow, finalRow) mapping covering both the sort and the shift.
*/
QList<QPair<int, int>> boardSort(InnerDecklistNode *node, Qt::SortOrder order)
QList<QPair<int, int>> boardSort(InnerDecklistNode *node, Qt::SortOrder order, bool numericGroups)
{
const QVector<QPair<int, int>> sortResult = node->sort(order);
@ -132,6 +134,27 @@ QList<QPair<int, int>> boardSort(InnerDecklistNode *node, Qt::SortOrder order)
}
}
if (numericGroups) {
// Only criteria groups are numeric. Mixing numeric and lexical fallback
// comparisons with custom zone names would not define a strict ordering.
std::sort(groups.begin(), groups.end(), [order](const auto *left, const auto *right) {
const QString leftName = left->getName();
const QString rightName = right->getName();
bool leftIsNumber, rightIsNumber;
const double leftValue = leftName.toDouble(&leftIsNumber);
const double rightValue = rightName.toDouble(&rightIsNumber);
leftIsNumber = leftIsNumber && std::isfinite(leftValue);
rightIsNumber = rightIsNumber && std::isfinite(rightValue);
if (leftIsNumber != rightIsNumber) {
return leftIsNumber;
}
if (leftIsNumber && leftValue != rightValue) {
return order == Qt::AscendingOrder ? leftValue < rightValue : leftValue > rightValue;
}
return order == Qt::AscendingOrder ? leftName < rightName : leftName > rightName;
});
}
QVector<AbstractDecklistNode *> ordered = groups + customZones;
for (int i = 0; i < ordered.size(); ++i) {
node->replace(i, ordered[i]);
@ -147,10 +170,11 @@ QList<QPair<int, int>> boardSort(InnerDecklistNode *node, Qt::SortOrder order)
} // namespace
QList<QPair<int, int>> sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order)
QList<QPair<int, int>>
sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order, bool numericGroups)
{
const bool isBoardZone = (node != root) && (node->getParent() == root);
return isBoardZone ? boardSort(node, order) : plainSort(node, order);
return isBoardZone ? boardSort(node, order, numericGroups) : plainSort(node, order);
}
} // namespace DeckListModelCustomZones

View file

@ -91,10 +91,15 @@ void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode
* @param root Root of the shadow tree (used to classify board zones).
* @param node The shadow node whose children are reordered.
* @param order Sort order to apply.
* @param numericGroups Compare criteria-group names as mana values. Custom zone
* names and card names retain their normal comparison; nonnumeric groups follow
* numeric groups in either sort direction.
* @return A list of (preSortRow, finalRow) pairs describing how each node moved.
*/
[[nodiscard]] QList<QPair<int, int>>
sortWithCustomZonesLast(InnerDecklistNode *root, InnerDecklistNode *node, Qt::SortOrder order);
[[nodiscard]] QList<QPair<int, int>> sortWithCustomZonesLast(InnerDecklistNode *root,
InnerDecklistNode *node,
Qt::SortOrder order,
bool numericGroups = false);
} // namespace DeckListModelCustomZones