Move models to lib (#6274)

* Move models to own library.

Took 35 minutes


Took 22 minutes

* Adjust CMakeLists

Took 20 seconds

* Reformat CMakeLists.

Took 2 minutes

* Revert "Reformat CMakeLists."

This reverts commit db5982ad1c.


Took 55 seconds

* Lint an include

Took 17 minutes

Took 9 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2025-11-05 18:33:40 +01:00 committed by GitHub
parent d914667238
commit adff828415
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 127 additions and 105 deletions

View file

@ -0,0 +1,41 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(HEADERS
libcockatrice/models/database/card_database_model.h
libcockatrice/models/database/card_database_display_model.h
libcockatrice/models/database/card/card_completer_proxy_model.h
libcockatrice/models/database/card/card_search_model.h
libcockatrice/models/database/card_set/card_sets_model.h
libcockatrice/models/database/token/token_display_model.h
libcockatrice/models/database/token/token_edit_model.h
libcockatrice/models/deck_list/deck_list_model.h
libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h
libcockatrice/models/deck_list/deck_loader.h
)
if(Qt6_FOUND)
qt6_wrap_cpp(MOC_SOURCES ${HEADERS})
elseif(Qt5_FOUND)
qt5_wrap_cpp(MOC_SOURCES ${HEADERS})
endif()
add_library(
libcockatrice_models STATIC
${MOC_SOURCES}
libcockatrice/models/database/card_database_model.cpp
libcockatrice/models/database/card_database_display_model.cpp
libcockatrice/models/database/card/card_completer_proxy_model.cpp
libcockatrice/models/database/card/card_search_model.cpp
libcockatrice/models/database/card_set/card_sets_model.cpp
libcockatrice/models/database/token/token_display_model.cpp
libcockatrice/models/database/token/token_edit_model.cpp
libcockatrice/models/deck_list/deck_list_model.cpp
libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp
libcockatrice/models/deck_list/deck_loader.cpp
)
target_include_directories(libcockatrice_models PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(libcockatrice_models PUBLIC libcockatrice_card libcockatrice_deck_list ${COCKATRICE_QT_MODULES})

View file

@ -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());
}

View file

@ -0,0 +1,22 @@
/**
* @file card_completer_proxy_model.h
* @ingroup CardDatabaseModels
* @brief TODO: Document this.
*/
#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:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};
#endif // CARD_COMPLETER_PROXY_MODEL_H

View file

@ -0,0 +1,73 @@
#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();
}

View file

@ -0,0 +1,36 @@
/**
* @file card_search_model.h
* @ingroup CardDatabaseModels
* @brief TODO: Document this.
*/
#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);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
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

View file

@ -0,0 +1,227 @@
#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, &CardDatabaseDisplayModel::invalidate);
loadedRowCount = 0;
}
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);
}

View file

@ -0,0 +1,99 @@
/**
* @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 "filter_string.h"
#include <QList>
#include <QSet>
#include <QSortFilterProxyModel>
#include <QTimer>
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 setFilterTree(FilterTree *_filterTree);
void setIsToken(FilterBool _isToken)
{
isToken = _isToken;
emit modelDirty();
dirty();
}
void setCardName(const QString &_cardName)
{
if (filterString != nullptr) {
delete filterString;
filterString = nullptr;
}
cardName = sanitizeCardName(_cardName, characterTranslation);
emit modelDirty();
dirty();
}
void setStringFilter(const QString &_src)
{
delete filterString;
filterString = new FilterString(_src);
emit modelDirty();
dirty();
}
void setCardNameSet(const QSet<QString> &_cardNameSet)
{
cardNameSet = _cardNameSet;
emit modelDirty();
dirty();
}
void dirty()
{
dirtyTimer.start(20);
}
void clearFilterAll();
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
signals:
void modelDirty();
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
static int lessThanNumerically(const QString &left, const QString &right);
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
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

View file

@ -0,0 +1,147 @@
#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(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(CardInfoPtr card)
{
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(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();
}

View file

@ -0,0 +1,61 @@
/**
* @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;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
CardDatabase *getDatabase() const
{
return db;
}
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(CardInfoPtr card);
private slots:
void cardAdded(CardInfoPtr card);
void cardRemoved(CardInfoPtr card);
void cardInfoChanged(CardInfoPtr card);
void cardDatabaseEnabledSetsChanged();
};
#endif

View file

@ -0,0 +1,303 @@
#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(set->getSortKey(), 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)
{
// order
for (int i = 0; i < sets.size(); i++)
sets[i]->setSortKey(static_cast<unsigned int>(i + 1));
// enabled sets
for (const CardSetPtr &set : sets)
set->setEnabled(enabledSets.contains(set));
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);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
const auto filter = filterRegularExpression();
#else
const auto filter = filterRegExp();
#endif
return (sourceModel()->data(typeIndex).toString().contains(filter) ||
sourceModel()->data(nameIndex).toString().contains(filter) ||
sourceModel()->data(shortNameIndex).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;
}

View file

@ -0,0 +1,105 @@
/**
* @file sets_model.h
* @ingroup CardDatabaseModels
* @brief TODO: Document this.
*/
#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)
{
}
int getOldRow() const
{
return oldRow;
}
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;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return NUM_COLS;
}
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
Qt::DropActions supportedDropActions() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
bool
dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
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:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
void fetchMore(const QModelIndex &index) override;
};
#endif

View file

@ -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);
}

View file

@ -0,0 +1,23 @@
/**
* @file token_display_model.h
* @ingroup CardDatabaseModels
* @brief TODO: Document this.
*/
#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);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};
#endif // COCKATRICE_TOKEN_DISPLAY_MODEL_H

View file

@ -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);
}

View file

@ -0,0 +1,23 @@
/**
* @file token_edit_model.h
* @ingroup CardDatabaseModels
* @brief TODO: Document this.
*/
#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);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};
#endif // COCKATRICE_TOKEN_EDIT_MODEL_H

View file

@ -0,0 +1,726 @@
#include "deck_list_model.h"
#include "deck_loader.h"
#include <QBrush>
#include <QFont>
#include <QPrinter>
#include <QProgressDialog>
#include <QTextCursor>
#include <QTextDocument>
#include <QTextStream>
#include <QTextTable>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/cache_settings.h>
DeckListModel::DeckListModel(QObject *parent)
: QAbstractItemModel(parent), lastKnownColumn(1), lastKnownOrder(Qt::AscendingOrder)
{
deckList = new DeckLoader;
deckList->setParent(this);
connect(deckList, &DeckLoader::deckLoaded, this, &DeckListModel::rebuildTree);
connect(deckList, &DeckLoader::deckHashChanged, this, &DeckListModel::deckHashChanged);
root = new InnerDecklistNode;
}
DeckListModel::~DeckListModel()
{
delete root;
}
QString DeckListModel::getGroupCriteriaForCard(CardInfoPtr info) const
{
if (!info) {
return "unknown";
}
switch (activeGroupCriteria) {
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->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 = getGroupCriteriaForCard(info);
auto *groupNode = dynamic_cast<InnerDecklistNode *>(node->findChild(groupCriteria));
if (!groupNode) {
groupNode = new InnerDecklistNode(groupCriteria, node);
}
new DecklistModelCardNode(currentCard, groupNode);
}
}
endResetModel();
}
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 *temp = static_cast<AbstractDecklistNode *>(index.internalPointer());
auto *card = dynamic_cast<DecklistModelCardNode *>(temp);
if (card == nullptr) {
const auto *node = dynamic_cast<InnerDecklistNode *>(temp);
switch (role) {
case Qt::FontRole: {
QFont f;
f.setBold(true);
return f;
}
case Qt::DisplayRole:
case Qt::EditRole: {
switch (index.column()) {
case 0:
return node->recursiveCount(true);
case 1: {
if (role == Qt::DisplayRole)
return node->getVisibleName();
return node->getName();
}
case 2: {
return node->getCardSetShortName();
}
case 3: {
return node->getCardCollectorNumber();
}
case 4: {
return node->getCardProviderId();
}
default:
return {};
}
}
case Qt::UserRole + 1:
return false;
case Qt::BackgroundRole: {
int color = 90 + 60 * node->depth();
return QBrush(QColor(color, 255, color));
}
case Qt::ForegroundRole: {
return QBrush(QColor(0, 0, 0));
}
default:
return {};
}
} else {
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole: {
switch (index.column()) {
case 0:
return card->getNumber();
case 1:
return card->getName();
case 2:
return card->getCardSetShortName();
case 3:
return card->getCardCollectorNumber();
case 4:
return card->getCardProviderId();
default:
return {};
}
}
case Qt::UserRole + 1:
return true;
case Qt::BackgroundRole: {
int color = 255 - (index.row() % 2) * 30;
return QBrush(QColor(color, color, color));
}
case Qt::ForegroundRole: {
return QBrush(QColor(0, 0, 0));
}
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 0:
return tr("Count");
case 1:
return tr("Card");
case 2:
return tr("Set");
case 3:
return tr("Number");
case 4:
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::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 0:
node->setNumber(value.toInt());
break;
case 1:
node->setName(value.toString());
break;
case 2:
node->setCardSetShortName(value.toString());
break;
case 3:
node->setCardCollectorNumber(value.toString());
break;
case 4:
node->setCardProviderId(value.toString());
break;
default:
return false;
}
emitRecursiveUpdates(index);
deckList->refreshDeckHash();
emit dataChanged(index, index);
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->deleteNode(temp->getDataNode());
}
delete toDelete;
}
endRemoveRows();
if (node->empty() && (node != root)) {
removeRows(parent.row(), 1, parent.parent());
} else {
emitRecursiveUpdates(parent);
}
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 = getGroupCriteriaForCard(info);
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 = getGroupCriteriaForCard(cardInfo);
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();
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();
} else {
cardNode->setNumber(cardNode->getNumber() + 1);
cardNode->setCardSetShortName(cardSetName);
cardNode->setCardCollectorNumber(printingInfo.getProperty("num"));
cardNode->setCardProviderId(printingInfo.getProperty("uuid"));
deckList->refreshDeckHash();
}
sort(lastKnownColumn, lastKnownOrder);
emitRecursiveUpdates(parentIndex);
return nodeToIndex(cardNode);
}
int DeckListModel::findSortedInsertRow(InnerDecklistNode *parent, 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 newCriteria)
{
activeGroupCriteria = newCriteria;
rebuildTree();
}
void DeckListModel::cleanList()
{
setDeckList(new DeckLoader);
}
/**
* @param _deck The deck. Takes ownership of the object
*/
void DeckListModel::setDeckList(DeckLoader *_deck)
{
deckList->deleteLater();
deckList = _deck;
deckList->setParent(this);
connect(deckList, &DeckLoader::deckLoaded, this, &DeckListModel::rebuildTree);
connect(deckList, &DeckLoader::deckHashChanged, this, &DeckListModel::deckHashChanged);
rebuildTree();
}
QList<ExactCard> DeckListModel::getCards() const
{
QList<ExactCard> cards;
DeckList *decklist = getDeckList();
if (!decklist) {
return cards;
}
InnerDecklistNode *listRoot = decklist->getRoot();
if (!listRoot)
return cards;
for (int i = 0; i < listRoot->size(); i++) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
if (!currentZone)
continue;
for (int j = 0; j < currentZone->size(); j++) {
DecklistCardNode *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
if (!currentCard)
continue;
for (int k = 0; k < currentCard->getNumber(); ++k) {
ExactCard card = CardDatabaseManager::query()->getCard(currentCard->toCardRef());
if (card) {
cards.append(card);
} else {
qDebug() << "Card not found in database!";
}
}
}
}
return cards;
}
QList<ExactCard> DeckListModel::getCardsForZone(const QString &zoneName) const
{
QList<ExactCard> cards;
DeckList *decklist = getDeckList();
if (!decklist) {
return cards;
}
InnerDecklistNode *listRoot = decklist->getRoot();
if (!listRoot)
return cards;
for (int i = 0; i < listRoot->size(); i++) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
if (!currentZone)
continue;
if (currentZone->getName() == zoneName) {
for (int j = 0; j < currentZone->size(); j++) {
DecklistCardNode *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
if (!currentCard)
continue;
for (int k = 0; k < currentCard->getNumber(); ++k) {
ExactCard card = CardDatabaseManager::query()->getCard(currentCard->toCardRef());
if (card) {
cards.append(card);
} else {
qDebug() << "Card not found in database!";
}
}
}
}
}
return cards;
}
QList<QString> *DeckListModel::getZones() const
{
QList<QString> *zones = new QList<QString>();
DeckList *decklist = getDeckList();
if (!decklist) {
return zones;
}
InnerDecklistNode *listRoot = decklist->getRoot();
if (!listRoot)
return zones;
for (int i = 0; i < listRoot->size(); i++) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
if (!currentZone)
continue;
zones->append(currentZone->getName());
}
return zones;
}
void DeckListModel::printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node)
{
const int totalColumns = 2;
if (node->height() == 1) {
QTextBlockFormat blockFormat;
QTextCharFormat charFormat;
charFormat.setFontPointSize(11);
charFormat.setFontWeight(QFont::Bold);
cursor->insertBlock(blockFormat, charFormat);
QTextTableFormat tableFormat;
tableFormat.setCellPadding(0);
tableFormat.setCellSpacing(0);
tableFormat.setBorder(0);
QTextTable *table = cursor->insertTable(node->size() + 1, totalColumns, tableFormat);
for (int i = 0; i < node->size(); i++) {
auto *card = dynamic_cast<AbstractDecklistCardNode *>(node->at(i));
QTextCharFormat cellCharFormat;
cellCharFormat.setFontPointSize(9);
QTextTableCell cell = table->cellAt(i, 0);
cell.setFormat(cellCharFormat);
QTextCursor cellCursor = cell.firstCursorPosition();
cellCursor.insertText(QString("%1 ").arg(card->getNumber()));
cell = table->cellAt(i, 1);
cell.setFormat(cellCharFormat);
cellCursor = cell.firstCursorPosition();
cellCursor.insertText(card->getName());
}
} else if (node->height() == 2) {
QTextBlockFormat blockFormat;
QTextCharFormat charFormat;
charFormat.setFontPointSize(14);
charFormat.setFontWeight(QFont::Bold);
cursor->insertBlock(blockFormat, charFormat);
QTextTableFormat tableFormat;
tableFormat.setCellPadding(10);
tableFormat.setCellSpacing(0);
tableFormat.setBorder(0);
QVector<QTextLength> constraints;
for (int i = 0; i < totalColumns; i++) {
constraints << QTextLength(QTextLength::PercentageLength, 100.0 / totalColumns);
}
tableFormat.setColumnWidthConstraints(constraints);
QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat);
for (int i = 0; i < node->size(); i++) {
QTextCursor cellCursor = table->cellAt(0, (i * totalColumns) / node->size()).lastCursorPosition();
printDeckListNode(&cellCursor, dynamic_cast<InnerDecklistNode *>(node->at(i)));
}
}
cursor->movePosition(QTextCursor::End);
}
void DeckListModel::printDeckList(QPrinter *printer)
{
QTextDocument doc;
QFont font("Serif");
font.setStyleHint(QFont::Serif);
doc.setDefaultFont(font);
QTextCursor cursor(&doc);
QTextBlockFormat headerBlockFormat;
QTextCharFormat headerCharFormat;
headerCharFormat.setFontPointSize(16);
headerCharFormat.setFontWeight(QFont::Bold);
cursor.insertBlock(headerBlockFormat, headerCharFormat);
cursor.insertText(deckList->getName());
headerCharFormat.setFontPointSize(12);
cursor.insertBlock(headerBlockFormat, headerCharFormat);
cursor.insertText(deckList->getComments());
cursor.insertBlock(headerBlockFormat, headerCharFormat);
for (int i = 0; i < root->size(); i++) {
cursor.insertHtml("<br><img src=theme:hr.jpg>");
// cursor.insertHtml("<hr>");
cursor.insertBlock(headerBlockFormat, headerCharFormat);
printDeckListNode(&cursor, dynamic_cast<InnerDecklistNode *>(root->at(i)));
}
doc.print(printer);
}

View file

@ -0,0 +1,267 @@
#ifndef DECKLISTMODEL_H
#define DECKLISTMODEL_H
#include <QAbstractItemModel>
#include <QList>
#include <libcockatrice/card/printing/exact_card.h>
#include <libcockatrice/deck_list/abstract_deck_list_card_node.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/deck_list_card_node.h>
class DeckLoader;
class CardDatabase;
class QPrinter;
class QTextCursor;
/**
* @brief Specifies the criteria used to group cards in the DeckListModel.
*/
enum DeckListModelGroupCriteria
{
MAIN_TYPE, /**< Group cards by their main type (e.g., creature, instant). */
MANA_COST, /**< Group cards by their mana cost. */
COLOR /**< Group cards by their color identity. */
};
/**
* @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)
{
}
int getNumber() const override
{
return dataNode->getNumber();
}
void setNumber(int _number) override
{
dataNode->setNumber(_number);
}
QString getName() const override
{
return dataNode->getName();
}
void setName(const QString &_name) override
{
dataNode->setName(_name);
}
QString getCardProviderId() const override
{
return dataNode->getCardProviderId();
}
void setCardProviderId(const QString &_cardProviderId) override
{
dataNode->setCardProviderId(_cardProviderId);
}
QString getCardSetShortName() const override
{
return dataNode->getCardSetShortName();
}
void setCardSetShortName(const QString &_cardSetShortName) override
{
dataNode->setCardSetShortName(_cardSetShortName);
}
QString getCardCollectorNumber() const override
{
return dataNode->getCardCollectorNumber();
}
void setCardCollectorNumber(const QString &_cardSetNumber) override
{
dataNode->setCardCollectorNumber(_cardSetNumber);
}
/**
* @brief Returns the underlying data node.
* @return Pointer to the DecklistCardNode wrapped by this node.
*/
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.
*
* Signals:
* - deckHashChanged(): emitted when the deck contents change in a way that
* affects its hash.
*
* Slots:
* - rebuildTree(): rebuilds the model structure from the underlying DeckLoader.
* - printDeckList(): renders the decklist to a QPrinter.
*/
class DeckListModel : public QAbstractItemModel
{
Q_OBJECT
private slots:
/**
* @brief Rebuilds the model tree from the underlying DeckLoader.
*
* This updates all indices and ensures the model reflects the current
* state of the deck.
*/
void rebuildTree();
public slots:
/**
* @brief Prints the decklist to the provided QPrinter.
* @param printer The printer to render the decklist to.
*/
void printDeckList(QPrinter *printer);
signals:
/**
* @brief Emitted whenever the deck hash changes due to modifications in the model.
*/
void deckHashChanged();
public:
explicit DeckListModel(QObject *parent = nullptr);
~DeckListModel() override;
/**
* @brief Returns the root index of the model.
* @return QModelIndex representing the root node.
*/
QModelIndex getRoot() const
{
return nodeToIndex(root);
};
/**
* @brief Returns the value of the grouping category for a card based on the current criteria.
* @param info Pointer to card information.
* @return String representing the value of the current grouping criteria for the card.
*/
QString getGroupCriteriaForCard(CardInfoPtr info) const;
// Qt model overrides
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QModelIndex index(int row, int column, const QModelIndex &parent) const override;
QModelIndex parent(const QModelIndex &index) const override;
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;
/**
* @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.
*/
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 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(InnerDecklistNode *parent, CardInfoPtr cardInfo) const;
void sort(int column, Qt::SortOrder order) override;
/**
* @brief Removes all cards and resets the model.
*/
void cleanList();
DeckLoader *getDeckList() const
{
return deckList;
}
void setDeckList(DeckLoader *_deck);
QList<ExactCard> getCards() const;
QList<ExactCard> getCardsForZone(const QString &zoneName) const;
QList<QString> *getZones() const;
/**
* @brief Sets the criteria used to group cards in the model.
* @param newCriteria The new grouping criteria.
*/
void setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria);
private:
DeckLoader *deckList; /**< Pointer to the deck loader providing the underlying data. */
InnerDecklistNode *root; /**< Root node of the model tree. */
DeckListModelGroupCriteria 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;
DecklistModelCardNode *findCardNode(const QString &cardName,
const QString &zoneName,
const QString &providerId = "",
const QString &cardNumber = "") const;
void emitRecursiveUpdates(const QModelIndex &index);
void sortHelper(InnerDecklistNode *node, Qt::SortOrder order);
void printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node);
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()));
}
};
#endif

View file

@ -0,0 +1,55 @@
#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(1), Qt::EditRole).toString();
QString rName = src->data(right.siblingAtColumn(1), 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;
}

View file

@ -0,0 +1,34 @@
/**
* @file deck_list_sort_filter_proxy_model.h
* @ingroup DeckEditorCardGroupWidgets
* @brief TODO: Document this.
*/
#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:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
private:
QStringList sortCriteria;
};
#endif // COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H

View file

@ -0,0 +1,605 @@
#include "deck_loader.h"
#include <QApplication>
#include <QClipboard>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFutureWatcher>
#include <QRegularExpression>
#include <QStringList>
#include <QtConcurrentRun>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/deck_list_card_node.h>
const QStringList DeckLoader::ACCEPTED_FILE_EXTENSIONS = {"*.cod", "*.dec", "*.dek", "*.txt", "*.mwDeck"};
const QStringList DeckLoader::FILE_NAME_FILTERS = {
tr("Common deck formats (%1)").arg(ACCEPTED_FILE_EXTENSIONS.join(" ")), tr("All files (*.*)")};
DeckLoader::DeckLoader() : DeckList(), lastFileName(QString()), lastFileFormat(CockatriceFormat), lastRemoteDeckId(-1)
{
}
DeckLoader::DeckLoader(const QString &nativeString)
: DeckList(nativeString), lastFileName(QString()), lastFileFormat(CockatriceFormat), lastRemoteDeckId(-1)
{
}
DeckLoader::DeckLoader(const DeckList &other)
: DeckList(other), lastFileName(QString()), lastFileFormat(CockatriceFormat), lastRemoteDeckId(-1)
{
}
DeckLoader::DeckLoader(const DeckLoader &other)
: DeckList(other), lastFileName(other.lastFileName), lastFileFormat(other.lastFileFormat),
lastRemoteDeckId(other.lastRemoteDeckId)
{
}
bool DeckLoader::loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
bool result = false;
switch (fmt) {
case PlainTextFormat:
result = loadFromFile_Plain(&file);
break;
case CockatriceFormat: {
result = loadFromFile_Native(&file);
qCInfo(DeckLoaderLog) << "Loaded from" << fileName << "-" << result;
if (!result) {
qCInfo(DeckLoaderLog) << "Retrying as plain format";
file.seek(0);
result = loadFromFile_Plain(&file);
fmt = PlainTextFormat;
}
break;
}
default:
break;
}
if (result) {
lastFileName = fileName;
lastFileFormat = fmt;
if (userRequest) {
updateLastLoadedTimestamp(fileName, fmt);
}
emit deckLoaded();
}
qCInfo(DeckLoaderLog) << "Deck was loaded -" << result;
return result;
}
bool DeckLoader::loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest)
{
auto *watcher = new QFutureWatcher<bool>(this);
connect(watcher, &QFutureWatcher<bool>::finished, this, [this, watcher, fileName, fmt, userRequest]() {
const bool result = watcher->result();
watcher->deleteLater();
if (result) {
lastFileName = fileName;
lastFileFormat = fmt;
if (userRequest) {
updateLastLoadedTimestamp(fileName, fmt);
}
emit deckLoaded();
}
emit loadFinished(result);
});
QFuture<bool> future = QtConcurrent::run([=, this]() {
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
switch (fmt) {
case PlainTextFormat:
return loadFromFile_Plain(&file);
case CockatriceFormat: {
bool result = false;
result = loadFromFile_Native(&file);
if (!result) {
file.seek(0);
return loadFromFile_Plain(&file);
}
return result;
}
default:
return false;
break;
}
});
watcher->setFuture(future);
return true; // Return immediately to indicate the async task was started
}
bool DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId)
{
bool result = loadFromString_Native(nativeString);
if (result) {
lastFileName = QString();
lastFileFormat = CockatriceFormat;
lastRemoteDeckId = remoteDeckId;
emit deckLoaded();
}
return result;
}
bool DeckLoader::saveToFile(const QString &fileName, FileFormat fmt)
{
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return false;
}
bool result = false;
switch (fmt) {
case PlainTextFormat:
result = saveToFile_Plain(&file);
break;
case CockatriceFormat:
result = saveToFile_Native(&file);
break;
}
if (result) {
lastFileName = fileName;
lastFileFormat = fmt;
}
file.flush();
file.close();
return result;
}
bool DeckLoader::updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt)
{
QFileInfo fileInfo(fileName);
if (!fileInfo.exists()) {
qCWarning(DeckLoaderLog) << "File does not exist:" << fileName;
return false;
}
QDateTime originalTimestamp = fileInfo.lastModified();
// Open the file for writing
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << fileName;
return false;
}
bool result = false;
// Perform file modifications
switch (fmt) {
case PlainTextFormat:
result = saveToFile_Plain(&file);
break;
case CockatriceFormat:
setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
result = saveToFile_Native(&file);
break;
}
file.close(); // Close the file to ensure changes are flushed
if (result) {
lastFileName = fileName;
lastFileFormat = fmt;
// Re-open the file and set the original timestamp
if (!file.open(QIODevice::ReadWrite)) {
qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName;
return false;
}
if (!file.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) {
qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName;
file.close();
return false;
}
file.close();
}
return result;
}
static QString getDomainForWebsite(DeckLoader::DecklistWebsite website)
{
switch (website) {
case DeckLoader::DecklistOrg:
return "www.decklist.org";
case DeckLoader::DecklistXyz:
return "www.decklist.xyz";
default:
qCWarning(DeckLoaderLog) << "Invalid decklist website enum:" << website;
return "";
}
}
/**
* Converts the card to the String that represents it in the decklist export
*/
static QString toDecklistExportString(const DecklistCardNode *card)
{
QString cardString;
// Get the number of cards and add the card name
cardString += QString::number(card->getNumber());
// Add a space between card num and name
cardString += "%20";
// Add card name
cardString += card->getName();
if (!card->getCardSetShortName().isNull()) {
cardString += "%20";
cardString += "(" + card->getCardSetShortName() + ")";
}
if (!card->getCardCollectorNumber().isNull()) {
cardString += "%20";
cardString += card->getCardCollectorNumber();
}
// Add a return at the end of the card
cardString += "%0A";
return cardString;
}
/**
* Export deck to decklist function, called to format the deck in a way to be sent to a server
* @param website The website we're sending the deck to
*/
QString DeckLoader::exportDeckToDecklist(DecklistWebsite website)
{
// Add the base url
QString deckString = "https://" + getDomainForWebsite(website) + "/?";
// Create two strings to pass to function
QString mainBoardCards, sideBoardCards;
// Set up the function to call
auto formatDeckListForExport = [&mainBoardCards, &sideBoardCards](const auto *node, const auto *card) {
// Get the card name
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
if (!dbCard || dbCard->getIsToken()) {
// If it's a token, we don't care about the card.
return;
}
// Check if it's a sideboard card.
if (node->getName() == DECK_ZONE_SIDE) {
sideBoardCards += toDecklistExportString(card);
} else {
// If it's a mainboard card, do the same thing, but for the mainboard card string
mainBoardCards += toDecklistExportString(card);
}
};
// call our struct function for each card in the deck
forEachCard(formatDeckListForExport);
// Remove the extra return at the end of the last cards
mainBoardCards.chop(3);
sideBoardCards.chop(3);
// if after we've called it for each card, and the strings are empty, we know that
// there were no non-token cards in the deck, so show an error message.
if ((QString::compare(mainBoardCards, "", Qt::CaseInsensitive) == 0) &&
(QString::compare(sideBoardCards, "", Qt::CaseInsensitive) == 0)) {
return "";
}
// return a string with the url for decklist export
deckString += "deckmain=" + mainBoardCards + "&deckside=" + sideBoardCards;
return deckString;
}
// This struct is here to support the forEachCard function call, defined in decklist.
// It requires a function to be called for each card, and it will set the providerId to the preferred printing.
struct SetProviderIdToPreferred
{
// Main operator for struct, allowing the foreachcard to work.
SetProviderIdToPreferred()
{
}
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
{
Q_UNUSED(node);
PrintingInfo preferredPrinting = CardDatabaseManager::query()->getPreferredPrinting(card->getName());
QString providerId = preferredPrinting.getUuid();
QString setShortName = preferredPrinting.getSet()->getShortName();
QString collectorNumber = preferredPrinting.getProperty("num");
card->setCardProviderId(providerId);
card->setCardCollectorNumber(collectorNumber);
card->setCardSetShortName(setShortName);
}
};
/**
* This function iterates through each card in the decklist and sets the providerId
* on each card based on its set name and collector number.
*/
void DeckLoader::setProviderIdToPreferredPrinting()
{
// Set up the struct to call.
SetProviderIdToPreferred setProviderIdToPreferred;
// Call the forEachCard method for each card in the deck
forEachCard(setProviderIdToPreferred);
}
/**
* Sets the providerId on each card in the decklist based on its set name and collector number.
*/
void DeckLoader::resolveSetNameAndNumberToProviderID()
{
auto setProviderId = [](const auto node, const auto card) {
Q_UNUSED(node);
// Retrieve the providerId based on setName and collectorNumber
QString providerId =
CardDatabaseManager::getInstance()
->query()
->getSpecificPrinting(card->getName(), card->getCardSetShortName(), card->getCardCollectorNumber())
.getUuid();
// Set the providerId on the card
card->setCardProviderId(providerId);
};
forEachCard(setProviderId);
}
// This struct is here to support the forEachCard function call, defined in decklist.
// It requires a function to be called for each card, and it will set the providerId.
struct ClearSetNameNumberAndProviderId
{
// Main operator for struct, allowing the foreachcard to work.
ClearSetNameNumberAndProviderId()
{
}
void operator()(const InnerDecklistNode *node, DecklistCardNode *card) const
{
Q_UNUSED(node);
// Set the providerId on the card
card->setCardSetShortName(nullptr);
card->setCardCollectorNumber(nullptr);
card->setCardProviderId(nullptr);
}
};
/**
* Clears the set name and numbers on each card in the decklist.
*/
void DeckLoader::clearSetNamesAndNumbers()
{
auto clearSetNameAndNumber = [](const auto node, auto card) {
Q_UNUSED(node)
// Set the providerId on the card
card->setCardSetShortName(nullptr);
card->setCardCollectorNumber(nullptr);
card->setCardProviderId(nullptr);
};
forEachCard(clearSetNameAndNumber);
}
DeckLoader::FileFormat DeckLoader::getFormatFromName(const QString &fileName)
{
if (fileName.endsWith(".cod", Qt::CaseInsensitive)) {
return CockatriceFormat;
}
return PlainTextFormat;
}
void DeckLoader::saveToClipboard(bool addComments, bool addSetNameAndNumber) const
{
QString buffer;
QTextStream stream(&buffer);
saveToStream_Plain(stream, addComments, addSetNameAndNumber);
QApplication::clipboard()->setText(buffer, QClipboard::Clipboard);
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
}
bool DeckLoader::saveToStream_Plain(QTextStream &out, bool addComments, bool addSetNameAndNumber) const
{
if (addComments) {
saveToStream_DeckHeader(out);
}
// loop zones
for (int i = 0; i < getRoot()->size(); i++) {
const auto *zoneNode = dynamic_cast<InnerDecklistNode *>(getRoot()->at(i));
saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber);
// end of zone
out << "\n";
}
return true;
}
void DeckLoader::saveToStream_DeckHeader(QTextStream &out) const
{
if (!getName().isEmpty()) {
out << "// " << getName() << "\n\n";
}
if (!getComments().isEmpty()) {
QStringList commentRows = getComments().split(QRegularExpression("\n|\r\n|\r"));
for (const QString &row : commentRows) {
out << "// " << row << "\n";
}
out << "\n";
}
}
void DeckLoader::saveToStream_DeckZone(QTextStream &out,
const InnerDecklistNode *zoneNode,
bool addComments,
bool addSetNameAndNumber) const
{
// group cards by card type and count the subtotals
QMultiMap<QString, DecklistCardNode *> cardsByType;
QMap<QString, int> cardTotalByType;
int cardTotal = 0;
for (int j = 0; j < zoneNode->size(); j++) {
auto *card = dynamic_cast<DecklistCardNode *>(zoneNode->at(j));
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName());
QString cardType = info ? info->getMainCardType() : "unknown";
cardsByType.insert(cardType, card);
if (cardTotalByType.contains(cardType)) {
cardTotalByType[cardType] += card->getNumber();
} else {
cardTotalByType[cardType] = card->getNumber();
}
cardTotal += card->getNumber();
}
if (addComments) {
out << "// " << cardTotal << " " << zoneNode->getVisibleName() << "\n";
}
// print cards to stream
for (const QString &cardType : cardsByType.uniqueKeys()) {
if (addComments) {
out << "// " << cardTotalByType[cardType] << " " << cardType << "\n";
}
QList<DecklistCardNode *> cards = cardsByType.values(cardType);
saveToStream_DeckZoneCards(out, zoneNode, cards, addComments, addSetNameAndNumber);
if (addComments) {
out << "\n";
}
}
}
void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
const InnerDecklistNode *zoneNode,
QList<DecklistCardNode *> cards,
bool addComments,
bool addSetNameAndNumber) const
{
// QMultiMap sorts values in reverse order
for (int i = cards.size() - 1; i >= 0; --i) {
DecklistCardNode *card = cards[i];
if (zoneNode->getName() == DECK_ZONE_SIDE && addComments) {
out << "SB: ";
}
if (card->getNumber()) {
out << card->getNumber();
}
if (!card->getName().isNull() && !card->getName().isEmpty()) {
out << " " << card->getName();
}
if (addSetNameAndNumber) {
if (!card->getCardSetShortName().isNull() && !card->getCardSetShortName().isEmpty()) {
out << " "
<< "(" << card->getCardSetShortName() << ")";
}
if (!card->getCardCollectorNumber().isNull()) {
out << " " << card->getCardCollectorNumber();
}
}
out << "\n";
}
}
bool DeckLoader::convertToCockatriceFormat(QString fileName)
{
// Change the file extension to .cod
QFileInfo fileInfo(fileName);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
// Open the new file for writing
QFile file(newFileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << newFileName;
return false;
}
bool result = false;
// Perform file modifications based on the detected format
switch (getFormatFromName(fileName)) {
case PlainTextFormat:
// Save in Cockatrice's native format
result = saveToFile_Native(&file);
break;
case CockatriceFormat:
qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed.";
result = true;
break;
default:
qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName;
result = false;
break;
}
file.close();
// Delete the old file if conversion was successful
if (result) {
if (!QFile::remove(fileName)) {
qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName;
} else {
qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName;
}
lastFileName = newFileName;
lastFileFormat = CockatriceFormat;
}
return result;
}
QString DeckLoader::getCardZoneFromName(QString cardName, QString currentZoneName)
{
CardInfoPtr card = CardDatabaseManager::query()->getCardInfo(cardName);
if (card && card->getIsToken()) {
return DECK_ZONE_TOKENS;
}
return currentZoneName;
}
QString DeckLoader::getCompleteCardName(const QString &cardName) const
{
if (CardDatabaseManager::getInstance()) {
ExactCard temp = CardDatabaseManager::query()->guessCard({cardName});
if (temp) {
return temp.getName();
}
}
return cardName;
}

View file

@ -0,0 +1,111 @@
/**
* @file deck_loader.h
* @ingroup ImportExport
* @brief TODO: Document this.
*/
#ifndef DECK_LOADER_H
#define DECK_LOADER_H
#include <QLoggingCategory>
#include <libcockatrice/deck_list/deck_list.h>
inline Q_LOGGING_CATEGORY(DeckLoaderLog, "deck_loader")
class DeckLoader : public DeckList
{
Q_OBJECT
signals:
void deckLoaded();
void loadFinished(bool success);
public:
enum FileFormat
{
PlainTextFormat,
CockatriceFormat
};
/**
* Supported file extensions for decklist files
*/
static const QStringList ACCEPTED_FILE_EXTENSIONS;
/**
* For use with `QFileDialog::setNameFilters`
*/
static const QStringList FILE_NAME_FILTERS;
enum DecklistWebsite
{
DecklistOrg,
DecklistXyz
};
private:
QString lastFileName;
FileFormat lastFileFormat;
int lastRemoteDeckId;
public:
DeckLoader();
explicit DeckLoader(const QString &nativeString);
explicit DeckLoader(const DeckList &other);
DeckLoader(const DeckLoader &other);
const QString &getLastFileName() const
{
return lastFileName;
}
void setLastFileName(const QString &_lastFileName)
{
lastFileName = _lastFileName;
}
FileFormat getLastFileFormat() const
{
return lastFileFormat;
}
int getLastRemoteDeckId() const
{
return lastRemoteDeckId;
}
bool hasNotBeenLoaded() const
{
return getLastFileName().isEmpty() && getLastRemoteDeckId() == -1;
}
void clearSetNamesAndNumbers();
static FileFormat getFormatFromName(const QString &fileName);
bool loadFromFile(const QString &fileName, FileFormat fmt, bool userRequest = false);
bool loadFromFileAsync(const QString &fileName, FileFormat fmt, bool userRequest);
bool loadFromRemote(const QString &nativeString, int remoteDeckId);
bool saveToFile(const QString &fileName, FileFormat fmt);
bool updateLastLoadedTimestamp(const QString &fileName, FileFormat fmt);
QString exportDeckToDecklist(DecklistWebsite website);
void setProviderIdToPreferredPrinting();
void resolveSetNameAndNumberToProviderID();
void saveToClipboard(bool addComments = true, bool addSetNameAndNumber = true) const;
// overload
bool saveToStream_Plain(QTextStream &out, bool addComments = true, bool addSetNameAndNumber = true) const;
bool convertToCockatriceFormat(QString fileName);
protected:
void saveToStream_DeckHeader(QTextStream &out) const;
void saveToStream_DeckZone(QTextStream &out,
const InnerDecklistNode *zoneNode,
bool addComments = true,
bool addSetNameAndNumber = true) const;
void saveToStream_DeckZoneCards(QTextStream &out,
const InnerDecklistNode *zoneNode,
QList<DecklistCardNode *> cards,
bool addComments = true,
bool addSetNameAndNumber = true) const;
[[nodiscard]] QString getCardZoneFromName(QString cardName, QString currentZoneName) override;
[[nodiscard]] QString getCompleteCardName(const QString &cardName) const override;
};
#endif