mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 00:42:19 -07:00
Merge 89ef10c862 into 42f890ea7c
This commit is contained in:
commit
c1baef6b0f
35 changed files with 1861 additions and 12 deletions
|
|
@ -223,6 +223,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
|
||||
|
|
|
|||
|
|
@ -103,6 +103,20 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
switchToDeckSelectView();
|
||||
}
|
||||
|
||||
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")});
|
||||
|
||||
visualDeckStorageWidget->generateTutorialSequence(deckViewContainerSequence);
|
||||
|
||||
return deckViewContainerSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the VDS widget and inserts it into the layout. No-ops if the widget already exists
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#ifndef TABBED_DECK_VIEW_CONTAINER_H
|
||||
#define TABBED_DECK_VIEW_CONTAINER_H
|
||||
#include "../../interface/widgets/general/tutorial/tutorial_controller.h"
|
||||
#include "deck_view_container.h"
|
||||
|
||||
#include <QTabWidget>
|
||||
|
|
@ -16,6 +17,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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -65,6 +68,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"));
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#define DECK_ANALYTICS_WIDGET_H
|
||||
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "../general/tutorial/tutorial_controller.h"
|
||||
#include "abstract_analytics_panel_widget.h"
|
||||
#include "deck_list_statistics_analyzer.h"
|
||||
#include "resizable_panel.h"
|
||||
|
|
@ -32,6 +33,7 @@ public:
|
|||
explicit DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer);
|
||||
void retranslateUi();
|
||||
void includeSideboardChanged(bool checked);
|
||||
TutorialSequence generateTutorialSequence();
|
||||
|
||||
private slots:
|
||||
void onAddPanel();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../general/tutorial/tutorial_controller.h"
|
||||
#include "../settings_page/user_interface_settings_page.h"
|
||||
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
|
|
@ -289,6 +290,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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -47,6 +48,8 @@ public:
|
|||
return deckView->selectionModel();
|
||||
}
|
||||
|
||||
TutorialSequence generateTutorialSequence();
|
||||
|
||||
public slots:
|
||||
void selectPrevCard();
|
||||
void selectNextCard();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "../../window_main.h"
|
||||
#include "background_sources.h"
|
||||
#include "home_styled_button.h"
|
||||
#include "tutorial/tutorial_controller.h"
|
||||
|
||||
#include <QGroupBox>
|
||||
#include <QPainter>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
|
||||
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||
|
|
@ -47,6 +49,33 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
&HomeWidget::initializeBackgroundFromSource);
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged,
|
||||
this, &HomeWidget::onBackgroundShuffleFrequencyChanged);
|
||||
|
||||
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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(visualDeckEditorButton, &QAbstractButton::clicked, controller,
|
||||
&TutorialController::checkValidation);
|
||||
};
|
||||
|
||||
sequence.addStep(vdeStep);
|
||||
sequence.addStep({visualDeckStorageButton, "Browse the decks in your local collection."});
|
||||
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().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
|
||||
[this] { repaint(); });
|
||||
|
|
@ -56,6 +85,16 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
}
|
||||
|
||||
void HomeWidget::showEvent(QShowEvent *event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
if (!tutorialStarted && !SettingsCache::instance().userInterface().getTutorialCompleted()) {
|
||||
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) {
|
||||
|
|
@ -206,29 +245,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);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
#include <QWidget>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
|
||||
class TutorialController;
|
||||
|
||||
class HomeWidget : public QWidget
|
||||
{
|
||||
|
||||
|
|
@ -27,6 +29,7 @@ public:
|
|||
|
||||
public slots:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void initializeBackgroundFromSource();
|
||||
void onBackgroundShuffleFrequencyChanged();
|
||||
void updateBackgroundProperties();
|
||||
|
|
@ -39,11 +42,21 @@ 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;
|
||||
HomeStyledButton *visualDeckEditorButton;
|
||||
HomeStyledButton *visualDeckStorageButton;
|
||||
HomeStyledButton *visualDatabaseDisplayButton;
|
||||
HomeStyledButton *edhrecButton;
|
||||
HomeStyledButton *archidektButton;
|
||||
HomeStyledButton *replaybutton;
|
||||
HomeStyledButton *exitButton;
|
||||
|
||||
void setRandomCard(ExactCard &newCard);
|
||||
void loadBackgroundSourceDeck();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
#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);
|
||||
QGridLayout *getLayout() const
|
||||
{
|
||||
return layout;
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
#include "tutorial_controller.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QMainWindow>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QTextEdit>
|
||||
#include <QTimer>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
TutorialController::~TutorialController()
|
||||
{
|
||||
// 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
|
||||
// up explicitly when the controller goes away without exitTutorial().
|
||||
if (tutorialOverlay) {
|
||||
tutorialOverlay->hide();
|
||||
tutorialOverlay->deleteLater();
|
||||
}
|
||||
tutorialOverlay = nullptr;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
tutorialOverlay->setParent(win); // triggers changeEvent and installs filter
|
||||
tutorialOverlay->setGeometry(win->rect());
|
||||
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;
|
||||
}
|
||||
|
||||
runExitForCurrentStep();
|
||||
|
||||
if (currentStep >= sequences[currentSequence].steps.size() - 1) {
|
||||
nextSequence();
|
||||
return;
|
||||
}
|
||||
|
||||
currentStep++;
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::prevStep()
|
||||
{
|
||||
if (currentSequence < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
runExitForCurrentStep();
|
||||
|
||||
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) {
|
||||
// Already at the very first step — just re-show it
|
||||
currentStep = 0;
|
||||
showStep();
|
||||
return;
|
||||
}
|
||||
|
||||
currentSequence--;
|
||||
currentStep = 0;
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::exitTutorial()
|
||||
{
|
||||
cleanupValidationMonitoring();
|
||||
if (tutorialOverlay) {
|
||||
tutorialOverlay->hide();
|
||||
tutorialOverlay->deleteLater();
|
||||
}
|
||||
tutorialOverlay = nullptr;
|
||||
currentSequence = -1;
|
||||
currentStep = -1;
|
||||
tutorialCompleted = true;
|
||||
SettingsCache::instance().userInterface().setTutorialCompleted(true);
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
void TutorialController::runExitForCurrentStep()
|
||||
{
|
||||
if (currentSequence < 0 || currentSequence >= sequences.size()) {
|
||||
return;
|
||||
}
|
||||
const auto &steps = sequences[currentSequence].steps;
|
||||
if (currentStep < 0 || currentStep >= steps.size()) {
|
||||
return;
|
||||
}
|
||||
if (steps[currentStep].onExit) {
|
||||
steps[currentStep].onExit();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
cleanupValidationMonitoring();
|
||||
advanceScheduled = false;
|
||||
|
||||
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);
|
||||
|
||||
if (!step.customInteractionHint.isEmpty()) {
|
||||
tutorialOverlay->setInteractionHint(step.customInteractionHint);
|
||||
} else if (step.requiresInteraction) {
|
||||
tutorialOverlay->setInteractionHint("👆 Click the highlighted area to continue");
|
||||
} else {
|
||||
tutorialOverlay->setInteractionHint("");
|
||||
}
|
||||
|
||||
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.signalHook) {
|
||||
validationConnection = step.signalHook(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle OnChange validation - widget-specific
|
||||
if (step.validationTiming == ValidationTiming::OnChange && step.validator) {
|
||||
if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(step.targetWidget)) {
|
||||
validationConnection =
|
||||
connect(lineEdit, &QLineEdit::textChanged, this, &TutorialController::checkValidation);
|
||||
} else if (QTextEdit *textEdit = qobject_cast<QTextEdit *>(step.targetWidget)) {
|
||||
validationConnection =
|
||||
connect(textEdit, &QTextEdit::textChanged, this, &TutorialController::checkValidation);
|
||||
} else if (QPlainTextEdit *plainText = qobject_cast<QPlainTextEdit *>(step.targetWidget)) {
|
||||
validationConnection =
|
||||
connect(plainText, &QPlainTextEdit::textChanged, this, &TutorialController::checkValidation);
|
||||
} else if (QComboBox *combo = qobject_cast<QComboBox *>(step.targetWidget)) {
|
||||
validationConnection = connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&TutorialController::checkValidation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TutorialController::cleanupValidationMonitoring()
|
||||
{
|
||||
if (validationConnection) {
|
||||
disconnect(validationConnection);
|
||||
validationConnection = QMetaObject::Connection();
|
||||
}
|
||||
}
|
||||
|
||||
void TutorialController::checkValidation()
|
||||
{
|
||||
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();
|
||||
|
||||
if (isValid) {
|
||||
// Clear any validation hints
|
||||
tutorialOverlay->showValidationHint("");
|
||||
|
||||
// Auto-advance if enabled
|
||||
if (step.autoAdvanceOnValid && !advanceScheduled) {
|
||||
advanceScheduled = true;
|
||||
QTimer::singleShot(500, this, [this]() {
|
||||
advanceScheduled = false;
|
||||
nextStep();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
#ifndef COCKATRICE_TUTORIAL_CONTROLLER_H
|
||||
#define COCKATRICE_TUTORIAL_CONTROLLER_H
|
||||
|
||||
#include "tutorial_overlay.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#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
|
||||
};
|
||||
|
||||
class TutorialController;
|
||||
|
||||
struct TutorialStep
|
||||
{
|
||||
// QPointer so a destroyed widget is detected instead of dereferencing a dangling pointer.
|
||||
QPointer<QWidget> targetWidget;
|
||||
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;
|
||||
|
||||
// Establishes the connection used for signal-based validation (ValidationTiming::OnSignal).
|
||||
// Called with the controller so call sites can connect a specific signal to
|
||||
// TutorialController::checkValidation using pointer-to-member, which is checked at
|
||||
// compile time unlike string-based SIGNAL() connections.
|
||||
std::function<QMetaObject::Connection(TutorialController *)> signalHook = nullptr;
|
||||
};
|
||||
|
||||
struct TutorialSequence
|
||||
{
|
||||
QString name;
|
||||
QList<TutorialStep> steps;
|
||||
|
||||
void addStep(const TutorialStep &step)
|
||||
{
|
||||
steps.append(step);
|
||||
}
|
||||
|
||||
void addSteps(const TutorialSequence &sequence)
|
||||
{
|
||||
steps.append(sequence.steps);
|
||||
}
|
||||
};
|
||||
|
||||
class TutorialController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TutorialController(QWidget *_tutorializedWidget);
|
||||
~TutorialController() override;
|
||||
|
||||
void addSequence(const TutorialSequence &seq);
|
||||
void start();
|
||||
|
||||
TutorialOverlay *getOverlay()
|
||||
{
|
||||
return tutorialOverlay;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void nextStep();
|
||||
void prevStep();
|
||||
void nextSequence();
|
||||
void prevSequence();
|
||||
void runExitForCurrentStep();
|
||||
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;
|
||||
|
||||
// True while an auto-advance timer is pending, so repeated signal emissions
|
||||
// can't queue more than one advance.
|
||||
bool advanceScheduled = false;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_TUTORIAL_CONTROLLER_H
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
#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_StaticContents, false);
|
||||
setAttribute(Qt::WA_NoSystemBackground, true);
|
||||
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()) {
|
||||
setGeometry(0, 0, parentWidget()->width(), parentWidget()->height());
|
||||
}
|
||||
|
||||
raise();
|
||||
parentResized();
|
||||
}
|
||||
|
||||
void TutorialOverlay::changeEvent(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::ParentChange) {
|
||||
// Remove filter from old parent (Qt has already changed parentWidget() by now,
|
||||
// so iterate sender list isn't easy — reinstall unconditionally on the new parent)
|
||||
QWidget *p = parentWidget();
|
||||
if (p) {
|
||||
p->installEventFilter(this); // safe to call multiple times
|
||||
setGeometry(p->rect());
|
||||
raise();
|
||||
}
|
||||
}
|
||||
QWidget::changeEvent(event);
|
||||
}
|
||||
|
||||
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);
|
||||
nextButton->setToolTip(interactive ? "Complete the highlighted action to continue" : "Next step");
|
||||
}
|
||||
|
||||
if (nextSeqButton) {
|
||||
nextSeqButton->setEnabled(!interactive);
|
||||
nextSeqButton->setToolTip(interactive ? "Complete the highlighted action to continue" : "Next chapter");
|
||||
}
|
||||
|
||||
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 p = targetWidget->mapToGlobal(QPoint(0, 0));
|
||||
QPoint local = mapFromGlobal(p);
|
||||
|
||||
QRect r(local, targetWidget->size());
|
||||
r = r.adjusted(-6, -6, 6, 6);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
void TutorialOverlay::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
QRect hole = currentHoleRect();
|
||||
|
||||
if (hole.contains(event->pos())) {
|
||||
|
||||
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(); });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void TutorialOverlay::updateMask()
|
||||
{
|
||||
clearMask();
|
||||
|
||||
if (!isVisible() || !parentWidget()) {
|
||||
return;
|
||||
}
|
||||
if (!allowClickThrough) {
|
||||
return;
|
||||
}
|
||||
|
||||
QRect hole = currentHoleRect();
|
||||
|
||||
if (!hole.isEmpty()) {
|
||||
QRegion full(rect());
|
||||
QRegion cut(hole);
|
||||
setMask(full.subtracted(cut));
|
||||
}
|
||||
}
|
||||
|
||||
bool TutorialOverlay::event(QEvent *event)
|
||||
{
|
||||
return QWidget::event(event);
|
||||
}
|
||||
|
||||
void TutorialOverlay::resizeEvent(QResizeEvent *)
|
||||
{
|
||||
recomputeLayout();
|
||||
}
|
||||
|
||||
bool TutorialOverlay::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (obj == parentWidget()) {
|
||||
switch (event->type()) {
|
||||
case QEvent::Resize:
|
||||
case QEvent::Move: // window dragged to another monitor / position
|
||||
parentResized();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj == targetWidget) {
|
||||
switch (event->type()) {
|
||||
case QEvent::Show:
|
||||
QMetaObject::invokeMethod(this, [this] { recomputeLayout(); }, Qt::QueuedConnection);
|
||||
break;
|
||||
case QEvent::Hide:
|
||||
case QEvent::Move:
|
||||
case QEvent::Resize:
|
||||
recomputeLayout();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return QWidget::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void TutorialOverlay::parentResized()
|
||||
{
|
||||
if (!parentWidget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QSize s = parentWidget()->size();
|
||||
|
||||
setGeometry(0, 0, s.width(), s.height());
|
||||
|
||||
clearMask();
|
||||
recomputeLayout();
|
||||
|
||||
setAttribute(Qt::WA_NoSystemBackground, false);
|
||||
setAttribute(Qt::WA_NoSystemBackground, true);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void TutorialOverlay::recomputeLayout()
|
||||
{
|
||||
if (!parentWidget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The overlay is parented to the top-level window; its client size (rect())
|
||||
// is the visible area, whereas window()->geometry() includes the frame.
|
||||
resize(parentWidget()->size());
|
||||
|
||||
bubble->adjustSize();
|
||||
|
||||
QRect hole = currentHoleRect();
|
||||
|
||||
if (hole.isEmpty()) {
|
||||
bubble->hide();
|
||||
controlBar->hide();
|
||||
|
||||
hide();
|
||||
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
show();
|
||||
raise();
|
||||
|
||||
QSize bsize = bubble->sizeHint().expandedTo(QSize(160, 60));
|
||||
highlightBubbleRect = computeBubbleRect(hole, bsize);
|
||||
|
||||
bubble->setGeometry(highlightBubbleRect);
|
||||
bubble->show();
|
||||
|
||||
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}};
|
||||
|
||||
QRect placedRect;
|
||||
|
||||
for (const QPoint &pos : positions) {
|
||||
QRect proposed(pos, controlBar->size());
|
||||
|
||||
if (!proposed.intersects(hole) && !proposed.intersects(highlightBubbleRect)) {
|
||||
placedRect = proposed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!placedRect.isValid()) {
|
||||
placedRect = QRect(QPoint(margin, margin), controlBar->size());
|
||||
}
|
||||
|
||||
controlBar->move(placedRect.topLeft());
|
||||
controlBar->show();
|
||||
|
||||
updateMask();
|
||||
update();
|
||||
}
|
||||
|
||||
QRect TutorialOverlay::computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const
|
||||
{
|
||||
QRect r = rect();
|
||||
|
||||
QRect bubble;
|
||||
|
||||
if (hole.isEmpty()) {
|
||||
bubble = QRect(r.center() - QPoint(bubbleSize.width() / 2, bubbleSize.height() / 2), bubbleSize);
|
||||
} else {
|
||||
const int margin = 16;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#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;
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
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();
|
||||
void changeEvent(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
|
||||
|
|
@ -138,6 +138,205 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
|
|||
}
|
||||
|
||||
QTimer::singleShot(0, this, &TabGame::loadLayout);
|
||||
|
||||
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 (!tutorialController || tutorialStarted || SettingsCache::instance().userInterface().getTutorialCompleted()) {
|
||||
return;
|
||||
}
|
||||
// Only start once all sequences are registered so advancing can't run past the
|
||||
// end of an incomplete sequence list.
|
||||
if (tutorialInitialized) {
|
||||
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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(game->getGameEventHandler(), &GameEventHandler::logDeckSelect, controller,
|
||||
&TutorialController::checkValidation);
|
||||
};
|
||||
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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(this, &TabGame::localPlayerReadyStateChanged, controller, &TutorialController::checkValidation);
|
||||
};
|
||||
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("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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(game->getPlayerManager()
|
||||
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
|
||||
->getPlayerEventHandler(),
|
||||
&PlayerEventHandler::logSetCounter, controller, &TutorialController::checkValidation);
|
||||
};
|
||||
lifeCounterStep.validator = [this] {
|
||||
auto counters =
|
||||
game->getPlayerManager()->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())->getCounters();
|
||||
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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(game->getPlayerManager()
|
||||
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
|
||||
->getPlayerEventHandler(),
|
||||
&PlayerEventHandler::logRollDie, controller, &TutorialController::checkValidation);
|
||||
};
|
||||
diceRollStep.validator = [] { return true; };
|
||||
diceRollStep.validationHint = tr("Roll a dice using any of these methods.");
|
||||
|
||||
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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(game->getPlayerManager()
|
||||
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
|
||||
->getPlayerEventHandler(),
|
||||
&PlayerEventHandler::logDrawCards, controller, &TutorialController::checkValidation);
|
||||
};
|
||||
mulliganStep.validator = [this] {
|
||||
return game->getPlayerManager()
|
||||
->getActiveLocalPlayer(game->getPlayerManager()->getLocalPlayerId())
|
||||
->getHandZone()
|
||||
->getCards()
|
||||
.size() == 7;
|
||||
};
|
||||
mulliganStep.validationHint = tr("Mulligan to 7 cards using any of these methods.");
|
||||
|
||||
gamePlaySequence.addStep(mulliganStep);
|
||||
|
||||
tutorialController->addSequence(gamePlaySequence);
|
||||
|
||||
if (tutorialStarted || SettingsCache::instance().userInterface().getTutorialCompleted()) {
|
||||
return;
|
||||
}
|
||||
// The tab may have been shown before the local player joined; start now that
|
||||
// the full sequence list is registered.
|
||||
if (isVisible()) {
|
||||
tutorialStarted = true;
|
||||
QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); });
|
||||
}
|
||||
}
|
||||
|
||||
void TabGame::connectToGameState()
|
||||
|
|
@ -702,6 +901,8 @@ void TabGame::addLocalPlayer(PlayerLogic *newPlayer, int playerId)
|
|||
deckView->playerDeckView->readyAndUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
finishTutorialInitialization();
|
||||
}
|
||||
|
||||
void TabGame::processPlayerLeave(PlayerLogic *leavingPlayer)
|
||||
|
|
@ -779,6 +980,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(PlayerLogic *newPlayer, int playerId)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ inline Q_LOGGING_CATEGORY(TabGameLog, "tab_game");
|
|||
class UserListProxy;
|
||||
class DeckViewContainer;
|
||||
class AbstractClient;
|
||||
class TutorialController;
|
||||
class CardDatabase;
|
||||
class GameView;
|
||||
class GameScene;
|
||||
|
|
@ -58,6 +59,9 @@ class TabGame : public Tab
|
|||
Q_OBJECT
|
||||
private:
|
||||
AbstractGame *game;
|
||||
TutorialController *tutorialController = nullptr;
|
||||
bool tutorialStarted = false;
|
||||
bool tutorialInitialized = false;
|
||||
const UserListProxy *userListProxy;
|
||||
ReplayWidget *replayWidget = nullptr;
|
||||
QStringList gameTypes;
|
||||
|
|
@ -127,6 +131,7 @@ private:
|
|||
void createDeckViewContainerWidget(bool bReplay = false);
|
||||
void createReplayDock(GameReplay *replay);
|
||||
signals:
|
||||
void localPlayerReadyStateChanged(bool ready);
|
||||
void gameClosing(TabGame *tab);
|
||||
void containerProcessingStarted(const GameEventContext &context);
|
||||
void containerProcessingDone();
|
||||
|
|
@ -166,16 +171,17 @@ private slots:
|
|||
void processPlayerLeave(PlayerLogic *leavingPlayer);
|
||||
void actResetLayout();
|
||||
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
|
||||
protected slots:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
|
||||
public:
|
||||
TabGame(TabSupervisor *_tabSupervisor,
|
||||
QList<AbstractClient *> &_clients,
|
||||
const Event_GameJoined &event,
|
||||
const QMap<int, QString> &_roomGameTypes);
|
||||
void finishTutorialInitialization();
|
||||
void connectToGameState();
|
||||
void connectToPlayerManager();
|
||||
void connectToGameEventHandler();
|
||||
|
|
|
|||
|
|
@ -152,6 +152,10 @@ public:
|
|||
{
|
||||
return tabServer;
|
||||
}
|
||||
[[nodiscard]] TabHome *getTabHome() const
|
||||
{
|
||||
return tabHome;
|
||||
}
|
||||
[[nodiscard]] const QMap<int, TabRoom *> &getRoomTabs() const
|
||||
{
|
||||
return roomTabs;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,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"
|
||||
|
||||
|
|
@ -31,6 +33,7 @@
|
|||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/layouts_settings.h>
|
||||
|
||||
/**
|
||||
|
|
@ -56,6 +59,85 @@ TabDeckEditorVisual::TabDeckEditorVisual(TabSupervisor *_tabSupervisor) : Abstra
|
|||
loadLayout();
|
||||
filterDockWidget->setHidden(true);
|
||||
cardDatabaseDockWidget->setHidden(true);
|
||||
tutorialController = new TutorialController(this);
|
||||
createTutorialSequences();
|
||||
}
|
||||
|
||||
void TabDeckEditorVisual::createTutorialSequences()
|
||||
{
|
||||
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); }});
|
||||
|
||||
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 && !SettingsCache::instance().userInterface().getTutorialCompleted()) {
|
||||
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. */
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "../tab.h"
|
||||
#include "tab_deck_editor_visual_tab_widget.h"
|
||||
|
||||
class TutorialController;
|
||||
/**
|
||||
* @class TabDeckEditorVisual
|
||||
* @ingroup DeckEditorTabs
|
||||
|
|
@ -55,7 +56,17 @@ class TabDeckEditorVisual : public AbstractTabDeckEditor
|
|||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
TutorialController *tutorialController = nullptr;
|
||||
bool tutorialStarted = false;
|
||||
|
||||
/**
|
||||
* @brief Build and register all tutorial sequences for this tab.
|
||||
*/
|
||||
void createTutorialSequences();
|
||||
|
||||
protected slots:
|
||||
void showEvent(QShowEvent *ev) override;
|
||||
/**
|
||||
* @brief Load the editor layout from settings.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ public:
|
|||
void initialize();
|
||||
void retranslateUi();
|
||||
|
||||
SettingsButtonWidget *getSetFilterWidget()
|
||||
{
|
||||
return quickFilterSetWidget;
|
||||
}
|
||||
|
||||
private:
|
||||
VisualDatabaseDisplayWidget *visualDatabaseDisplay;
|
||||
DeckListModel *deckListModel;
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@
|
|||
#include "../../pixel_map_generator.h"
|
||||
#include "../cards/card_info_picture_with_text_overlay_widget.h"
|
||||
#include "../deck_editor/card_database_view.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>
|
||||
|
|
@ -21,6 +22,7 @@
|
|||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
#include <utility>
|
||||
|
||||
VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
|
||||
|
|
@ -136,6 +138,81 @@ 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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(filterModel, &FilterTreeModel::layoutChanged, controller, &TutorialController::checkValidation);
|
||||
};
|
||||
setFilterStep.validator = [] { return true; };
|
||||
sequence.addStep(setFilterStep);
|
||||
|
||||
TutorialStep explorationStep;
|
||||
explorationStep.targetWidget = this;
|
||||
explorationStep.text = tr("Try it out!\n\nAdd 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] {
|
||||
auto deckEditor = QtUtils::findParentOfType<TabDeckEditorVisual>(this);
|
||||
// Only clear an empty starter deck; never destroy a deck the user may
|
||||
// have opened with existing cards.
|
||||
if (deckEditor->deckStateManager->getModel()->getDeckList()->getCardList().isEmpty()) {
|
||||
deckEditor->deckStateManager->clearDeck();
|
||||
}
|
||||
};
|
||||
auto deckModel = QtUtils::findParentOfType<TabDeckEditorVisual>(this)->deckStateManager->getModel();
|
||||
explorationStep.signalHook = [deckModel](TutorialController *controller) {
|
||||
return connect(deckModel, &DeckListModel::cardNodesChanged, controller,
|
||||
&TutorialController::checkValidation);
|
||||
};
|
||||
explorationStep.validator = [this] {
|
||||
if (QtUtils::findParentOfType<TabDeckEditorVisual>(this)) {
|
||||
return QtUtils::findParentOfType<TabDeckEditorVisual>(this)
|
||||
->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);
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
void VisualDatabaseDisplayWidget::initialize()
|
||||
{
|
||||
databaseLoadIndicator->setVisible(false);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -67,6 +69,7 @@ public:
|
|||
bool isVisualDisplayMode() const;
|
||||
|
||||
public slots:
|
||||
TutorialSequence addTutorialSteps();
|
||||
void onSearchModelChanged();
|
||||
|
||||
signals:
|
||||
|
|
|
|||
|
|
@ -127,6 +127,64 @@ void VisualDeckDisplayOptionsWidget::updateDisplayType()
|
|||
emit displayTypeChanged(currentDisplayType);
|
||||
}
|
||||
|
||||
void 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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(displayTypeButton, &QAbstractButton::clicked, controller, &TutorialController::checkValidation);
|
||||
};
|
||||
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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(this, &VisualDeckDisplayOptionsWidget::sortCriteriaChanged, controller,
|
||||
&TutorialController::checkValidation);
|
||||
};
|
||||
sortStep.validator = []() { return true; };
|
||||
|
||||
sequence.addStep(sortStep);
|
||||
}
|
||||
|
||||
void VisualDeckDisplayOptionsWidget::updateCompactMode(bool mode)
|
||||
{
|
||||
displayTypeButton->setCompact(mode);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#ifndef COCKATRICE_VISUAL_DECK_DISPLAY_OPTIONS_WIDGET_H
|
||||
#define COCKATRICE_VISUAL_DECK_DISPLAY_OPTIONS_WIDGET_H
|
||||
|
||||
#include "../general/tutorial/tutorial_controller.h"
|
||||
#include "visual_deck_editor_widget.h"
|
||||
|
||||
#include <QComboBox>
|
||||
|
|
@ -88,6 +89,8 @@ public:
|
|||
return activeSortCriteria;
|
||||
}
|
||||
|
||||
void generateTutorialSequence(TutorialSequence &sequence);
|
||||
|
||||
private slots:
|
||||
/**
|
||||
* @brief Slot triggered whenever the sort list is reordered.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -67,6 +70,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"));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
#include <libcockatrice/models/database/card_database_model.h>
|
||||
#include <libcockatrice/models/deck_list/deck_list_model.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
#include <qscrollarea.h>
|
||||
|
||||
VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent,
|
||||
|
|
@ -456,4 +457,85 @@ 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;
|
||||
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.signalHook = [this](TutorialController *controller) {
|
||||
return connect(deckListModel, &DeckListModel::cardAddedAt, controller, &TutorialController::checkValidation);
|
||||
};
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -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 "../utility/compact_push_button.h"
|
||||
#include "visual_deck_editor_placeholder_widget.h"
|
||||
|
|
@ -47,6 +48,7 @@ public:
|
|||
|
||||
void setSelectionModel(QItemSelectionModel *model);
|
||||
void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
|
||||
TutorialSequence addTutorialSteps();
|
||||
void updatePlaceholderVisibility();
|
||||
QItemSelectionModel *getSelectionModel() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -92,6 +93,28 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
}
|
||||
}
|
||||
|
||||
void 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.")});
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::refreshIfPossible()
|
||||
{
|
||||
if (scrollArea->widget() != databaseLoadIndicator) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
void generateTutorialSequence(TutorialSequence &vdsSequence);
|
||||
|
||||
VisualDeckStorageTagFilterWidget *tagFilterWidget;
|
||||
bool deckPreviewSelectionAnimationEnabled;
|
||||
|
|
|
|||
|
|
@ -160,6 +160,11 @@ bool InterfaceSettings::getShowGameSelectorFilterToolbar() const
|
|||
return getValue("showGameSelectorFilterToolbar", QString(), QString(), true).toBool();
|
||||
}
|
||||
|
||||
bool InterfaceSettings::getTutorialCompleted() const
|
||||
{
|
||||
return getValue("tutorialCompleted", QString(), QString(), false).toBool();
|
||||
}
|
||||
|
||||
void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus)
|
||||
{
|
||||
setValue(_useTearOffMenus, "useTearOffMenus");
|
||||
|
|
@ -326,3 +331,8 @@ void InterfaceSettings::setShowGameSelectorFilterToolbar(bool _showGameSelectorF
|
|||
setValue(_showGameSelectorFilterToolbar, "showGameSelectorFilterToolbar");
|
||||
emit showGameSelectorFilterToolbarChanged(_showGameSelectorFilterToolbar);
|
||||
}
|
||||
|
||||
void InterfaceSettings::setTutorialCompleted(bool value)
|
||||
{
|
||||
setValue(value, "tutorialCompleted");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public:
|
|||
[[nodiscard]] bool getShowStatusBar() const override;
|
||||
[[nodiscard]] bool getShowShortcuts() const override;
|
||||
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const override;
|
||||
[[nodiscard]] bool getTutorialCompleted() const;
|
||||
|
||||
void setUseTearOffMenus(bool _useTearOffMenus);
|
||||
void setCardViewInitialRowsMax(int _cardViewInitialRowsMax);
|
||||
|
|
@ -74,6 +75,7 @@ public:
|
|||
void setShowStatusBar(bool _showStatusBar);
|
||||
void setShowShortcuts(bool _showShortcuts);
|
||||
void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar);
|
||||
void setTutorialCompleted(bool value);
|
||||
|
||||
signals:
|
||||
void useTearOffMenusChanged(bool state);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue