mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -07:00
[Client] Expose custom zone management in the deck editor (#7205)
* [Client] Expose custom zone management in the deck editor Wires the state layer into every editor surface that shows deck zones. - Deck dock: context menu on zones gains New/Rename/Delete/Change board actions, with per-zone submenus for adding cards. - Card database dock and visual database display gain an add-to-zone submenu listing custom zones per board plus a create-zone entry. - All prompt call sites pass validateNewZoneName so duplicates and reserved names are rejected inline before Ok unlocks. - Rename reuses the same dialog in name-only mode, keeping one validation contract for every zone-name entry point. - Change board marks the current board instead of offering a no-op, and the state layer refuses moves onto boards holding a same-named zone from imported decks. * [DeckEditor] Address custom-zone menu and export review feedback * [DeckLoader] Keep the sideboard marker and block ordering when exporting nested zones - 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 * [DeckEditor] Fix move-to-zone menu use-after-free and per-zone enabled state - resolve the card name/provider/collector number before createNewCustomZone rebuilds the model tree, then re-find the refreshed index via findCard and move it (mirrors the decrementCard re-find pattern) - the enabled test now compares the card's own zone (nearest custom-zone ancestor, else its board), matching moveCardToZone's lookup, so moving a card out of a custom zone back to the board root is offered and the card's own zone is disabled --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
9677fad342
commit
0d09e633e3
12 changed files with 418 additions and 17 deletions
|
|
@ -375,15 +375,32 @@ void DeckLoader::saveToStream_DeckHeader(QTextStream &out, const DeckList &deckL
|
|||
void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
||||
const InnerDecklistNode *zoneNode,
|
||||
bool addComments,
|
||||
bool addSetNameAndNumber)
|
||||
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";
|
||||
|
|
@ -411,25 +428,30 @@ void DeckLoader::saveToStream_DeckZone(QTextStream &out,
|
|||
|
||||
QList<DecklistCardNode *> cards = cardsByType.values(cardType);
|
||||
|
||||
saveToStream_DeckZoneCards(out, zoneNode, cards, addComments, addSetNameAndNumber);
|
||||
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,
|
||||
const InnerDecklistNode *zoneNode,
|
||||
QList<DecklistCardNode *> cards,
|
||||
bool addComments,
|
||||
bool addSetNameAndNumber)
|
||||
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 (zoneNode->getName() == DECK_ZONE_SIDE && addComments) {
|
||||
if (boardZoneName == DECK_ZONE_SIDE && addComments) {
|
||||
out << "SB: ";
|
||||
}
|
||||
|
||||
|
|
@ -510,9 +532,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 +562,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 +579,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 +604,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);
|
||||
|
|
|
|||
|
|
@ -159,12 +159,13 @@ private:
|
|||
static void saveToStream_DeckZone(QTextStream &out,
|
||||
const InnerDecklistNode *zoneNode,
|
||||
bool addComments = true,
|
||||
bool addSetNameAndNumber = true);
|
||||
bool addSetNameAndNumber = true,
|
||||
const QString &boardZoneName = QString());
|
||||
static void saveToStream_DeckZoneCards(QTextStream &out,
|
||||
const InnerDecklistNode *zoneNode,
|
||||
QList<DecklistCardNode *> cards,
|
||||
bool addComments = true,
|
||||
bool addSetNameAndNumber = true);
|
||||
bool addSetNameAndNumber = true,
|
||||
const QString &boardZoneName = QString());
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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,213 @@ 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)
|
||||
{
|
||||
// The card's current *zone*, derived with the same ancestor walk as
|
||||
// DeckStateManager::moveCardToZone (nearest custom-zone ancestor, else the
|
||||
// top-level board/zone): a card inside "Removal" under the maindeck lives in
|
||||
// "Removal", not "main". Comparing against that instead of the board keeps
|
||||
// the enabled state and the same-zone no-op consistent with the move logic.
|
||||
QString currentZoneName;
|
||||
for (QModelIndex ancestor = sourceCardIndex.parent(); ancestor.isValid(); ancestor = ancestor.parent()) {
|
||||
if (ancestor.data(DeckRoles::IsCustomZoneRole).toBool() || !ancestor.parent().isValid()) {
|
||||
currentZoneName = ancestor.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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 != currentZoneName);
|
||||
for (const auto *customZone : customZones) {
|
||||
addMoveAction(boardSubmenu, customZone->getName(), customZone->getName(),
|
||||
customZone->getName() != currentZoneName);
|
||||
}
|
||||
} else {
|
||||
addMoveAction(moveMenu, boardName, boardLabel, boardName != currentZoneName);
|
||||
}
|
||||
}
|
||||
|
||||
moveMenu->addSeparator();
|
||||
|
||||
QAction *newZoneAction = moveMenu->addAction(tr("Create new zone and move &here..."));
|
||||
connect(newZoneAction, &QAction::triggered, this, [this, sourceCardIndex, currentBoardName, currentZoneName] {
|
||||
// Resolve the card's identity before creating the zone:
|
||||
// createNewCustomZone rebuilds the model tree, so sourceCardIndex's
|
||||
// internal pointer is freed by the time it would be used.
|
||||
const QString cardName =
|
||||
sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_NAME).data(Qt::EditRole).toString();
|
||||
const QString providerId =
|
||||
sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_PROVIDER_ID).data(Qt::DisplayRole).toString();
|
||||
const QString collectorNumber = sourceCardIndex.siblingAtColumn(DeckListModelColumns::CARD_COLLECTOR_NUMBER)
|
||||
.data(Qt::DisplayRole)
|
||||
.toString();
|
||||
|
||||
const QString zoneName = createNewCustomZone(currentBoardName);
|
||||
if (!zoneName.isEmpty()) {
|
||||
// Re-find the card: the old index is no longer safe since rows were
|
||||
// rebuilt. Mirror DeckStateManager::decrementCard's re-find pattern.
|
||||
const QModelIndex refreshed = getModel()->findCard(cardName, currentZoneName, providerId, collectorNumber);
|
||||
if (refreshed.isValid()) {
|
||||
deckStateManager->moveCardToZone(refreshed, 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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ AbstractDecklistNode *InnerDecklistNode::findCardChildByNameProviderIdAndNumber(
|
|||
|
||||
int InnerDecklistNode::height() const
|
||||
{
|
||||
if (isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
return at(0)->height() + 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue