Initial commit of mtgonline project

This commit is contained in:
2026-07-18 04:57:40 +00:00
commit 86c12376f8
1870 changed files with 547994 additions and 0 deletions
@@ -0,0 +1,35 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(HEADERS
card_database_model.h
card_database_display_model.h
card/card_completer_proxy_model.h
card/card_search_model.h
card_set/card_sets_model.h
token/token_display_model.h
token/token_edit_model.h
)
if(Qt6_FOUND)
qt6_wrap_cpp(MOC_SOURCES ${HEADERS})
elseif(Qt5_FOUND)
qt5_wrap_cpp(MOC_SOURCES ${HEADERS})
endif()
add_library(
libcockatrice_models_database STATIC
${MOC_SOURCES}
card_database_model.cpp
card_database_display_model.cpp
card/card_completer_proxy_model.cpp
card/card_search_model.cpp
card_set/card_sets_model.cpp
token/token_display_model.cpp
token/token_edit_model.cpp
)
target_include_directories(libcockatrice_models_database PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(libcockatrice_models_database PUBLIC libcockatrice_card libcockatrice_filters ${QT_CORE_MODULE})
@@ -0,0 +1,18 @@
#include "card_completer_proxy_model.h"
CardCompleterProxyModel::CardCompleterProxyModel(QObject *parent) : QSortFilterProxyModel(parent)
{
}
bool CardCompleterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
if (filterRegularExpression().pattern().isEmpty()) {
return true;
}
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
QString data = index.data(Qt::DisplayRole).toString();
// Ensure substring matching
return data.contains(filterRegularExpression());
}
@@ -0,0 +1,22 @@
/**
* @file card_completer_proxy_model.h
* @ingroup CardDatabaseModels
*/
//! \todo Document this file.
#ifndef CARD_COMPLETER_PROXY_MODEL_H
#define CARD_COMPLETER_PROXY_MODEL_H
#include <QSortFilterProxyModel>
class CardCompleterProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit CardCompleterProxyModel(QObject *parent = nullptr);
protected:
[[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};
#endif // CARD_COMPLETER_PROXY_MODEL_H
@@ -0,0 +1,78 @@
#include "card_search_model.h"
#include "../card_database_display_model.h"
#include "../card_database_model.h"
#include <algorithm>
#include <libcockatrice/utility/levenshtein.h>
CardSearchModel::CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent)
: QAbstractListModel(parent), sourceModel(sourceModel)
{
}
int CardSearchModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return searchResults.size();
}
QVariant CardSearchModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= searchResults.size()) {
return QVariant();
}
if (role == Qt::DisplayRole) {
return searchResults.at(index.row()).card->getName();
}
return QVariant();
}
void CardSearchModel::updateSearchResults(const QString &query)
{
beginResetModel();
searchResults.clear();
if (query.isEmpty() || !sourceModel) {
return;
}
// Set the filter for the display model
sourceModel->setCardName(query);
// Collect matching cards and compute Levenshtein distance
for (int i = 0; i < sourceModel->rowCount(); ++i) {
QModelIndex modelIndex = sourceModel->index(i, 0);
QModelIndex sourceIndex = sourceModel->mapToSource(modelIndex);
CardDatabaseModel *sourceDbModel = qobject_cast<CardDatabaseModel *>(sourceModel->sourceModel());
if (!sourceDbModel || !sourceIndex.isValid()) {
return;
}
CardInfoPtr card = sourceDbModel->getCard(sourceIndex.row());
if (!card) {
continue;
}
int distance = levenshteinDistance(query.toLower(), card->getName().toLower());
searchResults.append({card, distance});
}
// Sort by Levenshtein distance (lower distance = better match)
std::sort(searchResults.begin(), searchResults.end(),
[](const SearchResult &a, const SearchResult &b) { return a.distance < b.distance; });
// Keep only the top 5 results
if (searchResults.size() > 10) {
searchResults = searchResults.mid(0, 10);
}
emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
emit layoutChanged();
endResetModel();
}
@@ -0,0 +1,36 @@
/**
* @file card_search_model.h
* @ingroup CardDatabaseModels
*/
//! \todo Document this file.
#ifndef CARD_SEARCH_MODEL_H
#define CARD_SEARCH_MODEL_H
#include "../card_database_display_model.h"
#include <QAbstractListModel>
class CardSearchModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr);
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void updateSearchResults(const QString &query); // Update results based on input
private:
struct SearchResult
{
CardInfoPtr card;
int distance;
};
CardDatabaseDisplayModel *sourceModel;
QList<SearchResult> searchResults;
};
#endif // CARD_SEARCH_MODEL_H
@@ -0,0 +1,252 @@
#include "card_database_display_model.h"
#include "card_database_model.h"
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, [this]() {
invalidate();
emit modelDirty();
});
loadedRowCount = 0;
}
void CardDatabaseDisplayModel::setSourceModel(QAbstractItemModel *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();
});
}
QMap<wchar_t, wchar_t> CardDatabaseDisplayModel::characterTranslation = {{L'', L'\"'},
{L'', 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
{
QString leftString = sourceModel()->data(left, CardDatabaseModel::SortRole).toString();
QString rightString = sourceModel()->data(right, CardDatabaseModel::SortRole).toString();
if (!cardName.isEmpty() && left.column() == CardDatabaseModel::NameColumn) {
bool isLeftType = leftString.startsWith(cardName, Qt::CaseInsensitive);
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) {
QStringList leftList = leftString.split("/");
QStringList rightList = rightString.split("/");
if (leftList.size() == 2 && rightList.size() == 2) {
// cool, have both P/T in list now
int lessThanNum = lessThanNumerically(leftList.at(0), rightList.at(0));
if (lessThanNum != 0) {
return lessThanNum < 0;
} else {
// power equal, check toughness
return lessThanNumerically(leftList.at(1), rightList.at(1)) < 0;
}
}
}
return QString::localeAwareCompare(leftString, rightString) < 0;
}
int CardDatabaseDisplayModel::lessThanNumerically(const QString &left, const QString &right)
{
if (left == right) {
return 0;
}
bool okLeft, okRight;
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+*"
QString leftAfterNum = "";
QString rightAfterNum = "";
if (!okLeft) {
int leftNumIndex = 0;
for (; leftNumIndex < left.length(); 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.length(); 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 QString::localeAwareCompare(leftAfterNum, rightAfterNum);
}
} else if (okLeft) {
return -1;
} else if (okRight) {
return 1;
}
// couldn't parse it, just return String comparison
return QString::localeAwareCompare(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)) {
return false;
}
return filterString->check(info);
}
return rowMatchesCardName(info);
}
bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const
{
if (!cardName.isEmpty() && !info->getName().contains(cardName, Qt::CaseInsensitive)) {
return false;
}
if (!cardNameSet.isEmpty() && !cardNameSet.contains(info->getName())) {
return false;
}
if (filterTree != nullptr) {
return filterTree->acceptsCard(info);
}
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::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);
}
@@ -0,0 +1,93 @@
/**
* @file card_database_display_model.h
* @ingroup CardDatabaseModels
* @brief The CardDatabaseDisplayModel is a QSortFilterProxyModel that allows applying filters and sorting to a
* CardDatabaseModel.
*/
#ifndef COCKATRICE_CARD_DATABASE_DISPLAY_MODEL_H
#define COCKATRICE_CARD_DATABASE_DISPLAY_MODEL_H
#include <QSortFilterProxyModel>
#include <QTimer>
#include <libcockatrice/filters/filter_string.h>
class FilterTree;
class CardDatabaseDisplayModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
enum FilterBool
{
ShowTrue,
ShowFalse,
ShowAll
};
private:
FilterBool isToken;
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;
public:
explicit CardDatabaseDisplayModel(QObject *parent = nullptr);
void setSourceModel(QAbstractItemModel *model) override;
void setFilterTree(FilterTree *_filterTree);
void setIsToken(FilterBool _isToken)
{
isToken = _isToken;
dirty();
}
void setCardName(const QString &_cardName)
{
if (filterString != nullptr) {
delete filterString;
filterString = nullptr;
}
cardName = sanitizeCardName(_cardName, characterTranslation);
dirty();
}
void setStringFilter(const QString &_src)
{
delete filterString;
filterString = new FilterString(_src);
dirty();
}
void setCardNameSet(const QSet<QString> &_cardNameSet)
{
cardNameSet = _cardNameSet;
dirty();
}
void dirty()
{
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;
signals:
void modelDirty();
protected:
[[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
static int lessThanNumerically(const QString &left, const QString &right);
[[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
[[nodiscard]] bool rowMatchesCardName(CardInfoPtr info) const;
private slots:
void filterTreeChanged();
/** Will translate all undesirable characters in DIRTYNAME according to the TABLE. */
const QString sanitizeCardName(const QString &dirtyName, const QMap<wchar_t, wchar_t> &table);
};
#endif // COCKATRICE_CARD_DATABASE_DISPLAY_MODEL_H
@@ -0,0 +1,153 @@
#include "card_database_model.h"
#include <QMap>
#include <libcockatrice/card/database/card_database.h>
#define CARDDBMODEL_COLUMNS 6
CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent)
: QAbstractListModel(parent), db(_db), showOnlyCardsFromEnabledSets(_showOnlyCardsFromEnabledSets)
{
connect(db, &CardDatabase::cardAdded, this, &CardDatabaseModel::cardAdded);
connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseModel::cardRemoved);
connect(db, &CardDatabase::cardDatabaseEnabledSetsChanged, this,
&CardDatabaseModel::cardDatabaseEnabledSetsChanged);
cardDatabaseEnabledSetsChanged();
}
CardDatabaseModel::~CardDatabaseModel() = default;
int CardDatabaseModel::rowCount(const QModelIndex & /*parent*/) const
{
return cardList.size();
}
int CardDatabaseModel::columnCount(const QModelIndex & /*parent*/) const
{
return CARDDBMODEL_COLUMNS;
}
QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= cardList.size() || index.column() >= CARDDBMODEL_COLUMNS ||
(role != Qt::DisplayRole && role != SortRole)) {
return QVariant();
}
CardInfoPtr card = cardList.at(index.row());
switch (index.column()) {
case NameColumn:
return card->getName();
case SetListColumn:
return card->getSetsNames();
case ManaCostColumn:
return role == SortRole ? QString("%1%2").arg(card->getCmc(), 4, QChar('0')).arg(card->getManaCost())
: card->getManaCost();
case CardTypeColumn:
return card->getCardType();
case PTColumn:
return card->getPowTough();
case ColorColumn:
return card->getColors();
default:
return QVariant();
}
}
QVariant CardDatabaseModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role != Qt::DisplayRole) {
return QVariant();
}
if (orientation != Qt::Horizontal) {
return QVariant();
}
switch (section) {
case NameColumn:
return QString(tr("Name"));
case SetListColumn:
return QString(tr("Sets"));
case ManaCostColumn:
return QString(tr("Mana cost"));
case CardTypeColumn:
return QString(tr("Card type"));
case PTColumn:
return QString(tr("P/T"));
case ColorColumn:
return QString(tr("Color(s)"));
default:
return QVariant();
}
}
void CardDatabaseModel::cardInfoChanged(const CardInfoPtr &card)
{
const int row = cardList.indexOf(card);
if (row == -1) {
return;
}
emit dataChanged(index(row, 0), index(row, CARDDBMODEL_COLUMNS - 1));
}
bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(const CardInfoPtr &card) const
{
if (!showOnlyCardsFromEnabledSets) {
return true;
}
for (const auto &printings : card->getSets()) {
for (const auto &printing : printings) {
if (printing.getSet()->getEnabled()) {
return true;
}
}
}
return false;
}
void CardDatabaseModel::cardDatabaseEnabledSetsChanged()
{
// remove all the cards no more present in at least one enabled set
for (const CardInfoPtr &card : cardList) {
if (!checkCardHasAtLeastOneEnabledSet(card)) {
cardRemoved(card);
}
}
// re-check all the card currently not shown, maybe their part of a newly-enabled set
for (const CardInfoPtr &card : db->getCardList()) {
if (!cardListSet.contains(card)) {
cardAdded(card);
}
}
}
void CardDatabaseModel::cardAdded(const CardInfoPtr &card)
{
if (checkCardHasAtLeastOneEnabledSet(card)) {
// add the card if it's present in at least one enabled set
beginInsertRows(QModelIndex(), cardList.size(), cardList.size());
cardList.append(card);
cardListSet.insert(card);
connect(card.data(), &CardInfo::cardInfoChanged, this, &CardDatabaseModel::cardInfoChanged);
endInsertRows();
}
}
void CardDatabaseModel::cardRemoved(CardInfoPtr card)
{
const int row = cardList.indexOf(card);
if (row == -1) {
return;
}
beginRemoveRows(QModelIndex(), row, row);
disconnect(card.data(), nullptr, this, nullptr);
cardListSet.remove(card);
card.clear();
cardList.removeAt(row);
endRemoveRows();
}
@@ -0,0 +1,62 @@
/**
* @file card_database_model.h
* @ingroup CardDatabaseModels
* @brief The CardDatabaseModel maps the cardList contained in the CardDatabase as a QAbstractListModel.
*/
#ifndef CARDDATABASEMODEL_H
#define CARDDATABASEMODEL_H
#include <QAbstractListModel>
#include <QList>
#include <QSet>
#include <libcockatrice/card/database/card_database.h>
class CardDatabaseModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Columns
{
NameColumn,
SetListColumn,
ManaCostColumn,
PTColumn,
CardTypeColumn,
ColorColumn
};
enum Role
{
SortRole = Qt::UserRole
};
CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent = nullptr);
~CardDatabaseModel() override;
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
[[nodiscard]] QVariant
headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
[[nodiscard]] CardDatabase *getDatabase() const
{
return db;
}
[[nodiscard]] CardInfoPtr getCard(int index) const
{
return cardList[index];
}
private:
QList<CardInfoPtr> cardList;
QSet<CardInfoPtr> cardListSet; // Supports faster lookups in cardDatabaseEnabledSetsChanged()
CardDatabase *db;
bool showOnlyCardsFromEnabledSets;
inline bool checkCardHasAtLeastOneEnabledSet(const CardInfoPtr &card) const;
private slots:
void cardAdded(const CardInfoPtr &card);
void cardRemoved(CardInfoPtr card);
void cardInfoChanged(const CardInfoPtr &card);
void cardDatabaseEnabledSetsChanged();
};
#endif
@@ -0,0 +1,322 @@
#include "card_sets_model.h"
#include <QSortFilterProxyModel>
SetsModel::SetsModel(CardDatabase *_db, QObject *parent) : QAbstractTableModel(parent), sets(_db->getSetList())
{
sets.sortByKey();
for (const CardSetPtr &set : sets) {
if (set->getEnabled()) {
enabledSets.insert(set);
}
}
}
SetsModel::~SetsModel() = default;
int SetsModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
} else {
return sets.size();
}
}
QVariant SetsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || (index.column() >= NUM_COLS) || (index.row() >= rowCount())) {
return QVariant();
}
CardSetPtr set = sets[index.row()];
if (index.column() == EnabledCol) {
switch (role) {
case SortRole:
return enabledSets.contains(set) ? "1" : "0";
case Qt::CheckStateRole:
return static_cast<int>(enabledSets.contains(set) ? Qt::Checked : Qt::Unchecked);
case Qt::DisplayRole:
default:
return QVariant();
}
}
if (role != Qt::DisplayRole && role != SortRole) {
return QVariant();
}
switch (index.column()) {
case SortKeyCol:
return QString("%1").arg(index.row(), 8, 10, QChar('0'));
case IsKnownCol:
return set->getIsKnown();
case SetTypeCol:
return set->getSetType();
case ShortNameCol:
return set->getShortName();
case LongNameCol:
return set->getLongName();
case ReleaseDateCol:
return set->getReleaseDate().toString(Qt::ISODate);
default:
return QVariant();
}
}
bool SetsModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (role == Qt::CheckStateRole && index.column() == EnabledCol) {
toggleRow(index.row(), value == Qt::Checked);
return true;
}
return false;
}
QVariant SetsModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if ((role != Qt::DisplayRole) || (orientation != Qt::Horizontal)) {
return QVariant();
}
switch (section) {
case SortKeyCol:
return QString("Key"); /* no tr() for translations needed, column just used for sorting --> hidden */
case IsKnownCol:
return QString(
"Is known"); /* no tr() for translations needed, column is just used for sorting --> hidden */
case EnabledCol:
return tr("Enabled");
case SetTypeCol:
return tr("Set type");
case ShortNameCol:
return tr("Set code");
case LongNameCol:
return tr("Long name");
case ReleaseDateCol:
return tr("Release date");
default:
return QVariant();
}
}
Qt::ItemFlags SetsModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
Qt::ItemFlags flags = QAbstractTableModel::flags(index) | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
if (index.column() == EnabledCol) {
flags |= Qt::ItemIsUserCheckable;
}
return flags;
}
Qt::DropActions SetsModel::supportedDropActions() const
{
return Qt::MoveAction;
}
QMimeData *SetsModel::mimeData(const QModelIndexList &indexes) const
{
if (indexes.isEmpty()) {
return 0;
}
SetsMimeData *result = new SetsMimeData(indexes[0].row());
return qobject_cast<QMimeData *>(result);
}
bool SetsModel::dropMimeData(const QMimeData *data,
Qt::DropAction action,
int row,
int /*column*/,
const QModelIndex &parent)
{
if (action != Qt::MoveAction) {
return false;
}
if (row == -1) {
if (!parent.isValid()) {
return false;
}
row = parent.row();
}
int oldRow = qobject_cast<const SetsMimeData *>(data)->getOldRow();
if (oldRow < row) {
row--;
}
swapRows(oldRow, row);
return true;
}
void SetsModel::toggleRow(int row, bool enable)
{
CardSetPtr temp = sets.at(row);
if (enable) {
enabledSets.insert(temp);
} else {
enabledSets.remove(temp);
}
emit dataChanged(index(row, EnabledCol), index(row, EnabledCol));
}
void SetsModel::toggleRow(int row)
{
CardSetPtr tmp = sets.at(row);
if (tmp == nullptr) {
return;
}
if (enabledSets.contains(tmp)) {
enabledSets.remove(tmp);
} else {
enabledSets.insert(tmp);
}
emit dataChanged(index(row, EnabledCol), index(row, EnabledCol));
}
void SetsModel::toggleAll(bool enabled)
{
enabledSets.clear();
if (enabled) {
for (CardSetPtr set : sets) {
enabledSets.insert(set);
}
}
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
}
void SetsModel::swapRows(int oldRow, int newRow)
{
beginRemoveRows(QModelIndex(), oldRow, oldRow);
CardSetPtr temp = sets.takeAt(oldRow);
endRemoveRows();
beginInsertRows(QModelIndex(), newRow, newRow);
sets.insert(newRow, temp);
endInsertRows();
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
}
void SetsModel::restoreOriginalOrder()
{
int numRows = rowCount();
sets.defaultSort();
emit dataChanged(index(0, 0), index(numRows - 1, columnCount() - 1));
}
void SetsModel::sort(int column, Qt::SortOrder order)
{
QMultiMap<QString, CardSetPtr> setMap;
int numRows = rowCount();
int row;
for (row = 0; row < numRows; ++row) {
setMap.insert(index(row, column).data(SetsModel::SortRole).toString(), sets.at(row));
}
QList<CardSetPtr> tmp = setMap.values();
sets.clear();
if (order == Qt::AscendingOrder) {
for (row = 0; row < tmp.size(); row++) {
sets.append(tmp.at(row));
}
} else {
for (row = tmp.size() - 1; row >= 0; row--) {
sets.append(tmp.at(row));
}
}
emit dataChanged(index(0, 0), index(numRows - 1, columnCount() - 1));
}
void SetsModel::save(CardDatabase *db)
{
QVector<ICardSetPriorityController::SetSaveData> saveData;
saveData.reserve(sets.size());
for (int i = 0; i < sets.size(); ++i) {
const unsigned int sortKey = static_cast<unsigned int>(i + 1);
const bool enabled = enabledSets.contains(sets[i]);
sets[i]->setSortKeyInMemory(sortKey);
sets[i]->setEnabledInMemory(enabled);
saveData.append({sets[i]->getShortName(), sortKey, enabled});
}
db->getPriorityController()->saveSets(saveData);
sets.sortByKey();
db->notifyEnabledSetsChanged();
}
void SetsModel::restore(CardDatabase *db)
{
// order
sets = db->getSetList();
sets.sortByKey();
// enabled sets
enabledSets.clear();
for (const CardSetPtr &set : sets) {
if (set->getEnabled()) {
enabledSets.insert(set);
}
}
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
}
QStringList SetsModel::mimeTypes() const
{
return QStringList() << "application/x-cockatricecardset";
}
SetsDisplayModel::SetsDisplayModel(QObject *parent) : QSortFilterProxyModel(parent)
{
setFilterCaseSensitivity(Qt::CaseInsensitive);
setSortCaseSensitivity(Qt::CaseInsensitive);
}
void SetsDisplayModel::fetchMore(const QModelIndex &index)
{
int itemsToFetch = sourceModel()->rowCount(index);
beginInsertRows(QModelIndex(), 0, itemsToFetch - 1);
endInsertRows();
}
bool SetsDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
auto typeIndex = sourceModel()->index(sourceRow, SetsModel::SetTypeCol, sourceParent);
auto nameIndex = sourceModel()->index(sourceRow, SetsModel::LongNameCol, sourceParent);
auto shortNameIndex = sourceModel()->index(sourceRow, SetsModel::ShortNameCol, sourceParent);
auto dateIndex = sourceModel()->index(sourceRow, SetsModel::ReleaseDateCol, sourceParent);
const auto filter = filterRegularExpression();
return (sourceModel()->data(typeIndex).toString().contains(filter) ||
sourceModel()->data(nameIndex).toString().contains(filter) ||
sourceModel()->data(shortNameIndex).toString().contains(filter) ||
sourceModel()->data(dateIndex).toString().contains(filter));
}
bool SetsDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
QString leftString = sourceModel()->data(left, SetsModel::SortRole).toString();
QString rightString = sourceModel()->data(right, SetsModel::SortRole).toString();
return QString::localeAwareCompare(leftString, rightString) < 0;
}
@@ -0,0 +1,106 @@
/**
* @file sets_model.h
* @ingroup CardDatabaseModels
*/
//! \todo Document this file.
#ifndef SETSMODEL_H
#define SETSMODEL_H
#include <QAbstractTableModel>
#include <QMimeData>
#include <QSet>
#include <QSortFilterProxyModel>
#include <libcockatrice/card/database/card_database.h>
class SetsProxyModel;
class SetsMimeData : public QMimeData
{
Q_OBJECT
private:
int oldRow;
public:
SetsMimeData(int _oldRow) : oldRow(_oldRow)
{
}
[[nodiscard]] int getOldRow() const
{
return oldRow;
}
[[nodiscard]] QStringList formats() const
{
return QStringList() << "application/x-cockatricecardset";
}
};
class SetsModel : public QAbstractTableModel
{
Q_OBJECT
friend class SetsProxyModel;
private:
static const int NUM_COLS = 7;
CardSetList sets;
QSet<CardSetPtr> enabledSets;
public:
enum SetsColumns
{
SortKeyCol,
IsKnownCol,
EnabledCol,
LongNameCol,
ShortNameCol,
SetTypeCol,
ReleaseDateCol,
PriorityCol
};
enum Role
{
SortRole = Qt::UserRole
};
explicit SetsModel(CardDatabase *_db, QObject *parent = nullptr);
~SetsModel() override;
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
[[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return NUM_COLS;
}
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
[[nodiscard]] QVariant
headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
[[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override;
[[nodiscard]] Qt::DropActions supportedDropActions() const override;
[[nodiscard]] QMimeData *mimeData(const QModelIndexList &indexes) const override;
bool
dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
[[nodiscard]] QStringList mimeTypes() const override;
void swapRows(int oldRow, int newRow);
void toggleRow(int row, bool enable);
void toggleRow(int row);
void toggleAll(bool);
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
void save(CardDatabase *db);
void restore(CardDatabase *db);
void restoreOriginalOrder();
};
class SetsDisplayModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit SetsDisplayModel(QObject *parent = nullptr);
protected:
[[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
[[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
void fetchMore(const QModelIndex &index) override;
};
#endif
@@ -0,0 +1,19 @@
#include "token_display_model.h"
#include "../card_database_model.h"
TokenDisplayModel::TokenDisplayModel(QObject *parent) : CardDatabaseDisplayModel(parent)
{
}
bool TokenDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const
{
CardInfoPtr info = static_cast<CardDatabaseModel *>(sourceModel())->getCard(sourceRow);
return info->getIsToken() && rowMatchesCardName(info);
}
int TokenDisplayModel::rowCount(const QModelIndex &parent) const
{
// always load all tokens at start
return QSortFilterProxyModel::rowCount(parent);
}
@@ -0,0 +1,23 @@
/**
* @file token_display_model.h
* @ingroup CardDatabaseModels
*/
//! \todo Document this file.
#ifndef COCKATRICE_TOKEN_DISPLAY_MODEL_H
#define COCKATRICE_TOKEN_DISPLAY_MODEL_H
#include "../card_database_display_model.h"
class TokenDisplayModel : public CardDatabaseDisplayModel
{
Q_OBJECT
public:
explicit TokenDisplayModel(QObject *parent = nullptr);
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
protected:
[[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};
#endif // COCKATRICE_TOKEN_DISPLAY_MODEL_H
@@ -0,0 +1,22 @@
#include "token_edit_model.h"
#include "../card_database_display_model.h"
#include "../card_database_model.h"
#include <libcockatrice/card/card_info.h>
TokenEditModel::TokenEditModel(QObject *parent) : CardDatabaseDisplayModel(parent)
{
}
bool TokenEditModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const
{
CardInfoPtr info = static_cast<CardDatabaseModel *>(sourceModel())->getCard(sourceRow);
return info->getIsToken() && info->getSets().contains(CardSet::TOKENS_SETNAME) && rowMatchesCardName(info);
}
int TokenEditModel::rowCount(const QModelIndex &parent) const
{
// always load all tokens at start
return QSortFilterProxyModel::rowCount(parent);
}
@@ -0,0 +1,23 @@
/**
* @file token_edit_model.h
* @ingroup CardDatabaseModels
*/
//! \todo Document this file.
#ifndef COCKATRICE_TOKEN_EDIT_MODEL_H
#define COCKATRICE_TOKEN_EDIT_MODEL_H
#include "../card_database_display_model.h"
class TokenEditModel : public CardDatabaseDisplayModel
{
Q_OBJECT
public:
explicit TokenEditModel(QObject *parent = nullptr);
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
protected:
[[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};
#endif // COCKATRICE_TOKEN_EDIT_MODEL_H
@@ -0,0 +1,21 @@
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)
if(Qt6_FOUND)
qt6_wrap_cpp(MOC_SOURCES ${HEADERS})
elseif(Qt5_FOUND)
qt5_wrap_cpp(MOC_SOURCES ${HEADERS})
endif()
add_library(
libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp
)
target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(
libcockatrice_models_deck_list PUBLIC libcockatrice_card libcockatrice_deck_list ${QT_CORE_MODULE}
)
@@ -0,0 +1,779 @@
#include "deck_list_model.h"
#include <libcockatrice/card/database/card_database_manager.h>
DeckListModel::DeckListModel(QObject *parent)
: QAbstractItemModel(parent), lastKnownColumn(1), lastKnownOrder(Qt::AscendingOrder)
{
deckList = QSharedPointer<DeckList>(new DeckList());
root = new InnerDecklistNode;
}
DeckListModel::DeckListModel(QObject *parent, const QSharedPointer<DeckList> &deckList) : DeckListModel(parent)
{
setDeckList(deckList);
// forward change signals
connect(this, &DeckListModel::cardAddedAt, this, &DeckListModel::cardsChanged);
connect(this, &DeckListModel::cardRemoved, this, &DeckListModel::cardsChanged);
connect(this, &DeckListModel::deckReplaced, this, &DeckListModel::cardsChanged);
connect(this, &DeckListModel::cardNodeAddedAt, this, &DeckListModel::cardNodesChanged);
connect(this, &DeckListModel::cardNodeRemoved, this, &DeckListModel::cardNodesChanged);
connect(this, &DeckListModel::deckReplaced, this, &DeckListModel::cardNodesChanged);
}
DeckListModel::~DeckListModel()
{
delete root;
}
/**
* @brief Extract the value from the card that is used for the group criteria.
* @param info Pointer to card information.
* @param criteria The group criteria
* @return String representing the value of the criteria.
*/
static QString extractGroupCriteriaValue(const CardInfoPtr &info, DeckListModelGroupCriteria::Type criteria)
{
if (!info) {
return "unknown";
}
switch (criteria) {
case DeckListModelGroupCriteria::MAIN_TYPE:
return info->getMainCardType();
case DeckListModelGroupCriteria::MANA_COST:
return info->getCmc();
case DeckListModelGroupCriteria::COLOR:
return info->getColors() == "" ? "Colorless" : info->getColors();
default:
return "unknown";
}
}
void DeckListModel::rebuildTree()
{
beginResetModel();
root->clearTree();
InnerDecklistNode *listRoot = deckList->getTree()->getRoot();
for (int i = 0; i < listRoot->size(); i++) {
auto *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
auto *node = new InnerDecklistNode(currentZone->getName(), root);
for (int j = 0; j < currentZone->size(); j++) {
auto *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
//! \todo Better sanity checking.
if (currentCard == nullptr) {
continue;
}
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(currentCard->getName());
QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria);
auto *groupNode = dynamic_cast<InnerDecklistNode *>(node->findChild(groupCriteria));
if (!groupNode) {
groupNode = new InnerDecklistNode(groupCriteria, node);
}
new DecklistModelCardNode(currentCard, groupNode);
}
}
endResetModel();
refreshCardFormatLegalities();
}
int DeckListModel::rowCount(const QModelIndex &parent) const
{
// debugIndexInfo("rowCount", parent);
auto *node = getNode<InnerDecklistNode *>(parent);
if (node) {
return node->size();
} else {
return 0;
}
}
int DeckListModel::columnCount(const QModelIndex & /*parent*/) const
{
return 5;
}
QVariant DeckListModel::data(const QModelIndex &index, int role) const
{
// debugIndexInfo("data", index);
if (!index.isValid()) {
return {};
}
if (index.column() >= columnCount()) {
return {};
}
auto *node = static_cast<AbstractDecklistNode *>(index.internalPointer());
auto *card = dynamic_cast<DecklistModelCardNode *>(node);
// Group node
if (!card) {
const auto *group = dynamic_cast<InnerDecklistNode *>(node);
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole: {
switch (index.column()) {
case DeckListModelColumns::CARD_AMOUNT:
return group->recursiveCount(true);
case DeckListModelColumns::CARD_NAME:
if (role == Qt::DisplayRole) {
return group->getVisibleName();
}
return group->getName();
case DeckListModelColumns::CARD_SET:
return group->getCardSetShortName();
case DeckListModelColumns::CARD_COLLECTOR_NUMBER:
return group->getCardCollectorNumber();
case DeckListModelColumns::CARD_PROVIDER_ID:
return group->getCardProviderId();
default:
return {};
}
}
case DeckRoles::IsCardRole:
return false;
case DeckRoles::DepthRole:
return group->depth();
// legality does not apply to group nodes
case DeckRoles::IsLegalRole:
return true;
default:
return {};
}
}
// Card node
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
switch (index.column()) {
case DeckListModelColumns::CARD_AMOUNT:
return card->getNumber();
case DeckListModelColumns::CARD_NAME:
return card->getName();
case DeckListModelColumns::CARD_SET:
return card->getCardSetShortName();
case DeckListModelColumns::CARD_COLLECTOR_NUMBER:
return card->getCardCollectorNumber();
case DeckListModelColumns::CARD_PROVIDER_ID:
return card->getCardProviderId();
default:
return {};
}
case DeckRoles::IsCardRole: {
return true;
}
case DeckRoles::DepthRole: {
return card->depth();
}
case DeckRoles::IsLegalRole: {
return card->getFormatLegality();
}
default: {
return {};
}
}
}
QVariant DeckListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const
{
if ((role != Qt::DisplayRole) || (orientation != Qt::Horizontal)) {
return {};
}
if (section >= columnCount()) {
return {};
}
switch (section) {
case DeckListModelColumns::CARD_AMOUNT:
return tr("Count");
case DeckListModelColumns::CARD_NAME:
return tr("Card");
case DeckListModelColumns::CARD_SET:
return tr("Set");
case DeckListModelColumns::CARD_COLLECTOR_NUMBER:
return tr("Number");
case DeckListModelColumns::CARD_PROVIDER_ID:
return tr("Provider ID");
default:
return {};
}
}
QModelIndex DeckListModel::index(int row, int column, const QModelIndex &parent) const
{
// debugIndexInfo("index", parent);
if (!hasIndex(row, column, parent)) {
return {};
}
auto *parentNode = getNode<InnerDecklistNode *>(parent);
return row >= parentNode->size() ? QModelIndex() : createIndex(row, column, parentNode->at(row));
}
QModelIndex DeckListModel::parent(const QModelIndex &ind) const
{
if (!ind.isValid()) {
return {};
}
return nodeToIndex(static_cast<AbstractDecklistNode *>(ind.internalPointer())->getParent());
}
Qt::ItemFlags DeckListModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
Qt::ItemFlags result = Qt::ItemIsEnabled;
result |= Qt::ItemIsSelectable;
return result;
}
void DeckListModel::emitBackgroundUpdates(const QModelIndex &parent)
{
int rows = rowCount(parent);
if (rows == 0) {
return;
}
QModelIndex topLeft = index(0, 0, parent);
QModelIndex bottomRight = index(rows - 1, columnCount() - 1, parent);
emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole});
for (int r = 0; r < rows; ++r) {
QModelIndex child = index(r, 0, parent);
emitBackgroundUpdates(child);
}
}
void DeckListModel::emitRecursiveUpdates(const QModelIndex &index)
{
if (!index.isValid()) {
return;
}
emit dataChanged(index, index);
emitRecursiveUpdates(index.parent());
}
bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, const int role)
{
auto *node = getNode<DecklistModelCardNode *>(index);
if (!node || (role != Qt::EditRole)) {
return false;
}
switch (index.column()) {
case DeckListModelColumns::CARD_AMOUNT:
node->setNumber(value.toInt());
refreshCardFormatLegalities();
break;
case DeckListModelColumns::CARD_NAME:
node->setName(value.toString());
break;
case DeckListModelColumns::CARD_SET:
node->setCardSetShortName(value.toString());
break;
case DeckListModelColumns::CARD_COLLECTOR_NUMBER:
node->setCardCollectorNumber(value.toString());
break;
case DeckListModelColumns::CARD_PROVIDER_ID:
node->setCardProviderId(value.toString());
break;
default:
return false;
}
emitRecursiveUpdates(index);
deckList->refreshDeckHash();
emit deckHashChanged();
return true;
}
bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent)
{
auto *node = getNode<InnerDecklistNode *>(parent);
if (!node) {
return false;
}
if (row + count > node->size()) {
return false;
}
beginRemoveRows(parent, row, row + count - 1);
for (int i = 0; i < count; i++) {
AbstractDecklistNode *toDelete = node->takeAt(row);
if (auto *temp = dynamic_cast<DecklistModelCardNode *>(toDelete)) {
deckList->getTree()->deleteNode(temp->getDataNode());
}
delete toDelete;
}
endRemoveRows();
if (node->empty() && (node != root)) {
removeRows(parent.row(), 1, parent.parent());
} else {
emitRecursiveUpdates(parent);
}
deckList->refreshDeckHash();
emit deckHashChanged();
return true;
}
InnerDecklistNode *DeckListModel::createNodeIfNeeded(const QString &name, InnerDecklistNode *parent)
{
auto *newNode = dynamic_cast<InnerDecklistNode *>(parent->findChild(name));
if (!newNode) {
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
newNode = new InnerDecklistNode(name, parent);
endInsertRows();
}
return newNode;
}
DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName,
const QString &zoneName,
const QString &providerId,
const QString &cardNumber) const
{
InnerDecklistNode *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName));
if (!zoneNode) {
return nullptr;
}
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName);
if (!info) {
return nullptr;
}
QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria);
InnerDecklistNode *groupNode = dynamic_cast<InnerDecklistNode *>(zoneNode->findChild(groupCriteria));
if (!groupNode) {
return nullptr;
}
return dynamic_cast<DecklistModelCardNode *>(
groupNode->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber));
}
QModelIndex DeckListModel::findCard(const QString &cardName,
const QString &zoneName,
const QString &providerId,
const QString &cardNumber) const
{
DecklistModelCardNode *cardNode = findCardNode(cardName, zoneName, providerId, cardNumber);
if (!cardNode) {
return {};
}
return nodeToIndex(cardNode);
}
QModelIndex DeckListModel::addPreferredPrintingCard(const QString &cardName, const QString &zoneName, bool abAddAnyway)
{
ExactCard card = CardDatabaseManager::query()->getCard({cardName});
if (!card) {
if (abAddAnyway) {
// We need to keep this card added no matter what
// This is usually called from tab_deck_editor
// So we'll create a new CardInfo with the name
// and default values for all fields
card = ExactCard(CardInfo::newInstance(cardName));
} else {
return {};
}
}
return addCard(card, zoneName);
}
QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneName)
{
if (!card) {
return {};
}
InnerDecklistNode *zoneNode = createNodeIfNeeded(zoneName, root);
CardInfoPtr cardInfo = card.getCardPtr();
PrintingInfo printingInfo = card.getPrinting();
QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria);
InnerDecklistNode *groupNode = createNodeIfNeeded(groupCriteria, zoneNode);
const QModelIndex parentIndex = nodeToIndex(groupNode);
auto *cardNode = dynamic_cast<DecklistModelCardNode *>(groupNode->findCardChildByNameProviderIdAndNumber(
card.getName(), printingInfo.getUuid(), printingInfo.getProperty("num")));
const auto cardSetName = printingInfo.getSet().isNull() ? "" : printingInfo.getSet()->getCorrectedShortName();
bool cardNodeAdded = false;
if (!cardNode) {
// Determine the correct index
int insertRow = findSortedInsertRow(groupNode, cardInfo);
auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName,
printingInfo.getProperty("num"), printingInfo.getProperty("uuid"));
beginInsertRows(parentIndex, insertRow, insertRow);
cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow);
endInsertRows();
cardNodeAdded = true;
} else {
cardNode->setNumber(cardNode->getNumber() + 1);
cardNode->setCardSetShortName(cardSetName);
cardNode->setCardCollectorNumber(printingInfo.getProperty("num"));
cardNode->setCardProviderId(printingInfo.getProperty("uuid"));
// Emit dataChanged for the amount column since we modified it
QModelIndex cardIndex = nodeToIndex(cardNode);
QModelIndex amountIndex = cardIndex.sibling(cardIndex.row(), DeckListModelColumns::CARD_AMOUNT);
emit dataChanged(amountIndex, amountIndex, {Qt::EditRole});
}
sort(lastKnownColumn, lastKnownOrder);
refreshCardFormatLegalities();
emitRecursiveUpdates(parentIndex);
deckList->refreshDeckHash();
emit deckHashChanged();
auto index = nodeToIndex(cardNode);
if (cardNodeAdded) {
emit cardNodeAddedAt(index);
}
emit cardAddedAt(index);
return index;
}
bool DeckListModel::offsetCountAtIndex(const QModelIndex &idx, int offset)
{
if (!idx.isValid()) {
return false;
}
auto *node = static_cast<AbstractDecklistNode *>(idx.internalPointer());
auto *card = dynamic_cast<DecklistModelCardNode *>(node);
if (!card) {
return false;
}
const QModelIndex numberIndex = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT);
const int count = numberIndex.data(Qt::EditRole).toInt();
const int newCount = count + offset;
if (newCount <= 0) {
removeRow(idx.row(), idx.parent());
emit cardNodeRemoved();
} else {
setData(numberIndex, newCount, Qt::EditRole);
}
if (offset > 0) {
emit cardAddedAt(idx);
} else if (offset < 0) {
emit cardRemoved();
}
return true;
}
bool DeckListModel::removeCardAtIndex(const QModelIndex &idx)
{
if (!idx.isValid()) {
return false;
}
auto *node = static_cast<AbstractDecklistNode *>(idx.internalPointer());
auto *card = dynamic_cast<DecklistModelCardNode *>(node);
if (!card) {
return false;
}
bool success = removeRow(idx.row(), idx.parent());
if (success) {
emit cardRemoved();
}
return success;
}
int DeckListModel::findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const
{
if (!cardInfo) {
return parent->size(); // fallback: append at end
}
for (int i = 0; i < parent->size(); ++i) {
auto *existingCard = dynamic_cast<DecklistModelCardNode *>(parent->at(i));
if (!existingCard) {
continue;
}
bool lessThan = false;
switch (lastKnownColumn) {
case 0: // ByNumber
lessThan = lastKnownOrder == Qt::AscendingOrder
? cardInfo->getProperty("collectorNumber") < existingCard->getCardCollectorNumber()
: cardInfo->getProperty("collectorNumber") > existingCard->getCardCollectorNumber();
break;
case 1: // ByName
default:
lessThan = lastKnownOrder == Qt::AscendingOrder
? cardInfo->getName().localeAwareCompare(existingCard->getName()) < 0
: cardInfo->getName().localeAwareCompare(existingCard->getName()) > 0;
break;
}
if (lessThan) {
return i;
}
}
return parent->size(); // insert at end if no earlier match
}
QModelIndex DeckListModel::nodeToIndex(AbstractDecklistNode *node) const
{
if (node == nullptr || node == root) {
return {};
}
return createIndex(node->getParent()->indexOf(node), 0, node);
}
void DeckListModel::sortHelper(InnerDecklistNode *node, Qt::SortOrder order)
{
// Sort children of node and save the information needed to
// update the list of persistent indexes.
QVector<QPair<int, int>> sortResult = node->sort(order);
QModelIndexList from, to;
int columns = columnCount();
for (int i = sortResult.size() - 1; i >= 0; --i) {
const int fromRow = sortResult[i].first;
const int toRow = sortResult[i].second;
AbstractDecklistNode *temp = node->at(toRow);
for (int j = 0; j < columns; ++j) {
from << createIndex(fromRow, j, temp);
to << createIndex(toRow, j, temp);
}
}
changePersistentIndexList(from, to);
// Recursion
for (int i = node->size() - 1; i >= 0; --i) {
auto *subNode = dynamic_cast<InnerDecklistNode *>(node->at(i));
if (subNode) {
sortHelper(subNode, order);
}
}
}
void DeckListModel::sort(int column, Qt::SortOrder order)
{
lastKnownColumn = column;
lastKnownOrder = order;
emit layoutAboutToBeChanged();
DeckSortMethod sortMethod;
switch (column) {
case 0:
sortMethod = ByNumber;
break;
case 1:
sortMethod = ByName;
break;
default:
sortMethod = ByName;
}
root->setSortMethod(sortMethod);
sortHelper(root, order);
emit layoutChanged();
}
void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria)
{
activeGroupCriteria = newCriteria;
rebuildTree();
}
void DeckListModel::setActiveFormat(const QString &_format)
{
deckList->setGameFormat(_format);
refreshCardFormatLegalities();
emitBackgroundUpdates(QModelIndex()); // start from root
}
void DeckListModel::cleanList()
{
setDeckList(QSharedPointer<DeckList>(new DeckList()));
}
/**
* @param _deck The deck.
*/
void DeckListModel::setDeckList(const QSharedPointer<DeckList> &_deck)
{
if (deckList != _deck) {
deckList = _deck;
}
rebuildTree();
emit deckReplaced();
}
void DeckListModel::forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func)
{
deckList->forEachCard(func);
}
QList<const DecklistCardNode *> DeckListModel::getCardNodes() const
{
return deckList->getCardNodes();
}
QList<const DecklistCardNode *> DeckListModel::getCardNodesForZone(const QString &zoneName) const
{
return deckList->getCardNodes({zoneName});
}
QList<QString> DeckListModel::getCardNames() const
{
auto nodes = deckList->getCardNodes();
QList<QString> names;
std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(names), [](auto node) { return node->getName(); });
return names;
}
QList<CardRef> DeckListModel::getCardRefs() const
{
auto nodes = deckList->getCardNodes();
QList<CardRef> cardRefs;
std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(cardRefs),
[](auto node) { return node->toCardRef(); });
return cardRefs;
}
QList<QString> DeckListModel::getZones() const
{
auto zoneNodes = deckList->getZoneNodes();
QList<QString> zones;
std::transform(zoneNodes.cbegin(), zoneNodes.cend(), std::back_inserter(zones),
[](auto zoneNode) { return zoneNode->getName(); });
return zones;
}
static int maxAllowedForLegality(const FormatRules &format, const QString &legality)
{
for (const AllowedCount &c : format.allowedCounts) {
if (c.label == legality) {
return c.max;
}
}
return -1; // unknown legality → treat as illegal
}
static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &cardInfo, int quantity)
{
if (format.isEmpty()) {
return true;
}
auto formatRules = CardDatabaseManager::query()->getFormat(format);
// if format has no custom rules, then just do the default check
if (!formatRules) {
return cardInfo.isLegalInFormat(format);
}
// Exceptions always win
if (cardHasAnyException(cardInfo, *formatRules)) {
return true;
}
// check legality prop
const QString legality = cardInfo.getLegalityProp(format);
if (legality.isEmpty()) {
return false;
}
int maxAllowed = maxAllowedForLegality(*formatRules, legality);
if (maxAllowed == -1) {
return false;
}
if (maxAllowed < 0) { // unlimited
return true;
}
return quantity <= maxAllowed;
}
static bool isCardNodeLegalForFormat(const QString &format, const InnerDecklistNode *zone, const DecklistCardNode *card)
{
// Don't check legality for tokens
if (zone->getName() == DECK_ZONE_TOKENS) {
return true;
}
// unknown cards are not legal
ExactCard exactCard = CardDatabaseManager::query()->getCard(card->toCardRef());
if (!exactCard) {
return false;
}
// actual check
return isCardQuantityLegalForFormat(format, exactCard.getInfo(), card->getNumber());
}
void DeckListModel::refreshCardFormatLegalities()
{
QString format = deckList->getGameFormat();
deckList->forEachCard([&format](const InnerDecklistNode *zone, DecklistCardNode *card) {
bool legal = isCardNodeLegalForFormat(format, zone, card);
card->setFormatLegality(legal);
});
}
@@ -0,0 +1,442 @@
#ifndef DECKLISTMODEL_H
#define DECKLISTMODEL_H
#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h>
#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <QAbstractItemModel>
#include <QList>
#include <libcockatrice/card/printing/exact_card.h>
#include <libcockatrice/deck_list/deck_list.h>
class CardDatabase;
class QPrinter;
class QTextCursor;
/**
* @namespace DeckRoles
* @brief Custom model roles used by the DeckListModel for data retrieval.
*
* These roles extend Qt's item data roles starting at Qt::UserRole.
*/
namespace DeckRoles
{
/**
* @enum DeckRoles
* @brief Custom data roles for deck-related model items.
*
* These roles are used to retrieve specialized data from the DeckListModel.
*/
enum
{
IsCardRole = Qt::UserRole + 1, /**< Indicates whether the item represents a card. */
DepthRole, /**< Depth level within the deck's grouping hierarchy. */
IsLegalRole /**< Whether the card is legal in the current deck format. */
};
} // namespace DeckRoles
/**
* @namespace DeckListModelColumns
* @brief Column indices for the DeckListModel.
*
* These values map to the columns in the deck list table representation.
*/
namespace DeckListModelColumns
{
/**
* @enum DeckListModelColumns
* @brief Column identifiers for displaying card information in the deck list.
*/
enum
{
CARD_AMOUNT = 0, /**< The number of copies of the card. */
CARD_NAME = 1, /**< The card's name. */
CARD_SET = 2, /**< The set or expansion the card belongs to. */
CARD_COLLECTOR_NUMBER = 3, /**< Collector number of the card within the set. */
CARD_PROVIDER_ID = 4 /**< ID used by the external data provider (e.g., Scryfall). */
};
} // namespace DeckListModelColumns
/**
* @namespace DeckListModelGroupCriteria
* @brief Specifies criteria used to group cards in the DeckListModel.
*
* These values determine how cards are grouped in UI views such as the deck editor.
*/
namespace DeckListModelGroupCriteria
{
/**
* @enum DeckListModelGroupCriteria
* @brief Available grouping strategies for deck visualization.
*/
enum Type
{
MAIN_TYPE, /**< Group cards by their main type (e.g., creature, instant). */
MANA_COST, /**< Group cards by their total mana cost. */
COLOR /**< Group cards by their color identity. */
};
static inline QString toString(Type t)
{
switch (t) {
case MAIN_TYPE:
return "Main Type";
case MANA_COST:
return "Mana Cost";
case COLOR:
return "Colors";
}
return {};
}
static inline Type fromString(const QString &s)
{
if (s == "Main Type") {
return MAIN_TYPE;
}
if (s == "Mana Cost") {
return MANA_COST;
}
if (s == "Colors") {
return COLOR;
}
return MAIN_TYPE; // default
}
} // namespace DeckListModelGroupCriteria
/**
* @class DecklistModelCardNode
* @ingroup DeckModels
* @brief Adapter node that wraps a DecklistCardNode for use in the DeckListModel tree.
*
* This class forwards all property accessors (name, number, provider ID, set info, etc.)
* to the underlying DecklistCardNode. It exists so the model can represent cards
* in the same hierarchy as InnerDecklistNode containers.
*/
class DecklistModelCardNode : public AbstractDecklistCardNode
{
private:
DecklistCardNode *dataNode; /**< Pointer to the underlying data node. */
public:
/**
* @brief Constructs a model node wrapping a DecklistCardNode.
* @param _dataNode The underlying DecklistCardNode to wrap.
* @param _parent The parent InnerDecklistNode in the model tree.
* @param position Optional position to insert in parent (-1 appends at end).
*/
DecklistModelCardNode(DecklistCardNode *_dataNode, InnerDecklistNode *_parent, int position = -1)
: AbstractDecklistCardNode(_parent, position), dataNode(_dataNode)
{
}
[[nodiscard]] int getNumber() const override
{
return dataNode->getNumber();
}
void setNumber(int _number) override
{
dataNode->setNumber(_number);
}
[[nodiscard]] QString getName() const override
{
return dataNode->getName();
}
void setName(const QString &_name) override
{
dataNode->setName(_name);
}
[[nodiscard]] QString getCardProviderId() const override
{
return dataNode->getCardProviderId();
}
void setCardProviderId(const QString &_cardProviderId) override
{
dataNode->setCardProviderId(_cardProviderId);
}
[[nodiscard]] QString getCardSetShortName() const override
{
return dataNode->getCardSetShortName();
}
void setCardSetShortName(const QString &_cardSetShortName) override
{
dataNode->setCardSetShortName(_cardSetShortName);
}
[[nodiscard]] QString getCardCollectorNumber() const override
{
return dataNode->getCardCollectorNumber();
}
void setCardCollectorNumber(const QString &_cardSetNumber) override
{
dataNode->setCardCollectorNumber(_cardSetNumber);
}
bool getFormatLegality() const override
{
return dataNode->getFormatLegality();
}
void setFormatLegality(const bool _formatLegal) override
{
dataNode->setFormatLegality(_formatLegal);
}
/**
* @brief Returns the underlying data node.
* @return Pointer to the DecklistCardNode wrapped by this node.
*/
[[nodiscard]] DecklistCardNode *getDataNode() const
{
return dataNode;
}
[[nodiscard]] bool isDeckHeader() const override
{
return false;
}
};
/**
* @class DeckListModel
* @ingroup DeckModels
* @brief Qt model representing a decklist for use in views (tree/table).
*
* DeckListModel is a QAbstractItemModel that exposes the structure of a deck
* (zones and cards) to Qt views. It organizes cards hierarchically under
* InnerDecklistNode containers and supports grouping, sorting, adding/removing
* cards, and printing decklists.
*
* Outside code should refrain from modifying the model with methods inherited from QAbstractItemModel, such as with
* `setData` or `removeRow`.
* Instead, use the custom methods on this class to modify the model, such as `addCard`, `offsetCountAtIndex`, or
* `removeCardAtIndex`.
* This ensures the custom signals for this class are correctly emitted.
*
* Signals:
* - deckHashChanged(): emitted when the deck contents change in a way that
* affects its hash.
*
* Slots:
* - rebuildTree(): rebuilds the model structure from the underlying node tree.
*/
class DeckListModel : public QAbstractItemModel
{
Q_OBJECT
public slots:
/**
* @brief Rebuilds the model tree from the underlying node tree.
*
* This updates all indices and ensures the model reflects the current
* state of the deck.
*/
void rebuildTree();
/**
* @brief Sets the criteria used to group cards in the model.
* @param newCriteria The new grouping criteria.
*/
void setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria);
void setActiveFormat(const QString &_format);
signals:
/**
* @brief Emitted whenever the deck hash changes due to modifications in the model.
*/
void deckHashChanged();
/**
* @brief Emitted whenever the cards in the deck changes. This includes when the deck is replaced.
*/
void cardsChanged();
/**
* @brief Emitted whenever a card is added to the deck, regardless of whether it's an entirely new card or an
* existing card that got incremented.
* @param index The index of the card that got added.
*/
void cardAddedAt(const QModelIndex &index);
/**
* @brief Emitted whenever a card is removed from the deck, regardless of whether a card node was removed or an
* existing card got decremented.
*/
void cardRemoved();
/**
* @brief Emitted whenever a card node is added or removed. This includes when the deck is replaced.
*/
void cardNodesChanged();
/**
* @brief Emitted whenever a new card node is added.
* @param index The index of the card node that got added.
*/
void cardNodeAddedAt(const QModelIndex &index);
/**
* @brief Emitted whenever a card node is removed.
*/
void cardNodeRemoved();
/**
* @brief Emitted whenever the deck in the model has been replaced with a new one
*/
void deckReplaced();
public:
explicit DeckListModel(QObject *parent = nullptr);
explicit DeckListModel(QObject *parent, const QSharedPointer<DeckList> &deckList);
~DeckListModel() override;
/**
* @brief Returns the root index of the model.
* @return QModelIndex representing the root node.
*/
[[nodiscard]] QModelIndex getRoot() const
{
return nodeToIndex(root);
}
/// @name Qt model overrides
///@{
[[nodiscard]] int rowCount(const QModelIndex &parent) const override;
[[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
[[nodiscard]] QModelIndex index(int row, int column, const QModelIndex &parent) const override;
[[nodiscard]] QModelIndex parent(const QModelIndex &index) const override;
[[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
bool removeRows(int row, int count, const QModelIndex &parent) override;
void sort(int column, Qt::SortOrder order) override;
///@}
/**
* @brief Finds a card by name, zone, and optional identifiers.
* @param cardName The card's name.
* @param zoneName The zone to search in (main/side/etc.).
* @param providerId Optional provider-specific ID.
* @param cardNumber Optional collector number.
* @return QModelIndex of the card, or invalid index if not found.
*/
[[nodiscard]] QModelIndex findCard(const QString &cardName,
const QString &zoneName,
const QString &providerId = "",
const QString &cardNumber = "") const;
/**
* @brief Adds a card using the preferred printing if available.
*
* @param cardName Name of the card to add.
* @param zoneName Zone to insert the card into.
* @param abAddAnyway Whether to add the card even if resolution fails.
* @return QModelIndex pointing to the newly inserted card node.
*/
QModelIndex addPreferredPrintingCard(const QString &cardName, const QString &zoneName, bool abAddAnyway);
/**
* @brief Adds an ExactCard to the specified zone.
* @param card The card to add.
* @param zoneName The zone to insert the card into.
* @return QModelIndex pointing to the newly inserted card node.
*/
QModelIndex addCard(const ExactCard &card, const QString &zoneName);
/**
* @brief Changes the `amount` field in the card node at the index by the amount.
* Removes the node if it causes the amount to fall to 0 or below.
* @param idx The index of a card node. No-ops if the index is invalid or not a card node.
* @param offset The amount to change the amount field by.
* @return Whether the operation was successful
*/
bool offsetCountAtIndex(const QModelIndex &idx, int offset);
/**
* @brief Removes the card node at the index
* @param idx The index of a card node. No-ops if the index is invalid or not a card node.
* @return Whether the node was removed.
*/
bool removeCardAtIndex(const QModelIndex &idx);
/**
* @brief Removes all cards and resets the model.
*/
void cleanList();
[[nodiscard]] QSharedPointer<DeckList> getDeckList() const
{
return deckList;
}
void setDeckList(const QSharedPointer<DeckList> &_deck);
/**
* @brief Apply a function to every card in the deck tree.
*
* @param func Function taking (zone node, card node).
*/
void forEachCard(const std::function<void(InnerDecklistNode *, DecklistCardNode *)> &func);
/**
* @brief Gets a list of all card nodes in the deck.
*/
[[nodiscard]] QList<const DecklistCardNode *> getCardNodes() const;
[[nodiscard]] QList<const DecklistCardNode *> getCardNodesForZone(const QString &zoneName) const;
/**
* @brief Gets a deduplicated list of all card names that appear in the model
*/
[[nodiscard]] QList<QString> getCardNames() const;
/**
* @brief Gets a deduplicated list of all CardRefs that appear in the model
*/
[[nodiscard]] QList<CardRef> getCardRefs() const;
/**
* @brief Gets a list of all zone names that appear in the model
*/
[[nodiscard]] QList<QString> getZones() const;
private:
QSharedPointer<DeckList> deckList; /**< Pointer to the decklist providing the underlying data. */
InnerDecklistNode *root; /**< Root node of the model tree. */
DeckListModelGroupCriteria::Type activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE;
int lastKnownColumn; /**< Last column used for sorting. */
Qt::SortOrder lastKnownOrder; /**< Last known sort order. */
InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent);
QModelIndex nodeToIndex(AbstractDecklistNode *node) const;
[[nodiscard]] DecklistModelCardNode *findCardNode(const QString &cardName,
const QString &zoneName,
const QString &providerId = "",
const QString &cardNumber = "") const;
/**
* @brief Determines the sorted insertion row for a card.
* @param parent The parent node where the card will be inserted.
* @param cardInfo The card info to insert.
* @return Row index where the card should be inserted to maintain sort order.
*/
int findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const;
/**
* @brief Recursively emits the dataChanged signal with role as Qt::BackgroundRole for all indices that are children
* of the given node. This is used to update the background color when changing formats.
* @param parent The parent node
*/
void emitBackgroundUpdates(const QModelIndex &parent);
/**
* @brief Recursively emits the dataChanged signal for the given node and all parent nodes.
* @param index The parent node
*/
void emitRecursiveUpdates(const QModelIndex &index);
void sortHelper(InnerDecklistNode *node, Qt::SortOrder order);
template <typename T> T getNode(const QModelIndex &index) const
{
if (!index.isValid()) {
return dynamic_cast<T>(root);
}
return dynamic_cast<T>(static_cast<AbstractDecklistNode *>(index.internalPointer()));
}
void refreshCardFormatLegalities();
};
#endif
@@ -0,0 +1,59 @@
#include "deck_list_sort_filter_proxy_model.h"
#include "deck_list_model.h"
bool DeckListSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
auto *src = sourceModel();
// Inner nodes? -> sort alphabetically by column 1
bool leftIsCard = src->data(left, Qt::UserRole + 1).toBool();
bool rightIsCard = src->data(right, Qt::UserRole + 1).toBool();
if (!leftIsCard || !rightIsCard) {
QString lName = src->data(left.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString();
QString rName = src->data(right.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString();
return lName.localeAwareCompare(rName) < 0;
}
// Both are cards -> apply sort criteria
auto *lNode = static_cast<DecklistModelCardNode *>(left.internalPointer());
auto *rNode = static_cast<DecklistModelCardNode *>(right.internalPointer());
CardInfoPtr lInfo = CardDatabaseManager::query()->guessCard({lNode->getName()}).getCardPtr();
CardInfoPtr rInfo = CardDatabaseManager::query()->guessCard({rNode->getName()}).getCardPtr();
// Example: multiple tie-break criteria (colors > cmc > name)
for (const QString &crit : sortCriteria) {
if (crit == "name") {
QString ln = lNode->getName();
QString rn = rNode->getName();
int cmp = ln.localeAwareCompare(rn);
if (cmp != 0) {
return cmp < 0;
}
} else if (crit == "cmc") {
int lc = lInfo ? lInfo->getCmc().toInt() : 0;
int rc = rInfo ? rInfo->getCmc().toInt() : 0;
if (lc != rc) {
return lc < rc;
}
} else if (crit == "colors") {
QString lr = lInfo ? lInfo->getColors() : QString();
QString rr = rInfo ? rInfo->getColors() : QString();
int cmp = lr.localeAwareCompare(rr);
if (cmp != 0) {
return cmp < 0;
}
} else if (crit == "maintype") {
QString lr = lInfo ? lInfo->getMainCardType() : QString();
QString rr = rInfo ? rInfo->getMainCardType() : QString();
int cmp = lr.localeAwareCompare(rr);
if (cmp != 0) {
return cmp < 0;
}
}
}
return false;
}
@@ -0,0 +1,34 @@
/**
* @file deck_list_sort_filter_proxy_model.h
* @ingroup DeckEditorCardGroupWidgets
*/
//! \todo Document this file.
#ifndef COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H
#define COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H
#include <QSortFilterProxyModel>
#include <libcockatrice/card/database/card_database_manager.h>
class DeckListSortFilterProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit DeckListSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent)
{
}
void setSortCriteria(const QStringList &criteria)
{
sortCriteria = criteria;
invalidate(); // re-sort
}
protected:
[[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
private:
QStringList sortCriteria;
};
#endif // COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H