Compare commits

...

6 Commits

Author SHA1 Message Date
BruebachL
a9284596a8 Update cockatrice/src/interface/widgets/tabs/tab_game.cpp
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
2026-01-24 12:51:20 +01:00
Lukas Brübach
ca820c52c3 More tutorials.
Took 3 hours 23 minutes
2026-01-24 11:55:23 +01:00
Lukas Brübach
80075e4089 Expand tutorial again.
Took 46 minutes

Took 6 seconds
2026-01-24 11:55:23 +01:00
Lukas Brübach
bec4ae3aa0 Include debug for a minute
Took 18 minutes
2026-01-24 11:55:23 +01:00
Lukas Brübach
7927f8a73a Lint against the linter.
Took 3 minutes
2026-01-24 11:55:23 +01:00
Brübach, Lukas
60e293dc2d [App] First-run tutorial
Took 3 seconds

Took 10 minutes

Took 1 minute

Took 4 minutes


Took 23 minutes
2026-01-24 11:55:23 +01:00
33 changed files with 1781 additions and 12 deletions

View File

@@ -199,6 +199,9 @@ set(cockatrice_SOURCES
src/interface/widgets/general/layout_containers/flow_widget.cpp
src/interface/widgets/general/layout_containers/overlap_control_widget.cpp
src/interface/widgets/general/layout_containers/overlap_widget.cpp
src/interface/widgets/general/tutorial/tutorial_bubble_widget.cpp
src/interface/widgets/general/tutorial/tutorial_controller.cpp
src/interface/widgets/general/tutorial/tutorial_overlay.cpp
src/interface/widgets/menus/deck_editor_menu.cpp
src/interface/widgets/printing_selector/all_zones_card_amount_widget.cpp
src/interface/widgets/printing_selector/card_amount_widget.cpp

View File

@@ -99,6 +99,21 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
&DeckViewContainer::setVisualDeckStorageExists);
switchToDeckSelectView();
generateTutorialSequence();
}
TutorialSequence DeckViewContainer::generateTutorialSequence()
{
TutorialSequence deckViewContainerSequence;
deckViewContainerSequence.name = tr("Loading and selecting decks");
deckViewContainerSequence.addStep(
{this, tr("There are multiple ways to select a deck:\n\n- From a local file"
"\n- From the contents of your clipboard\nFrom an external online service")});
deckViewContainerSequence = visualDeckStorageWidget->generateTutorialSequence(deckViewContainerSequence);
return deckViewContainerSequence;
}
/**

View File

@@ -8,6 +8,7 @@
#define DECK_VIEW_CONTAINER_H
#include "../../interface/deck_loader/deck_loader.h"
#include "../../interface/widgets/general/tutorial/tutorial_controller.h"
#include <QPushButton>
@@ -82,6 +83,7 @@ signals:
public:
DeckViewContainer(int _playerId, TabGame *parent);
void retranslateUi();
TutorialSequence generateTutorialSequence();
void setReadyStart(bool ready);
void readyAndUpdate();
void setSideboardLocked(bool locked);

View File

@@ -15,6 +15,11 @@ TabbedDeckViewContainer::TabbedDeckViewContainer(int _playerId, TabGame *parent)
updateTabBarVisibility();
}
TutorialSequence TabbedDeckViewContainer::generateTutorialSequence()
{
return playerDeckView->generateTutorialSequence();
}
void TabbedDeckViewContainer::addOpponentDeckView(const DeckList &opponentDeck, int opponentId, QString opponentName)
{
if (opponentDeckViews.contains(opponentId)) {

View File

@@ -16,6 +16,7 @@ class TabbedDeckViewContainer : public QTabWidget
public:
explicit TabbedDeckViewContainer(int _playerId, TabGame *parent);
TutorialSequence generateTutorialSequence();
void closeTab(int index);
void updateTabBarVisibility();
void addOpponentDeckView(const DeckList &opponentDeck, int opponentId, QString opponentName);

View File

@@ -1,5 +1,7 @@
#include "deck_analytics_widget.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h"
#include "abstract_analytics_panel_widget.h"
#include "add_analytics_panel_dialog.h"
#include "analytics_panel_widget_factory.h"
@@ -7,6 +9,7 @@
#include "analyzer_modules/mana_curve/mana_curve_config.h"
#include "analyzer_modules/mana_devotion/mana_devotion_config.h"
#include "deck_list_statistics_analyzer.h"
#include "libcockatrice/utility/qt_utils.h"
#include "resizable_panel.h"
#include <QEvent>
@@ -60,6 +63,46 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal
retranslateUi();
}
TutorialSequence DeckAnalyticsWidget::generateTutorialSequence()
{
TutorialSequence analyticsSequence;
analyticsSequence.name = tr("Deck Analytics");
TutorialStep introStep;
introStep.targetWidget = this;
introStep.text = tr("This is the deck analytics tab.\n\nHere, you can view more detailed information about your "
"deck via the use of specialized analytics widgets.");
introStep.onEnter = [this]() {
auto tabWidget = QtUtils::findParentOfType<TabDeckEditorVisualTabWidget>(this);
if (tabWidget) {
tabWidget->setCurrentWidget(tabWidget->deckAnalytics);
}
};
analyticsSequence.addStep(introStep);
TutorialStep controlStep;
controlStep.targetWidget = controlContainer;
controlStep.text = tr(
"These controls will allow you to customize your analytics widget layout.\n\nAll widgets can be resized or "
"reordered with the handle at their bottom.\nTo remove a widget, you first have to select it.\nSaving your "
"layout will ensure that it is the default for all future decks you open, whereas loading the layout will "
"allow you to revert back to your previous configuration in case you decide you do not like your new layout.");
analyticsSequence.addStep(controlStep);
TutorialStep widgetStep;
widgetStep.targetWidget = this;
widgetStep.text =
tr("Finally, let's talk about the analytics widgets themselves.\n\nHow the various analytics are displayed for "
"each widget can be configured by clicking on the cogwheel next to the respective banner.\nHovering over "
"the segments of different diagram types will reveal which cards belong to the respective segment.");
analyticsSequence.addStep(widgetStep);
return analyticsSequence;
}
void DeckAnalyticsWidget::retranslateUi()
{
addButton->setText(tr("Add Panel"));

View File

@@ -7,6 +7,7 @@
#ifndef DECK_ANALYTICS_WIDGET_H
#define DECK_ANALYTICS_WIDGET_H
#include "../general/tutorial/tutorial_controller.h"
#include "abstract_analytics_panel_widget.h"
#include "deck_list_statistics_analyzer.h"
#include "resizable_panel.h"
@@ -29,6 +30,7 @@ public slots:
public:
explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
void retranslateUi();
TutorialSequence generateTutorialSequence();
private slots:
void onAddPanel();

View File

@@ -1,6 +1,7 @@
#include "deck_editor_deck_dock_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../general/tutorial/tutorial_controller.h"
#include "deck_list_style_proxy.h"
#include "deck_state_manager.h"
@@ -280,6 +281,22 @@ void DeckEditorDeckDockWidget::createDeckDock()
}
}
TutorialSequence DeckEditorDeckDockWidget::generateTutorialSequence()
{
TutorialSequence sequence;
sequence.name = tr("The Deck Info Widget");
TutorialStep introStep;
introStep.targetWidget = this;
introStep.text = tr("This is the deck info widget.\n\nHere, you can adjust all kinds of metadata such as the name, "
"the comments, or the tags of a deck.\nIt also displays the contents of your deck in a list "
"and provides buttons to manipulate the decklist.");
sequence.addStep(introStep);
return sequence;
}
void DeckEditorDeckDockWidget::initializeFormats()
{
QStringList allFormats = CardDatabaseManager::query()->getAllFormatsWithCount().keys();

View File

@@ -10,6 +10,7 @@
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
#include "../../key_signals.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../utility/custom_line_edit.h"
#include "../visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h"
#include "deck_list_history_manager_widget.h"
@@ -46,6 +47,8 @@ public:
return deckView->selectionModel();
}
TutorialSequence generateTutorialSequence();
public slots:
void selectPrevCard();
void selectNextCard();

View File

@@ -5,6 +5,7 @@
#include "../../window_main.h"
#include "background_sources.h"
#include "home_styled_button.h"
#include "tutorial/tutorial_controller.h"
#include <QGroupBox>
#include <QPainter>
@@ -13,6 +14,7 @@
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/remote/remote_client.h>
#include <libcockatrice/utility/qt_utils.h>
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
: QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice")
@@ -44,10 +46,51 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
&HomeWidget::initializeBackgroundFromSource);
connect(&SettingsCache::instance(), &SettingsCache::homeTabBackgroundShuffleFrequencyChanged, this,
&HomeWidget::onBackgroundShuffleFrequencyChanged);
auto mainWindow = QtUtils::findParentOfType<QMainWindow>(this);
if (mainWindow) {
tutorialController = new TutorialController(mainWindow);
} else {
tutorialController = new TutorialController(this);
}
auto sequence = TutorialSequence();
sequence.addStep({connectButton, "Connect to a server to play here!"});
auto vdeStep = TutorialStep(visualDeckEditorButton, "Create a new deck from cards in the database here!");
vdeStep.requiresInteraction = true;
vdeStep.allowClickThrough = true;
vdeStep.validationHint = "Open the deck editor to try it out!";
vdeStep.validationTiming = ValidationTiming::OnSignal;
vdeStep.autoAdvanceOnValid = true;
vdeStep.validator = []() { return true; };
vdeStep.signalSource = visualDeckEditorButton;
vdeStep.signalName = SIGNAL(clicked());
sequence.addStep(vdeStep);
sequence.addStep({visualDeckStorageButton, "Browse the decks in your local collection."});
sequence.addStep({visualDatabaseDisplayButton, "View the card database here."});
sequence.addStep(
{edhrecButton, "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 "
"decklists and import them to your local collection."});
sequence.addStep({replaybutton, "View replays of your past games here."});
sequence.addStep({exitButton, "Exit the application."});
tutorialController->addSequence(sequence);
// Lambda is cleaner to read than overloading this
connect(&SettingsCache::instance(), &SettingsCache::homeTabDisplayCardNameChanged, this, [this] { repaint(); });
}
void HomeWidget::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
if (!tutorialStarted) {
tutorialStarted = true;
// Start on next event loop iteration so everything is fully painted
QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); });
}
}
void HomeWidget::initializeBackgroundFromSource()
{
if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) {
@@ -185,29 +228,29 @@ QGroupBox *HomeWidget::createButtons()
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
boxLayout->addWidget(connectButton);
auto visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors);
visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors);
connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor,
[this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); });
boxLayout->addWidget(visualDeckEditorButton);
auto visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors);
visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors);
connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor,
[this] { tabSupervisor->actTabVisualDeckStorage(true); });
boxLayout->addWidget(visualDeckStorageButton);
auto visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors);
visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors);
connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor,
&TabSupervisor::addVisualDatabaseDisplayTab);
boxLayout->addWidget(visualDatabaseDisplayButton);
auto edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors);
edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors);
connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab);
boxLayout->addWidget(edhrecButton);
auto archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors);
archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors);
connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab);
boxLayout->addWidget(archidektButton);
auto replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors);
replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors);
connect(replaybutton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); });
boxLayout->addWidget(replaybutton);
if (qobject_cast<MainWindow *>(tabSupervisor->parentWidget())) {
auto exitButton = new HomeStyledButton(tr("Quit"), gradientColors);
exitButton = new HomeStyledButton(tr("Quit"), gradientColors);
connect(exitButton, &QPushButton::clicked, qobject_cast<MainWindow *>(tabSupervisor->parentWidget()),
&MainWindow::actExit);
boxLayout->addWidget(exitButton);

View File

@@ -24,9 +24,18 @@ public:
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
void updateRandomCard();
QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
HomeStyledButton *connectButton;
HomeStyledButton *visualDeckEditorButton;
HomeStyledButton *visualDeckStorageButton;
HomeStyledButton *visualDatabaseDisplayButton;
HomeStyledButton *edhrecButton;
HomeStyledButton *archidektButton;
HomeStyledButton *replaybutton;
HomeStyledButton *exitButton;
public slots:
void paintEvent(QPaintEvent *event) override;
void showEvent(QShowEvent *event) override;
void initializeBackgroundFromSource();
void onBackgroundShuffleFrequencyChanged();
void updateBackgroundProperties();
@@ -39,11 +48,12 @@ private:
QTimer *cardChangeTimer;
TabSupervisor *tabSupervisor;
QPixmap background;
TutorialController *tutorialController;
bool tutorialStarted = false;
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
DeckList backgroundSourceDeck;
QPixmap overlay;
QPair<QColor, QColor> gradientColors;
HomeStyledButton *connectButton;
void loadBackgroundSourceDeck();
};

View File

@@ -0,0 +1,107 @@
#include "tutorial_bubble_widget.h"
BubbleWidget::BubbleWidget(QWidget *parent) : QFrame(parent)
{
setFrameStyle(QFrame::StyledPanel | QFrame::Raised);
setStyleSheet("QFrame { background:white; border-radius:8px; }"
"QLabel { color:black; }");
layout = new QGridLayout(this);
layout->setContentsMargins(12, 10, 12, 10);
layout->setHorizontalSpacing(8);
layout->setVerticalSpacing(8);
// Step counter (e.g., "Step 2 of 5")
counterLabel = new QLabel(this);
counterLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
counterLabel->setStyleSheet("color: #555; font-size: 11px;");
// Overall progress (e.g., "12 of 45 total")
progressLabel = new QLabel(this);
progressLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
progressLabel->setStyleSheet("color: #888; font-size: 10px;");
progressLabel->setAlignment(Qt::AlignRight);
// Main tutorial text
textLabel = new QLabel(this);
textLabel->setWordWrap(true);
textLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
textLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
textLabel->setStyleSheet("color:black;");
// Interaction hint (e.g., "Click the highlighted area")
interactionLabel = new QLabel(this);
interactionLabel->setWordWrap(true);
interactionLabel->setStyleSheet("color: #0066cc; font-style: italic; font-size: 11px;");
interactionLabel->hide();
// Validation hint (error message)
validationLabel = new QLabel(this);
validationLabel->setWordWrap(true);
validationLabel->setStyleSheet("color: #cc3300; background: #ffe6e6; padding: 6px; "
"border-radius: 4px; font-size: 11px;");
validationLabel->hide();
// Layout
layout->addWidget(counterLabel, 0, 0, Qt::AlignLeft | Qt::AlignVCenter);
layout->addWidget(progressLabel, 0, 1, Qt::AlignRight | Qt::AlignVCenter);
layout->addWidget(textLabel, 1, 0, 1, 2);
layout->addWidget(interactionLabel, 2, 0, 1, 2);
layout->addWidget(validationLabel, 3, 0, 1, 2);
layout->setColumnStretch(1, 1);
setMaximumWidth(420);
// Timer for auto-hiding validation hints
validationTimer = new QTimer(this);
validationTimer->setSingleShot(true);
connect(validationTimer, &QTimer::timeout, this, &BubbleWidget::clearValidationHint);
}
void BubbleWidget::setText(const QString &text)
{
textLabel->setText(text);
update();
}
void BubbleWidget::setProgress(int stepNum, int totalSteps, int overallStep, int overallTotal)
{
// Per-sequence progress
counterLabel->setText(QString("Step %1 of %2").arg(stepNum).arg(totalSteps));
// Overall progress across all sequences
progressLabel->setText(QString("(%1 of %2 total)").arg(overallStep).arg(overallTotal));
progressLabel->show();
}
void BubbleWidget::setInteractionHint(const QString &hint)
{
if (hint.isEmpty()) {
interactionLabel->hide();
} else {
interactionLabel->setText(hint);
interactionLabel->show();
}
adjustSize();
}
void BubbleWidget::setValidationHint(const QString &hint)
{
if (hint.isEmpty()) {
clearValidationHint();
} else {
validationLabel->setText("⚠️ " + hint);
validationLabel->show();
adjustSize();
// Auto-hide after 4 seconds
validationTimer->start(4000);
}
}
void BubbleWidget::clearValidationHint()
{
validationLabel->hide();
adjustSize();
}

View File

@@ -0,0 +1,33 @@
#ifndef COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
#define COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
#include <QFrame>
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include <QTimer>
class BubbleWidget : public QFrame
{
Q_OBJECT
public:
explicit BubbleWidget(QWidget *parent = nullptr);
void setText(const QString &text);
void setProgress(int stepNum, int totalSteps, int overallStep, int overallTotal);
void setInteractionHint(const QString &hint);
void setValidationHint(const QString &hint);
private:
void clearValidationHint();
QLabel *counterLabel;
QLabel *textLabel;
QLabel *interactionLabel; // Shows "Click to continue"
QLabel *validationLabel; // Shows validation errors
QLabel *progressLabel; // Shows overall progress
QGridLayout *layout;
QTimer *validationTimer; // Auto-hide validation hint
};
#endif // COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H

View File

@@ -0,0 +1,374 @@
#include "tutorial_controller.h"
#include <QComboBox>
#include <QDebug>
#include <QLineEdit>
#include <QMainWindow>
#include <QPlainTextEdit>
#include <QTextEdit>
#include <QTimer>
TutorialController::TutorialController(QWidget *_tutorializedWidget)
: QObject(_tutorializedWidget), tutorializedWidget(_tutorializedWidget)
{
tutorialOverlay = new TutorialOverlay(tutorializedWidget->window());
tutorialOverlay->setWindowFlags(tutorialOverlay->windowFlags() | Qt::FramelessWindowHint);
tutorialOverlay->hide();
connect(tutorialOverlay, &TutorialOverlay::nextStep, this, &TutorialController::attemptAdvance);
connect(tutorialOverlay, &TutorialOverlay::prevStep, this, &TutorialController::prevStep);
connect(tutorialOverlay, &TutorialOverlay::nextSequence, this, &TutorialController::nextSequence);
connect(tutorialOverlay, &TutorialOverlay::prevSequence, this, &TutorialController::prevSequence);
connect(tutorialOverlay, &TutorialOverlay::skipTutorial, this, &TutorialController::exitTutorial);
connect(tutorialOverlay, &TutorialOverlay::targetClicked, this, &TutorialController::handleTargetClicked);
}
void TutorialController::addSequence(const TutorialSequence &seq)
{
sequences.append(seq);
}
void TutorialController::start()
{
if (sequences.isEmpty() || tutorialCompleted) {
return;
}
QTimer::singleShot(0, this, [this]() {
QWidget *win = tutorializedWidget->window();
// Reparent to make absolutely sure
tutorialOverlay->setParent(win);
tutorialOverlay->setGeometry(0, 0, win->width(), win->height());
// Stack order
tutorialOverlay->stackUnder(nullptr);
tutorialOverlay->show();
tutorialOverlay->raise();
currentSequence = 0;
currentStep = 0;
showStep();
});
}
void TutorialController::handleTargetClicked()
{
if (currentSequence < 0 || currentStep < 0) {
return;
}
const auto &step = sequences[currentSequence].steps[currentStep];
// If this step requires interaction AND uses OnAdvance validation, advance when clicked
// For OnSignal/OnChange, the click just triggers the action - validation happens via signal
if (step.requiresInteraction && step.validationTiming == ValidationTiming::OnAdvance) {
attemptAdvance();
}
}
void TutorialController::attemptAdvance()
{
if (currentSequence < 0 || currentStep < 0) {
return;
}
const auto &step = sequences[currentSequence].steps[currentStep];
// Only validate on advance if timing is set to OnAdvance
if (step.validationTiming == ValidationTiming::OnAdvance) {
if (!validateCurrentStep()) {
return; // Validation failed, stay on current step
}
}
// Validation passed or not required, proceed to next step
nextStep();
}
bool TutorialController::validateCurrentStep()
{
if (currentSequence < 0 || currentSequence >= sequences.size()) {
return true; // No validation needed
}
const auto &step = sequences[currentSequence].steps[currentStep];
// If there's a validator function, check it
if (step.validator) {
bool valid = step.validator();
if (!valid) {
// Show validation hint
tutorialOverlay->showValidationHint(step.validationHint);
return false;
}
}
return true;
}
void TutorialController::nextStep()
{
if (currentSequence < 0) {
return;
}
if (currentStep >= sequences[currentSequence].steps.size() - 1) {
// We're on the last step of this sequence, run its onExit before advancing
const auto &lastStep = sequences[currentSequence].steps[currentStep];
if (lastStep.onExit) {
lastStep.onExit();
}
nextSequence();
return;
}
currentStep++;
showStep();
}
void TutorialController::prevStep()
{
if (currentSequence < 0) {
return;
}
if (currentStep == 0) {
prevSequence();
return;
}
currentStep--;
showStep();
}
void TutorialController::nextSequence()
{
if (currentSequence < 0) {
return;
}
currentSequence++;
currentStep = 0;
if (currentSequence >= sequences.size()) {
exitTutorial();
return;
}
showStep();
}
void TutorialController::prevSequence()
{
if (currentSequence <= 0) {
currentStep = 0;
showStep();
return;
}
currentSequence--;
currentStep = 0;
showStep();
}
void TutorialController::exitTutorial()
{
if (currentSequence >= 0 && currentStep >= 0 && currentSequence < sequences.size() &&
currentStep < sequences[currentSequence].steps.size()) {
const auto &curStep = sequences[currentSequence].steps[currentStep];
if (curStep.onExit) {
curStep.onExit();
}
}
cleanupValidationMonitoring();
tutorialOverlay->hide();
currentSequence = -1;
currentStep = -1;
tutorialCompleted = true;
}
void TutorialController::updateProgress()
{
if (currentSequence < 0 || currentSequence >= sequences.size()) {
return;
}
const auto &seq = sequences[currentSequence];
// Calculate total steps across all sequences
int totalSteps = 0;
int currentOverallStep = 0;
for (int i = 0; i < sequences.size(); ++i) {
int seqSteps = sequences[i].steps.size();
totalSteps += seqSteps;
if (i < currentSequence) {
currentOverallStep += seqSteps;
}
}
currentOverallStep += currentStep + 1; // +1 because steps are 0-indexed
// Update overlay with progress info
tutorialOverlay->setProgress(currentStep + 1, // Current step in sequence (1-indexed)
seq.steps.size(), // Total steps in sequence
currentOverallStep, // Overall step number
totalSteps, // Total steps in tutorial
seq.name); // Sequence title
}
void TutorialController::showStep()
{
if (currentSequence < 0 || currentSequence >= sequences.size()) {
return;
}
const auto &seq = sequences[currentSequence];
if (currentStep < 0 || currentStep >= seq.steps.size()) {
return;
}
// Clean up validation monitoring from previous step
cleanupValidationMonitoring();
// Run onExit for the previous step
if (!(currentSequence == 0 && currentStep == 0)) {
int prevSeq = currentSequence;
int prevStepIndex = currentStep - 1;
if (prevStepIndex < 0) {
prevSeq = currentSequence - 1;
if (prevSeq >= 0) {
prevStepIndex = sequences[prevSeq].steps.size() - 1;
} else {
prevStepIndex = -1;
}
}
if (prevSeq >= 0 && prevStepIndex >= 0) {
const auto &previousStep = sequences[prevSeq].steps[prevStepIndex];
if (previousStep.onExit) {
previousStep.onExit();
}
}
}
const auto &step = seq.steps[currentStep];
if (step.onEnter) {
step.onEnter();
}
tutorialOverlay->setTargetWidget(step.targetWidget);
tutorialOverlay->setText(step.text);
tutorialOverlay->setInteractive(step.requiresInteraction, step.allowClickThrough);
// Set custom interaction hint if provided
if (!step.customInteractionHint.isEmpty()) {
tutorialOverlay->setInteractionHint(step.customInteractionHint);
} else if (step.requiresInteraction) {
tutorialOverlay->setInteractionHint("👆 Click the highlighted area to continue");
} else {
tutorialOverlay->setInteractionHint("");
}
// Setup validation monitoring for this step
setupValidationMonitoring();
updateProgress();
tutorialOverlay->parentResized();
tutorialOverlay->raise();
tutorialOverlay->update();
}
void TutorialController::setupValidationMonitoring()
{
if (currentSequence < 0 || currentSequence >= sequences.size()) {
return;
}
if (currentStep < 0 || currentStep >= sequences[currentSequence].steps.size()) {
return;
}
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) {
qInfo() << "Setting up signal-based validation for signal:" << step.signalName;
validationConnection = connect(step.signalSource, step.signalName, this, SLOT(checkValidation()));
if (!validationConnection) {
qInfo() << "Warning: Failed to connect to signal" << step.signalName;
}
} else {
qInfo() << "Warning: OnSignal validation timing set but signalSource or signalName is null";
}
return;
}
// Handle OnChange validation - widget-specific
if (step.validationTiming == ValidationTiming::OnChange && step.validator) {
if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(step.targetWidget)) {
qInfo() << "Setting up validation monitoring for QLineEdit";
validationConnection =
connect(lineEdit, &QLineEdit::textChanged, this, &TutorialController::checkValidation);
} else if (QTextEdit *textEdit = qobject_cast<QTextEdit *>(step.targetWidget)) {
qInfo() << "Setting up validation monitoring for QTextEdit";
validationConnection =
connect(textEdit, &QTextEdit::textChanged, this, &TutorialController::checkValidation);
} else if (QPlainTextEdit *plainText = qobject_cast<QPlainTextEdit *>(step.targetWidget)) {
qInfo() << "Setting up validation monitoring for QPlainTextEdit";
validationConnection =
connect(plainText, &QPlainTextEdit::textChanged, this, &TutorialController::checkValidation);
} else if (QComboBox *combo = qobject_cast<QComboBox *>(step.targetWidget)) {
qInfo() << "Setting up validation monitoring for QComboBox";
validationConnection = connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&TutorialController::checkValidation);
} else {
qInfo() << "Warning: OnChange validation timing set but widget type not supported:"
<< (step.targetWidget ? step.targetWidget->metaObject()->className() : "null");
}
}
}
void TutorialController::cleanupValidationMonitoring()
{
if (validationConnection) {
qInfo() << "Cleaning up validation connection";
disconnect(validationConnection);
validationConnection = QMetaObject::Connection();
}
}
void TutorialController::checkValidation()
{
qInfo() << "checkValidation() called";
if (currentSequence < 0 || currentSequence >= sequences.size()) {
return;
}
if (currentStep < 0 || currentStep >= sequences[currentSequence].steps.size()) {
return;
}
const auto &step = sequences[currentSequence].steps[currentStep];
if (step.validator) {
bool isValid = step.validator();
qInfo() << "Validation result:" << isValid;
if (isValid) {
// Clear any validation hints
tutorialOverlay->showValidationHint("");
// Auto-advance if enabled
if (step.autoAdvanceOnValid) {
qInfo() << "Auto-advancing to next step";
QTimer::singleShot(500, this, &TutorialController::nextStep);
}
}
}
}

View File

@@ -0,0 +1,99 @@
#ifndef COCKATRICE_TUTORIAL_CONTROLLER_H
#define COCKATRICE_TUTORIAL_CONTROLLER_H
#include "tutorial_overlay.h"
#include <QObject>
#include <QVector>
#include <functional>
enum class ValidationTiming
{
OnAdvance, // Validate when user clicks next/clicks target (default)
OnChange, // Validate whenever target widget changes (for text input)
OnSignal, // Validate when a specific signal is emitted
Manual // Only validate when explicitly triggered
};
struct TutorialStep
{
QWidget *targetWidget = nullptr;
QString text;
std::function<void()> onEnter = nullptr;
std::function<void()> onExit = nullptr;
// Interactive features
bool requiresInteraction = false; // Must click target to advance
bool allowClickThrough = false; // Clicks pass through to target widget
std::function<bool()> validator = nullptr; // Check if task completed
QString validationHint = ""; // Show if validation fails
ValidationTiming validationTiming = ValidationTiming::OnAdvance;
// Auto-advance when validation passes (useful for text input)
bool autoAdvanceOnValid = false;
// 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)
};
struct TutorialSequence
{
QString name;
QVector<TutorialStep> steps;
void addStep(const TutorialStep &step)
{
steps.append(step);
}
};
class TutorialController : public QObject
{
Q_OBJECT
public:
explicit TutorialController(QWidget *_tutorializedWidget);
void addSequence(const TutorialSequence &seq);
void start();
TutorialOverlay *getOverlay()
{
return tutorialOverlay;
};
public slots:
void nextStep();
void prevStep();
void nextSequence();
void prevSequence();
void exitTutorial();
void handleTargetClicked(); // Handle clicks on highlighted widget
void attemptAdvance(); // Try to advance with validation
void checkValidation(); // Check validation for OnChange timing
private:
void showStep();
void updateProgress(); // Update progress indicators
bool validateCurrentStep(); // Check if step requirements met
void setupValidationMonitoring(); // Setup automatic validation checking
void cleanupValidationMonitoring(); // Cleanup validation watchers
QWidget *tutorializedWidget;
TutorialOverlay *tutorialOverlay;
QVector<TutorialSequence> sequences;
bool tutorialCompleted = false;
int currentSequence = -1;
int currentStep = -1;
// For OnChange validation monitoring
QMetaObject::Connection validationConnection;
};
#endif // COCKATRICE_TUTORIAL_CONTROLLER_H

View File

@@ -0,0 +1,373 @@
#include "tutorial_overlay.h"
#include "tutorial_bubble_widget.h"
#include <QApplication>
#include <QComboBox>
#include <QEvent>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QResizeEvent>
#include <QTextEdit>
#include <QTimer>
TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent)
{
setAttribute(Qt::WA_TranslucentBackground, true);
setAttribute(Qt::WA_TransparentForMouseEvents, false);
setAttribute(Qt::WA_OpaquePaintEvent, false);
setAutoFillBackground(false);
if (parent) {
parent->installEventFilter(this);
setGeometry(parent->rect());
raise();
}
controlBar = new QFrame(this);
controlBar->setStyleSheet(
"QFrame { background: rgba(30,30,30,200); border-radius: 6px; }"
"QPushButton { padding: 6px 10px; border: 1px solid #aaa; border-radius: 4px; background:#f5f5f5; }"
"QPushButton:hover { background:#eaeaea; }");
QHBoxLayout *barLayout = new QHBoxLayout(controlBar);
barLayout->setContentsMargins(8, 4, 8, 4);
titleLabel = new QLabel("Tutorial", controlBar);
titleLabel->setStyleSheet("color:white; font-weight:bold;");
barLayout->addWidget(titleLabel);
barLayout->addStretch();
auto mkBtn = [&](const QString &t, const QString &tip) {
QPushButton *b = new QPushButton(t, controlBar);
b->setToolTip(tip);
return b;
};
QPushButton *prevSeq = mkBtn("", "Previous chapter");
QPushButton *prev = mkBtn("", "Previous step");
nextButton = mkBtn("", "Next step");
nextSeqButton = mkBtn("", "Next chapter");
QPushButton *close = mkBtn("", "Exit tutorial");
barLayout->addWidget(prevSeq);
barLayout->addWidget(prev);
barLayout->addWidget(nextButton);
barLayout->addWidget(nextSeqButton);
barLayout->addWidget(close);
connect(prev, &QPushButton::clicked, this, &TutorialOverlay::prevStep);
connect(nextButton, &QPushButton::clicked, this, &TutorialOverlay::nextStep);
connect(prevSeq, &QPushButton::clicked, this, &TutorialOverlay::prevSequence);
connect(nextSeqButton, &QPushButton::clicked, this, &TutorialOverlay::nextSequence);
connect(close, &QPushButton::clicked, this, &TutorialOverlay::skipTutorial);
bubble = new BubbleWidget(this);
bubble->hide();
controlBar->hide();
}
void TutorialOverlay::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
if (parentWidget()) {
QWidget *parent = parentWidget();
setGeometry(0, 0, parent->width(), parent->height());
}
raise();
parentResized();
}
void TutorialOverlay::setTitle(const QString &title)
{
titleLabel->setText(title);
}
void TutorialOverlay::setBlocking(bool block)
{
blockInput = block;
setAttribute(Qt::WA_TransparentForMouseEvents, !blockInput);
}
void TutorialOverlay::setInteractive(bool interactive, bool clickThrough)
{
isInteractive = interactive;
allowClickThrough = clickThrough;
if (nextButton) {
nextButton->setEnabled(!interactive);
if (interactive) {
nextButton->setToolTip("Complete the highlighted action to continue");
} else {
nextButton->setToolTip("Next step");
}
}
if (nextSeqButton) {
nextSeqButton->setEnabled(!interactive);
if (interactive) {
nextSeqButton->setToolTip("Complete the highlighted action to continue");
} else {
nextSeqButton->setToolTip("Next chapter");
}
}
// Update mask when clickThrough changes
updateMask();
}
void TutorialOverlay::setInteractionHint(const QString &hint)
{
bubble->setInteractionHint(hint);
}
void TutorialOverlay::showValidationHint(const QString &hint)
{
if (!hint.isEmpty()) {
bubble->setValidationHint(hint);
}
}
void TutorialOverlay::setProgress(int stepNum,
int totalSteps,
int overallStep,
int overallTotal,
const QString &sequenceTitle)
{
bubble->setProgress(stepNum, totalSteps, overallStep, overallTotal);
if (!sequenceTitle.isEmpty()) {
titleLabel->setText(sequenceTitle);
}
}
void TutorialOverlay::setTargetWidget(QWidget *w)
{
if (targetWidget)
targetWidget->removeEventFilter(this);
targetWidget = w;
if (targetWidget)
targetWidget->installEventFilter(this);
recomputeLayout();
}
void TutorialOverlay::setText(const QString &t)
{
tutorialText = t;
bubble->setText(t);
bubble->adjustSize();
recomputeLayout();
}
QRect TutorialOverlay::currentHoleRect() const
{
if (!targetWidget || !targetWidget->isVisible())
return QRect();
QPoint targetGlobal = targetWidget->mapToGlobal(QPoint(0, 0));
QPoint targetInOverlay = mapFromGlobal(targetGlobal);
return QRect(targetInOverlay, targetWidget->size()).adjusted(-6, -6, 6, 6);
}
void TutorialOverlay::mousePressEvent(QMouseEvent *event)
{
QRect hole = currentHoleRect();
// Check if click is in the highlighted area
if (hole.contains(event->pos())) {
// For non-clickthrough steps, emit targetClicked for advancement
if (!allowClickThrough && isInteractive && !qobject_cast<QLineEdit *>(targetWidget) &&
!qobject_cast<QTextEdit *>(targetWidget) && !qobject_cast<QPlainTextEdit *>(targetWidget) &&
!qobject_cast<QComboBox *>(targetWidget)) {
QTimer::singleShot(100, this, [this]() { emit targetClicked(); });
}
// If allowClickThrough, the mask ensures events pass through
return;
}
// Click outside highlighted area - block it
event->accept();
}
void TutorialOverlay::updateMask()
{
if (allowClickThrough) {
QRect hole = currentHoleRect();
if (!hole.isEmpty()) {
// Create a mask that excludes the hole area
QRegion fullRegion(rect());
QRegion holeRegion(hole);
QRegion maskRegion = fullRegion.subtracted(holeRegion);
setMask(maskRegion);
} else {
clearMask();
}
} else {
clearMask();
}
}
bool TutorialOverlay::event(QEvent *event)
{
// Update mask on any event that might change geometry
if (event->type() == QEvent::Move || event->type() == QEvent::Resize) {
updateMask();
}
return QWidget::event(event);
}
void TutorialOverlay::resizeEvent(QResizeEvent *)
{
recomputeLayout();
}
bool TutorialOverlay::eventFilter(QObject *obj, QEvent *event)
{
if (obj == parentWidget() && (event->type() == QEvent::Resize || event->type() == QEvent::Move)) {
parentResized();
}
if (obj == targetWidget) {
if (event->type() == QEvent::Show) {
QMetaObject::invokeMethod(
this, [this]() { recomputeLayout(); }, Qt::QueuedConnection);
} else if (event->type() == QEvent::Hide || event->type() == QEvent::Move || event->type() == QEvent::Resize) {
recomputeLayout();
}
}
return QWidget::eventFilter(obj, event);
}
void TutorialOverlay::parentResized()
{
if (!parentWidget())
return;
setGeometry(0, 0, parentWidget()->width(), parentWidget()->height());
recomputeLayout();
}
void TutorialOverlay::recomputeLayout()
{
QRect hole = currentHoleRect();
if (hole.isEmpty()) {
if (bubble) {
bubble->hide();
}
if (controlBar) {
controlBar->hide();
}
hide();
return;
}
show();
raise();
QSize bsize = bubble->sizeHint().expandedTo(QSize(160, 60));
highlightBubbleRect = computeBubbleRect(hole, bsize);
bubble->setGeometry(highlightBubbleRect);
bubble->show();
bubble->raise();
controlBar->adjustSize();
controlBar->show();
const int margin = 8;
QRect r = rect();
QList<QPoint> positions = {{r.right() - controlBar->width() - margin, r.bottom() - controlBar->height() - margin},
{r.right() - controlBar->width() - margin, margin},
{margin, r.bottom() - controlBar->height() - margin},
{margin, margin}};
for (const QPoint &pos : positions) {
QRect proposed(pos, controlBar->size());
if (!proposed.intersects(hole)) {
controlBar->move(pos);
break;
}
}
controlBar->raise();
update();
updateMask(); // Update mask after layout changes
}
QRect TutorialOverlay::computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const
{
const int margin = 16;
QRect r = rect();
QRect bubble;
if (hole.isEmpty()) {
bubble = QRect(r.center() - QPoint(bubbleSize.width() / 2, bubbleSize.height() / 2), bubbleSize);
} else {
bubble = QRect(hole.right() + margin, hole.top(), bubbleSize.width(), bubbleSize.height());
if (!r.contains(bubble))
bubble.moveLeft(hole.left() - margin - bubbleSize.width());
if (!r.contains(bubble)) {
bubble.moveLeft(hole.center().x() - bubbleSize.width() / 2);
bubble.moveTop(hole.top() - margin - bubbleSize.height());
}
if (!r.contains(bubble))
bubble.moveTop(hole.bottom() + margin);
}
int maxLeft = qMax(r.left(), r.right() - bubble.width());
int maxTop = qMax(r.top(), r.bottom() - bubble.height());
bubble.moveLeft(qBound(r.left(), bubble.left(), maxLeft));
bubble.moveTop(qBound(r.top(), bubble.top(), maxTop));
return bubble;
}
void TutorialOverlay::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);
QRect hole = currentHoleRect();
if (hole.isEmpty()) {
p.fillRect(rect(), QColor(0, 0, 0, 160));
} else {
QPainterPath fullPath;
fullPath.addRect(rect());
QPainterPath holePath;
holePath.addRoundedRect(hole, 8, 8);
QPainterPath overlayPath = fullPath.subtracted(holePath);
p.fillPath(overlayPath, QColor(0, 0, 0, 160));
if (isInteractive) {
QPen pen(QColor(100, 200, 255, 180), 2);
pen.setStyle(Qt::DashLine);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(hole, 8, 8);
}
}
}

View File

@@ -0,0 +1,65 @@
#ifndef TUTORIAL_OVERLAY_H
#define TUTORIAL_OVERLAY_H
#include <QWidget>
class QFrame;
class QLabel;
class QPushButton;
class BubbleWidget;
class TutorialOverlay : public QWidget
{
Q_OBJECT
public:
explicit TutorialOverlay(QWidget *parent = nullptr);
void setTitle(const QString &title);
void setBlocking(bool block);
void setTargetWidget(QWidget *w);
void setText(const QString &t);
void setInteractive(bool interactive, bool clickThrough);
void setInteractionHint(const QString &hint);
void showValidationHint(const QString &hint);
void setProgress(int stepNum, int totalSteps, int overallStep, int overallTotal, const QString &sequenceTitle);
void parentResized();
QRect currentHoleRect() const;
signals:
void nextStep();
void prevStep();
void nextSequence();
void prevSequence();
void skipTutorial();
void targetClicked();
protected:
void showEvent(QShowEvent *event) override;
bool event(QEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void updateMask();
bool eventFilter(QObject *obj, QEvent *event) override;
private:
void recomputeLayout();
QRect computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const;
QWidget *targetWidget = nullptr;
QFrame *controlBar = nullptr;
QLabel *titleLabel = nullptr;
QPushButton *nextButton = nullptr;
QPushButton *nextSeqButton = nullptr;
BubbleWidget *bubble = nullptr;
QString tutorialText;
QRect highlightBubbleRect;
bool blockInput = true;
bool isInteractive = false;
bool allowClickThrough = false;
};
#endif // TUTORIAL_OVERLAY_H

View File

@@ -21,6 +21,7 @@
#include "../interface/window_main.h"
#include "../main.h"
#include "../utility/visibility_change_listener.h"
#include "libcockatrice/utility/qt_utils.h"
#include "tab_supervisor.h"
#include <QAction>
@@ -129,6 +130,194 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
gameTypes.append(game->getGameMetaInfo()->findRoomGameType(i));
QTimer::singleShot(0, this, &TabGame::loadLayout);
auto mainWindow = QtUtils::findParentOfType<QMainWindow>(this);
if (mainWindow) {
tutorialController = new TutorialController(mainWindow);
} else {
tutorialController = new TutorialController(this);
}
TutorialSequence lobbySequence;
TutorialStep introStep(deckViewContainerWidget, tr("Let's try this out."));
lobbySequence.addStep(introStep);
tutorialController->addSequence(lobbySequence);
}
void TabGame::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
if (!tutorialStarted) {
tutorialStarted = true;
// Start on next event loop iteration so everything is fully painted
QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); });
}
}
void TabGame::finishTutorialInitialization()
{
if (tutorialInitialized) {
return;
} else {
tutorialInitialized = true;
}
auto deckViewSequence = deckViewContainers.first()->generateTutorialSequence();
tutorialController->addSequence(deckViewSequence);
TutorialSequence deckSelectSequence;
deckSelectSequence.name = tr("Deck selection and readying up");
TutorialStep loadDeckStep;
loadDeckStep.targetWidget = deckViewContainers.first();
loadDeckStep.text = tr("Let's load a deck now.");
loadDeckStep.allowClickThrough = true;
loadDeckStep.requiresInteraction = true;
loadDeckStep.autoAdvanceOnValid = true;
loadDeckStep.validationTiming = ValidationTiming::OnSignal;
loadDeckStep.signalSource = game->getGameEventHandler();
loadDeckStep.signalName = SIGNAL(logDeckSelect(Player *, QString, int));
loadDeckStep.validator = [] { return true; };
deckSelectSequence.addStep(loadDeckStep);
TutorialStep readyUpStep;
readyUpStep.targetWidget = deckViewContainers.first();
readyUpStep.text = tr("Let's ready up now.");
readyUpStep.allowClickThrough = true;
readyUpStep.requiresInteraction = true;
readyUpStep.autoAdvanceOnValid = true;
readyUpStep.validationTiming = ValidationTiming::OnSignal;
readyUpStep.signalSource = this;
readyUpStep.signalName = SIGNAL(localPlayerReadyStateChanged(bool));
readyUpStep.validator = [] { return true; };
deckSelectSequence.addStep(readyUpStep);
tutorialController->addSequence(deckSelectSequence);
TutorialSequence gamePlaySequence;
gamePlaySequence.name = tr("Gameplay");
gamePlaySequence.addStep(
{gamePlayAreaWidget,
tr("Welcome to your first game! It's just a singleplayer game for now to teach you the controls.")});
gamePlaySequence.addStep(
{gamePlayAreaWidget,
tr("Welcome to your first game! It's just a singleplayer game for now to teach you the controls.")});
gamePlaySequence.addStep(
{gamePlayAreaWidget,
tr("Unfortunately, due to the way the game tab works, we can't highlight any specific gameplay elements but "
"we're confident you'll be able to spot all the relevant elements on-screen.")});
gamePlaySequence.addStep(
{gamePlayAreaWidget,
tr("Let's go over them quickly, left-to-right.\n\nThe phase toolbar\nThe player area\nThe battlefield")});
gamePlaySequence.addStep(
{gamePlayAreaWidget,
tr("First up, is the phase toolbar. This toolbar shows the current phase of the turn. You can advance it by "
"pressing\n\n"
"- Tab (simply advances the phase)\n"
"- Ctrl+Space (advances the phase and performes any associated actions)\n"
"- Clicking directly on the phase you want to change to.\n\n"
"You can also pass the turn here, although, you should note that most players prefer you simply leave your "
"turn on the end step and allow them to 'take' the turn from you by pressing 'Next turn' themselves.")});
gamePlaySequence.addStep({gamePlayAreaWidget, tr("Next up, is your player area.\n\nHere you can find:\n\n- Your "
"avatar\n- Your life-counter\n- Various counters you can use to "
"track temporary resources (i.e. mana)\n- Your "
"library,\n- Your hand\n- Your graveyard\n- Your exile")});
gamePlaySequence.addStep(
{gamePlayAreaWidget,
tr("To the right of your player area, and taking up most of the screen, is your battlefield.\nThe relevant "
"zones here are, left-to-right, top-to-bottom:\n- The Stack\n- The Battlefield (Currently highlighted "
"because it is your turn)\n- Your Hand")});
gamePlaySequence.addStep(
{gamePlayAreaWidget,
tr("Before we dive any deeper into the actual controls, remember this:\n\nYou can perform almost "
"EVERY action by right-clicking the relevant object or zone!")});
gamePlaySequence.addStep(
{gamePlayAreaWidget, tr("However, there are shortcuts and conveniences to speed up your games and make your "
"life easier.\n\nLet's run through a typical game start now to get you up to speed.")});
TutorialStep lifeCounterStep;
lifeCounterStep.targetWidget = gamePlayAreaWidget;
lifeCounterStep.text =
tr("To control your life total, you can:\n\nSet it directly using Ctrl+L\nLeft-click the "
"number on your avatar to increment it.\nRight-click the number on your avatar to decrement "
"it.\nMiddle-click the number on your avatar to open up an interval menu up to +-10.");
lifeCounterStep.requiresInteraction = true;
lifeCounterStep.allowClickThrough = true;
lifeCounterStep.autoAdvanceOnValid = true;
lifeCounterStep.validationTiming = ValidationTiming::OnSignal;
lifeCounterStep.signalSource = game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getPlayerEventHandler();
lifeCounterStep.signalName = SIGNAL(logSetCounter(Player *, QString, int, int));
lifeCounterStep.validator = [this] {
auto counters =
game->getPlayerManager()->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())->getCounters();
for (auto counter : counters) {
if (counter->getName() == "life") {
return counter->getValue() == 10;
}
}
return false;
};
lifeCounterStep.validationHint = tr("Set your life total to 10 using any of these methods.");
gamePlaySequence.addStep(lifeCounterStep);
TutorialStep diceRollStep;
diceRollStep.targetWidget = gamePlayAreaWidget;
diceRollStep.text = tr("Fantastic! Let's roll a dice now. Many players use this to determine the initial turn "
"order.\nYou can right-click the battlefield and choose the menu "
"option or use the shortcut (Default Ctrl+I).");
diceRollStep.requiresInteraction = true;
diceRollStep.allowClickThrough = true;
diceRollStep.autoAdvanceOnValid = true;
diceRollStep.validationTiming = ValidationTiming::OnSignal;
diceRollStep.signalSource = game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getPlayerEventHandler();
diceRollStep.signalName = SIGNAL(logRollDie(Player *, int, const QList<uint> &));
diceRollStep.validator = [this] { return true; };
diceRollStep.validationHint = tr("Roll a dice using any of these methods.");
gamePlaySequence.addStep(diceRollStep);
TutorialStep mulliganStep;
mulliganStep.targetWidget = gamePlayAreaWidget;
mulliganStep.text =
tr("Alright, with that out of the way, we can get down to business:\n\nDrawing cards!\n\nTo draw your initial "
"hand:\n\n- Right-click your hand in the player area and select 'Take mulligan'\n-n Right-click your hand "
"zone on the battlefield and select 'Take mulligan'\n- Use the default shortcut (Ctrl+M)");
mulliganStep.requiresInteraction = true;
mulliganStep.allowClickThrough = true;
mulliganStep.autoAdvanceOnValid = true;
mulliganStep.validationTiming = ValidationTiming::OnSignal;
mulliganStep.signalSource = game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getPlayerEventHandler();
mulliganStep.signalName = SIGNAL(logDrawCards(Player *, int, bool));
mulliganStep.validator = [this] {
return game->getPlayerManager()
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
->getHandZone()
->getCards()
.size() == 7;
};
mulliganStep.validationHint = tr("Mulligan to 7 cards using any of these methods.");
gamePlaySequence.addStep(mulliganStep);
gamePlaySequence.addStep({gamePlayAreaWidget, tr("")});
gamePlaySequence.addStep({gamePlayAreaWidget, tr("")});
gamePlaySequence.addStep({gamePlayAreaWidget, tr("")});
tutorialController->addSequence(gamePlaySequence);
}
void TabGame::connectToGameState()
@@ -687,6 +876,8 @@ void TabGame::addLocalPlayer(Player *newPlayer, int playerId)
deckView->playerDeckView->readyAndUpdate();
});
}
finishTutorialInitialization();
}
void TabGame::processPlayerLeave(Player *leavingPlayer)
@@ -750,6 +941,8 @@ void TabGame::loadDeckForLocalPlayer(Player *localPlayer, int playerId, ServerIn
CardPictureLoader::cacheCardPixmaps(CardDatabaseManager::query()->getCards(deckList.getCardRefList()));
deckViewContainer->playerDeckView->setDeck(deckList);
localPlayer->setDeck(deckList);
emit localPlayerDeckSelected();
}
}
@@ -767,6 +960,7 @@ void TabGame::processLocalPlayerSideboardLocked(int playerId, bool sideboardLock
void TabGame::processLocalPlayerReadyStateChanged(int playerId, bool ready)
{
deckViewContainers.value(playerId)->playerDeckView->setReadyStart(ready);
emit localPlayerReadyStateChanged(ready);
}
void TabGame::createZoneForPlayer(Player *newPlayer, int playerId)

View File

@@ -57,6 +57,9 @@ class TabGame : public Tab
Q_OBJECT
private:
AbstractGame *game;
TutorialController *tutorialController;
bool tutorialStarted = false;
bool tutorialInitialized = false;
const UserListProxy *userListProxy;
ReplayManager *replayManager = nullptr;
QStringList gameTypes;
@@ -125,6 +128,8 @@ private:
void createDeckViewContainerWidget(bool bReplay = false);
void createReplayDock(GameReplay *replay);
signals:
void localPlayerDeckSelected();
void localPlayerReadyStateChanged(bool ready);
void gameClosing(TabGame *tab);
void containerProcessingStarted(const GameEventContext &context);
void containerProcessingDone();
@@ -176,6 +181,8 @@ public:
QList<AbstractClient *> &_clients,
const Event_GameJoined &event,
const QMap<int, QString> &_roomGameTypes);
void showEvent(QShowEvent *event);
void finishTutorialInitialization();
void connectToGameState();
void connectToPlayerManager();
void connectToGameEventHandler();

View File

@@ -149,6 +149,10 @@ public:
{
return userListManager;
}
[[nodiscard]] TabHome *getTabHome() const
{
return tabHome;
}
[[nodiscard]] const QMap<int, TabRoom *> &getRoomTabs() const
{
return roomTabs;

View File

@@ -6,8 +6,10 @@
#include "../../interface/pixel_map_generator.h"
#include "../../interface/widgets/cards/card_info_frame_widget.h"
#include "../../interface/widgets/deck_analytics/deck_analytics_widget.h"
#include "../../interface/widgets/general/tutorial/tutorial_controller.h"
#include "../../interface/widgets/visual_deck_editor/visual_deck_editor_widget.h"
#include "../tab_deck_editor.h"
#include "../tab_home.h"
#include "../tab_supervisor.h"
#include "tab_deck_editor_visual_tab_widget.h"
@@ -51,6 +53,83 @@ TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor) : Abstra
loadLayout();
cardDatabaseDockWidget->setHidden(true);
tutorialController = new TutorialController(this);
auto deckDockSequence = deckDockWidget->generateTutorialSequence();
tutorialController->addSequence(deckDockSequence);
auto sequence = TutorialSequence();
sequence.addStep({tabContainer->tabBar(),
"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."});
sequence.addStep({tabContainer->visualDeckView,
"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!",
[this]() { tabContainer->setCurrentWidget(tabContainer->visualDeckView); }});
// sequence.addStep({printingSelectorDockWidget, "Change the printings in your deck here."});
tutorialController->addSequence(sequence);
auto vdeSequence = tabContainer->visualDeckView->addTutorialSteps();
vdeSequence.addStep({tabContainer->tabBar(), "Let's look at the database tab now."});
tutorialController->addSequence(vdeSequence);
auto vddSequence = tabContainer->visualDatabaseDisplay->addTutorialSteps();
vddSequence.steps.prepend(
{tabContainer->visualDatabaseDisplay,
"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!",
[this]() { tabContainer->setCurrentWidget(tabContainer->visualDatabaseDisplay); }});
tutorialController->addSequence(vddSequence);
auto analyticsSequence = tabContainer->deckAnalytics->generateTutorialSequence();
analyticsSequence.steps.prepend({tabContainer->tabBar(), "Let's look at the analytics tab now."});
TutorialStep analyticsConclusionStep;
analyticsConclusionStep.targetWidget = tabContainer->tabBar();
analyticsConclusionStep.text =
tr("That was it for the analytics tab.\n\nLet's now look at an equally useful tab, which provides you with "
"detailed information about possible hands, invaluable information when testing out a new deck.");
analyticsSequence.addStep(analyticsConclusionStep);
tutorialController->addSequence(analyticsSequence);
auto sampleHandSequence = tabContainer->sampleHandWidget->generateTutorialSequence();
tutorialController->addSequence(sampleHandSequence);
TutorialSequence endSequence;
endSequence.name = tr("Visual Deck Editor Conclusion");
TutorialStep introStep;
introStep.targetWidget = this;
introStep.text = tr("This concludes the Visual Deck Editor tutorial.");
introStep.onEnter = [this]() { tabContainer->setCurrentWidget(tabContainer->visualDeckView); };
endSequence.addStep(introStep);
TutorialStep conclusionStep;
conclusionStep.targetWidget = tabSupervisor->tabBar();
conclusionStep.text =
tr("Let's go back to the Home Tab now to explore where you can manage your newly created deck.");
conclusionStep.onExit = [this]() { tabSupervisor->setCurrentWidget(tabSupervisor->getTabHome()); };
endSequence.addStep(conclusionStep);
tutorialController->addSequence(endSequence);
}
void TabDeckEditorVisual::showEvent(QShowEvent *ev)
{
QWidget::showEvent(ev);
if (!tutorialStarted) {
tutorialStarted = true;
// Start on next event loop iteration so everything is fully painted
QTimer::singleShot(0, tutorialController, [this] { tutorialController->start(); });
}
}
/** @brief Creates the central frame containing the tab container. */

View File

@@ -4,6 +4,7 @@
#include "../tab.h"
#include "tab_deck_editor_visual_tab_widget.h"
class TutorialController;
/**
* @class TabDeckEditorVisual
* @ingroup DeckEditorTabs
@@ -55,7 +56,12 @@ class TabDeckEditorVisual : public AbstractTabDeckEditor
{
Q_OBJECT
private:
TutorialController *tutorialController = nullptr;
bool tutorialStarted = false;
protected slots:
void showEvent(QShowEvent *ev) override;
/**
* @brief Load the editor layout from settings.
*/

View File

@@ -22,6 +22,11 @@ public:
void initialize();
void retranslateUi();
SettingsButtonWidget *getSetFilterWidget()
{
return quickFilterSetWidget;
};
private:
VisualDatabaseDisplayWidget *visualDatabaseDisplay;

View File

@@ -5,14 +5,14 @@
#include "../../../filters/syntax_help.h"
#include "../../pixel_map_generator.h"
#include "../cards/card_info_picture_with_text_overlay_widget.h"
#include "../deck_editor/deck_state_manager.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../quick_settings/settings_button_widget.h"
#include "../tabs/visual_deck_editor/tab_deck_editor_visual.h"
#include "../utility/custom_line_edit.h"
#include "visual_database_display_color_filter_widget.h"
#include "visual_database_display_filter_save_load_widget.h"
#include "visual_database_display_main_type_filter_widget.h"
#include "visual_database_display_name_filter_widget.h"
#include "visual_database_display_set_filter_widget.h"
#include "visual_database_display_sub_type_filter_widget.h"
#include <QHeaderView>
#include <QScrollBar>
@@ -20,7 +20,7 @@
#include <libcockatrice/card/card_info_comparator.h>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <utility>
#include <libcockatrice/utility/qt_utils.h>
VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
AbstractTabDeckEditor *_deckEditor,
@@ -135,6 +135,87 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
retranslateUi();
}
TutorialSequence VisualDatabaseDisplayWidget::addTutorialSteps()
{
auto sequence = TutorialSequence();
sequence.addStep({colorFilterWidget, "Filter the database by colors with these controls"});
TutorialStep displayModeStep;
displayModeStep.targetWidget = displayModeButton;
displayModeStep.text = tr("You can change back to the old table display-style with this button.");
displayModeStep.allowClickThrough = true;
sequence.addStep(displayModeStep);
sequence.addStep({filterContainer, "Use these controls for quick access to common filters."});
TutorialStep setFilterStep;
setFilterStep.targetWidget = filterContainer->getSetFilterWidget();
setFilterStep.text = tr("Let's try it out now by selecting a set filter!");
setFilterStep.allowClickThrough = true;
setFilterStep.requiresInteraction = true;
setFilterStep.autoAdvanceOnValid = true;
setFilterStep.validationTiming = ValidationTiming::OnSignal;
setFilterStep.signalSource = filterModel;
setFilterStep.signalName = SIGNAL(layoutChanged());
setFilterStep.validator = [] { return true; };
sequence.addStep(setFilterStep);
TutorialStep explorationStep;
explorationStep.targetWidget = this;
explorationStep.text = tr(
"Try it out!\n\nWe've cleared the previous deck. Add 5 different new cards to the deck by clicking on them!");
explorationStep.allowClickThrough = true;
explorationStep.requiresInteraction = true;
explorationStep.autoAdvanceOnValid = true;
explorationStep.validationTiming = ValidationTiming::OnSignal;
if (QtUtils::findParentOfType<TabDeckEditorVisual>(this)) {
explorationStep.onEnter = [this] {
QtUtils::findParentOfType<TabDeckEditorVisual>(this)->deckStateManager->clearDeck();
};
explorationStep.signalSource =
QtUtils::findParentOfType<TabDeckEditorVisual>(this)->deckStateManager->getModel();
explorationStep.signalName = SIGNAL(cardNodesChanged());
explorationStep.validator = [this] {
if (QtUtils::findParentOfType<TabDeckEditorVisual>(this)) {
return QtUtils::findParentOfType<TabDeckEditorVisual>(this)
->deckStateManager->getModel()
->getDeckList()
->getCardList()
.size() >= 5;
}
return true;
};
}
sequence.addStep(explorationStep);
TutorialStep conclusionStep;
conclusionStep.targetWidget = this;
conclusionStep.text = tr(
"Great!\n\nLet's look at them in the deck view before we conclude this tutorial with the analytics widgets.");
conclusionStep.onExit = [this]() {
auto tabWidget = QtUtils::findParentOfType<TabDeckEditorVisualTabWidget>(this);
if (tabWidget) {
tabWidget->setCurrentWidget(tabWidget->visualDeckView);
}
};
sequence.addStep(conclusionStep);
/*sequence.addStep(
{quickFilterSaveLoadWidget, "This button will let you save and load all currently applied filters to files."});
sequence.addStep({quickFilterNameWidget,
"This button will let you apply name filters. Optionally, you can import every card in "
"your deck as a name filter and then save this as a filter using the save/load button "
"to make your own quick access collections!"});
sequence.addStep({mainTypeFilterWidget, "Use these buttons to quickly filter by card types."});
sequence.addStep({quickFilterSubTypeWidget, "This button will let you apply filters for card sub-types."});
sequence.addStep(
{quickFilterSetWidget,
"This button will let you apply filters for card sets. You can also filter to the X most recent sets. "
"Filtering to a set will display all printings of a card within that set."});*/
return sequence;
}
void VisualDatabaseDisplayWidget::initialize()
{
databaseLoadIndicator->setVisible(false);

View File

@@ -14,6 +14,7 @@
#include "../cards/card_size_widget.h"
#include "../general/layout_containers/flow_widget.h"
#include "../general/layout_containers/overlap_control_widget.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../utility/custom_line_edit.h"
#include "visual_database_display_color_filter_widget.h"
#include "visual_database_display_filter_toolbar_widget.h"
@@ -26,6 +27,7 @@
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <qscrollarea.h>
class TutorialController;
inline Q_LOGGING_CATEGORY(VisualDatabaseDisplayLog, "visual_database_display");
class VisualDatabaseDisplayWidget : public QWidget
@@ -70,6 +72,7 @@ public:
VisualDatabaseDisplayColorFilterWidget *colorFilterWidget;
public slots:
TutorialSequence addTutorialSteps();
void onSearchModelChanged();
signals:

View File

@@ -121,3 +121,60 @@ void VisualDeckDisplayOptionsWidget::updateDisplayType()
}
emit displayTypeChanged(currentDisplayType);
}
TutorialSequence VisualDeckDisplayOptionsWidget::generateTutorialSequence(TutorialSequence sequence)
{
TutorialStep introStep;
introStep.targetWidget = this;
introStep.text = tr("You can change how the deck is displayed, grouped, and sorted here.");
sequence.addStep(introStep);
TutorialStep displayTypeStep;
displayTypeStep.targetWidget = displayTypeButton;
displayTypeStep.text =
tr("You can change the layout of the displayed cards by clicking on this button.\n\nThe overlap type will "
"stack cards on top of each other, leaving the top exposed for easy skimming.\nYou can always hover your "
"mouse over a card to display a zoomed version of it.\n\nThe flat layout will display cards next to each "
"other, without any overlap.\n\nLet's switch to flat now!");
displayTypeStep.allowClickThrough = true;
displayTypeStep.requiresInteraction = true;
displayTypeStep.validationTiming = ValidationTiming::OnSignal;
displayTypeStep.signalSource = displayTypeButton;
displayTypeStep.signalName = SIGNAL(clicked());
displayTypeStep.autoAdvanceOnValid = true;
displayTypeStep.validator = [] { return true; };
sequence.addStep(displayTypeStep);
TutorialStep groupStep;
groupStep.targetWidget = groupByComboBox;
groupStep.text = tr("You can change how cards are grouped here.\n\nLet's change cards to be grouped by 'Color'");
groupStep.allowClickThrough = true;
groupStep.requiresInteraction = true;
groupStep.validationTiming = ValidationTiming::OnChange;
groupStep.autoAdvanceOnValid = true;
groupStep.validator = [this]() { return groupByComboBox->currentIndex() == 2; };
groupStep.validationHint = tr("Select the 'Color' option");
sequence.addStep(groupStep);
TutorialStep sortStep;
sortStep.targetWidget = sortCriteriaButton;
sortStep.text =
tr("Let's check out sorting now. In the visual deck view, sort modifiers are hierarchical,\n meaning "
"that the cards will first be sorted using the top-most criteria\nand then, if cards are equal using this "
"criteria,\nthe next criteria in the list will be used as a tie-breaker.\n\n"
"Change the sorting to be based primarily on converted mana cost (cmc) by dragging it to the top.");
sortStep.allowClickThrough = true;
sortStep.requiresInteraction = true;
sortStep.autoAdvanceOnValid = true;
sortStep.validationTiming = ValidationTiming::OnSignal;
sortStep.signalSource = this;
sortStep.signalName = SIGNAL(sortCriteriaChanged(const QStringList &));
sortStep.validator = []() { return true; };
sequence.addStep(sortStep);
return sequence;
}

View File

@@ -85,6 +85,8 @@ public:
return activeSortCriteria;
}
TutorialSequence generateTutorialSequence(TutorialSequence sequence);
private slots:
/**
* @brief Slot triggered whenever the sort list is reordered.

View File

@@ -5,6 +5,9 @@
#include "../cards/card_info_picture_widget.h"
#include "../deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h"
#include "../deck_analytics/deck_list_statistics_analyzer.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h"
#include "libcockatrice/utility/qt_utils.h"
#include <QSplitter>
#include <libcockatrice/card/database/card_database_manager.h>
@@ -66,6 +69,27 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare
retranslateUi();
}
TutorialSequence VisualDeckEditorSampleHandWidget::generateTutorialSequence()
{
TutorialSequence sampleHandSequence;
sampleHandSequence.name = tr("Sample Hand");
TutorialStep introStep;
introStep.targetWidget = this;
introStep.text = tr("This is the sample hand tab.\n\nHere, you can draw a sample hand from your deck without "
"having to start a game as well as view statistical information about your draws.");
introStep.onEnter = [this]() {
auto tabWidget = QtUtils::findParentOfType<TabDeckEditorVisualTabWidget>(this);
if (tabWidget) {
tabWidget->setCurrentWidget(tabWidget->sampleHandWidget);
}
};
sampleHandSequence.addStep(introStep);
return sampleHandSequence;
}
void VisualDeckEditorSampleHandWidget::retranslateUi()
{
resetButton->setText(tr("Draw a new sample hand"));

View File

@@ -10,6 +10,7 @@
#include "../cards/card_size_widget.h"
#include "../deck_analytics/deck_list_statistics_analyzer.h"
#include "../general/layout_containers/flow_widget.h"
#include "../general/tutorial/tutorial_controller.h"
#include <QPushButton>
#include <QSpinBox>
@@ -25,6 +26,7 @@ public:
DeckListModel *deckListModel,
DeckListStatisticsAnalyzer *statsAnalyzer);
QList<ExactCard> getRandomCards(int amountToGet);
TutorialSequence generateTutorialSequence();
public slots:
void updateDisplay();

View File

@@ -24,6 +24,7 @@
#include <libcockatrice/models/database/card/card_search_model.h>
#include <libcockatrice/models/database/card_database_model.h>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qscrollarea.h>
VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent,
@@ -413,4 +414,84 @@ void VisualDeckEditorWidget::onSelectionChanged(const QItemSelection &selected,
}
}
}
}
TutorialSequence VisualDeckEditorWidget::addTutorialSteps()
{
TutorialSequence sequence;
sequence.name = "Adding Cards to Your Deck";
TutorialStep introStep;
introStep.targetWidget = displayOptionsAndSearch;
introStep.text = "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"
"This is helpful if you already know which card you would like to add "
"and will provide name suggestions as you type.\n\n"
"We'll look at the second way, through the database tab, later.";
sequence.addStep(introStep);
TutorialStep searchStep;
searchStep.targetWidget = searchBar;
searchStep.text = "Let's try it out now!\nType the name of a card into the search bar.";
searchStep.allowClickThrough = true;
searchStep.requiresInteraction = true;
searchStep.autoAdvanceOnValid = true;
searchStep.validationTiming = ValidationTiming::OnChange; // Make sure this is set!
searchStep.validator = [this]() {
return CardDatabaseManager::query()->getCard({searchBar->text()}) != ExactCard();
};
searchStep.validationHint = "Please enter a valid card name.";
searchStep.customInteractionHint = "✏️ Type a valid card name to continue";
sequence.addStep(searchStep);
TutorialStep addStep;
addStep.targetWidget = searchPushButton;
addStep.text = "Click this button to add the card to your deck.";
addStep.allowClickThrough = true;
addStep.requiresInteraction = true;
addStep.autoAdvanceOnValid = true;
addStep.validationTiming = ValidationTiming::OnSignal;
addStep.signalSource = deckListModel;
addStep.signalName = SIGNAL(cardAddedAt(const QModelIndex &));
addStep.validator = [this]() { return deckListModel->getCardNodes().size() >= 1; };
sequence.addStep(addStep);
TutorialStep organizationStep;
organizationStep.targetWidget = this;
organizationStep.text = "Let's look at how cards are organized and displayed now.\n\nWe'll add some random cards "
"from the database to your deck, so you can see it in action properly.";
organizationStep.onExit = [this]() {
while (deckListModel->getDeckList()->getCardList().size() < 60) {
deckListModel->addCard(CardDatabaseManager::query()->getRandomCard(), DECK_ZONE_MAIN);
}
};
sequence.addStep(organizationStep);
TutorialStep hoverStep;
hoverStep.targetWidget = this;
hoverStep.text = "Great! Take some time to explore these new cards in the current display mode.\n\nYou can select "
"a card by clicking on it with the left mouse button.\nYou can select multiple cards by holding "
"down CTRL or Shift.\nYou can clear the current selection by clicking on an area without a "
"card.\nDouble-clicking a card will move it between main and sideboard.\nRight-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;
sequence.addStep(hoverStep);
sequence = displayOptionsWidget->generateTutorialSequence(sequence);
TutorialStep conclusionStep;
conclusionStep.targetWidget = this;
conclusionStep.text =
"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 "
"on this screen to play around with the display options and advance when you are ready.";
conclusionStep.allowClickThrough = true;
sequence.addStep(conclusionStep);
return sequence;
}

View File

@@ -10,6 +10,7 @@
#include "../cards/card_info_picture_with_text_overlay_widget.h"
#include "../cards/card_size_widget.h"
#include "../general/layout_containers/overlap_control_widget.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../quick_settings/settings_button_widget.h"
#include "visual_deck_editor_placeholder_widget.h"
@@ -45,6 +46,7 @@ public:
void setSelectionModel(QItemSelectionModel *model);
void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
TutorialSequence addTutorialSteps();
void updatePlaceholderVisibility();
QItemSelectionModel *getSelectionModel() const
{

View File

@@ -1,6 +1,7 @@
#include "visual_deck_storage_widget.h"
#include "../../../client/settings/cache_settings.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../quick_settings/settings_button_widget.h"
#include "deck_preview/deck_preview_widget.h"
#include "visual_deck_storage_folder_display_widget.h"
@@ -88,6 +89,30 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
}
}
TutorialSequence VisualDeckStorageWidget::generateTutorialSequence(TutorialSequence vdsSequence)
{
vdsSequence.addStep(
{this, tr("This is the visual deck storage. It displays all the files and folders located in "
"your default deck storage location. You can adjust this location in the settings.")});
vdsSequence.addStep({searchAndSortContainer,
tr("You can filter the decks in your collection using these widgets. Check the (i) symbol in "
"the search bar for more information on the syntax used to filter decks.")});
vdsSequence.addStep(
{tagFilterWidget,
tr("Additionally, the VDS allows you to assign and filter by tags. This is very helpful for large deck "
"collections, as it allows you to group similar decks (i.e. by power level or theme) and then 'drill down' "
"to exactly the combination of tags that interests you (i.e. 'I want to play a mid-power deck focused on "
"this type of card that wins with this strategy.')\n\nYou can left-click a tag to add it to the "
"filter.\nThe widget will then automatically filter the list to only include tags from decks which also "
"contain your original tag.\n\nYou can exclude a tag by right-clicking it.\n\nYou can clear a tags filter "
"status with the middle mouse button.")});
vdsSequence.addStep({scrollArea, tr("This is where all your local decks will be displayed. You can customize their "
"display status using the cogwheel in the top right.\nYou can select a deck by "
"double-clicking it.\n\nRight-click a deck for more options.")});
return vdsSequence;
}
void VisualDeckStorageWidget::refreshIfPossible()
{
if (scrollArea->widget() != databaseLoadIndicator) {

View File

@@ -9,6 +9,7 @@
#include "../../deck_loader/deck_loader.h"
#include "../cards/card_size_widget.h"
#include "../general/tutorial/tutorial_controller.h"
#include "../quick_settings/settings_button_widget.h"
#include "deck_preview/deck_preview_color_identity_filter_widget.h"
#include "visual_deck_storage_folder_display_widget.h"
@@ -32,6 +33,7 @@ public:
explicit VisualDeckStorageWidget(QWidget *parent);
void refreshIfPossible();
void retranslateUi();
TutorialSequence generateTutorialSequence(TutorialSequence vdsSequence);
VisualDeckStorageTagFilterWidget *tagFilterWidget;
bool deckPreviewSelectionAnimationEnabled;