mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-22 17:45:09 -07:00
[App] First-run tutorial
Took 5 minutes Took 3 minutes Took 2 minutes
This commit is contained in:
parent
46bdb6df32
commit
f4a1e9ff53
33 changed files with 1787 additions and 11 deletions
|
|
@ -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>
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
#include <libcockatrice/settings/paths_settings.h>
|
||||
#include <libcockatrice/utility/qt_utils.h>
|
||||
|
||||
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
||||
: QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice")
|
||||
|
|
@ -47,6 +49,37 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
&HomeWidget::initializeBackgroundFromSource);
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged,
|
||||
this, &HomeWidget::onBackgroundShuffleFrequencyChanged);
|
||||
|
||||
auto mainWindow = QtUtils::findParentOfType<QMainWindow>(this);
|
||||
|
||||
if (mainWindow) {
|
||||
tutorialController = new TutorialController(mainWindow);
|
||||
} else {
|
||||
tutorialController = new TutorialController(this);
|
||||
}
|
||||
auto sequence = TutorialSequence();
|
||||
sequence.addStep({connectButton, "Connect to a server to play here!"});
|
||||
auto vdeStep = TutorialStep(visualDeckEditorButton, "Create a new deck from cards in the database here!");
|
||||
vdeStep.requiresInteraction = true;
|
||||
vdeStep.allowClickThrough = true;
|
||||
vdeStep.validationHint = "Open the deck editor to try it out!";
|
||||
vdeStep.validationTiming = ValidationTiming::OnSignal;
|
||||
vdeStep.autoAdvanceOnValid = true;
|
||||
vdeStep.validator = []() { return true; };
|
||||
vdeStep.signalSource = visualDeckEditorButton;
|
||||
vdeStep.signalName = SIGNAL(clicked());
|
||||
|
||||
sequence.addStep(vdeStep);
|
||||
sequence.addStep({visualDeckStorageButton, "Browse the decks in your local collection."});
|
||||
sequence.addStep({visualDatabaseDisplayButton, "View the card database here."});
|
||||
sequence.addStep(
|
||||
{edhrecButton, "Browse EDHRec, an external service designed to provide card recommendations for decks."});
|
||||
sequence.addStep({archidektButton, "Browse Archidekt, an external service that allows users to store "
|
||||
"decklists and import them to your local collection."});
|
||||
sequence.addStep({replaybutton, "View replays of your past games here."});
|
||||
sequence.addStep({exitButton, "Exit the application."});
|
||||
tutorialController->addSequence(sequence);
|
||||
|
||||
// Lambda is cleaner to read than overloading this
|
||||
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
|
||||
[this] { repaint(); });
|
||||
|
|
@ -56,6 +89,16 @@ HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|||
&HomeWidget::updateButtonsToBackgroundColor);
|
||||
}
|
||||
|
||||
void HomeWidget::showEvent(QShowEvent *event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
if (!tutorialStarted) {
|
||||
tutorialStarted = true;
|
||||
// Start on next event loop iteration so everything is fully painted
|
||||
QTimer::singleShot(3, tutorialController, [this] { tutorialController->start(); });
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWidget::initializeBackgroundFromSource()
|
||||
{
|
||||
if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) {
|
||||
|
|
@ -206,29 +249,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);
|
||||
|
|
|
|||
|
|
@ -24,9 +24,18 @@ public:
|
|||
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
|
||||
void updateRandomCard();
|
||||
QPair<QColor, QColor> extractDominantColors(const QPixmap &pixmap);
|
||||
HomeStyledButton *connectButton;
|
||||
HomeStyledButton *visualDeckEditorButton;
|
||||
HomeStyledButton *visualDeckStorageButton;
|
||||
HomeStyledButton *visualDatabaseDisplayButton;
|
||||
HomeStyledButton *edhrecButton;
|
||||
HomeStyledButton *archidektButton;
|
||||
HomeStyledButton *replaybutton;
|
||||
HomeStyledButton *exitButton;
|
||||
|
||||
public slots:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void initializeBackgroundFromSource();
|
||||
void onBackgroundShuffleFrequencyChanged();
|
||||
void updateBackgroundProperties();
|
||||
|
|
@ -39,11 +48,12 @@ private:
|
|||
QTimer *cardChangeTimer;
|
||||
TabSupervisor *tabSupervisor;
|
||||
QPixmap background;
|
||||
TutorialController *tutorialController;
|
||||
bool tutorialStarted = false;
|
||||
CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr;
|
||||
DeckList backgroundSourceDeck;
|
||||
QPixmap overlay;
|
||||
QPair<QColor, QColor> gradientColors;
|
||||
HomeStyledButton *connectButton;
|
||||
|
||||
void 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,33 @@
|
|||
#ifndef COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
|
||||
#define COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
|
||||
class BubbleWidget : public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BubbleWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setText(const QString &text);
|
||||
void setProgress(int stepNum, int totalSteps, int overallStep, int overallTotal);
|
||||
void setInteractionHint(const QString &hint);
|
||||
void setValidationHint(const QString &hint);
|
||||
|
||||
private:
|
||||
void clearValidationHint();
|
||||
|
||||
QLabel *counterLabel;
|
||||
QLabel *textLabel;
|
||||
QLabel *interactionLabel; // Shows "Click to continue"
|
||||
QLabel *validationLabel; // Shows validation errors
|
||||
QLabel *progressLabel; // Shows overall progress
|
||||
QGridLayout *layout;
|
||||
QTimer *validationTimer; // Auto-hide validation hint
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_TUTORIAL_BUBBLE_WIDGET_H
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
#include "tutorial_controller.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDebug>
|
||||
#include <QLineEdit>
|
||||
#include <QMainWindow>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QTextEdit>
|
||||
#include <QTimer>
|
||||
|
||||
TutorialController::TutorialController(QWidget *_tutorializedWidget)
|
||||
: QObject(_tutorializedWidget), tutorializedWidget(_tutorializedWidget)
|
||||
{
|
||||
tutorialOverlay = new TutorialOverlay(tutorializedWidget->window());
|
||||
|
||||
tutorialOverlay->setWindowFlags(tutorialOverlay->windowFlags() | Qt::FramelessWindowHint);
|
||||
tutorialOverlay->hide();
|
||||
|
||||
connect(tutorialOverlay, &TutorialOverlay::nextStep, this, &TutorialController::attemptAdvance);
|
||||
connect(tutorialOverlay, &TutorialOverlay::prevStep, this, &TutorialController::prevStep);
|
||||
connect(tutorialOverlay, &TutorialOverlay::nextSequence, this, &TutorialController::nextSequence);
|
||||
connect(tutorialOverlay, &TutorialOverlay::prevSequence, this, &TutorialController::prevSequence);
|
||||
connect(tutorialOverlay, &TutorialOverlay::skipTutorial, this, &TutorialController::exitTutorial);
|
||||
connect(tutorialOverlay, &TutorialOverlay::targetClicked, this, &TutorialController::handleTargetClicked);
|
||||
}
|
||||
|
||||
void TutorialController::addSequence(const TutorialSequence &seq)
|
||||
{
|
||||
sequences.append(seq);
|
||||
}
|
||||
|
||||
void TutorialController::start()
|
||||
{
|
||||
if (sequences.isEmpty() || tutorialCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
QWidget *win = tutorializedWidget->window();
|
||||
|
||||
// Reparent to make absolutely sure
|
||||
tutorialOverlay->setParent(win);
|
||||
tutorialOverlay->setGeometry(0, 0, win->width(), win->height());
|
||||
|
||||
// Stack order
|
||||
tutorialOverlay->stackUnder(nullptr);
|
||||
tutorialOverlay->show();
|
||||
tutorialOverlay->raise();
|
||||
|
||||
currentSequence = 0;
|
||||
currentStep = 0;
|
||||
showStep();
|
||||
});
|
||||
}
|
||||
|
||||
void TutorialController::handleTargetClicked()
|
||||
{
|
||||
if (currentSequence < 0 || currentStep < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &step = sequences[currentSequence].steps[currentStep];
|
||||
|
||||
// If this step requires interaction AND uses OnAdvance validation, advance when clicked
|
||||
// For OnSignal/OnChange, the click just triggers the action - validation happens via signal
|
||||
if (step.requiresInteraction && step.validationTiming == ValidationTiming::OnAdvance) {
|
||||
attemptAdvance();
|
||||
}
|
||||
}
|
||||
|
||||
void TutorialController::attemptAdvance()
|
||||
{
|
||||
if (currentSequence < 0 || currentStep < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &step = sequences[currentSequence].steps[currentStep];
|
||||
|
||||
// Only validate on advance if timing is set to OnAdvance
|
||||
if (step.validationTiming == ValidationTiming::OnAdvance) {
|
||||
if (!validateCurrentStep()) {
|
||||
return; // Validation failed, stay on current step
|
||||
}
|
||||
}
|
||||
|
||||
// Validation passed or not required, proceed to next step
|
||||
nextStep();
|
||||
}
|
||||
|
||||
bool TutorialController::validateCurrentStep()
|
||||
{
|
||||
if (currentSequence < 0 || currentSequence >= sequences.size()) {
|
||||
return true; // No validation needed
|
||||
}
|
||||
|
||||
const auto &step = sequences[currentSequence].steps[currentStep];
|
||||
|
||||
// If there's a validator function, check it
|
||||
if (step.validator) {
|
||||
bool valid = step.validator();
|
||||
if (!valid) {
|
||||
// Show validation hint
|
||||
tutorialOverlay->showValidationHint(step.validationHint);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TutorialController::nextStep()
|
||||
{
|
||||
if (currentSequence < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep >= sequences[currentSequence].steps.size() - 1) {
|
||||
// We're on the last step of this sequence, run its onExit before advancing
|
||||
const auto &lastStep = sequences[currentSequence].steps[currentStep];
|
||||
if (lastStep.onExit) {
|
||||
lastStep.onExit();
|
||||
}
|
||||
nextSequence();
|
||||
return;
|
||||
}
|
||||
|
||||
currentStep++;
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::prevStep()
|
||||
{
|
||||
if (currentSequence < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep == 0) {
|
||||
prevSequence();
|
||||
return;
|
||||
}
|
||||
|
||||
currentStep--;
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::nextSequence()
|
||||
{
|
||||
if (currentSequence < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentSequence++;
|
||||
currentStep = 0;
|
||||
|
||||
if (currentSequence >= sequences.size()) {
|
||||
exitTutorial();
|
||||
return;
|
||||
}
|
||||
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::prevSequence()
|
||||
{
|
||||
if (currentSequence <= 0) {
|
||||
currentStep = 0;
|
||||
showStep();
|
||||
return;
|
||||
}
|
||||
|
||||
currentSequence--;
|
||||
currentStep = 0;
|
||||
showStep();
|
||||
}
|
||||
|
||||
void TutorialController::exitTutorial()
|
||||
{
|
||||
if (currentSequence >= 0 && currentStep >= 0 && currentSequence < sequences.size() &&
|
||||
currentStep < sequences[currentSequence].steps.size()) {
|
||||
const auto &curStep = sequences[currentSequence].steps[currentStep];
|
||||
if (curStep.onExit) {
|
||||
curStep.onExit();
|
||||
}
|
||||
}
|
||||
|
||||
cleanupValidationMonitoring();
|
||||
tutorialOverlay->hide();
|
||||
currentSequence = -1;
|
||||
currentStep = -1;
|
||||
tutorialCompleted = true;
|
||||
}
|
||||
|
||||
void TutorialController::updateProgress()
|
||||
{
|
||||
if (currentSequence < 0 || currentSequence >= sequences.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &seq = sequences[currentSequence];
|
||||
|
||||
// Calculate total steps across all sequences
|
||||
int totalSteps = 0;
|
||||
int currentOverallStep = 0;
|
||||
|
||||
for (int i = 0; i < sequences.size(); ++i) {
|
||||
int seqSteps = sequences[i].steps.size();
|
||||
totalSteps += seqSteps;
|
||||
|
||||
if (i < currentSequence) {
|
||||
currentOverallStep += seqSteps;
|
||||
}
|
||||
}
|
||||
|
||||
currentOverallStep += currentStep + 1; // +1 because steps are 0-indexed
|
||||
|
||||
// Update overlay with progress info
|
||||
tutorialOverlay->setProgress(currentStep + 1, // Current step in sequence (1-indexed)
|
||||
seq.steps.size(), // Total steps in sequence
|
||||
currentOverallStep, // Overall step number
|
||||
totalSteps, // Total steps in tutorial
|
||||
seq.name); // Sequence title
|
||||
}
|
||||
|
||||
void TutorialController::showStep()
|
||||
{
|
||||
if (currentSequence < 0 || currentSequence >= sequences.size()) {
|
||||
return;
|
||||
}
|
||||
const auto &seq = sequences[currentSequence];
|
||||
if (currentStep < 0 || currentStep >= seq.steps.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up validation monitoring from previous step
|
||||
cleanupValidationMonitoring();
|
||||
|
||||
// Run onExit for the previous step
|
||||
if (!(currentSequence == 0 && currentStep == 0)) {
|
||||
int prevSeq = currentSequence;
|
||||
int prevStepIndex = currentStep - 1;
|
||||
if (prevStepIndex < 0) {
|
||||
prevSeq = currentSequence - 1;
|
||||
if (prevSeq >= 0) {
|
||||
prevStepIndex = sequences[prevSeq].steps.size() - 1;
|
||||
} else {
|
||||
prevStepIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSeq >= 0 && prevStepIndex >= 0) {
|
||||
const auto &previousStep = sequences[prevSeq].steps[prevStepIndex];
|
||||
if (previousStep.onExit) {
|
||||
previousStep.onExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto &step = seq.steps[currentStep];
|
||||
|
||||
if (step.onEnter) {
|
||||
step.onEnter();
|
||||
}
|
||||
|
||||
tutorialOverlay->setTargetWidget(step.targetWidget);
|
||||
tutorialOverlay->setText(step.text);
|
||||
tutorialOverlay->setInteractive(step.requiresInteraction, step.allowClickThrough);
|
||||
|
||||
// Set custom interaction hint if provided
|
||||
if (!step.customInteractionHint.isEmpty()) {
|
||||
tutorialOverlay->setInteractionHint(step.customInteractionHint);
|
||||
} else if (step.requiresInteraction) {
|
||||
tutorialOverlay->setInteractionHint("👆 Click the highlighted area to continue");
|
||||
} else {
|
||||
tutorialOverlay->setInteractionHint("");
|
||||
}
|
||||
|
||||
// Setup validation monitoring for this step
|
||||
setupValidationMonitoring();
|
||||
|
||||
updateProgress();
|
||||
|
||||
tutorialOverlay->parentResized();
|
||||
tutorialOverlay->raise();
|
||||
tutorialOverlay->update();
|
||||
}
|
||||
|
||||
void TutorialController::setupValidationMonitoring()
|
||||
{
|
||||
if (currentSequence < 0 || currentSequence >= sequences.size()) {
|
||||
return;
|
||||
}
|
||||
if (currentStep < 0 || currentStep >= sequences[currentSequence].steps.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &step = sequences[currentSequence].steps[currentStep];
|
||||
|
||||
// Handle OnSignal validation - connect to any custom signal
|
||||
if (step.validationTiming == ValidationTiming::OnSignal && step.validator) {
|
||||
if (step.signalSource && step.signalName) {
|
||||
qInfo() << "Setting up signal-based validation for signal:" << step.signalName;
|
||||
validationConnection = connect(step.signalSource, step.signalName, this, SLOT(checkValidation()));
|
||||
if (!validationConnection) {
|
||||
qInfo() << "Warning: Failed to connect to signal" << step.signalName;
|
||||
}
|
||||
} else {
|
||||
qInfo() << "Warning: OnSignal validation timing set but signalSource or signalName is null";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle OnChange validation - widget-specific
|
||||
if (step.validationTiming == ValidationTiming::OnChange && step.validator) {
|
||||
if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(step.targetWidget)) {
|
||||
qInfo() << "Setting up validation monitoring for QLineEdit";
|
||||
validationConnection =
|
||||
connect(lineEdit, &QLineEdit::textChanged, this, &TutorialController::checkValidation);
|
||||
} else if (QTextEdit *textEdit = qobject_cast<QTextEdit *>(step.targetWidget)) {
|
||||
qInfo() << "Setting up validation monitoring for QTextEdit";
|
||||
validationConnection =
|
||||
connect(textEdit, &QTextEdit::textChanged, this, &TutorialController::checkValidation);
|
||||
} else if (QPlainTextEdit *plainText = qobject_cast<QPlainTextEdit *>(step.targetWidget)) {
|
||||
qInfo() << "Setting up validation monitoring for QPlainTextEdit";
|
||||
validationConnection =
|
||||
connect(plainText, &QPlainTextEdit::textChanged, this, &TutorialController::checkValidation);
|
||||
} else if (QComboBox *combo = qobject_cast<QComboBox *>(step.targetWidget)) {
|
||||
qInfo() << "Setting up validation monitoring for QComboBox";
|
||||
validationConnection = connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&TutorialController::checkValidation);
|
||||
} else {
|
||||
qInfo() << "Warning: OnChange validation timing set but widget type not supported:"
|
||||
<< (step.targetWidget ? step.targetWidget->metaObject()->className() : "null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TutorialController::cleanupValidationMonitoring()
|
||||
{
|
||||
if (validationConnection) {
|
||||
qInfo() << "Cleaning up validation connection";
|
||||
disconnect(validationConnection);
|
||||
validationConnection = QMetaObject::Connection();
|
||||
}
|
||||
}
|
||||
|
||||
void TutorialController::checkValidation()
|
||||
{
|
||||
qInfo() << "checkValidation() called";
|
||||
|
||||
if (currentSequence < 0 || currentSequence >= sequences.size()) {
|
||||
return;
|
||||
}
|
||||
if (currentStep < 0 || currentStep >= sequences[currentSequence].steps.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &step = sequences[currentSequence].steps[currentStep];
|
||||
|
||||
if (step.validator) {
|
||||
bool isValid = step.validator();
|
||||
qInfo() << "Validation result:" << isValid;
|
||||
|
||||
if (isValid) {
|
||||
// Clear any validation hints
|
||||
tutorialOverlay->showValidationHint("");
|
||||
|
||||
// Auto-advance if enabled
|
||||
if (step.autoAdvanceOnValid) {
|
||||
qInfo() << "Auto-advancing to next step";
|
||||
QTimer::singleShot(500, this, &TutorialController::nextStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
#ifndef COCKATRICE_TUTORIAL_CONTROLLER_H
|
||||
#define COCKATRICE_TUTORIAL_CONTROLLER_H
|
||||
|
||||
#include "tutorial_overlay.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
#include <functional>
|
||||
|
||||
enum class ValidationTiming
|
||||
{
|
||||
OnAdvance, // Validate when user clicks next/clicks target (default)
|
||||
OnChange, // Validate whenever target widget changes (for text input)
|
||||
OnSignal, // Validate when a specific signal is emitted
|
||||
Manual // Only validate when explicitly triggered
|
||||
};
|
||||
|
||||
struct TutorialStep
|
||||
{
|
||||
QWidget *targetWidget = nullptr;
|
||||
QString text;
|
||||
std::function<void()> onEnter = nullptr;
|
||||
std::function<void()> onExit = nullptr;
|
||||
|
||||
// Interactive features
|
||||
bool requiresInteraction = false; // Must click target to advance
|
||||
bool allowClickThrough = false; // Clicks pass through to target widget
|
||||
std::function<bool()> validator = nullptr; // Check if task completed
|
||||
QString validationHint = ""; // Show if validation fails
|
||||
ValidationTiming validationTiming = ValidationTiming::OnAdvance;
|
||||
|
||||
// Auto-advance when validation passes (useful for text input)
|
||||
bool autoAdvanceOnValid = false;
|
||||
|
||||
// Custom interaction hint (overrides default "Click to continue")
|
||||
QString customInteractionHint = nullptr;
|
||||
|
||||
// Signal-based validation (for ValidationTiming::OnSignal)
|
||||
QObject *signalSource = nullptr; // Object that emits the signal
|
||||
const char *signalName = nullptr; // Signal to connect to (use SIGNAL() macro)
|
||||
};
|
||||
|
||||
struct TutorialSequence
|
||||
{
|
||||
QString name;
|
||||
QVector<TutorialStep> steps;
|
||||
|
||||
void addStep(const TutorialStep &step)
|
||||
{
|
||||
steps.append(step);
|
||||
}
|
||||
};
|
||||
|
||||
class TutorialController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TutorialController(QWidget *_tutorializedWidget);
|
||||
|
||||
void addSequence(const TutorialSequence &seq);
|
||||
void start();
|
||||
|
||||
TutorialOverlay *getOverlay()
|
||||
{
|
||||
return tutorialOverlay;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void nextStep();
|
||||
void prevStep();
|
||||
void nextSequence();
|
||||
void prevSequence();
|
||||
void exitTutorial();
|
||||
void handleTargetClicked(); // Handle clicks on highlighted widget
|
||||
void attemptAdvance(); // Try to advance with validation
|
||||
void checkValidation(); // Check validation for OnChange timing
|
||||
|
||||
private:
|
||||
void showStep();
|
||||
void updateProgress(); // Update progress indicators
|
||||
bool validateCurrentStep(); // Check if step requirements met
|
||||
void setupValidationMonitoring(); // Setup automatic validation checking
|
||||
void cleanupValidationMonitoring(); // Cleanup validation watchers
|
||||
|
||||
QWidget *tutorializedWidget;
|
||||
TutorialOverlay *tutorialOverlay;
|
||||
QVector<TutorialSequence> sequences;
|
||||
|
||||
bool tutorialCompleted = false;
|
||||
|
||||
int currentSequence = -1;
|
||||
int currentStep = -1;
|
||||
|
||||
// For OnChange validation monitoring
|
||||
QMetaObject::Connection validationConnection;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_TUTORIAL_CONTROLLER_H
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
#include "tutorial_overlay.h"
|
||||
|
||||
#include "tutorial_bubble_widget.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QEvent>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QResizeEvent>
|
||||
#include <QTextEdit>
|
||||
#include <QTimer>
|
||||
|
||||
TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_TranslucentBackground, true);
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||||
|
||||
setAttribute(Qt::WA_OpaquePaintEvent, false);
|
||||
setAutoFillBackground(false);
|
||||
|
||||
if (parent) {
|
||||
parent->installEventFilter(this);
|
||||
setGeometry(parent->rect());
|
||||
raise();
|
||||
}
|
||||
|
||||
controlBar = new QFrame(this);
|
||||
controlBar->setStyleSheet(
|
||||
"QFrame { background: rgba(30,30,30,200); border-radius: 6px; }"
|
||||
"QPushButton { padding: 6px 10px; border: 1px solid #aaa; border-radius: 4px; background:#f5f5f5; }"
|
||||
"QPushButton:hover { background:#eaeaea; }");
|
||||
|
||||
QHBoxLayout *barLayout = new QHBoxLayout(controlBar);
|
||||
barLayout->setContentsMargins(8, 4, 8, 4);
|
||||
|
||||
titleLabel = new QLabel("Tutorial", controlBar);
|
||||
titleLabel->setStyleSheet("color:white; font-weight:bold;");
|
||||
barLayout->addWidget(titleLabel);
|
||||
barLayout->addStretch();
|
||||
|
||||
auto mkBtn = [&](const QString &t, const QString &tip) {
|
||||
QPushButton *b = new QPushButton(t, controlBar);
|
||||
b->setToolTip(tip);
|
||||
return b;
|
||||
};
|
||||
|
||||
QPushButton *prevSeq = mkBtn("⏮", "Previous chapter");
|
||||
QPushButton *prev = mkBtn("◀", "Previous step");
|
||||
nextButton = mkBtn("▶", "Next step");
|
||||
nextSeqButton = mkBtn("⏭", "Next chapter");
|
||||
QPushButton *close = mkBtn("✕", "Exit tutorial");
|
||||
|
||||
barLayout->addWidget(prevSeq);
|
||||
barLayout->addWidget(prev);
|
||||
barLayout->addWidget(nextButton);
|
||||
barLayout->addWidget(nextSeqButton);
|
||||
barLayout->addWidget(close);
|
||||
|
||||
connect(prev, &QPushButton::clicked, this, &TutorialOverlay::prevStep);
|
||||
connect(nextButton, &QPushButton::clicked, this, &TutorialOverlay::nextStep);
|
||||
connect(prevSeq, &QPushButton::clicked, this, &TutorialOverlay::prevSequence);
|
||||
connect(nextSeqButton, &QPushButton::clicked, this, &TutorialOverlay::nextSequence);
|
||||
connect(close, &QPushButton::clicked, this, &TutorialOverlay::skipTutorial);
|
||||
|
||||
bubble = new BubbleWidget(this);
|
||||
bubble->hide();
|
||||
|
||||
controlBar->hide();
|
||||
}
|
||||
|
||||
void TutorialOverlay::showEvent(QShowEvent *event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
|
||||
if (parentWidget()) {
|
||||
QWidget *parent = parentWidget();
|
||||
setGeometry(0, 0, parent->width(), parent->height());
|
||||
}
|
||||
|
||||
raise();
|
||||
parentResized();
|
||||
}
|
||||
|
||||
void TutorialOverlay::setTitle(const QString &title)
|
||||
{
|
||||
titleLabel->setText(title);
|
||||
}
|
||||
|
||||
void TutorialOverlay::setBlocking(bool block)
|
||||
{
|
||||
blockInput = block;
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, !blockInput);
|
||||
}
|
||||
|
||||
void TutorialOverlay::setInteractive(bool interactive, bool clickThrough)
|
||||
{
|
||||
isInteractive = interactive;
|
||||
allowClickThrough = clickThrough;
|
||||
|
||||
if (nextButton) {
|
||||
nextButton->setEnabled(!interactive);
|
||||
if (interactive) {
|
||||
nextButton->setToolTip("Complete the highlighted action to continue");
|
||||
} else {
|
||||
nextButton->setToolTip("Next step");
|
||||
}
|
||||
}
|
||||
|
||||
if (nextSeqButton) {
|
||||
nextSeqButton->setEnabled(!interactive);
|
||||
if (interactive) {
|
||||
nextSeqButton->setToolTip("Complete the highlighted action to continue");
|
||||
} else {
|
||||
nextSeqButton->setToolTip("Next chapter");
|
||||
}
|
||||
}
|
||||
|
||||
// Update mask when clickThrough changes
|
||||
updateMask();
|
||||
}
|
||||
|
||||
void TutorialOverlay::setInteractionHint(const QString &hint)
|
||||
{
|
||||
bubble->setInteractionHint(hint);
|
||||
}
|
||||
|
||||
void TutorialOverlay::showValidationHint(const QString &hint)
|
||||
{
|
||||
if (!hint.isEmpty()) {
|
||||
bubble->setValidationHint(hint);
|
||||
}
|
||||
}
|
||||
|
||||
void TutorialOverlay::setProgress(int stepNum,
|
||||
int totalSteps,
|
||||
int overallStep,
|
||||
int overallTotal,
|
||||
const QString &sequenceTitle)
|
||||
{
|
||||
bubble->setProgress(stepNum, totalSteps, overallStep, overallTotal);
|
||||
|
||||
if (!sequenceTitle.isEmpty()) {
|
||||
titleLabel->setText(sequenceTitle);
|
||||
}
|
||||
}
|
||||
|
||||
void TutorialOverlay::setTargetWidget(QWidget *w)
|
||||
{
|
||||
if (targetWidget) {
|
||||
targetWidget->removeEventFilter(this);
|
||||
}
|
||||
|
||||
targetWidget = w;
|
||||
|
||||
if (targetWidget) {
|
||||
targetWidget->installEventFilter(this);
|
||||
}
|
||||
|
||||
recomputeLayout();
|
||||
}
|
||||
|
||||
void TutorialOverlay::setText(const QString &t)
|
||||
{
|
||||
tutorialText = t;
|
||||
bubble->setText(t);
|
||||
bubble->adjustSize();
|
||||
recomputeLayout();
|
||||
}
|
||||
|
||||
QRect TutorialOverlay::currentHoleRect() const
|
||||
{
|
||||
if (!targetWidget || !targetWidget->isVisible()) {
|
||||
return QRect();
|
||||
}
|
||||
|
||||
QPoint targetGlobal = targetWidget->mapToGlobal(QPoint(0, 0));
|
||||
QPoint targetInOverlay = mapFromGlobal(targetGlobal);
|
||||
|
||||
return QRect(targetInOverlay, targetWidget->size()).adjusted(-6, -6, 6, 6);
|
||||
}
|
||||
|
||||
void TutorialOverlay::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
QRect hole = currentHoleRect();
|
||||
|
||||
// Check if click is in the highlighted area
|
||||
if (hole.contains(event->pos())) {
|
||||
// For non-clickthrough steps, emit targetClicked for advancement
|
||||
if (!allowClickThrough && isInteractive && !qobject_cast<QLineEdit *>(targetWidget) &&
|
||||
!qobject_cast<QTextEdit *>(targetWidget) && !qobject_cast<QPlainTextEdit *>(targetWidget) &&
|
||||
!qobject_cast<QComboBox *>(targetWidget)) {
|
||||
QTimer::singleShot(100, this, [this]() { emit targetClicked(); });
|
||||
}
|
||||
// If allowClickThrough, the mask ensures events pass through
|
||||
return;
|
||||
}
|
||||
|
||||
// Click outside highlighted area - block it
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void TutorialOverlay::updateMask()
|
||||
{
|
||||
if (allowClickThrough) {
|
||||
QRect hole = currentHoleRect();
|
||||
if (!hole.isEmpty()) {
|
||||
// Create a mask that excludes the hole area
|
||||
QRegion fullRegion(rect());
|
||||
QRegion holeRegion(hole);
|
||||
QRegion maskRegion = fullRegion.subtracted(holeRegion);
|
||||
setMask(maskRegion);
|
||||
} else {
|
||||
clearMask();
|
||||
}
|
||||
} else {
|
||||
clearMask();
|
||||
}
|
||||
}
|
||||
|
||||
bool TutorialOverlay::event(QEvent *event)
|
||||
{
|
||||
// Update mask on any event that might change geometry
|
||||
if (event->type() == QEvent::Move || event->type() == QEvent::Resize) {
|
||||
updateMask();
|
||||
}
|
||||
return QWidget::event(event);
|
||||
}
|
||||
|
||||
void TutorialOverlay::resizeEvent(QResizeEvent *)
|
||||
{
|
||||
recomputeLayout();
|
||||
}
|
||||
|
||||
bool TutorialOverlay::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (obj == parentWidget() && (event->type() == QEvent::Resize || event->type() == QEvent::Move)) {
|
||||
parentResized();
|
||||
}
|
||||
|
||||
if (obj == targetWidget) {
|
||||
if (event->type() == QEvent::Show) {
|
||||
QMetaObject::invokeMethod(this, [this]() { recomputeLayout(); }, Qt::QueuedConnection);
|
||||
} else if (event->type() == QEvent::Hide || event->type() == QEvent::Move || event->type() == QEvent::Resize) {
|
||||
recomputeLayout();
|
||||
}
|
||||
}
|
||||
|
||||
return QWidget::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void TutorialOverlay::parentResized()
|
||||
{
|
||||
if (!parentWidget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGeometry(0, 0, parentWidget()->width(), parentWidget()->height());
|
||||
recomputeLayout();
|
||||
}
|
||||
|
||||
void TutorialOverlay::recomputeLayout()
|
||||
{
|
||||
QRect hole = currentHoleRect();
|
||||
|
||||
if (hole.isEmpty()) {
|
||||
if (bubble) {
|
||||
bubble->hide();
|
||||
}
|
||||
if (controlBar) {
|
||||
controlBar->hide();
|
||||
}
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
show();
|
||||
raise();
|
||||
|
||||
QSize bsize = bubble->sizeHint().expandedTo(QSize(160, 60));
|
||||
highlightBubbleRect = computeBubbleRect(hole, bsize);
|
||||
bubble->setGeometry(highlightBubbleRect);
|
||||
bubble->show();
|
||||
bubble->raise();
|
||||
|
||||
controlBar->adjustSize();
|
||||
controlBar->show();
|
||||
|
||||
const int margin = 8;
|
||||
QRect r = rect();
|
||||
|
||||
QList<QPoint> positions = {{r.right() - controlBar->width() - margin, r.bottom() - controlBar->height() - margin},
|
||||
{r.right() - controlBar->width() - margin, margin},
|
||||
{margin, r.bottom() - controlBar->height() - margin},
|
||||
{margin, margin}};
|
||||
|
||||
for (const QPoint &pos : positions) {
|
||||
QRect proposed(pos, controlBar->size());
|
||||
if (!proposed.intersects(hole)) {
|
||||
controlBar->move(pos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
controlBar->raise();
|
||||
update();
|
||||
updateMask(); // Update mask after layout changes
|
||||
}
|
||||
|
||||
QRect TutorialOverlay::computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const
|
||||
{
|
||||
const int margin = 16;
|
||||
QRect r = rect();
|
||||
QRect bubble;
|
||||
|
||||
if (hole.isEmpty()) {
|
||||
bubble = QRect(r.center() - QPoint(bubbleSize.width() / 2, bubbleSize.height() / 2), bubbleSize);
|
||||
} else {
|
||||
bubble = QRect(hole.right() + margin, hole.top(), bubbleSize.width(), bubbleSize.height());
|
||||
|
||||
if (!r.contains(bubble)) {
|
||||
bubble.moveLeft(hole.left() - margin - bubbleSize.width());
|
||||
}
|
||||
|
||||
if (!r.contains(bubble)) {
|
||||
bubble.moveLeft(hole.center().x() - bubbleSize.width() / 2);
|
||||
bubble.moveTop(hole.top() - margin - bubbleSize.height());
|
||||
}
|
||||
|
||||
if (!r.contains(bubble)) {
|
||||
bubble.moveTop(hole.bottom() + margin);
|
||||
}
|
||||
}
|
||||
|
||||
int maxLeft = qMax(r.left(), r.right() - bubble.width());
|
||||
int maxTop = qMax(r.top(), r.bottom() - bubble.height());
|
||||
|
||||
bubble.moveLeft(qBound(r.left(), bubble.left(), maxLeft));
|
||||
bubble.moveTop(qBound(r.top(), bubble.top(), maxTop));
|
||||
|
||||
return bubble;
|
||||
}
|
||||
|
||||
void TutorialOverlay::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
QRect hole = currentHoleRect();
|
||||
|
||||
if (hole.isEmpty()) {
|
||||
p.fillRect(rect(), QColor(0, 0, 0, 160));
|
||||
} else {
|
||||
QPainterPath fullPath;
|
||||
fullPath.addRect(rect());
|
||||
|
||||
QPainterPath holePath;
|
||||
holePath.addRoundedRect(hole, 8, 8);
|
||||
|
||||
QPainterPath overlayPath = fullPath.subtracted(holePath);
|
||||
p.fillPath(overlayPath, QColor(0, 0, 0, 160));
|
||||
|
||||
if (isInteractive) {
|
||||
QPen pen(QColor(100, 200, 255, 180), 2);
|
||||
pen.setStyle(Qt::DashLine);
|
||||
p.setPen(pen);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawRoundedRect(hole, 8, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
#ifndef TUTORIAL_OVERLAY_H
|
||||
#define TUTORIAL_OVERLAY_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QFrame;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class BubbleWidget;
|
||||
|
||||
class TutorialOverlay : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TutorialOverlay(QWidget *parent = nullptr);
|
||||
|
||||
void setTitle(const QString &title);
|
||||
void setBlocking(bool block);
|
||||
void setTargetWidget(QWidget *w);
|
||||
void setText(const QString &t);
|
||||
void setInteractive(bool interactive, bool clickThrough);
|
||||
void setInteractionHint(const QString &hint);
|
||||
void showValidationHint(const QString &hint);
|
||||
void setProgress(int stepNum, int totalSteps, int overallStep, int overallTotal, const QString &sequenceTitle);
|
||||
|
||||
void parentResized();
|
||||
QRect currentHoleRect() const;
|
||||
|
||||
signals:
|
||||
void nextStep();
|
||||
void prevStep();
|
||||
void nextSequence();
|
||||
void prevSequence();
|
||||
void skipTutorial();
|
||||
void targetClicked();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
bool event(QEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void updateMask();
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
private:
|
||||
void recomputeLayout();
|
||||
QRect computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const;
|
||||
|
||||
QWidget *targetWidget = nullptr;
|
||||
QFrame *controlBar = nullptr;
|
||||
QLabel *titleLabel = nullptr;
|
||||
QPushButton *nextButton = nullptr;
|
||||
QPushButton *nextSeqButton = nullptr;
|
||||
BubbleWidget *bubble = nullptr;
|
||||
|
||||
QString tutorialText;
|
||||
QRect highlightBubbleRect;
|
||||
bool blockInput = true;
|
||||
bool isInteractive = false;
|
||||
bool allowClickThrough = false;
|
||||
};
|
||||
|
||||
#endif // TUTORIAL_OVERLAY_H
|
||||
Loading…
Add table
Add a link
Reference in a new issue