mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 17:15:09 -07:00
- saveToStream_DeckZone threads the owning board zone name down to the card writer, so cards in a custom zone under the sideboard keep their SB: prefix instead of being re-imported into the maindeck - nested sub-zones are collected during the loop and written after the parent zone's own header and cards, so they no longer read as part of the zone printed before them
645 lines
21 KiB
C++
645 lines
21 KiB
C++
#include "deck_loader.h"
|
|
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QFutureWatcher>
|
|
#include <QPrinter>
|
|
#include <QRegularExpression>
|
|
#include <QSaveFile>
|
|
#include <QStringList>
|
|
#include <QTextCursor>
|
|
#include <QTextDocument>
|
|
#include <QTextStream>
|
|
#include <QTextTable>
|
|
#include <QtConcurrentRun>
|
|
#include <libcockatrice/card/database/card_database.h>
|
|
#include <libcockatrice/card/database/card_database_manager.h>
|
|
#include <libcockatrice/card/import/card_name_normalizer.h>
|
|
#include <libcockatrice/deck_list/deck_list.h>
|
|
#include <libcockatrice/deck_list/tree/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(QObject *parent) : QObject(parent)
|
|
{
|
|
}
|
|
|
|
std::optional<LoadedDeck>
|
|
DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
|
|
{
|
|
QFile file(fileName);
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
qCWarning(DeckLoaderLog) << "File does not exist:" << fileName;
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool result = false;
|
|
DeckList deckList;
|
|
switch (fmt) {
|
|
case DeckFileFormat::PlainText:
|
|
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
|
|
break;
|
|
case DeckFileFormat::Cockatrice: {
|
|
result = deckList.loadFromFile_Native(&file);
|
|
if (!result) {
|
|
qCInfo(DeckLoaderLog) << "Failed to load " << fileName
|
|
<< "as cockatrice format; retrying as plain format";
|
|
file.seek(0);
|
|
result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
|
|
fmt = DeckFileFormat::PlainText;
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!result) {
|
|
qCWarning(DeckLoaderLog) << "Failed to load " << fileName << "as" << fmt;
|
|
return std::nullopt;
|
|
}
|
|
|
|
LoadedDeck::LoadInfo lastLoadInfo = {
|
|
.fileName = fileName,
|
|
.fileFormat = fmt,
|
|
};
|
|
LoadedDeck loadedDeck = {deckList, lastLoadInfo};
|
|
|
|
if (userRequest) {
|
|
updateLastLoadedTimestamp(loadedDeck);
|
|
}
|
|
|
|
qCDebug(DeckLoaderLog) << "Loaded deck" << fileName << "with userRequest:" << userRequest;
|
|
|
|
return loadedDeck;
|
|
}
|
|
|
|
void DeckLoader::loadFromFileAsync(const QString &fileName, DeckFileFormat::Format fmt, bool userRequest)
|
|
{
|
|
QFuture<void> future = QtConcurrent::run([=, this] {
|
|
std::optional<LoadedDeck> deckOpt = loadFromFile(fileName, fmt, userRequest);
|
|
if (deckOpt) {
|
|
loadedDeck = deckOpt.value();
|
|
}
|
|
emit loadFinished(deckOpt.has_value());
|
|
});
|
|
}
|
|
|
|
bool DeckLoader::reload()
|
|
{
|
|
QString lastFileName = loadedDeck.lastLoadInfo.fileName;
|
|
if (lastFileName.isEmpty()) {
|
|
return false;
|
|
}
|
|
std::optional<LoadedDeck> deck = loadFromFile(lastFileName, loadedDeck.lastLoadInfo.fileFormat, false);
|
|
|
|
if (!deck) {
|
|
return false;
|
|
}
|
|
|
|
loadedDeck = *deck;
|
|
return true;
|
|
}
|
|
|
|
std::optional<LoadedDeck> DeckLoader::loadFromRemote(const QString &nativeString, int remoteDeckId)
|
|
{
|
|
DeckList deckList;
|
|
bool success = deckList.loadFromString_Native(nativeString);
|
|
|
|
if (!success) {
|
|
qCWarning(DeckLoaderLog) << "Failed to load remote deck with id" << remoteDeckId << ":" << nativeString;
|
|
return std::nullopt;
|
|
}
|
|
|
|
LoadedDeck::LoadInfo lastLoadInfo = {.remoteDeckId = remoteDeckId};
|
|
LoadedDeck loadedDeck = {deckList, lastLoadInfo};
|
|
|
|
qCDebug(DeckLoaderLog) << "Loaded remote deck with id" << remoteDeckId;
|
|
|
|
return loadedDeck;
|
|
}
|
|
|
|
std::optional<LoadedDeck::LoadInfo>
|
|
DeckLoader::saveToFile(const DeckList &deck, const QString &fileName, DeckFileFormat::Format fmt)
|
|
{
|
|
// Use QSaveFile so that a failed write (e.g. a full disk) leaves the existing deck untouched
|
|
// instead of truncating it to a 0-byte file. The target is only replaced once every byte has
|
|
// been flushed successfully in commit().
|
|
QSaveFile file(fileName);
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
|
qCWarning(DeckLoaderLog) << "Could not create or open file:" << fileName;
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool success = false;
|
|
switch (fmt) {
|
|
case DeckFileFormat::PlainText:
|
|
success = deck.saveToFile_Plain(&file);
|
|
break;
|
|
case DeckFileFormat::Cockatrice:
|
|
success = deck.saveToFile_Native(&file);
|
|
break;
|
|
}
|
|
|
|
if (!success) {
|
|
file.cancelWriting();
|
|
qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << fileName;
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (!file.commit()) {
|
|
qCWarning(DeckLoaderLog) << "Failed to save deck to " << fileName << ":" << file.errorString();
|
|
return std::nullopt;
|
|
}
|
|
|
|
qCInfo(DeckLoaderLog) << "Saved deck to " << fileName << "with format" << fmt;
|
|
|
|
LoadedDeck::LoadInfo lastLoadInfo = {fileName, fmt};
|
|
return lastLoadInfo;
|
|
}
|
|
|
|
bool DeckLoader::saveToFile(const LoadedDeck &deck)
|
|
{
|
|
auto opt = saveToFile(deck.deckList, deck.lastLoadInfo.fileName, deck.lastLoadInfo.fileFormat);
|
|
return opt.has_value();
|
|
}
|
|
|
|
bool DeckLoader::saveToNewFile(LoadedDeck &deck, const QString &fileName, DeckFileFormat::Format fmt)
|
|
{
|
|
std::optional<LoadedDeck::LoadInfo> infoOpt = saveToFile(deck.deckList, fileName, fmt);
|
|
|
|
if (infoOpt) {
|
|
deck.lastLoadInfo = infoOpt.value();
|
|
}
|
|
|
|
return infoOpt.has_value();
|
|
}
|
|
|
|
/**
|
|
* @brief Updates the lastLoadedTimestamp field in the file corresponding to the deck, without changing the
|
|
* FileModificationTime of the file.
|
|
*/
|
|
bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck)
|
|
{
|
|
// text format doesn't support lastLoadedTimestamp, so there's no point in proceeding
|
|
if (deck.lastLoadInfo.fileFormat != DeckFileFormat::Cockatrice) {
|
|
return false;
|
|
}
|
|
|
|
QString fileName = deck.lastLoadInfo.fileName;
|
|
|
|
QFileInfo fileInfo(fileName);
|
|
if (!fileInfo.exists()) {
|
|
qCWarning(DeckLoaderLog) << "File does not exist:" << fileName;
|
|
return false;
|
|
}
|
|
|
|
QDateTime originalTimestamp = fileInfo.lastModified();
|
|
|
|
// Use QSaveFile so that a failed write (e.g. a full disk) cannot truncate an existing deck to a
|
|
// 0-byte file while merely bumping its timestamp.
|
|
QSaveFile file(fileName);
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
|
qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << fileName;
|
|
return false;
|
|
}
|
|
|
|
// Perform file modifications
|
|
deck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString());
|
|
|
|
if (!deck.deckList.saveToFile_Native(&file)) {
|
|
file.cancelWriting();
|
|
qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << fileName;
|
|
return false;
|
|
}
|
|
|
|
if (!file.commit()) {
|
|
qCWarning(DeckLoaderLog) << "Failed to update timestamp for file:" << fileName << ":" << file.errorString();
|
|
return false;
|
|
}
|
|
|
|
// Re-open the file and restore the original timestamp, so that updating the lastLoadedTimestamp
|
|
// does not change the file's modification time.
|
|
QFile timestampFile(fileName);
|
|
if (!timestampFile.open(QIODevice::ReadWrite)) {
|
|
qCWarning(DeckLoaderLog) << "Failed to re-open file to set timestamp:" << fileName;
|
|
return false;
|
|
}
|
|
|
|
if (!timestampFile.setFileTime(originalTimestamp, QFileDevice::FileModificationTime)) {
|
|
qCWarning(DeckLoaderLog) << "Failed to set modification time for file:" << fileName;
|
|
timestampFile.close();
|
|
return false;
|
|
}
|
|
|
|
timestampFile.close();
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Converts all cards in the list to their decklist export string and joins them into one string
|
|
*/
|
|
static QString toDecklistExportString(const QList<const DecklistCardNode *> &cardNodes)
|
|
{
|
|
QString result;
|
|
|
|
for (auto cardNode : cardNodes) {
|
|
result += toDecklistExportString(cardNode);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Export deck to decklist function, called to format the deck in a way to be sent to a server
|
|
*
|
|
* @param deckList The decklist to export
|
|
* @param website The website we're sending the deck to
|
|
*/
|
|
QString DeckLoader::exportDeckToDecklist(const DeckList &deckList, DecklistWebsite website)
|
|
{
|
|
// Add the base url
|
|
QString deckString = "https://" + getDomainForWebsite(website) + "/?";
|
|
|
|
// export all cards in zone
|
|
QString mainBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_MAIN}));
|
|
QString sideBoardCards = toDecklistExportString(deckList.getCardNodes({DECK_ZONE_SIDE}));
|
|
|
|
// 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;
|
|
}
|
|
|
|
void DeckLoader::saveToClipboard(const DeckList &deckList, bool addComments, bool addSetNameAndNumber)
|
|
{
|
|
QString buffer;
|
|
QTextStream stream(&buffer);
|
|
saveToStream_Plain(stream, deckList, addComments, addSetNameAndNumber);
|
|
QApplication::clipboard()->setText(buffer, QClipboard::Clipboard);
|
|
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
|
|
}
|
|
|
|
bool DeckLoader::saveToStream_Plain(QTextStream &out,
|
|
const DeckList &deckList,
|
|
bool addComments,
|
|
bool addSetNameAndNumber)
|
|
{
|
|
if (addComments) {
|
|
saveToStream_DeckHeader(out, deckList);
|
|
}
|
|
|
|
// loop zones
|
|
for (auto zoneNode : deckList.getZoneNodes()) {
|
|
saveToStream_DeckZone(out, zoneNode, addComments, addSetNameAndNumber);
|
|
|
|
// end of zone
|
|
out << "\n";
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckList)
|
|
{
|
|
if (!deckList.getName().isEmpty()) {
|
|
out << "// " << deckList.getName() << "\n\n";
|
|
}
|
|
|
|
if (!deckList.getComments().isEmpty()) {
|
|
QStringList commentRows = deckList.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 QString &boardZoneName)
|
|
{
|
|
// Nested sub-zones keep their owning board's identity: the top-level call
|
|
// passes no board, so the zone's own name is used; recursive calls carry the
|
|
// owning board down so the sideboard marker survives sub-zone nesting.
|
|
const QString owningBoardZoneName = boardZoneName.isEmpty() ? zoneNode->getName() : boardZoneName;
|
|
|
|
// group cards by card type and count the subtotals
|
|
QMultiMap<QString, DecklistCardNode *> cardsByType;
|
|
QMap<QString, int> cardTotalByType;
|
|
int cardTotal = 0;
|
|
QList<const InnerDecklistNode *> subZones;
|
|
|
|
for (int j = 0; j < zoneNode->size(); j++) {
|
|
auto *card = dynamic_cast<DecklistCardNode *>(zoneNode->at(j));
|
|
if (!card) {
|
|
// Cards collected in nested sub-zones are exported by recursion so
|
|
// they don't end up invisible in the plain text output. They are
|
|
// deferred until after this zone's own header and cards so they read
|
|
// as part of this zone's block.
|
|
if (auto *subZone = dynamic_cast<const InnerDecklistNode *>(zoneNode->at(j))) {
|
|
subZones.append(subZone);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
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, cards, addComments, addSetNameAndNumber, owningBoardZoneName);
|
|
|
|
if (addComments) {
|
|
out << "\n";
|
|
}
|
|
}
|
|
|
|
// Nested sub-zones come last, after the parent's own header and cards.
|
|
for (const auto *subZone : subZones) {
|
|
saveToStream_DeckZone(out, subZone, addComments, addSetNameAndNumber, owningBoardZoneName);
|
|
}
|
|
}
|
|
|
|
void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out,
|
|
QList<DecklistCardNode *> cards,
|
|
bool addComments,
|
|
bool addSetNameAndNumber,
|
|
const QString &boardZoneName)
|
|
{
|
|
// QMultiMap sorts values in reverse order
|
|
for (int i = cards.size() - 1; i >= 0; --i) {
|
|
DecklistCardNode *card = cards[i];
|
|
|
|
if (boardZoneName == 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(LoadedDeck &deck)
|
|
{
|
|
QString fileName = deck.lastLoadInfo.fileName;
|
|
if (fileName.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
// Determine the format before touching any file, so an already-converted or
|
|
// unsupported deck never truncates or deletes anything.
|
|
switch (DeckFileFormat::getFormatFromName(fileName)) {
|
|
case DeckFileFormat::PlainText:
|
|
break;
|
|
case DeckFileFormat::Cockatrice:
|
|
qCInfo(DeckLoaderLog) << "File is already in Cockatrice format. No conversion needed.";
|
|
return true;
|
|
default:
|
|
qCWarning(DeckLoaderLog) << "Unsupported file format for conversion:" << fileName;
|
|
return false;
|
|
}
|
|
|
|
// Change the file extension to .cod
|
|
QFileInfo fileInfo(fileName);
|
|
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
|
|
|
// Use QSaveFile so a failed write (e.g. a full disk) cannot leave a 0-byte .cod
|
|
// behind and then delete the original deck.
|
|
QSaveFile file(newFileName);
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
|
qCWarning(DeckLoaderLog) << "Failed to open file for writing:" << newFileName;
|
|
return false;
|
|
}
|
|
|
|
if (!deck.deckList.saveToFile_Native(&file)) {
|
|
file.cancelWriting();
|
|
qCWarning(DeckLoaderLog) << "Failed to serialize deck for file:" << newFileName;
|
|
return false;
|
|
}
|
|
|
|
if (!file.commit()) {
|
|
qCWarning(DeckLoaderLog) << "Failed to convert deck to " << newFileName << ":" << file.errorString();
|
|
return false;
|
|
}
|
|
|
|
// Conversion succeeded: delete the original file.
|
|
if (!QFile::remove(fileName)) {
|
|
qCWarning(DeckLoaderLog) << "Failed to delete original file:" << fileName;
|
|
} else {
|
|
qCInfo(DeckLoaderLog) << "Original file deleted successfully:" << fileName;
|
|
}
|
|
deck.lastLoadInfo = {
|
|
.fileName = newFileName,
|
|
.fileFormat = DeckFileFormat::Cockatrice,
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
|
|
{
|
|
if (!node || node->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const int totalColumns = 2;
|
|
|
|
// Dispatch children by type instead of trusting a whole-node height: a deck
|
|
// node may hold direct cards and nested zones side by side (custom zones),
|
|
// and an empty node would previously crash on at(0).
|
|
QVector<const AbstractDecklistCardNode *> cards;
|
|
QVector<const InnerDecklistNode *> subZones;
|
|
for (int i = 0; i < node->size(); i++) {
|
|
if (auto *card = dynamic_cast<const AbstractDecklistCardNode *>(node->at(i))) {
|
|
cards.append(card);
|
|
} else if (auto *zone = dynamic_cast<const InnerDecklistNode *>(node->at(i))) {
|
|
subZones.append(zone);
|
|
}
|
|
}
|
|
|
|
if (!cards.isEmpty()) {
|
|
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(cards.size() + 1, totalColumns, tableFormat);
|
|
for (int i = 0; i < cards.size(); i++) {
|
|
const AbstractDecklistCardNode *card = cards[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());
|
|
}
|
|
}
|
|
|
|
for (const InnerDecklistNode *subZone : subZones) {
|
|
if (subZone->isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
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);
|
|
QTextCursor cellCursor = table->cellAt(0, 0).firstCursorPosition();
|
|
printDeckListNode(&cellCursor, subZone);
|
|
}
|
|
|
|
cursor->movePosition(QTextCursor::End);
|
|
}
|
|
|
|
void DeckLoader::printDeckList(QPrinter *printer, const DeckList &deckList)
|
|
{
|
|
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 (auto zoneNode : deckList.getZoneNodes()) {
|
|
cursor.insertHtml("<br><img src=theme:hr.jpg>");
|
|
cursor.insertBlock(headerBlockFormat, headerCharFormat);
|
|
|
|
printDeckListNode(&cursor, zoneNode);
|
|
}
|
|
|
|
doc.print(printer);
|
|
}
|