mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -07:00
[Client] Add zone management to the deck state manager (#7204)
* [Client] Add zone management to the deck state manager State-layer operations for custom deck zones, plus the shared prompt dialog that later editor menus will call into. - moveCardToZone relocates every copy of a card row into any zone, refusing non-card rows and tokens so miswired selections can never shred a group or turn tokens into deck cards. The current zone is found by walking ancestors, which also handles legacy top-level zones. - createCustomZone, renameCustomZone, moveCustomZone and removeCustomZone wrap the tree API with memento history, model rebuilds and deck hash refreshes via modifyTree. - Same-board zone moves return success without minting a history entry, keeping the undo log honest. - promptForNewZone asks for a name and the parent zone, keeps Ok disabled until the trimmed name passes a caller-supplied validator (shown inline as an error), and reports its own translation context. Took 14 minutes # Commit time for manual adjustment: # Took 6 minutes # Commit time for manual adjustment: # Took 33 seconds * [DeckEditor] Address zone-management review feedback - Expose DecklistNodeTree::hasZoneName and use it in validateNewZoneName so the uniqueness scan covers custom zones on every board, not just the standard ones. - Hide the board selector in the rename dialog path where it is not used. - Emit deckHashChanged after refreshDeckHash so the deck hash label stays current after zone create/rename/move/remove. * [DeckEditor] Notify card set changes after zone edits and drop the board scan - modifyTree emits cardNodesChanged alongside deckHashChanged so the banner-card combo and printing in-deck counts refresh after removing a zone that still holds cards - DecklistNodeTree::findCustomZoneByName is public and moveCustomZone uses it, locating zones under non-standard boards (e.g. tokens) instead of scanning only main/side/maybeboard --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
e8ec28572f
commit
9677fad342
9 changed files with 589 additions and 2 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
|
||||
|
|
|
|||
|
|
@ -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,170 @@ 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 zone through the tree's own lookup, which walks every top-level
|
||||
// zone (not just the standard boards) and covers the same-board no-op below.
|
||||
const auto *zone = tree->findCustomZoneByName(zoneName);
|
||||
if (!zone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same-board moves are no-ops and must not pollute the history.
|
||||
const QString currentBoardName = zone->getParent() ? zone->getParent()->getName() : QString();
|
||||
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 *targetZone : tree->getCustomZones(newBoardZoneName)) {
|
||||
if (targetZone->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 +532,25 @@ 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();
|
||||
// removeCustomZone can drop whole card sets the model never notified
|
||||
// about (rebuildTree emits no cardNodesChanged), so tell the consumers.
|
||||
emit deckListModel->cardNodesChanged();
|
||||
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
|
||||
|
|
@ -115,6 +115,25 @@ 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 Finds a custom zone anywhere in the deck by name.
|
||||
*
|
||||
* Walks the children of every top-level zone, so a zone nested under any
|
||||
* board (and not just the standard ones) is found.
|
||||
* @param zoneName The zone name to find.
|
||||
* @return The matching zone node, or nullptr if none exists.
|
||||
*/
|
||||
InnerDecklistNode *findCustomZoneByName(const QString &zoneName) const;
|
||||
|
||||
/**
|
||||
* @brief Applies a function to every card in the deck tree. This can modify the cards.
|
||||
*
|
||||
|
|
@ -128,8 +147,6 @@ private:
|
|||
InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const;
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -213,6 +213,42 @@ TEST(DeckListZones, MoveCustomZoneMovesCards)
|
|||
EXPECT_FALSE(tree->moveCustomZone("Removal", "not_a_board"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, MoveCustomZoneFailsForUnknownBoard)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
|
||||
ASSERT_NE(tree->addCustomZone(DECK_ZONE_MAIN, "Removal"), nullptr);
|
||||
tree->addCard("Lightning Bolt", 2, "Removal", -1);
|
||||
|
||||
EXPECT_FALSE(tree->moveCustomZone("Removal", "not_a_board"));
|
||||
|
||||
// The zone is still under main.
|
||||
EXPECT_EQ(tree->getCustomZones(DECK_ZONE_MAIN).size(), 1);
|
||||
}
|
||||
|
||||
// Regression: findCustomZoneByName walks every top-level zone, so a custom zone
|
||||
// an imported deck carries under a non-standard board (tokens) is still found and
|
||||
// movable. The pre-fix manager-level moveCustomZone only scanned the standard
|
||||
// boards and returned false for these with no feedback.
|
||||
TEST(DeckListZones, MoveCustomZoneNestedUnderTokensBoard)
|
||||
{
|
||||
DeckList deck;
|
||||
auto *tree = deck.getTree();
|
||||
auto *root = tree->getRoot();
|
||||
|
||||
auto *tokens = new InnerDecklistNode(DECK_ZONE_TOKENS, root);
|
||||
auto *removal = new InnerDecklistNode("Removal", tokens);
|
||||
new DecklistCardNode("Lightning Bolt", 2, removal, -1);
|
||||
|
||||
EXPECT_TRUE(tree->findCustomZoneByName("Removal"));
|
||||
EXPECT_TRUE(tree->moveCustomZone("Removal", DECK_ZONE_SIDE));
|
||||
|
||||
auto pairs = collectBoardCardPairs(deck);
|
||||
EXPECT_FALSE(hasPair(pairs, DECK_ZONE_TOKENS, "Lightning Bolt"));
|
||||
EXPECT_TRUE(hasPair(pairs, DECK_ZONE_SIDE, "Lightning Bolt"));
|
||||
}
|
||||
|
||||
TEST(DeckListZones, RemoveCustomZoneRemovesCards)
|
||||
{
|
||||
DeckList deck;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue