Use pointer-to-member signal connections for tutorial validation

Took 10 minutes
This commit is contained in:
Lukas Brübach
2026-08-09 03:30:57 +02:00
parent 569b531113
commit 9c0c480b2e
7 changed files with 56 additions and 36 deletions

View File

@@ -60,8 +60,10 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
vdeStep.validationTiming = ValidationTiming::OnSignal;
vdeStep.autoAdvanceOnValid = true;
vdeStep.validator = []() { return true; };
vdeStep.signalSource = visualDeckEditorButton;
vdeStep.signalName = SIGNAL(clicked());
vdeStep.signalHook = [this](TutorialController *controller) {
return connect(visualDeckEditorButton, &QAbstractButton::clicked, controller,
&TutorialController::checkValidation);
};
sequence.addStep(vdeStep);
sequence.addStep({visualDeckStorageButton, "Browse the decks in your local collection."});

View File

@@ -294,10 +294,8 @@ void TutorialController::setupValidationMonitoring()
const auto &step = sequences[currentSequence].steps[currentStep];
// Handle OnSignal validation - connect to any custom signal
if (step.validationTiming == ValidationTiming::OnSignal && step.validator) {
if (step.signalSource && step.signalName) {
validationConnection = connect(step.signalSource, step.signalName, this, SLOT(checkValidation()));
}
if (step.validationTiming == ValidationTiming::OnSignal && step.signalHook) {
validationConnection = step.signalHook(this);
return;
}

View File

@@ -15,6 +15,8 @@ enum class ValidationTiming
Manual // Only validate when explicitly triggered
};
class TutorialController;
struct TutorialStep
{
QWidget *targetWidget = nullptr;
@@ -35,9 +37,11 @@ struct TutorialStep
// Custom interaction hint (overrides default "Click to continue")
QString customInteractionHint = nullptr;
// Signal-based validation (for ValidationTiming::OnSignal)
QObject *signalSource = nullptr; // Object that emits the signal
const char *signalName = nullptr; // Signal to connect to (use SIGNAL() macro)
// Establishes the connection used for signal-based validation (ValidationTiming::OnSignal).
// Called with the controller so call sites can connect a specific signal to
// TutorialController::checkValidation using pointer-to-member, which is checked at
// compile time unlike string-based SIGNAL() connections.
std::function<QMetaObject::Connection(TutorialController *)> signalHook = nullptr;
};
struct TutorialSequence

View File

@@ -186,8 +186,10 @@ void TabGame::finishTutorialInitialization()
loadDeckStep.requiresInteraction = true;
loadDeckStep.autoAdvanceOnValid = true;
loadDeckStep.validationTiming = ValidationTiming::OnSignal;
loadDeckStep.signalSource = game->getGameEventHandler();
loadDeckStep.signalName = SIGNAL(logDeckSelect(PlayerLogic *, QString, int));
loadDeckStep.signalHook = [this](TutorialController *controller) {
return connect(game->getGameEventHandler(), &GameEventHandler::logDeckSelect, controller,
&TutorialController::checkValidation);
};
loadDeckStep.validator = [] { return true; };
deckSelectSequence.addStep(loadDeckStep);
@@ -199,8 +201,9 @@ void TabGame::finishTutorialInitialization()
readyUpStep.requiresInteraction = true;
readyUpStep.autoAdvanceOnValid = true;
readyUpStep.validationTiming = ValidationTiming::OnSignal;
readyUpStep.signalSource = this;
readyUpStep.signalName = SIGNAL(localPlayerReadyStateChanged(bool));
readyUpStep.signalHook = [this](TutorialController *controller) {
return connect(this, &TabGame::localPlayerReadyStateChanged, controller, &TutorialController::checkValidation);
};
readyUpStep.validator = [] { return true; };
deckSelectSequence.addStep(readyUpStep);
@@ -256,10 +259,12 @@ void TabGame::finishTutorialInitialization()
lifeCounterStep.allowClickThrough = true;
lifeCounterStep.autoAdvanceOnValid = true;
lifeCounterStep.validationTiming = ValidationTiming::OnSignal;
lifeCounterStep.signalSource = game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getPlayerEventHandler();
lifeCounterStep.signalName = SIGNAL(logSetCounter(PlayerLogic *, QString, int, int));
lifeCounterStep.signalHook = [this](TutorialController *controller) {
return connect(game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getPlayerEventHandler(),
&PlayerEventHandler::logSetCounter, controller, &TutorialController::checkValidation);
};
lifeCounterStep.validator = [this] {
auto counters =
game->getPlayerManager()->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())->getCounters();
@@ -283,10 +288,12 @@ void TabGame::finishTutorialInitialization()
diceRollStep.allowClickThrough = true;
diceRollStep.autoAdvanceOnValid = true;
diceRollStep.validationTiming = ValidationTiming::OnSignal;
diceRollStep.signalSource = game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getPlayerEventHandler();
diceRollStep.signalName = SIGNAL(logRollDie(PlayerLogic *, int, const QList<uint> &));
diceRollStep.signalHook = [this](TutorialController *controller) {
return connect(game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getPlayerEventHandler(),
&PlayerEventHandler::logRollDie, controller, &TutorialController::checkValidation);
};
diceRollStep.validator = [] { return true; };
diceRollStep.validationHint = tr("Roll a dice using any of these methods.");
@@ -302,10 +309,12 @@ void TabGame::finishTutorialInitialization()
mulliganStep.allowClickThrough = true;
mulliganStep.autoAdvanceOnValid = true;
mulliganStep.validationTiming = ValidationTiming::OnSignal;
mulliganStep.signalSource = game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getPlayerEventHandler();
mulliganStep.signalName = SIGNAL(logDrawCards(PlayerLogic *, int, bool));
mulliganStep.signalHook = [this](TutorialController *controller) {
return connect(game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getPlayerEventHandler(),
&PlayerEventHandler::logDrawCards, controller, &TutorialController::checkValidation);
};
mulliganStep.validator = [this] {
return game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())

