mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-23 10:05:10 -07:00
Compare commits
5 commits
78f3abfb50
...
c19d3896a6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c19d3896a6 | ||
|
|
3f62022d07 | ||
|
|
17ed97be13 | ||
|
|
f1923e7c13 | ||
|
|
257fe7be82 |
24 changed files with 1218 additions and 22 deletions
|
|
@ -214,6 +214,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp
|
||||
src/interface/widgets/deck_editor/deck_list_style_proxy.cpp
|
||||
src/interface/widgets/deck_editor/deck_state_manager.cpp
|
||||
src/interface/widgets/deck_editor/deck_zone_dialog.cpp
|
||||
src/interface/widgets/deck_editor/printing_disabled_info_widget.cpp
|
||||
src/interface/widgets/general/background_sources.cpp
|
||||
src/interface/widgets/general/display/background_plate_widget.cpp
|
||||
|
|
|
|||
|
|
@ -384,6 +384,14 @@ void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
|||
|
||||
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.
|
||||
if (auto *subZone = dynamic_cast<const InnerDecklistNode *>(zoneNode->at(j))) {
|
||||
saveToStream_DeckZone(out, subZone, addComments, addSetNameAndNumber);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(card->getName());
|
||||
QString cardType = info ? info->getMainCardType() : "unknown";
|
||||
|
|
@ -510,9 +518,26 @@ bool DeckLoader::convertToCockatriceFormat(LoadedDeck &deck)
|
|||
|
||||
void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode *node)
|
||||
{
|
||||
if (!node || node->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int totalColumns = 2;
|
||||
|
||||
if (node->height() == 1) {
|
||||
// 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);
|
||||
|
|
@ -523,9 +548,9 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
|||
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));
|
||||
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);
|
||||
|
|
@ -540,7 +565,13 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
|||
cellCursor = cell.firstCursorPosition();
|
||||
cellCursor.insertText(card->getName());
|
||||
}
|
||||
} else if (node->height() == 2) {
|
||||
}
|
||||
|
||||
for (const InnerDecklistNode *subZone : subZones) {
|
||||
if (subZone->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QTextBlockFormat blockFormat;
|
||||
QTextCharFormat charFormat;
|
||||
charFormat.setFontPointSize(14);
|
||||
|
|
@ -559,10 +590,8 @@ void DeckLoader::printDeckListNode(QTextCursor *cursor, const InnerDecklistNode
|
|||
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)));
|
||||
}
|
||||
QTextCursor cellCursor = table->cellAt(0, 0).firstCursorPosition();
|
||||
printDeckListNode(&cellCursor, subZone);
|
||||
}
|
||||
|
||||
cursor->movePosition(QTextCursor::End);
|
||||
|
|
|
|||
|
|
@ -90,6 +90,13 @@ void CardDatabaseView::decrementCard(const QString &zoneName)
|
|||
emit cardDecremented(currentCardName(), zoneName);
|
||||
}
|
||||
|
||||
void CardDatabaseView::setZoneMenuProvider(const std::function<QList<QPair<QString, QStringList>>()> &provider,
|
||||
const std::function<QString()> &newZoneHandler)
|
||||
{
|
||||
zoneMenuProvider = provider;
|
||||
this->newZoneHandler = newZoneHandler;
|
||||
}
|
||||
|
||||
void CardDatabaseView::updateCard(const QModelIndex ¤t, const QModelIndex & /*previous*/)
|
||||
{
|
||||
if (!current.isValid()) {
|
||||
|
|
@ -142,6 +149,50 @@ void CardDatabaseView::openCustomMenu(QPoint point)
|
|||
[this, card] { emit cardAdded(card->getName(), DECK_ZONE_SIDE); });
|
||||
connect(selectPrinting, &QAction::triggered, this, &CardDatabaseView::selectPrintingClicked);
|
||||
|
||||
if (zoneMenuProvider) {
|
||||
QMenu *addToZoneMenu = menu.addMenu(tr("Add to Zone"));
|
||||
const auto zoneBoards = zoneMenuProvider();
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
// Boards with zones nest their children so no two menu entries
|
||||
// share a visible name: "Maindeck ▸ { Maindeck (whole board), … }".
|
||||
const QStringList customZones = [&zoneBoards, boardName] {
|
||||
for (const auto &zoneBoard : zoneBoards) {
|
||||
if (zoneBoard.first == boardName) {
|
||||
return zoneBoard.second;
|
||||
}
|
||||
}
|
||||
return QStringList();
|
||||
}();
|
||||
if (customZones.isEmpty()) {
|
||||
QAction *action = addToZoneMenu->addAction(InnerDecklistNode::visibleNameFromName(boardName));
|
||||
connect(action, &QAction::triggered, this,
|
||||
[this, card, boardName] { emit cardAdded(card->getName(), boardName); });
|
||||
} else {
|
||||
QMenu *boardSubmenu = addToZoneMenu->addMenu(InnerDecklistNode::visibleNameFromName(boardName));
|
||||
QAction *wholeBoardAction = boardSubmenu->addAction(InnerDecklistNode::visibleNameFromName(boardName));
|
||||
connect(wholeBoardAction, &QAction::triggered, this,
|
||||
[this, card, boardName] { emit cardAdded(card->getName(), boardName); });
|
||||
for (const QString &zoneName : customZones) {
|
||||
QAction *action = boardSubmenu->addAction(zoneName);
|
||||
connect(action, &QAction::triggered, this,
|
||||
[this, card, zoneName] { emit cardAdded(card->getName(), zoneName); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newZoneHandler) {
|
||||
addToZoneMenu->addSeparator();
|
||||
|
||||
QAction *newZoneAction = addToZoneMenu->addAction(tr("Create &new zone..."));
|
||||
connect(newZoneAction, &QAction::triggered, this, [this, card] {
|
||||
const QString zoneName = newZoneHandler();
|
||||
if (!zoneName.isEmpty()) {
|
||||
emit cardAdded(card->getName(), zoneName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (canBeCommander(*card)) {
|
||||
QAction *edhRecCommander = menu.addAction(tr("Show on EDHRec (Commander)"));
|
||||
connect(edhRecCommander, &QAction::triggered, this, [this, card] { emit edhrecClicked(card, true); });
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "../../key_signals.h"
|
||||
|
||||
#include <QTreeView>
|
||||
#include <functional>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
|
||||
class CardDatabaseModel;
|
||||
|
|
@ -19,6 +20,13 @@ class CardDatabaseView : public QTreeView
|
|||
KeySignals searchKeySignals;
|
||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||
|
||||
/// Provides the custom zones available in the current deck, grouped by board zone.
|
||||
/// The list contains (board zone name, custom zone names) pairs for every board.
|
||||
std::function<QList<QPair<QString, QStringList>>()> zoneMenuProvider;
|
||||
/// Handler invoked when the user picks "New zone..." from the add-to-zone menu.
|
||||
/// Returns the name of the created zone, or an empty string if creation was cancelled.
|
||||
std::function<QString()> newZoneHandler;
|
||||
|
||||
public:
|
||||
explicit CardDatabaseView(QWidget *parent, CardDatabaseDisplayModel *model);
|
||||
|
||||
|
|
@ -33,6 +41,17 @@ public:
|
|||
return &searchKeySignals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the provider used to populate the "Add to zone" submenu of the context menu.
|
||||
* If no provider is set, the submenu is not shown.
|
||||
*
|
||||
* @param provider Returns the custom zones of the current deck, grouped by board zone
|
||||
* @param newZoneHandler Creates a new custom zone and returns its name, or an empty string
|
||||
* if creation was cancelled. The menu entry is hidden when not provided.
|
||||
*/
|
||||
void setZoneMenuProvider(const std::function<QList<QPair<QString, QStringList>>()> &provider,
|
||||
const std::function<QString()> &newZoneHandler);
|
||||
|
||||
signals:
|
||||
void cardChanged(const QString &cardName);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
#include "deck_editor_card_database_dock_widget.h"
|
||||
|
||||
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
|
||||
#include "card_database_view.h"
|
||||
#include "deck_state_manager.h"
|
||||
#include "deck_zone_dialog.h"
|
||||
|
||||
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||
|
||||
DeckEditorCardDatabaseDockWidget::DeckEditorCardDatabaseDockWidget(AbstractTabDeckEditor *parent) : QDockWidget(parent)
|
||||
{
|
||||
setObjectName("databaseDisplayDock");
|
||||
|
|
@ -15,6 +22,27 @@ void DeckEditorCardDatabaseDockWidget::createDatabaseDisplayDock(AbstractTabDeck
|
|||
{
|
||||
databaseDisplayWidget = new DeckEditorDatabaseDisplayWidget(this, deckEditor->databaseModel);
|
||||
|
||||
databaseDisplayWidget->getDatabaseView()->setZoneMenuProvider(
|
||||
[deckEditor]() -> QList<QPair<QString, QStringList>> {
|
||||
QList<QPair<QString, QStringList>> result;
|
||||
auto *deckListModel = deckEditor->deckStateManager->getModel();
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
result.append({boardName, deckListModel->getCustomZoneNames(boardName)});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[this, deckEditor]() -> QString {
|
||||
QString boardName;
|
||||
const QString zoneName =
|
||||
DeckZoneDialog::promptForNewZone(this, {}, &boardName, [deckEditor](const QString &candidate) {
|
||||
return deckEditor->deckStateManager->validateNewZoneName(candidate);
|
||||
});
|
||||
if (!zoneName.isEmpty()) {
|
||||
deckEditor->deckStateManager->createCustomZone(boardName, zoneName);
|
||||
}
|
||||
return zoneName;
|
||||
});
|
||||
|
||||
auto *frame = new QVBoxLayout;
|
||||
frame->setObjectName("databaseDisplayFrame");
|
||||
frame->addWidget(databaseDisplayWidget);
|
||||
|
|
|
|||
|
|
@ -7,15 +7,18 @@
|
|||
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
#include "deck_state_manager.h"
|
||||
#include "deck_zone_dialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDockWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTextEdit>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
|
|
@ -772,14 +775,183 @@ void DeckEditorDeckDockWidget::offsetCountAtIndex(const QModelIndex &idx, bool i
|
|||
|
||||
void DeckEditorDeckDockWidget::decklistCustomMenu(QPoint point)
|
||||
{
|
||||
const QModelIndex sourceIndex = proxy->mapToSource(deckView->indexAt(point));
|
||||
|
||||
QMenu menu;
|
||||
|
||||
const bool isCustomZoneRow = sourceIndex.isValid() && sourceIndex.data(DeckRoles::IsCustomZoneRole).toBool();
|
||||
const bool isBoardZoneRow = sourceIndex.isValid() && !isCustomZoneRow && !sourceIndex.parent().isValid();
|
||||
const bool isCardRow =
|
||||
sourceIndex.isValid() && !isCustomZoneRow && !isBoardZoneRow && !getModel()->hasChildren(sourceIndex);
|
||||
|
||||
// Walk the row up to its top-level node to find the hosting board. Cards in
|
||||
// the tokens board cannot be moved (moveCardToZone bails for it), so the
|
||||
// move menu is skipped for them.
|
||||
QString currentBoardName;
|
||||
QModelIndex board = sourceIndex.parent();
|
||||
while (board.isValid() && board.parent().isValid()) {
|
||||
board = board.parent();
|
||||
}
|
||||
if (board.isValid()) {
|
||||
currentBoardName = board.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
}
|
||||
|
||||
if (isCardRow) {
|
||||
if (currentBoardName != DECK_ZONE_TOKENS) {
|
||||
addMoveToZoneMenu(&menu, sourceIndex, currentBoardName);
|
||||
menu.addSeparator();
|
||||
}
|
||||
} else if (isCustomZoneRow) {
|
||||
const QString zoneName =
|
||||
sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
|
||||
QAction *renameAction = menu.addAction(tr("&Rename zone..."));
|
||||
connect(renameAction, &QAction::triggered, this, [this, zoneName] {
|
||||
// The unchanged name must not validate as a duplicate.
|
||||
const QString newName =
|
||||
DeckZoneDialog::promptForRename(this, zoneName, [this, zoneName](const QString &candidate) {
|
||||
return candidate == zoneName ? QString() : deckStateManager->validateNewZoneName(candidate);
|
||||
});
|
||||
if (!newName.isEmpty() && newName != zoneName) {
|
||||
deckStateManager->renameCustomZone(zoneName, newName);
|
||||
}
|
||||
});
|
||||
|
||||
QMenu *boardMenu = menu.addMenu(tr("Change &board"));
|
||||
addChangeBoardMenu(boardMenu, zoneName);
|
||||
|
||||
QAction *deleteAction = menu.addAction(tr("&Delete zone"));
|
||||
const bool zoneHasCards = getModel()->hasChildren(sourceIndex);
|
||||
deleteAction->setEnabled(!zoneHasCards);
|
||||
if (zoneHasCards) {
|
||||
deleteAction->setToolTip(tr("Move or remove all cards first."));
|
||||
menu.setToolTipsVisible(true);
|
||||
}
|
||||
connect(deleteAction, &QAction::triggered, this, [this, zoneName] {
|
||||
const auto result =
|
||||
QMessageBox::warning(this, tr("Delete zone"), tr("Delete the zone \"%1\"?").arg(zoneName),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (result == QMessageBox::Yes) {
|
||||
deckStateManager->removeCustomZone(zoneName);
|
||||
}
|
||||
});
|
||||
menu.addSeparator();
|
||||
} else if (isBoardZoneRow) {
|
||||
const QString boardName =
|
||||
sourceIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
// Tokens cannot host custom zones, so only offer the action on real boards.
|
||||
const bool canHostCustomZones =
|
||||
boardName == DECK_ZONE_MAIN || boardName == DECK_ZONE_SIDE || boardName == DECK_ZONE_MAYBEBOARD;
|
||||
if (canHostCustomZones) {
|
||||
addNewZoneAction(&menu, boardName);
|
||||
menu.addSeparator();
|
||||
}
|
||||
} else if (!sourceIndex.isValid()) {
|
||||
addNewZoneAction(&menu);
|
||||
menu.addSeparator();
|
||||
}
|
||||
|
||||
QAction *selectPrinting = menu.addAction(tr("Select Printing"));
|
||||
connect(selectPrinting, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::showPrintingSelector);
|
||||
|
||||
menu.exec(deckView->mapToGlobal(point));
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::addMoveToZoneMenu(QMenu *menu,
|
||||
const QModelIndex &sourceCardIndex,
|
||||
const QString ¤tBoardName)
|
||||
{
|
||||
const auto addMoveAction = [this, sourceCardIndex](QMenu *targetMenu, const QString &targetZoneName,
|
||||
const QString &label, bool enabled) {
|
||||
QAction *action = targetMenu->addAction(label);
|
||||
action->setEnabled(enabled);
|
||||
if (enabled) {
|
||||
connect(action, &QAction::triggered, this, [this, sourceCardIndex, targetZoneName] {
|
||||
deckStateManager->moveCardToZone(sourceCardIndex, targetZoneName);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const auto tree = deckStateManager->getDeckListShared()->getTree();
|
||||
|
||||
QMenu *moveMenu = menu->addMenu(tr("Move to &zone"));
|
||||
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
const QString boardLabel = InnerDecklistNode::visibleNameFromName(boardName);
|
||||
const auto customZones = tree->getCustomZones(boardName);
|
||||
|
||||
// Boards with zones nest their children so no two menu entries share a
|
||||
// visible name: "Maindeck ▸ { Maindeck (whole board), Removal, … }".
|
||||
// The board the card already lives on is marked instead of offered.
|
||||
if (!customZones.isEmpty()) {
|
||||
QMenu *boardSubmenu = moveMenu->addMenu(boardLabel);
|
||||
addMoveAction(boardSubmenu, boardName, boardLabel, boardName != currentBoardName);
|
||||
for (const auto *customZone : customZones) {
|
||||
addMoveAction(boardSubmenu, customZone->getName(), customZone->getName(), true);
|
||||
}
|
||||
} else {
|
||||
addMoveAction(moveMenu, boardName, boardLabel, boardName != currentBoardName);
|
||||
}
|
||||
}
|
||||
|
||||
moveMenu->addSeparator();
|
||||
|
||||
QAction *newZoneAction = moveMenu->addAction(tr("Create new zone and move &here..."));
|
||||
connect(newZoneAction, &QAction::triggered, this, [this, sourceCardIndex, currentBoardName] {
|
||||
const QString zoneName = createNewCustomZone(currentBoardName);
|
||||
if (!zoneName.isEmpty()) {
|
||||
deckStateManager->moveCardToZone(sourceCardIndex, zoneName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::addChangeBoardMenu(QMenu *menu, const QString &zoneName)
|
||||
{
|
||||
const auto tree = deckStateManager->getDeckListShared()->getTree();
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
QAction *action = menu->addAction(InnerDecklistNode::visibleNameFromName(boardName));
|
||||
|
||||
// The board currently holding the zone is marked instead of offered.
|
||||
// Duplicate names cannot come up through the editor, so this doubles as
|
||||
// the uniqueness guard for imported decks.
|
||||
bool holdsTheZone = false;
|
||||
for (const auto *customZone : tree->getCustomZones(boardName)) {
|
||||
if (customZone->getName() == zoneName) {
|
||||
holdsTheZone = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (holdsTheZone) {
|
||||
action->setCheckable(true);
|
||||
action->setChecked(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
connect(action, &QAction::triggered, this,
|
||||
[this, zoneName, boardName] { deckStateManager->moveCustomZone(zoneName, boardName); });
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::addNewZoneAction(QMenu *menu, const QString &initialBoardName)
|
||||
{
|
||||
QAction *newZoneAction = menu->addAction(tr("Create &new zone..."));
|
||||
connect(newZoneAction, &QAction::triggered, this,
|
||||
[this, initialBoardName] { createNewCustomZone(initialBoardName); });
|
||||
}
|
||||
|
||||
QString DeckEditorDeckDockWidget::createNewCustomZone(const QString &initialBoardName)
|
||||
{
|
||||
QString boardName;
|
||||
const QString zoneName =
|
||||
DeckZoneDialog::promptForNewZone(this, initialBoardName, &boardName, [this](const QString &candidate) {
|
||||
return deckStateManager->validateNewZoneName(candidate);
|
||||
});
|
||||
if (!zoneName.isEmpty()) {
|
||||
deckStateManager->createCustomZone(boardName, zoneName);
|
||||
}
|
||||
return zoneName;
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::refreshShortcuts()
|
||||
{
|
||||
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include <QComboBox>
|
||||
#include <QDockWidget>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeView>
|
||||
|
|
@ -102,6 +103,11 @@ private:
|
|||
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
|
||||
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);
|
||||
|
||||
void addMoveToZoneMenu(QMenu *menu, const QModelIndex &sourceCardIndex, const QString ¤tBoardName);
|
||||
void addChangeBoardMenu(QMenu *menu, const QString &zoneName);
|
||||
QString createNewCustomZone(const QString &initialBoardName = {});
|
||||
void addNewZoneAction(QMenu *menu, const QString &initialBoardName = {});
|
||||
|
||||
private slots:
|
||||
void decklistCustomMenu(QPoint point);
|
||||
void updateCard(QModelIndex, const QModelIndex ¤t);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list_history_manager.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
DeckStateManager::DeckStateManager(QObject *parent)
|
||||
: QObject(parent), deckList(QSharedPointer<DeckList>(new DeckList)),
|
||||
|
|
@ -307,6 +308,180 @@ bool DeckStateManager::decrementCountAtIndex(const QModelIndex &idx)
|
|||
return offsetCountAtIndex(idx, -1);
|
||||
}
|
||||
|
||||
bool DeckStateManager::moveCardToZone(const QModelIndex &idx, const QString &targetZoneName)
|
||||
{
|
||||
if (!idx.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only actual card rows can be moved. Group or zone rows report an
|
||||
// aggregate amount and must never be deleted by this operation.
|
||||
if (!idx.data(DeckRoles::IsCardRole).toBool()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString cardName = idx.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
QString providerId = idx.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString();
|
||||
int copies = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT).data(Qt::EditRole).toInt();
|
||||
|
||||
if (copies <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tokens only live in the tokens zone and cannot be moved into decks.
|
||||
CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName);
|
||||
if (info && info->getIsToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine the zone the card currently lives in: the enclosing custom
|
||||
// zone, or the nearest top-level zone (board zone or legacy zone).
|
||||
QString currentZoneName;
|
||||
for (QModelIndex ancestor = idx.parent(); ancestor.isValid(); ancestor = ancestor.parent()) {
|
||||
bool isCustomZone = ancestor.data(DeckRoles::IsCustomZoneRole).toBool();
|
||||
if (isCustomZone || !ancestor.parent().isValid()) {
|
||||
currentZoneName = ancestor.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentZoneName == targetZoneName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString reason = tr("Moved %1 × \"%2\" (%3) to %4")
|
||||
.arg(copies)
|
||||
.arg(cardName)
|
||||
.arg(providerId)
|
||||
.arg(InnerDecklistNode::visibleNameFromName(targetZoneName));
|
||||
|
||||
return modifyDeck(reason, [&idx, &cardName, &providerId, &targetZoneName, copies](auto model) {
|
||||
if (!model->removeRow(idx.row(), idx.parent())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId})) {
|
||||
for (int i = 0; i < copies; ++i) {
|
||||
model->addCard(card, targetZoneName);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < copies; ++i) {
|
||||
model->addPreferredPrintingCard(cardName, targetZoneName, true);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool DeckStateManager::createCustomZone(const QString &boardZoneName, const QString &zoneName)
|
||||
{
|
||||
const QString trimmedZoneName = zoneName.trimmed();
|
||||
if (trimmedZoneName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString reason =
|
||||
tr("Created zone \"%1\" in %2").arg(trimmedZoneName, InnerDecklistNode::visibleNameFromName(boardZoneName));
|
||||
|
||||
return modifyTree(reason, [&boardZoneName, &trimmedZoneName](DecklistNodeTree *tree) {
|
||||
return tree->addCustomZone(boardZoneName, trimmedZoneName) != nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
bool DeckStateManager::renameCustomZone(const QString &oldZoneName, const QString &newZoneName)
|
||||
{
|
||||
const QString trimmedNewZoneName = newZoneName.trimmed();
|
||||
if (trimmedNewZoneName.isEmpty() || oldZoneName == trimmedNewZoneName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString reason = tr("Renamed zone \"%1\" to \"%2\"").arg(oldZoneName, trimmedNewZoneName);
|
||||
|
||||
return modifyTree(reason, [&oldZoneName, &trimmedNewZoneName](DecklistNodeTree *tree) {
|
||||
return tree->renameCustomZone(oldZoneName, trimmedNewZoneName);
|
||||
});
|
||||
}
|
||||
|
||||
bool DeckStateManager::moveCustomZone(const QString &zoneName, const QString &newBoardZoneName)
|
||||
{
|
||||
const auto *tree = deckList->getTree();
|
||||
|
||||
// Locate the board currently holding the zone.
|
||||
QString currentBoardName;
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
for (const auto *zone : tree->getCustomZones(boardName)) {
|
||||
if (zone->getName() == zoneName) {
|
||||
currentBoardName = boardName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!currentBoardName.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBoardName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same-board moves are no-ops and must not pollute the history.
|
||||
if (currentBoardName == newBoardZoneName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Zone names are deck-unique among zones created through this manager, so a
|
||||
// same-named zone on the target board can only come from an imported deck.
|
||||
// Refuse the move instead of silently stacking same-named zones.
|
||||
for (const auto *zone : tree->getCustomZones(newBoardZoneName)) {
|
||||
if (zone->getName() == zoneName) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString reason =
|
||||
tr("Moved zone \"%1\" to %2").arg(zoneName, InnerDecklistNode::visibleNameFromName(newBoardZoneName));
|
||||
|
||||
return modifyTree(reason, [&zoneName, &newBoardZoneName](DecklistNodeTree *tree) {
|
||||
return tree->moveCustomZone(zoneName, newBoardZoneName);
|
||||
});
|
||||
}
|
||||
|
||||
bool DeckStateManager::removeCustomZone(const QString &zoneName)
|
||||
{
|
||||
QString reason = tr("Deleted zone \"%1\"").arg(zoneName);
|
||||
|
||||
return modifyTree(reason, [&zoneName](DecklistNodeTree *tree) { return tree->removeCustomZone(zoneName); });
|
||||
}
|
||||
|
||||
QString DeckStateManager::validateNewZoneName(const QString &zoneName) const
|
||||
{
|
||||
if (zoneName.trimmed().isEmpty()) {
|
||||
return tr("Enter a zone name.");
|
||||
}
|
||||
|
||||
const QString trimmedZoneName = zoneName.trimmed();
|
||||
|
||||
// The standard zone names are reserved even before they exist.
|
||||
if (trimmedZoneName == DECK_ZONE_MAIN || trimmedZoneName == DECK_ZONE_SIDE ||
|
||||
trimmedZoneName == DECK_ZONE_MAYBEBOARD || trimmedZoneName == DECK_ZONE_TOKENS) {
|
||||
return tr("This name is reserved.");
|
||||
}
|
||||
|
||||
const auto *tree = deckList->getTree();
|
||||
|
||||
// Reuse the tree's own uniqueness contract: any top-level zone and any
|
||||
// custom zone on *every* board claims the name (hasZoneName also reserves
|
||||
// the standard board names, which we already rejected with a dedicated
|
||||
// message above). Scanning only the standard boards here would miss a
|
||||
// custom zone an imported deck carries under `tokens`.
|
||||
if (tree->hasZoneName(trimmedZoneName)) {
|
||||
return tr("A zone with this name already exists.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool DeckStateManager::offsetCountAtIndex(const QModelIndex &idx, int offset)
|
||||
{
|
||||
if (!idx.isValid()) {
|
||||
|
|
@ -367,6 +542,22 @@ void DeckStateManager::requestHistorySave(const QString &reason)
|
|||
historyManager->save(deckList->createMemento(reason));
|
||||
}
|
||||
|
||||
bool DeckStateManager::modifyTree(const QString &reason, const std::function<bool(DecklistNodeTree *)> &operation)
|
||||
{
|
||||
DeckListMemento memento = deckList->createMemento(reason);
|
||||
bool success = operation(deckList->getTree());
|
||||
|
||||
if (success) {
|
||||
historyManager->save(memento);
|
||||
deckListModel->rebuildTree();
|
||||
deckList->refreshDeckHash();
|
||||
emit deckListModel->deckHashChanged();
|
||||
doCardModified();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles updating state and emitting signals whenever the cards are modified
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "deck_list_model.h"
|
||||
|
||||
#include <QSharedPointer>
|
||||
#include <functional>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class DeckListHistoryManager;
|
||||
|
|
@ -236,6 +237,68 @@ public:
|
|||
*/
|
||||
bool decrementCountAtIndex(const QModelIndex &idx);
|
||||
|
||||
/**
|
||||
* @brief Moves all copies of the card at the given index to the given zone.
|
||||
* No-ops if the index is invalid, not a card node, the card is a token, or the
|
||||
* card is already in the target zone.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param idx The model index of the card to move
|
||||
* @param targetZoneName The zone to move the card to (board zone or custom zone name)
|
||||
* @return Whether the operation was successfully performed
|
||||
*/
|
||||
bool moveCardToZone(const QModelIndex &idx, const QString &targetZoneName);
|
||||
|
||||
/**
|
||||
* @brief Creates a new custom zone nested under a board zone.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param boardZoneName The board zone to nest the custom zone under
|
||||
* @param zoneName The name of the new custom zone. Gets trimmed and must be
|
||||
* unique across the deck.
|
||||
* @return Whether the zone was created
|
||||
*/
|
||||
bool createCustomZone(const QString &boardZoneName, const QString &zoneName);
|
||||
|
||||
/**
|
||||
* @brief Renames a custom zone.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param oldZoneName The current name of the custom zone
|
||||
* @param newZoneName The new name. Gets trimmed and must be unique across the deck.
|
||||
* @return Whether the rename succeeded
|
||||
*/
|
||||
bool renameCustomZone(const QString &oldZoneName, const QString &newZoneName);
|
||||
|
||||
/**
|
||||
* @brief Moves a custom zone (and its cards) to a different board zone.
|
||||
* Same-board moves succeed without creating a history entry.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param zoneName The custom zone to move
|
||||
* @param newBoardZoneName The board zone to move the custom zone under
|
||||
* @return Whether the move succeeded
|
||||
*/
|
||||
bool moveCustomZone(const QString &zoneName, const QString &newBoardZoneName);
|
||||
|
||||
/**
|
||||
* @brief Removes a custom zone and all its cards.
|
||||
* Saves the operation to history if successful.
|
||||
*
|
||||
* @param zoneName The custom zone to remove
|
||||
* @return Whether the zone was removed
|
||||
*/
|
||||
bool removeCustomZone(const QString &zoneName);
|
||||
|
||||
/**
|
||||
* @brief Checks whether a candidate name is usable for a new custom zone.
|
||||
*
|
||||
* @param zoneName The candidate name
|
||||
* @return An empty string when the name is usable, otherwise a user-facing
|
||||
* error message describing the problem
|
||||
*/
|
||||
[[nodiscard]] QString validateNewZoneName(const QString &zoneName) const;
|
||||
|
||||
/**
|
||||
* Undoes n steps of the history, setting the decklist state and updating the current step in the historyManager.
|
||||
* @param steps Number of steps to undo.
|
||||
|
|
@ -257,6 +320,7 @@ public slots:
|
|||
|
||||
private:
|
||||
bool offsetCountAtIndex(const QModelIndex &idx, int offset);
|
||||
bool modifyTree(const QString &reason, const std::function<bool(DecklistNodeTree *)> &operation);
|
||||
void doCardModified();
|
||||
void doMetadataModified();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
#include "deck_zone_dialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
DeckZoneDialog::DeckZoneDialog(QWidget *parent,
|
||||
const QString &initialBoardName,
|
||||
const std::function<QString(const QString &)> &_nameValidator,
|
||||
bool _allowBoardSelection)
|
||||
: QDialog(parent), nameValidator(_nameValidator), allowBoardSelection(_allowBoardSelection)
|
||||
{
|
||||
nameLabel = new QLabel(this);
|
||||
nameEdit = new QLineEdit(this);
|
||||
nameEdit->setMaxLength(MAX_NAME_LENGTH);
|
||||
|
||||
errorLabel = new QLabel(this);
|
||||
errorLabel->hide();
|
||||
|
||||
boardLabel = new QLabel(this);
|
||||
boardCombo = new QComboBox(this);
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
// Use the icon overload explicitly so `boardName` lands in the user data role
|
||||
// (visible text is applied below in retranslateUi). The two-argument form
|
||||
// addItem({}, boardName) would be ambiguous and resolve to the icon overload
|
||||
// with empty user data, yielding empty entries and an empty getBoardName().
|
||||
boardCombo->addItem({}, {}, boardName);
|
||||
}
|
||||
if (!initialBoardName.isEmpty()) {
|
||||
int idx = boardCombo->findData(initialBoardName);
|
||||
if (idx != -1) {
|
||||
boardCombo->setCurrentIndex(idx);
|
||||
}
|
||||
}
|
||||
|
||||
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(nameLabel);
|
||||
layout->addWidget(nameEdit);
|
||||
layout->addWidget(errorLabel);
|
||||
if (allowBoardSelection) {
|
||||
layout->addWidget(boardLabel);
|
||||
layout->addWidget(boardCombo);
|
||||
} else {
|
||||
boardLabel->hide();
|
||||
boardCombo->hide();
|
||||
}
|
||||
layout->addWidget(buttonBox);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
connect(nameEdit, &QLineEdit::textChanged, this, [this] { validateName(); });
|
||||
validateName();
|
||||
|
||||
nameEdit->setFocus();
|
||||
}
|
||||
|
||||
QString DeckZoneDialog::getZoneName() const
|
||||
{
|
||||
return nameEdit->text().trimmed();
|
||||
}
|
||||
|
||||
QString DeckZoneDialog::getBoardName() const
|
||||
{
|
||||
return boardCombo->currentData().toString();
|
||||
}
|
||||
|
||||
void DeckZoneDialog::setZoneName(const QString &zoneName)
|
||||
{
|
||||
nameEdit->setText(zoneName);
|
||||
nameEdit->selectAll();
|
||||
}
|
||||
|
||||
void DeckZoneDialog::changeEvent(QEvent *event)
|
||||
{
|
||||
QDialog::changeEvent(event);
|
||||
|
||||
if (event->type() == QEvent::LanguageChange) {
|
||||
retranslateUi();
|
||||
}
|
||||
}
|
||||
|
||||
void DeckZoneDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(allowBoardSelection ? tr("New zone") : tr("Rename zone"));
|
||||
|
||||
nameLabel->setText(tr("Zone &name:"));
|
||||
nameLabel->setBuddy(nameEdit);
|
||||
|
||||
boardLabel->setText(tr("&Parent zone:"));
|
||||
boardLabel->setBuddy(boardCombo);
|
||||
|
||||
for (int i = 0; i < boardCombo->count(); i++) {
|
||||
boardCombo->setItemText(i, InnerDecklistNode::visibleNameFromName(boardCombo->itemData(i).toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void DeckZoneDialog::validateName()
|
||||
{
|
||||
const QString zoneName = nameEdit->text().trimmed();
|
||||
QString error;
|
||||
if (zoneName.isEmpty()) {
|
||||
error = tr("Enter a zone name.");
|
||||
} else if (nameValidator) {
|
||||
error = nameValidator(zoneName);
|
||||
}
|
||||
|
||||
errorLabel->setText(error);
|
||||
errorLabel->setVisible(!error.isEmpty());
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error.isEmpty());
|
||||
}
|
||||
|
||||
QString DeckZoneDialog::promptForNewZone(QWidget *parent,
|
||||
const QString &initialBoardName,
|
||||
QString *chosenBoardName,
|
||||
const std::function<QString(const QString &)> &nameValidator)
|
||||
{
|
||||
DeckZoneDialog dialog(parent, initialBoardName, nameValidator);
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (chosenBoardName) {
|
||||
*chosenBoardName = dialog.getBoardName();
|
||||
}
|
||||
return dialog.getZoneName();
|
||||
}
|
||||
|
||||
QString DeckZoneDialog::promptForRename(QWidget *parent,
|
||||
const QString ¤tZoneName,
|
||||
const std::function<QString(const QString &)> &nameValidator)
|
||||
{
|
||||
DeckZoneDialog dialog(parent, {}, nameValidator, false);
|
||||
dialog.setZoneName(currentZoneName);
|
||||
return dialog.exec() == QDialog::Accepted ? dialog.getZoneName() : QString();
|
||||
}
|
||||
123
cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h
Normal file
123
cockatrice/src/interface/widgets/deck_editor/deck_zone_dialog.h
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* @file deck_zone_dialog.h
|
||||
* @ingroup DeckEditorWidgets
|
||||
* @brief Shared dialog for creating custom deck zones.
|
||||
*/
|
||||
|
||||
#ifndef DECK_ZONE_DIALOG_H
|
||||
#define DECK_ZONE_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QEvent>
|
||||
#include <QString>
|
||||
#include <functional>
|
||||
|
||||
class QComboBox;
|
||||
class QDialogButtonBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QWidget;
|
||||
|
||||
/**
|
||||
* @brief Modal dialog asking for the name and parent zone of a new custom deck zone.
|
||||
*
|
||||
* Menus construct the dialog transiently around exec(), so validation state only
|
||||
* ever reflects the name currently typed.
|
||||
*/
|
||||
class DeckZoneDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs the dialog and runs the initial validation pass.
|
||||
*
|
||||
* @param parent The parent widget for the dialog
|
||||
* @param initialBoardName The board zone to preselect in the combo. Unknown names
|
||||
* fall back to main.
|
||||
* @param _nameValidator Given the trimmed candidate name, returns an empty string
|
||||
* when it is usable, otherwise a user-facing error message. May be empty.
|
||||
* @param _allowBoardSelection When false the parent-zone combo is hidden and the
|
||||
* dialog acts as a rename prompt for an existing zone.
|
||||
*/
|
||||
explicit DeckZoneDialog(QWidget *parent = nullptr,
|
||||
const QString &initialBoardName = {},
|
||||
const std::function<QString(const QString &)> &_nameValidator = {},
|
||||
bool _allowBoardSelection = true);
|
||||
|
||||
/**
|
||||
* @brief The trimmed zone name entered by the user.
|
||||
*/
|
||||
[[nodiscard]] QString getZoneName() const;
|
||||
|
||||
/**
|
||||
* @brief The internal name of the board zone selected in the combo.
|
||||
*/
|
||||
[[nodiscard]] QString getBoardName() const;
|
||||
|
||||
/**
|
||||
* @brief Prefills the name field, e.g. with the current name when renaming.
|
||||
*
|
||||
* @param zoneName The text to put into the name field, selected for quick editing
|
||||
*/
|
||||
void setZoneName(const QString &zoneName);
|
||||
|
||||
/**
|
||||
* @brief Prompts the user for a new custom zone name and the board zone to nest it under.
|
||||
*
|
||||
* Convenience wrapper that runs DeckZoneDialog modally.
|
||||
*
|
||||
* @param parent The parent widget for the dialog
|
||||
* @param initialBoardName The board zone to preselect in the dialog. Unknown names fall
|
||||
* back to main.
|
||||
* @param chosenBoardName (out) The internal name of the board zone the user chose
|
||||
* @param nameValidator Optional validator forwarded to the dialog
|
||||
* @return The trimmed zone name, or an empty string if the user cancelled
|
||||
*/
|
||||
static QString promptForNewZone(QWidget *parent,
|
||||
const QString &initialBoardName,
|
||||
QString *chosenBoardName,
|
||||
const std::function<QString(const QString &)> &nameValidator = {});
|
||||
|
||||
/**
|
||||
* @brief Prompts the user for a new name for an existing custom zone.
|
||||
*
|
||||
* Same inline validation as promptForNewZone, but without a parent-zone picker.
|
||||
*
|
||||
* @param parent The parent widget for the dialog
|
||||
* @param currentZoneName The current name, prefilled for editing
|
||||
* @param nameValidator Validator deciding whether a candidate name is usable. It sees
|
||||
* the current name too, so callers wanting to allow unchanged names must
|
||||
* special-case that themselves.
|
||||
* @return The trimmed new name, or an empty string if the user cancelled
|
||||
*/
|
||||
static QString promptForRename(QWidget *parent,
|
||||
const QString ¤tZoneName,
|
||||
const std::function<QString(const QString &)> &nameValidator = {});
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Sets every user-visible string. Runs on construction and on runtime
|
||||
* language changes.
|
||||
*/
|
||||
void retranslateUi();
|
||||
|
||||
/**
|
||||
* @brief Validates the current input, toggling Ok and the inline error label.
|
||||
*/
|
||||
void validateName();
|
||||
|
||||
QLabel *nameLabel;
|
||||
QLineEdit *nameEdit;
|
||||
QLabel *errorLabel;
|
||||
QLabel *boardLabel;
|
||||
QComboBox *boardCombo;
|
||||
QDialogButtonBox *buttonBox;
|
||||
std::function<QString(const QString &)> nameValidator;
|
||||
bool allowBoardSelection;
|
||||
};
|
||||
|
||||
#endif // DECK_ZONE_DIALOG_H
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
#include "../../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../cards/card_info_display_widget.h"
|
||||
#include "../../deck_editor/deck_state_manager.h"
|
||||
#include "../../deck_editor/deck_zone_dialog.h"
|
||||
#include "../../filters/filter_builder.h"
|
||||
#include "../../interface/pixel_map_generator.h"
|
||||
#include "../../interface/widgets/cards/card_info_frame_widget.h"
|
||||
|
|
@ -84,6 +85,7 @@ void TabDeckEditorVisual::createCentralFrame()
|
|||
connect(tabContainer, &TabDeckEditorVisualTabWidget::printingSelectorRequested, this,
|
||||
&TabDeckEditorVisual::showPrintingSelector);
|
||||
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardInfoRequested, this, &TabDeckEditorVisual::updateCardInfo);
|
||||
tabContainer->visualDatabaseDisplay->setNewZoneCreator([this] { return createNewZone(); });
|
||||
|
||||
centralFrame->addWidget(tabContainer);
|
||||
setCentralWidget(centralWidget);
|
||||
|
|
@ -269,6 +271,19 @@ bool TabDeckEditorVisual::actSaveDeckAs()
|
|||
return result;
|
||||
}
|
||||
|
||||
/** @brief Prompts for and creates a new custom deck zone. Returns the name of the created zone. */
|
||||
QString TabDeckEditorVisual::createNewZone()
|
||||
{
|
||||
QString boardName;
|
||||
const QString zoneName = DeckZoneDialog::promptForNewZone(this, {}, &boardName, [this](const QString &candidate) {
|
||||
return deckStateManager->validateNewZoneName(candidate);
|
||||
});
|
||||
if (!zoneName.isEmpty()) {
|
||||
deckStateManager->createCustomZone(boardName, zoneName);
|
||||
}
|
||||
return zoneName;
|
||||
}
|
||||
|
||||
/** @brief Refreshes keyboard shortcuts for this tab from settings. */
|
||||
void TabDeckEditorVisual::refreshShortcuts()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -165,6 +165,12 @@ public slots:
|
|||
*/
|
||||
bool actSaveDeckAs() override;
|
||||
|
||||
/**
|
||||
* @brief Prompts for and creates a new custom deck zone.
|
||||
* @return The name of the created zone, or an empty string if creation was cancelled.
|
||||
*/
|
||||
QString createNewZone();
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Sets the deck for this tab and selects the sub-tab to open on
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <libcockatrice/card/card_info_comparator.h>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <utility>
|
||||
|
||||
|
|
@ -89,6 +90,19 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
|||
databaseView->setItemDelegate(nullptr);
|
||||
databaseView->setVisible(false);
|
||||
|
||||
// Without a deck model there is nothing to add cards to, so the zone menu stays hidden.
|
||||
if (deckListModel) {
|
||||
databaseView->setZoneMenuProvider(
|
||||
[deckListModel]() -> QList<QPair<QString, QStringList>> {
|
||||
QList<QPair<QString, QStringList>> result;
|
||||
for (const QString &boardName : InnerDecklistNode::boardZoneNames()) {
|
||||
result.append({boardName, deckListModel->getCustomZoneNames(boardName)});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[this] { return newZoneCreator ? newZoneCreator() : QString(); });
|
||||
}
|
||||
|
||||
searchEdit->setTreeView(databaseView);
|
||||
searchEdit->installEventFilter(databaseView->getKeySignals());
|
||||
|
||||
|
|
@ -195,6 +209,11 @@ void VisualDatabaseDisplayWidget::showEvent(QShowEvent *event)
|
|||
initializeFilters();
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::setNewZoneCreator(const std::function<QString()> &creator)
|
||||
{
|
||||
newZoneCreator = creator;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::retranslateUi()
|
||||
{
|
||||
databaseLoadIndicator->setText(tr("Loading database ..."));
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
#include <QVBoxLayout>
|
||||
#include <QWheelEvent>
|
||||
#include <QWidget>
|
||||
#include <functional>
|
||||
#include <libcockatrice/models/database/card_database_model.h>
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
#include <qscrollarea.h>
|
||||
|
|
@ -46,6 +47,12 @@ public:
|
|||
void sortCardList(const QStringList &properties, Qt::SortOrder order) const;
|
||||
void setDeckList(const DeckList &new_deck_list_model);
|
||||
|
||||
/**
|
||||
* @brief Sets the callback used to create a custom zone from the add-to-zone menu.
|
||||
* The callback returns the name of the created zone, or an empty string if creation was cancelled.
|
||||
*/
|
||||
void setNewZoneCreator(const std::function<QString()> &creator);
|
||||
|
||||
CardDatabaseDisplayModel *getDatabaseDisplayModel()
|
||||
{
|
||||
return databaseDisplayModel;
|
||||
|
|
@ -106,6 +113,7 @@ private:
|
|||
VisualDatabaseDisplayFilterToolbarWidget *filterContainer;
|
||||
CardDatabaseDisplayModel *databaseDisplayModel;
|
||||
CardDatabaseView *databaseView;
|
||||
std::function<QString()> newZoneCreator;
|
||||
QList<ExactCard> *cards;
|
||||
QVBoxLayout *mainLayout;
|
||||
QScrollArea *scrollArea;
|
||||
|
|
|
|||
|
|
@ -115,6 +115,15 @@ public:
|
|||
*/
|
||||
QList<const InnerDecklistNode *> getCustomZones(const QString &boardZoneName) const;
|
||||
|
||||
/**
|
||||
* @brief Checks whether a zone name is taken anywhere in the deck.
|
||||
*
|
||||
* Covers the standard board names and any top-level or nested custom zone.
|
||||
* @param zoneName The checked name.
|
||||
* @return true if the name is reserved or already in use.
|
||||
*/
|
||||
bool hasZoneName(const QString &zoneName) const;
|
||||
|
||||
/**
|
||||
* @brief Applies a function to every card in the deck tree. This can modify the cards.
|
||||
*
|
||||
|
|
@ -129,7 +138,6 @@ private:
|
|||
InnerDecklistNode *findBoardZone(const QString &boardZoneName) const;
|
||||
InnerDecklistNode *findOrCreateBoardZone(const QString &boardZoneName);
|
||||
InnerDecklistNode *findCustomZoneByName(const QString &zoneName) const;
|
||||
bool hasZoneName(const QString &zoneName) const;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DECKLIST_NODE_TREE_H
|
||||
|
|
|
|||
|
|
@ -43,6 +43,13 @@ void InnerDecklistNode::setSortMethod(DeckSortMethod method)
|
|||
}
|
||||
}
|
||||
|
||||
const QList<QString> &InnerDecklistNode::boardZoneNames()
|
||||
{
|
||||
static const QList<QString> names = {QString(DECK_ZONE_MAIN), QString(DECK_ZONE_SIDE),
|
||||
QString(DECK_ZONE_MAYBEBOARD)};
|
||||
return names;
|
||||
}
|
||||
|
||||
QString InnerDecklistNode::getVisibleName() const
|
||||
{
|
||||
return visibleNameFromName(name);
|
||||
|
|
@ -87,6 +94,9 @@ AbstractDecklistNode *InnerDecklistNode::findCardChildByNameProviderIdAndNumber(
|
|||
|
||||
int InnerDecklistNode::height() const
|
||||
{
|
||||
if (isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
return at(0)->height() + 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
|
||||
#include "abstract_deck_list_node.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
/** @brief Constant for the "main" deck zone name. */
|
||||
#define DECK_ZONE_MAIN "main"
|
||||
/** @brief Constant for the "sideboard" zone name. */
|
||||
|
|
@ -118,6 +121,13 @@ public:
|
|||
*/
|
||||
static QString visibleNameFromName(const QString &_name);
|
||||
|
||||
/**
|
||||
* @brief The standard board zone names, in display order.
|
||||
*
|
||||
* @return main, side and maybeboard.
|
||||
*/
|
||||
static const QList<QString> &boardZoneNames();
|
||||
|
||||
/**
|
||||
* @brief Get this node’s display-friendly name.
|
||||
* @return Human-readable name (zone/group name).
|
||||
|
|
|
|||
|
|
@ -378,7 +378,8 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent)
|
|||
|
||||
InnerDecklistNode *DeckListModel::createNodeIfNeeded(const QString &name, InnerDecklistNode *parent)
|
||||
{
|
||||
auto *newNode = dynamic_cast<InnerDecklistNode *>(parent->findChild(name));
|
||||
// Group lookups must not resolve a mirrored custom zone that shares the name.
|
||||
auto *newNode = DeckListModelCustomZones::findGroupChild(parent, name);
|
||||
if (!newNode) {
|
||||
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
|
||||
newNode = new InnerDecklistNode(name, parent);
|
||||
|
|
@ -401,7 +402,7 @@ DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName,
|
|||
// nested under the board.
|
||||
if (auto *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName))) {
|
||||
QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria);
|
||||
if (auto *groupNode = dynamic_cast<InnerDecklistNode *>(zoneNode->findChild(groupCriteria))) {
|
||||
if (auto *groupNode = DeckListModelCustomZones::findGroupChild(zoneNode, groupCriteria)) {
|
||||
if (auto *card = dynamic_cast<DecklistModelCardNode *>(
|
||||
groupNode->findCardChildByNameProviderIdAndNumber(cardName, providerId, cardNumber))) {
|
||||
return card;
|
||||
|
|
@ -486,6 +487,26 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam
|
|||
// Custom zone: cards live flat inside the zone.
|
||||
cardParent = customZoneNode;
|
||||
} else {
|
||||
// Not present in the shadow tree. The deck tree may still hold a custom
|
||||
// zone that has not been mirrored (callers can add a zone and then a
|
||||
// card without a rebuild). Check before falling back to creating a
|
||||
// top-level zone the deck does not actually have.
|
||||
auto *listRoot = deckList->getTree()->getRoot();
|
||||
bool hasDeckZone = false;
|
||||
for (int i = 0; i < listRoot->size(); ++i) {
|
||||
if (auto *boardZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i))) {
|
||||
if (boardZone->findChild(zoneName)) {
|
||||
hasDeckZone = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDeckZone) {
|
||||
rebuildTree();
|
||||
return addCard(card, zoneName);
|
||||
}
|
||||
|
||||
// Unknown zone: create a top-level zone (legacy behavior).
|
||||
QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria);
|
||||
auto *newZone = createNodeIfNeeded(zoneName, root);
|
||||
|
|
|
|||
|
|
@ -14,26 +14,55 @@ bool isCustomZone(const AbstractDecklistNode *node)
|
|||
return dynamic_cast<const DecklistModelSubZoneNode *>(node) != nullptr;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Flattens every card under @p zone into @p shadowZone, preserving order.
|
||||
*
|
||||
* Custom zones mirror as a single row level: cards nested in sub-zones of any
|
||||
* depth are added as direct children of the mirrored zone so no card is left
|
||||
* without a model row.
|
||||
*/
|
||||
void flattenCards(const InnerDecklistNode *zone, InnerDecklistNode *shadowZone)
|
||||
{
|
||||
for (int k = 0; k < zone->size(); k++) {
|
||||
if (auto *zoneCard = dynamic_cast<DecklistCardNode *>(zone->at(k))) {
|
||||
new DecklistModelCardNode(zoneCard, shadowZone);
|
||||
} else if (auto *subZone = dynamic_cast<const InnerDecklistNode *>(zone->at(k))) {
|
||||
flattenCards(subZone, shadowZone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void mirrorCustomZones(const InnerDecklistNode *deckBoardZone, InnerDecklistNode *shadowBoardZone)
|
||||
{
|
||||
for (int j = 0; j < deckBoardZone->size(); j++) {
|
||||
auto *customCard = dynamic_cast<DecklistCardNode *>(deckBoardZone->at(j));
|
||||
if (customCard) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *customZone = dynamic_cast<const InnerDecklistNode *>(deckBoardZone->at(j));
|
||||
if (!customZone) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *shadowZone = new DecklistModelSubZoneNode(customZone->getName(), shadowBoardZone);
|
||||
for (int k = 0; k < customZone->size(); k++) {
|
||||
if (auto *zoneCard = dynamic_cast<DecklistCardNode *>(customZone->at(k))) {
|
||||
new DecklistModelCardNode(zoneCard, shadowZone);
|
||||
}
|
||||
flattenCards(customZone, shadowZone);
|
||||
}
|
||||
}
|
||||
|
||||
InnerDecklistNode *findGroupChild(InnerDecklistNode *parent, const QString &name)
|
||||
{
|
||||
for (int i = 0; i < parent->size(); i++) {
|
||||
AbstractDecklistNode *child = parent->at(i);
|
||||
if (isCustomZone(child)) {
|
||||
continue;
|
||||
}
|
||||
auto *group = dynamic_cast<InnerDecklistNode *>(child);
|
||||
if (group && group->getName() == name) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DecklistModelSubZoneNode *findSubZoneByName(InnerDecklistNode *root, const QString &zoneName)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,21 @@ namespace DeckListModelCustomZones
|
|||
*/
|
||||
[[nodiscard]] bool isCustomZone(const AbstractDecklistNode *node);
|
||||
|
||||
/**
|
||||
* @brief Finds a criteria-group child of @p parent by name, skipping custom zones.
|
||||
*
|
||||
* The shadow tree keeps criteria groups and mirrored custom zones as siblings
|
||||
* under a board zone, and `InnerDecklistNode::findChild` matches both by name.
|
||||
* Group lookups must not resolve a custom zone that happens to share the group
|
||||
* name (e.g. a zone called "Creature"), so this searches only non-custom
|
||||
* children.
|
||||
*
|
||||
* @param parent The shadow node whose children are searched.
|
||||
* @param name The group name to find.
|
||||
* @return The matching group node, or nullptr if none exists.
|
||||
*/
|
||||
[[nodiscard]] InnerDecklistNode *findGroupChild(InnerDecklistNode *parent, const QString &name);
|
||||
|
||||
/**
|
||||
* @brief Mirrors the custom zones of a deck board zone into its shadow board node.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
add_executable(deck_list_model_custom_zones_test deck_list_model_custom_zones_test.cpp)
|
||||
add_executable(deck_list_model_custom_zones_test ${VERSION_STRING_CPP} deck_list_model_custom_zones_test.cpp)
|
||||
|
||||
if(NOT GTEST_FOUND)
|
||||
add_dependencies(deck_list_model_custom_zones_test gtest)
|
||||
|
|
|
|||
|
|
@ -129,6 +129,51 @@ TEST(DeckListModelCustomZones, MirrorCustomZonesWithNoCustomZonesIsNoop)
|
|||
EXPECT_EQ(shadowBoard->size(), 0);
|
||||
}
|
||||
|
||||
TEST(DeckListModelCustomZones, MirrorCustomZonesFlattensNestedSubzones)
|
||||
{
|
||||
// Cards deeper than one level under a custom zone still get a model row.
|
||||
auto *deckBoard = new InnerDecklistNode(DECK_ZONE_MAIN);
|
||||
auto *deckZone = new InnerDecklistNode("Removal", deckBoard);
|
||||
auto *deckCard1 = new DecklistCardNode("Bolt", 1, deckZone);
|
||||
auto *deeper = new InnerDecklistNode("Deeper", deckZone);
|
||||
auto *deckCard2 = new DecklistCardNode("Swords", 1, deeper);
|
||||
|
||||
InnerDecklistNode shadowRoot;
|
||||
auto *shadowBoard = new InnerDecklistNode(DECK_ZONE_MAIN, &shadowRoot);
|
||||
|
||||
DeckListModelCustomZones::mirrorCustomZones(deckBoard, shadowBoard);
|
||||
|
||||
ASSERT_EQ(shadowBoard->size(), 1);
|
||||
auto *shadowZone = dynamic_cast<DecklistModelSubZoneNode *>(shadowBoard->at(0));
|
||||
ASSERT_NE(shadowZone, nullptr);
|
||||
EXPECT_EQ(shadowZone->getName(), QString("Removal"));
|
||||
|
||||
// Both cards are flattened into the mirrored zone, preserving order.
|
||||
ASSERT_EQ(shadowZone->size(), 2);
|
||||
auto *shadowCard1 = dynamic_cast<DecklistModelCardNode *>(shadowZone->at(0));
|
||||
auto *shadowCard2 = dynamic_cast<DecklistModelCardNode *>(shadowZone->at(1));
|
||||
ASSERT_NE(shadowCard1, nullptr);
|
||||
ASSERT_NE(shadowCard2, nullptr);
|
||||
EXPECT_EQ(shadowCard1->getDataNode(), deckCard1);
|
||||
EXPECT_EQ(shadowCard2->getDataNode(), deckCard2);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// findGroupChild
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListModelCustomZones, FindGroupChildSkipsCustomZones)
|
||||
{
|
||||
InnerDecklistNode root;
|
||||
auto *board = new InnerDecklistNode(DECK_ZONE_MAIN, &root);
|
||||
auto *group = new InnerDecklistNode("Creature", board);
|
||||
new DecklistModelSubZoneNode("Creature", board);
|
||||
|
||||
EXPECT_EQ(DeckListModelCustomZones::findGroupChild(board, "Creature"), group);
|
||||
EXPECT_EQ(DeckListModelCustomZones::findGroupChild(board, "Missing"), nullptr);
|
||||
EXPECT_EQ(DeckListModelCustomZones::findGroupChild(&root, DECK_ZONE_MAIN), board);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// sortWithCustomZonesLast
|
||||
// =====================================================================================================================
|
||||
|
|
@ -223,3 +268,9 @@ TEST(DeckListModelCustomZones, SortPlainNodeDoesNotReorderCustomZones)
|
|||
EXPECT_EQ(mapping[1].first, 0);
|
||||
EXPECT_EQ(mapping[1].second, 1);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/card/game_specific_terms.h>
|
||||
#include <libcockatrice/card/printing/exact_card.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/deck_list_node_tree.h>
|
||||
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
|
||||
|
|
@ -25,6 +29,32 @@ int totalCustomZoneRows(const DeckListModel &model)
|
|||
return count;
|
||||
}
|
||||
|
||||
QModelIndex findBoardIndex(const DeckListModel &model, const QString &boardName)
|
||||
{
|
||||
for (int r = 0; r < model.rowCount(QModelIndex()); ++r) {
|
||||
const QModelIndex idx = model.index(r, 0, QModelIndex());
|
||||
if (idx.data(DeckRoles::IsCardRole).toBool()) {
|
||||
continue;
|
||||
}
|
||||
const QString name = idx.sibling(idx.row(), DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
if (name == boardName) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QModelIndex findZoneRow(const DeckListModel &model, const QModelIndex &board)
|
||||
{
|
||||
for (int r = 0; r < model.rowCount(board); ++r) {
|
||||
const QModelIndex child = model.index(r, 0, board);
|
||||
if (child.data(DeckRoles::IsCustomZoneRole).toBool()) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The "Add to Zone" combobox/submenu lists getCustomZoneNames(), which reads the
|
||||
|
|
@ -69,3 +99,148 @@ TEST(DeckListModelZoneIntegration, RebuildTreeMirrorsEachZoneOnce)
|
|||
EXPECT_EQ(model.getCustomZoneNames(DECK_ZONE_MAIN), (QStringList{"Removal", "Utility"}));
|
||||
EXPECT_EQ(totalCustomZoneRows(model), 2);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// Model behaviour: addCard routing, findCard lookup, removeRows guard, empty-zone survival.
|
||||
// =====================================================================================================================
|
||||
|
||||
TEST(DeckListModelZoneIntegration, AddCardRoutesIntoMirroredCustomZone)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
model.rebuildTree();
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), "Removal");
|
||||
ASSERT_TRUE(added.isValid());
|
||||
|
||||
// The card is a direct child of the mirrored custom zone, not a new top-level zone.
|
||||
const QModelIndex zoneParent = added.parent();
|
||||
ASSERT_TRUE(zoneParent.isValid());
|
||||
EXPECT_TRUE(zoneParent.data(DeckRoles::IsCustomZoneRole).toBool());
|
||||
EXPECT_EQ(zoneParent.sibling(zoneParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(),
|
||||
QString("Removal"));
|
||||
|
||||
// No "Removal" top-level zone appeared in the deck tree.
|
||||
auto *listRoot = tree->getRoot();
|
||||
bool topLevelRemoval = false;
|
||||
for (int i = 0; i < listRoot->size(); ++i) {
|
||||
if (auto *zone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i))) {
|
||||
topLevelRemoval |= zone->getName() == "Removal";
|
||||
}
|
||||
}
|
||||
EXPECT_FALSE(topLevelRemoval);
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, AddCardToUnmirroredCustomZoneRebuildsNotCreatesTopLevel)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
// The zone exists on the deck tree but the shadow tree has never mirrored it.
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(CardInfo::newInstance("Lightning Bolt")), "Removal");
|
||||
ASSERT_TRUE(added.isValid());
|
||||
|
||||
const QModelIndex zoneParent = added.parent();
|
||||
ASSERT_TRUE(zoneParent.isValid());
|
||||
EXPECT_TRUE(zoneParent.data(DeckRoles::IsCustomZoneRole).toBool());
|
||||
EXPECT_EQ(zoneParent.sibling(zoneParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(),
|
||||
QString("Removal"));
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, AddCardCreatesGroupSeparatelyFromSameNamedZone)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
// A custom zone named exactly like a grouping criterion.
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Creature"), nullptr);
|
||||
model.rebuildTree();
|
||||
|
||||
CardInfoPtr bear = CardInfo::newInstance("Grizzly Bears");
|
||||
bear->setProperty(Mtg::MainCardType, "Creature");
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(bear), DECK_ZONE_MAIN);
|
||||
ASSERT_TRUE(added.isValid());
|
||||
|
||||
// The card lands in a *group* node called "Creature", not swallowed by the custom zone.
|
||||
const QModelIndex groupParent = added.parent();
|
||||
ASSERT_TRUE(groupParent.isValid());
|
||||
EXPECT_FALSE(groupParent.data(DeckRoles::IsCustomZoneRole).toBool());
|
||||
EXPECT_EQ(groupParent.sibling(groupParent.row(), DeckListModelColumns::CARD_NAME).data(Qt::DisplayRole).toString(),
|
||||
QString("Creature"));
|
||||
|
||||
// The board keeps both rows: the "Creature" group and the "Creature" custom zone.
|
||||
const QModelIndex boardIndex = groupParent.parent();
|
||||
ASSERT_TRUE(boardIndex.isValid());
|
||||
EXPECT_EQ(model.rowCount(boardIndex), 2);
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, FindCardResolvesCardInsideCustomZone)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
model.rebuildTree();
|
||||
|
||||
// findCard resolves through the card database; register the card we add.
|
||||
const QString cardName = "Swords to Plowshares";
|
||||
CardInfoPtr info = CardInfo::newInstance(cardName);
|
||||
CardDatabaseManager::getInstance()->addCard(info);
|
||||
|
||||
QModelIndex added = model.addCard(ExactCard(info), "Removal");
|
||||
ASSERT_TRUE(added.isValid());
|
||||
|
||||
QModelIndex found = model.findCard(cardName, "Removal");
|
||||
EXPECT_TRUE(found.isValid());
|
||||
EXPECT_EQ(found, added);
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, RemoveRowsRefusesCustomZoneRow)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, DECK_ZONE_MAIN, -1);
|
||||
model.rebuildTree();
|
||||
|
||||
const QModelIndex mainIndex = findBoardIndex(model, DECK_ZONE_MAIN);
|
||||
ASSERT_TRUE(mainIndex.isValid());
|
||||
const QModelIndex zoneRow = findZoneRow(model, mainIndex);
|
||||
ASSERT_TRUE(zoneRow.isValid());
|
||||
|
||||
EXPECT_FALSE(model.removeRow(zoneRow.row(), zoneRow.parent()));
|
||||
EXPECT_EQ(model.rowCount(mainIndex), 2); // the zone survives, alongside the card group
|
||||
}
|
||||
|
||||
TEST(DeckListModelZoneIntegration, EmptyCustomZoneSurvivesMirrorAndPruning)
|
||||
{
|
||||
QSharedPointer<DeckList> deck(new DeckList());
|
||||
DeckListModel model(nullptr, deck);
|
||||
auto *tree = deck->getTree();
|
||||
|
||||
// An empty custom zone must be mirrored (the stack deliberately keeps it alive).
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
model.rebuildTree();
|
||||
|
||||
const QModelIndex mainIndex = findBoardIndex(model, DECK_ZONE_MAIN);
|
||||
ASSERT_TRUE(mainIndex.isValid());
|
||||
EXPECT_EQ(model.rowCount(mainIndex), 1);
|
||||
EXPECT_TRUE(findZoneRow(model, mainIndex).isValid());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue