Address review findings

This commit is contained in:
Lukas Brübach 2026-11-08 14:43:20 +01:00
parent 178d392361
commit 7c90211247
13 changed files with 229 additions and 67 deletions

View file

@ -169,10 +169,28 @@ SettingsCache::SettingsCache()
cardsDisplaySettings = new CardsDisplaySettings(settingsPath, this); cardsDisplaySettings = new CardsDisplaySettings(settingsPath, this);
interfaceSettings = new InterfaceSettings(settingsPath, this); interfaceSettings = new InterfaceSettings(settingsPath, this);
if (hadExistingSettings) { if (hadExistingSettings) {
// Installs that predate the tutorial have no tutorial keys at all. Seed each
// per-tutorial key from the legacy flag, but never overwrite a key that is already
// there. Fresh installs keep the false defaults so every tutorial runs.
QSettings iface(settingsPath + "interface.ini", QSettings::IniFormat); QSettings iface(settingsPath + "interface.ini", QSettings::IniFormat);
if (!iface.contains("interface/tutorialCompleted")) { const bool hasLegacyCompleted = iface.contains("interface/tutorialCompleted");
const bool hasHomeCompleted = iface.contains("interface/tutorialHomeCompleted");
const bool hasDeckEditorCompleted = iface.contains("interface/tutorialDeckEditorCompleted");
const bool hasGameCompleted = iface.contains("interface/tutorialGameCompleted");
if (!hasLegacyCompleted) {
// No legacy key means an install from before the tutorial existed: treat it as done.
interfaceSettings->setTutorialCompleted(true); interfaceSettings->setTutorialCompleted(true);
} }
const bool legacyCompleted = interfaceSettings->getTutorialCompleted();
if (!hasHomeCompleted) {
interfaceSettings->setHomeTutorialCompleted(legacyCompleted);
}
if (!hasDeckEditorCompleted) {
interfaceSettings->setDeckEditorTutorialCompleted(legacyCompleted);
}
if (!hasGameCompleted) {
interfaceSettings->setGameTutorialCompleted(legacyCompleted);
}
} }
deckEditorSettings = new DeckEditorSettings(settingsPath, this); deckEditorSettings = new DeckEditorSettings(settingsPath, this);
pathsSettings = new PathsSettings(settingsPath, this); pathsSettings = new PathsSettings(settingsPath, this);

View file

@ -98,13 +98,13 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged, connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged,
this, &HomeWidget::onBackgroundShuffleFrequencyChanged); this, &HomeWidget::onBackgroundShuffleFrequencyChanged);
tutorialController = new TutorialController(this); tutorialController = new TutorialController(TutorialId::Home, this);
auto sequence = TutorialSequence(); auto sequence = TutorialSequence();
sequence.addStep({connectButton, "Connect to a server to play here!"}); sequence.addStep({connectButton, tr("Connect to a server to play here!")});
auto vdeStep = TutorialStep{visualDeckEditorButton, "Create a new deck from cards in the database here!"}; auto vdeStep = TutorialStep{visualDeckEditorButton, tr("Create a new deck from cards in the database here!")};
vdeStep.requiresInteraction = true; vdeStep.requiresInteraction = true;
vdeStep.allowClickThrough = true; vdeStep.allowClickThrough = true;
vdeStep.validationHint = "Open the deck editor to try it out!"; vdeStep.validationHint = tr("Open the deck editor to try it out!");
vdeStep.validationTiming = ValidationTiming::OnSignal; vdeStep.validationTiming = ValidationTiming::OnSignal;
vdeStep.autoAdvanceOnValid = true; vdeStep.autoAdvanceOnValid = true;
vdeStep.validator = []() { return true; }; vdeStep.validator = []() { return true; };
@ -114,14 +114,14 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
}; };
sequence.addStep(vdeStep); sequence.addStep(vdeStep);
sequence.addStep({visualDeckStorageButton, "Browse the decks in your local collection."}); sequence.addStep({visualDeckStorageButton, tr("Browse the decks in your local collection.")});
sequence.addStep({visualDatabaseDisplayButton, "View the card database here."}); sequence.addStep({visualDatabaseDisplayButton, tr("View the card database here.")});
sequence.addStep( sequence.addStep(
{edhrecButton, "Browse EDHRec, an external service designed to provide card recommendations for decks."}); {edhrecButton, tr("Browse EDHRec, an external service designed to provide card recommendations for decks.")});
sequence.addStep({archidektButton, "Browse Archidekt, an external service that allows users to store " sequence.addStep({archidektButton, tr("Browse Archidekt, an external service that allows users to store "
"decklists and import them to your local collection."}); "decklists and import them to your local collection.")});
sequence.addStep({replayButton, "View replays of your past games here."}); sequence.addStep({replayButton, tr("View replays of your past games here.")});
sequence.addStep({exitButton, "Exit the application."}); sequence.addStep({exitButton, tr("Exit the application.")});
tutorialController->addSequence(sequence); tutorialController->addSequence(sequence);
// Lambda is cleaner to read than overloading this // Lambda is cleaner to read than overloading this
@ -171,7 +171,7 @@ void HomeWidget::retranslateUi()
void HomeWidget::showEvent(QShowEvent *event) void HomeWidget::showEvent(QShowEvent *event)
{ {
QWidget::showEvent(event); QWidget::showEvent(event);
if (!tutorialStarted && !SettingsCache::instance().userInterface().getTutorialCompleted()) { if (!tutorialStarted && !SettingsCache::instance().userInterface().getHomeTutorialCompleted()) {
tutorialStarted = true; tutorialStarted = true;
// Start on next event loop iteration so everything is fully painted // Start on next event loop iteration so everything is fully painted
QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); }); QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); });

View file

@ -68,10 +68,10 @@ void BubbleWidget::setText(const QString &text)
void BubbleWidget::setProgress(int stepNum, int totalSteps, int overallStep, int overallTotal) void BubbleWidget::setProgress(int stepNum, int totalSteps, int overallStep, int overallTotal)
{ {
// Per-sequence progress // Per-sequence progress
counterLabel->setText(QString("Step %1 of %2").arg(stepNum).arg(totalSteps)); counterLabel->setText(tr("Step %1 of %2").arg(stepNum).arg(totalSteps));
// Overall progress across all sequences // Overall progress across all sequences
progressLabel->setText(QString("(%1 of %2 total)").arg(overallStep).arg(overallTotal)); progressLabel->setText(tr("(%1 of %2 total)").arg(overallStep).arg(overallTotal));
progressLabel->show(); progressLabel->show();
} }

View file

@ -10,9 +10,21 @@
#include <QTimer> #include <QTimer>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
TutorialController::TutorialController(QWidget *_tutorializedWidget) namespace
: QObject(_tutorializedWidget), tutorializedWidget(_tutorializedWidget)
{ {
/// Every controller that currently exists, across all screens, so the overlay's X can end
/// all of them at once. Only touched from the GUI thread, which creates and destroys them.
QList<TutorialController *> liveControllers;
/// True while skipTutorials() closes the controllers, so a nested call is a no-op.
bool skippingTutorials = false;
} // namespace
TutorialController::TutorialController(TutorialId _tutorialId, QWidget *_tutorializedWidget)
: QObject(_tutorializedWidget), tutorialId(_tutorialId), tutorializedWidget(_tutorializedWidget)
{
liveControllers.append(this);
tutorialOverlay = new TutorialOverlay(tutorializedWidget->window()); tutorialOverlay = new TutorialOverlay(tutorializedWidget->window());
tutorialOverlay->setWindowFlags(tutorialOverlay->windowFlags() | Qt::FramelessWindowHint); tutorialOverlay->setWindowFlags(tutorialOverlay->windowFlags() | Qt::FramelessWindowHint);
@ -22,12 +34,14 @@ TutorialController::TutorialController(QWidget *_tutorializedWidget)
connect(tutorialOverlay, &TutorialOverlay::prevStep, this, &TutorialController::prevStep); connect(tutorialOverlay, &TutorialOverlay::prevStep, this, &TutorialController::prevStep);
connect(tutorialOverlay, &TutorialOverlay::nextSequence, this, &TutorialController::nextSequence); connect(tutorialOverlay, &TutorialOverlay::nextSequence, this, &TutorialController::nextSequence);
connect(tutorialOverlay, &TutorialOverlay::prevSequence, this, &TutorialController::prevSequence); connect(tutorialOverlay, &TutorialOverlay::prevSequence, this, &TutorialController::prevSequence);
connect(tutorialOverlay, &TutorialOverlay::skipTutorial, this, &TutorialController::exitTutorial); connect(tutorialOverlay, &TutorialOverlay::skipTutorial, this, &TutorialController::skipTutorials);
connect(tutorialOverlay, &TutorialOverlay::targetClicked, this, &TutorialController::handleTargetClicked); connect(tutorialOverlay, &TutorialOverlay::targetClicked, this, &TutorialController::handleTargetClicked);
} }
TutorialController::~TutorialController() TutorialController::~TutorialController()
{ {
liveControllers.removeAll(this);
// The overlay is parented to the top-level window, which outlives the widget // The overlay is parented to the top-level window, which outlives the widget
// this controller is attached to (e.g. a closed tab), so it must be cleaned // this controller is attached to (e.g. a closed tab), so it must be cleaned
// up explicitly when the controller goes away without exitTutorial(). // up explicitly when the controller goes away without exitTutorial().
@ -185,6 +199,9 @@ void TutorialController::prevSequence()
void TutorialController::exitTutorial() void TutorialController::exitTutorial()
{ {
if (tutorialCompleted) {
return;
}
cleanupValidationMonitoring(); cleanupValidationMonitoring();
if (tutorialOverlay) { if (tutorialOverlay) {
tutorialOverlay->hide(); tutorialOverlay->hide();
@ -194,10 +211,46 @@ void TutorialController::exitTutorial()
currentSequence = -1; currentSequence = -1;
currentStep = -1; currentStep = -1;
tutorialCompleted = true; tutorialCompleted = true;
SettingsCache::instance().userInterface().setTutorialCompleted(true);
// Only this screen's tutorial is done; the others keep their own state.
auto &iface = SettingsCache::instance().userInterface();
switch (tutorialId) {
case TutorialId::Home:
iface.setHomeTutorialCompleted(true);
break;
case TutorialId::DeckEditor:
iface.setDeckEditorTutorialCompleted(true);
break;
case TutorialId::Game:
iface.setGameTutorialCompleted(true);
break;
}
deleteLater(); deleteLater();
} }
void TutorialController::skipTutorials()
{
if (skippingTutorials) {
return;
}
skippingTutorials = true;
// The X means "I am done with the tutorial", not "close this one window", so every
// screen is marked done — including screens with no live controller right now.
auto &iface = SettingsCache::instance().userInterface();
iface.setHomeTutorialCompleted(true);
iface.setDeckEditorTutorialCompleted(true);
iface.setGameTutorialCompleted(true);
// exitTutorial() only schedules deletion, so iterating a copy cannot dangle.
const QList<TutorialController *> controllers = liveControllers;
for (TutorialController *controller : controllers) {
controller->exitTutorial();
}
skippingTutorials = false;
}
void TutorialController::runExitForCurrentStep() void TutorialController::runExitForCurrentStep()
{ {
if (currentSequence < 0 || currentSequence >= sequences.size()) { if (currentSequence < 0 || currentSequence >= sequences.size()) {
@ -269,7 +322,7 @@ void TutorialController::showStep()
if (!step.customInteractionHint.isEmpty()) { if (!step.customInteractionHint.isEmpty()) {
tutorialOverlay->setInteractionHint(step.customInteractionHint); tutorialOverlay->setInteractionHint(step.customInteractionHint);
} else if (step.requiresInteraction) { } else if (step.requiresInteraction) {
tutorialOverlay->setInteractionHint("👆 Click the highlighted area to continue"); tutorialOverlay->setInteractionHint(tr("👆 Click the highlighted area to continue"));
} else { } else {
tutorialOverlay->setInteractionHint(""); tutorialOverlay->setInteractionHint("");
} }

View file

@ -16,6 +16,14 @@ enum class ValidationTiming
Manual // Only validate when explicitly triggered Manual // Only validate when explicitly triggered
}; };
/// Which screen a controller belongs to; each tutorial tracks its own completion flag.
enum class TutorialId
{
Home,
DeckEditor,
Game
};
class TutorialController; class TutorialController;
struct TutorialStep struct TutorialStep
@ -67,7 +75,7 @@ class TutorialController : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit TutorialController(QWidget *_tutorializedWidget); TutorialController(TutorialId _tutorialId, QWidget *_tutorializedWidget);
~TutorialController() override; ~TutorialController() override;
void addSequence(const TutorialSequence &seq); void addSequence(const TutorialSequence &seq);
@ -85,6 +93,7 @@ public slots:
void prevSequence(); void prevSequence();
void runExitForCurrentStep(); void runExitForCurrentStep();
void exitTutorial(); void exitTutorial();
void skipTutorials(); // Close the tutorial on every live screen and mark all three done
void handleTargetClicked(); // Handle clicks on highlighted widget void handleTargetClicked(); // Handle clicks on highlighted widget
void attemptAdvance(); // Try to advance with validation void attemptAdvance(); // Try to advance with validation
void checkValidation(); // Check validation for OnChange timing void checkValidation(); // Check validation for OnChange timing
@ -96,6 +105,7 @@ private:
void setupValidationMonitoring(); // Setup automatic validation checking void setupValidationMonitoring(); // Setup automatic validation checking
void cleanupValidationMonitoring(); // Cleanup validation watchers void cleanupValidationMonitoring(); // Cleanup validation watchers
TutorialId tutorialId;
QWidget *tutorializedWidget; QWidget *tutorializedWidget;
TutorialOverlay *tutorialOverlay; TutorialOverlay *tutorialOverlay;
QList<TutorialSequence> sequences; QList<TutorialSequence> sequences;

View file

@ -41,7 +41,7 @@ TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent)
QHBoxLayout *barLayout = new QHBoxLayout(controlBar); QHBoxLayout *barLayout = new QHBoxLayout(controlBar);
barLayout->setContentsMargins(8, 4, 8, 4); barLayout->setContentsMargins(8, 4, 8, 4);
titleLabel = new QLabel("Tutorial", controlBar); titleLabel = new QLabel(tr("Tutorial"), controlBar);
titleLabel->setStyleSheet("color:white; font-weight:bold;"); titleLabel->setStyleSheet("color:white; font-weight:bold;");
barLayout->addWidget(titleLabel); barLayout->addWidget(titleLabel);
barLayout->addStretch(); barLayout->addStretch();
@ -52,11 +52,11 @@ TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent)
return b; return b;
}; };
QPushButton *prevSeq = mkBtn("⏮", "Previous chapter"); QPushButton *prevSeq = mkBtn("⏮", tr("Previous chapter"));
QPushButton *prev = mkBtn("◀", "Previous step"); QPushButton *prev = mkBtn("◀", tr("Previous step"));
nextButton = mkBtn("▶", "Next step"); nextButton = mkBtn("▶", tr("Next step"));
nextSeqButton = mkBtn("⏭", "Next chapter"); nextSeqButton = mkBtn("⏭", tr("Next chapter"));
QPushButton *close = mkBtn("✕", "Exit tutorial"); QPushButton *close = mkBtn("✕", tr("Exit tutorial"));
barLayout->addWidget(prevSeq); barLayout->addWidget(prevSeq);
barLayout->addWidget(prev); barLayout->addWidget(prev);
@ -121,12 +121,12 @@ void TutorialOverlay::setInteractive(bool interactive, bool clickThrough)
if (nextButton) { if (nextButton) {
nextButton->setEnabled(!interactive); nextButton->setEnabled(!interactive);
nextButton->setToolTip(interactive ? "Complete the highlighted action to continue" : "Next step"); nextButton->setToolTip(interactive ? tr("Complete the highlighted action to continue") : tr("Next step"));
} }
if (nextSeqButton) { if (nextSeqButton) {
nextSeqButton->setEnabled(!interactive); nextSeqButton->setEnabled(!interactive);
nextSeqButton->setToolTip(interactive ? "Complete the highlighted action to continue" : "Next chapter"); nextSeqButton->setToolTip(interactive ? tr("Complete the highlighted action to continue") : tr("Next chapter"));
} }
updateMask(); updateMask();

View file

@ -189,7 +189,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
QTimer::singleShot(0, this, &TabGame::loadLayout); QTimer::singleShot(0, this, &TabGame::loadLayout);
tutorialController = new TutorialController(this); tutorialController = new TutorialController(TutorialId::Game, this);
TutorialSequence lobbySequence; TutorialSequence lobbySequence;
@ -202,7 +202,8 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
void TabGame::showEvent(QShowEvent *event) void TabGame::showEvent(QShowEvent *event)
{ {
QWidget::showEvent(event); QWidget::showEvent(event);
if (!tutorialController || tutorialStarted || SettingsCache::instance().userInterface().getTutorialCompleted()) { if (!tutorialController || tutorialStarted ||
SettingsCache::instance().userInterface().getGameTutorialCompleted()) {
return; return;
} }
// Only start once all sequences are registered so advancing can't run past the // Only start once all sequences are registered so advancing can't run past the
@ -223,7 +224,7 @@ void TabGame::finishTutorialInitialization()
} }
// No sequence building (and no widget access) once the tutorial is done. // No sequence building (and no widget access) once the tutorial is done.
if (SettingsCache::instance().userInterface().getTutorialCompleted()) { if (SettingsCache::instance().userInterface().getGameTutorialCompleted()) {
return; return;
} }

View file

@ -76,7 +76,7 @@ TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor) : Abstra
loadLayout(); loadLayout();
filterDockWidget->setHidden(true); filterDockWidget->setHidden(true);
cardDatabaseDockWidget->setHidden(true); cardDatabaseDockWidget->setHidden(true);
tutorialController = new TutorialController(this); tutorialController = new TutorialController(TutorialId::DeckEditor, this);
createTutorialSequences(); createTutorialSequences();
} }
@ -88,31 +88,31 @@ void TabDeckEditorVisual::createTutorialSequences()
auto sequence = TutorialSequence(); auto sequence = TutorialSequence();
sequence.addStep({tabContainer->tabBar(), sequence.addStep(
"The Visual Deck Editor has multiple different functionalities.\n\nYou can cycle " {tabContainer->tabBar(), tr("The Visual Deck Editor has multiple different functionalities.\n\nYou can cycle "
"through them by using these tabs.\n\nLet's start with the Visual Deck View."}); "through them by using these tabs.\n\nLet's start with the Visual Deck View.")});
sequence.addStep({tabContainer->visualDeckView, sequence.addStep({tabContainer->visualDeckView,
"The cards in your deck will be displayed here, allowing for an easy overview.\n\nLet's try " tr("The cards in your deck will be displayed here, allowing for an easy overview.\n\nLet's try "
"adding some now, so you can see it in action!", "adding some now, so you can see it in action!"),
[this]() { tabContainer->setCurrentWidget(tabContainer->visualDeckView); }}); [this]() { tabContainer->setCurrentWidget(tabContainer->visualDeckView); }});
tutorialController->addSequence(sequence); tutorialController->addSequence(sequence);
auto vdeSequence = tabContainer->visualDeckView->addTutorialSteps(); auto vdeSequence = tabContainer->visualDeckView->addTutorialSteps();
vdeSequence.addStep({tabContainer->tabBar(), "Let's look at the database tab now."}); vdeSequence.addStep({tabContainer->tabBar(), tr("Let's look at the database tab now.")});
tutorialController->addSequence(vdeSequence); tutorialController->addSequence(vdeSequence);
auto vddSequence = tabContainer->visualDatabaseDisplay->addTutorialSteps(); auto vddSequence = tabContainer->visualDatabaseDisplay->addTutorialSteps();
vddSequence.steps.prepend( vddSequence.steps.prepend(
{tabContainer->visualDatabaseDisplay, {tabContainer->visualDatabaseDisplay,
"You can view the database here, either as card images or in the old table display " tr("You can view the database here, either as card images or in the old table display "
"style.\n\nAdditionally, there are many powerful and easy to use filters available.\n\nLet's dive in!", "style.\n\nAdditionally, there are many powerful and easy to use filters available.\n\nLet's dive in!"),
[this]() { tabContainer->setCurrentWidget(tabContainer->visualDatabaseDisplay); }}); [this]() { tabContainer->setCurrentWidget(tabContainer->visualDatabaseDisplay); }});
tutorialController->addSequence(vddSequence); tutorialController->addSequence(vddSequence);
auto analyticsSequence = tabContainer->deckAnalytics->generateTutorialSequence(); auto analyticsSequence = tabContainer->deckAnalytics->generateTutorialSequence();
analyticsSequence.steps.prepend({tabContainer->tabBar(), "Let's look at the analytics tab now."}); analyticsSequence.steps.prepend({tabContainer->tabBar(), tr("Let's look at the analytics tab now.")});
TutorialStep analyticsConclusionStep; TutorialStep analyticsConclusionStep;
analyticsConclusionStep.targetWidget = tabContainer->tabBar(); analyticsConclusionStep.targetWidget = tabContainer->tabBar();
@ -150,7 +150,7 @@ void TabDeckEditorVisual::createTutorialSequences()
void TabDeckEditorVisual::showEvent(QShowEvent *ev) void TabDeckEditorVisual::showEvent(QShowEvent *ev)
{ {
QWidget::showEvent(ev); QWidget::showEvent(ev);
if (!tutorialStarted && !SettingsCache::instance().userInterface().getTutorialCompleted()) { if (!tutorialStarted && !SettingsCache::instance().userInterface().getDeckEditorTutorialCompleted()) {
tutorialStarted = true; tutorialStarted = true;
// Start on next event loop iteration so everything is fully painted // Start on next event loop iteration so everything is fully painted
QTimer::singleShot(0, tutorialController, [this] { tutorialController->start(); }); QTimer::singleShot(0, tutorialController, [this] { tutorialController->start(); });

View file

@ -194,13 +194,13 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
TutorialSequence VisualDatabaseDisplayWidget::addTutorialSteps() TutorialSequence VisualDatabaseDisplayWidget::addTutorialSteps()
{ {
auto sequence = TutorialSequence(); auto sequence = TutorialSequence();
sequence.addStep({colorFilterWidget, "Filter the database by colors with these controls"}); sequence.addStep({colorFilterWidget, tr("Filter the database by colors with these controls")});
TutorialStep displayModeStep; TutorialStep displayModeStep;
displayModeStep.targetWidget = displayModeButton; displayModeStep.targetWidget = displayModeButton;
displayModeStep.text = tr("You can change back to the old table display-style with this button."); displayModeStep.text = tr("You can change back to the old table display-style with this button.");
displayModeStep.allowClickThrough = true; displayModeStep.allowClickThrough = true;
sequence.addStep(displayModeStep); sequence.addStep(displayModeStep);
sequence.addStep({filterContainer, "Use these controls for quick access to common filters."}); sequence.addStep({filterContainer, tr("Use these controls for quick access to common filters.")});
TutorialStep setFilterStep; TutorialStep setFilterStep;
setFilterStep.targetWidget = filterContainer->getSetFilterWidget(); setFilterStep.targetWidget = filterContainer->getSetFilterWidget();

View file

@ -464,20 +464,20 @@ void VisualDeckEditorWidget::onSelectionChanged(const QItemSelection &selected,
TutorialSequence VisualDeckEditorWidget::addTutorialSteps() TutorialSequence VisualDeckEditorWidget::addTutorialSteps()
{ {
TutorialSequence sequence; TutorialSequence sequence;
sequence.name = "Adding Cards to Your Deck"; sequence.name = tr("Adding Cards to Your Deck");
TutorialStep introStep; TutorialStep introStep;
introStep.targetWidget = displayOptionsAndSearch; introStep.targetWidget = displayOptionsAndSearch;
introStep.text = "There are two ways of adding cards to your deck:\n\n" introStep.text = tr("There are two ways of adding cards to your deck:\n\n"
"The first is by using the quick search bar in the deck view tab.\n" "The first is by using the quick search bar in the deck view tab.\n"
"This is helpful if you already know which card you would like to add " "This is helpful if you already know which card you would like to add "
"and will provide name suggestions as you type.\n\n" "and will provide name suggestions as you type.\n\n"
"We'll look at the second way, through the database tab, later."; "We'll look at the second way, through the database tab, later.");
sequence.addStep(introStep); sequence.addStep(introStep);
TutorialStep searchStep; TutorialStep searchStep;
searchStep.targetWidget = searchBar; searchStep.targetWidget = searchBar;
searchStep.text = "Let's try it out now!\nType the name of a card into the search bar."; searchStep.text = tr("Let's try it out now!\nType the name of a card into the search bar.");
searchStep.allowClickThrough = true; searchStep.allowClickThrough = true;
searchStep.requiresInteraction = true; searchStep.requiresInteraction = true;
searchStep.autoAdvanceOnValid = true; searchStep.autoAdvanceOnValid = true;
@ -485,14 +485,14 @@ TutorialSequence VisualDeckEditorWidget::addTutorialSteps()
searchStep.validator = [this]() { searchStep.validator = [this]() {
return CardDatabaseManager::query()->getCard({searchBar->text()}) != ExactCard(); return CardDatabaseManager::query()->getCard({searchBar->text()}) != ExactCard();
}; };
searchStep.validationHint = "Please enter a valid card name."; searchStep.validationHint = tr("Please enter a valid card name.");
searchStep.customInteractionHint = "✏️ Type a valid card name to continue"; searchStep.customInteractionHint = tr("✏️ Type a valid card name to continue");
sequence.addStep(searchStep); sequence.addStep(searchStep);
TutorialStep addStep; TutorialStep addStep;
addStep.targetWidget = searchPushButton; addStep.targetWidget = searchPushButton;
addStep.text = "Click this button to add the card to your deck."; addStep.text = tr("Click this button to add the card to your deck.");
addStep.allowClickThrough = true; addStep.allowClickThrough = true;
addStep.requiresInteraction = true; addStep.requiresInteraction = true;
addStep.autoAdvanceOnValid = true; addStep.autoAdvanceOnValid = true;
@ -506,8 +506,8 @@ TutorialSequence VisualDeckEditorWidget::addTutorialSteps()
TutorialStep organizationStep; TutorialStep organizationStep;
organizationStep.targetWidget = this; organizationStep.targetWidget = this;
organizationStep.text = "Let's look at how cards are organized and displayed now.\n\nWe'll add some random cards " organizationStep.text = tr("Let's look at how cards are organized and displayed now.\n\nWe'll add some random "
"from the database to your deck, so you can see it in action properly."; "cards from the database to your deck, so you can see it in action properly.");
organizationStep.onExit = [this]() { organizationStep.onExit = [this]() {
// getCardList() counts distinct card nodes, not copies, so once every random pick only bumps an // getCardList() counts distinct card nodes, not copies, so once every random pick only bumps an
// existing node's count the size never grows. With fewer than ~60 distinct cards in the database // existing node's count the size never grows. With fewer than ~60 distinct cards in the database
@ -524,11 +524,12 @@ TutorialSequence VisualDeckEditorWidget::addTutorialSteps()
TutorialStep hoverStep; TutorialStep hoverStep;
hoverStep.targetWidget = this; hoverStep.targetWidget = this;
hoverStep.text = "Great! Take some time to explore these new cards in the current display mode.\n\nYou can select " hoverStep.text = tr("Great! Take some time to explore these new cards in the current display mode.\n\nYou can "
"a card by clicking on it with the left mouse button.\nYou can select multiple cards by holding " "select a card by clicking on it with the left mouse button.\nYou can select multiple cards "
"down CTRL or Shift.\nYou can clear the current selection by clicking on an area without a " "by holding down CTRL or Shift.\nYou can clear the current selection by clicking on an area "
"card.\nDouble-clicking a card will move it between main and sideboard.\nRight-clicking a card " "without a card.\nDouble-clicking a card will move it between main and sideboard.\n"
"will remove it from the deck.\n\nYou can hover over a card to see a zoomed version of it."; "Right-clicking a card will remove it from the deck.\n\nYou can hover over a card to see a "
"zoomed version of it.");
hoverStep.allowClickThrough = true; hoverStep.allowClickThrough = true;
sequence.addStep(hoverStep); sequence.addStep(hoverStep);
@ -538,9 +539,9 @@ TutorialSequence VisualDeckEditorWidget::addTutorialSteps()
TutorialStep conclusionStep; TutorialStep conclusionStep;
conclusionStep.targetWidget = this; conclusionStep.targetWidget = this;
conclusionStep.text = conclusionStep.text =
"Great!\n\nNow that you've learned about all the different ways of displaying the cards in " tr("Great!\n\nNow that you've learned about all the different ways of displaying the cards in "
"your deck, it's time to move on to searching for new cards for your deck in style and ease.\n\nYou can stay " "your deck, it's time to move on to searching for new cards for your deck in style and ease.\n\nYou can "
"on this screen to play around with the display options and advance when you are ready."; "stay on this screen to play around with the display options and advance when you are ready.");
conclusionStep.allowClickThrough = true; conclusionStep.allowClickThrough = true;
sequence.addStep(conclusionStep); sequence.addStep(conclusionStep);

View file

@ -5,8 +5,6 @@
#include "libcockatrice/utility/playmat_params.h" #include "libcockatrice/utility/playmat_params.h"
#include <QChar> #include <QChar>
#include <QDir>
#include <QFileInfo>
#include <QList> #include <QList>
#include <QVariant> #include <QVariant>
#include <QtGlobal> #include <QtGlobal>
@ -253,6 +251,21 @@ bool InterfaceSettings::getTutorialCompleted() const
return getValue("tutorialCompleted", QString(), QString(), false).toBool(); return getValue("tutorialCompleted", QString(), QString(), false).toBool();
} }
bool InterfaceSettings::getHomeTutorialCompleted() const
{
return getValue("tutorialHomeCompleted", QString(), QString(), false).toBool();
}
bool InterfaceSettings::getDeckEditorTutorialCompleted() const
{
return getValue("tutorialDeckEditorCompleted", QString(), QString(), false).toBool();
}
bool InterfaceSettings::getGameTutorialCompleted() const
{
return getValue("tutorialGameCompleted", QString(), QString(), false).toBool();
}
void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus) void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus)
{ {
setValue(_useTearOffMenus, "useTearOffMenus"); setValue(_useTearOffMenus, "useTearOffMenus");
@ -481,3 +494,18 @@ void InterfaceSettings::setTutorialCompleted(bool value)
{ {
setValue(value, "tutorialCompleted"); setValue(value, "tutorialCompleted");
} }
void InterfaceSettings::setHomeTutorialCompleted(bool value)
{
setValue(value, "tutorialHomeCompleted");
}
void InterfaceSettings::setDeckEditorTutorialCompleted(bool value)
{
setValue(value, "tutorialDeckEditorCompleted");
}
void InterfaceSettings::setGameTutorialCompleted(bool value)
{
setValue(value, "tutorialGameCompleted");
}

View file

@ -58,6 +58,9 @@ public:
[[nodiscard]] bool getBattlefieldFlashEnabled() const override; [[nodiscard]] bool getBattlefieldFlashEnabled() const override;
[[nodiscard]] QStringList getUserListExpandedSections() const override; [[nodiscard]] QStringList getUserListExpandedSections() const override;
[[nodiscard]] bool getTutorialCompleted() const; [[nodiscard]] bool getTutorialCompleted() const;
[[nodiscard]] bool getHomeTutorialCompleted() const;
[[nodiscard]] bool getDeckEditorTutorialCompleted() const;
[[nodiscard]] bool getGameTutorialCompleted() const;
void setUseTearOffMenus(bool _useTearOffMenus); void setUseTearOffMenus(bool _useTearOffMenus);
void setCardViewInitialRowsMax(int _cardViewInitialRowsMax); void setCardViewInitialRowsMax(int _cardViewInitialRowsMax);
@ -98,6 +101,9 @@ public:
void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled); void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled);
void setUserListExpandedSections(const QStringList &sections); void setUserListExpandedSections(const QStringList &sections);
void setTutorialCompleted(bool value); void setTutorialCompleted(bool value);
void setHomeTutorialCompleted(bool value);
void setDeckEditorTutorialCompleted(bool value);
void setGameTutorialCompleted(bool value);
signals: signals:
void useTearOffMenusChanged(bool state); void useTearOffMenusChanged(bool state);

View file

@ -543,6 +543,51 @@ TEST_F(SettingsDefaultsTest, Interface_BuddyConnectNotificationsEnabled_Default)
ASSERT_EQ(s.getBuddyConnectNotificationsEnabled(), true); ASSERT_EQ(s.getBuddyConnectNotificationsEnabled(), true);
} }
TEST_F(SettingsDefaultsTest, Interface_HomeTutorialCompleted_Default)
{
InterfaceSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getHomeTutorialCompleted(), false);
}
TEST_F(SettingsDefaultsTest, Interface_HomeTutorialCompleted_SetAndGet)
{
InterfaceSettings s(settingsPath, nullptr);
s.setHomeTutorialCompleted(true);
ASSERT_EQ(s.getHomeTutorialCompleted(), true);
s.setHomeTutorialCompleted(false);
ASSERT_EQ(s.getHomeTutorialCompleted(), false);
}
TEST_F(SettingsDefaultsTest, Interface_DeckEditorTutorialCompleted_Default)
{
InterfaceSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getDeckEditorTutorialCompleted(), false);
}
TEST_F(SettingsDefaultsTest, Interface_DeckEditorTutorialCompleted_SetAndGet)
{
InterfaceSettings s(settingsPath, nullptr);
s.setDeckEditorTutorialCompleted(true);
ASSERT_EQ(s.getDeckEditorTutorialCompleted(), true);
s.setDeckEditorTutorialCompleted(false);
ASSERT_EQ(s.getDeckEditorTutorialCompleted(), false);
}
TEST_F(SettingsDefaultsTest, Interface_GameTutorialCompleted_Default)
{
InterfaceSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getGameTutorialCompleted(), false);
}
TEST_F(SettingsDefaultsTest, Interface_GameTutorialCompleted_SetAndGet)
{
InterfaceSettings s(settingsPath, nullptr);
s.setGameTutorialCompleted(true);
ASSERT_EQ(s.getGameTutorialCompleted(), true);
s.setGameTutorialCompleted(false);
ASSERT_EQ(s.getGameTutorialCompleted(), false);
}
// --- DeckEditorSettings --- // --- DeckEditorSettings ---
TEST_F(SettingsDefaultsTest, DeckEditor_OpenDeckInNewTab_Default) TEST_F(SettingsDefaultsTest, DeckEditor_OpenDeckInNewTab_Default)