View File

@@ -156,8 +156,9 @@ TutorialSequence VisualDatabaseDisplayWidget::addTutorialSteps()
setFilterStep.requiresInteraction = true;
setFilterStep.autoAdvanceOnValid = true;
setFilterStep.validationTiming = ValidationTiming::OnSignal;
setFilterStep.signalSource = filterModel;
setFilterStep.signalName = SIGNAL(layoutChanged());
setFilterStep.signalHook = [this](TutorialController *controller) {
return connect(filterModel, &FilterTreeModel::layoutChanged, controller, &TutorialController::checkValidation);
};
setFilterStep.validator = [] { return true; };
sequence.addStep(setFilterStep);
@@ -177,9 +178,11 @@ TutorialSequence VisualDatabaseDisplayWidget::addTutorialSteps()
deckEditor->deckStateManager->clearDeck();
}
};
explorationStep.signalSource =
QtUtils::findParentOfType<TabDeckEditorVisual>(this)->deckStateManager->getModel();
explorationStep.signalName = SIGNAL(cardNodesChanged());
auto deckModel = QtUtils::findParentOfType<TabDeckEditorVisual>(this)->deckStateManager->getModel();
explorationStep.signalHook = [deckModel](TutorialController *controller) {
return connect(deckModel, &DeckListModel::cardNodesChanged, controller,
&TutorialController::checkValidation);
};
explorationStep.validator = [this] {
if (QtUtils::findParentOfType<TabDeckEditorVisual>(this)) {
return QtUtils::findParentOfType<TabDeckEditorVisual>(this)

View File

@@ -145,8 +145,9 @@ TutorialSequence VisualDeckDisplayOptionsWidget::generateTutorialSequence(Tutori
displayTypeStep.allowClickThrough = true;
displayTypeStep.requiresInteraction = true;
displayTypeStep.validationTiming = ValidationTiming::OnSignal;
displayTypeStep.signalSource = displayTypeButton;
displayTypeStep.signalName = SIGNAL(clicked());
displayTypeStep.signalHook = [this](TutorialController *controller) {
return connect(displayTypeButton, &QAbstractButton::clicked, controller, &TutorialController::checkValidation);
};
displayTypeStep.autoAdvanceOnValid = true;
displayTypeStep.validator = [] { return true; };
@@ -175,8 +176,10 @@ TutorialSequence VisualDeckDisplayOptionsWidget::generateTutorialSequence(Tutori
sortStep.requiresInteraction = true;
sortStep.autoAdvanceOnValid = true;
sortStep.validationTiming = ValidationTiming::OnSignal;
sortStep.signalSource = this;
sortStep.signalName = SIGNAL(sortCriteriaChanged(const QStringList &));
sortStep.signalHook = [this](TutorialController *controller) {
return connect(this, &VisualDeckDisplayOptionsWidget::sortCriteriaChanged, controller,
&TutorialController::checkValidation);
};
sortStep.validator = []() { return true; };
sequence.addStep(sortStep);

View File

@@ -495,8 +495,9 @@ TutorialSequence VisualDeckEditorWidget::addTutorialSteps()
addStep.requiresInteraction = true;
addStep.autoAdvanceOnValid = true;
addStep.validationTiming = ValidationTiming::OnSignal;
addStep.signalSource = deckListModel;
addStep.signalName = SIGNAL(cardAddedAt(const QModelIndex &));
addStep.signalHook = [this](TutorialController *controller) {
return connect(deckListModel, &DeckListModel::cardAddedAt, controller, &TutorialController::checkValidation);
};
addStep.validator = [this]() { return deckListModel->getCardNodes().size() >= 1; };
sequence.addStep(addStep);