Improve with sequencing and better rendering.

Took 3 minutes


Took 18 seconds
This commit is contained in:
Lukas Brübach
2025-12-07 22:02:44 +01:00
parent fe61f5c7d4
commit fdb547c0e2
3 changed files with 160 additions and 37 deletions

View File

@@ -1,70 +1,168 @@
#include "tutorial_controller.h"
#include <QTimer>
TutorialController::TutorialController(QWidget *_tutorializedWidget)
: QObject(_tutorializedWidget), tutorializedWidget(_tutorializedWidget)
{
tutorialOverlay = new TutorialOverlay(nullptr); // nullptr parent because we overlay this on top of *everything*
// Stay above everything
tutorialOverlay->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
tutorialOverlay = new TutorialOverlay(tutorializedWidget->window());
// Make it frameless + translucent
tutorialOverlay->setWindowFlags(tutorialOverlay->windowFlags() | Qt::FramelessWindowHint);
tutorialOverlay->setAttribute(Qt::WA_TranslucentBackground);
// Hide until started
// hide until start
tutorialOverlay->hide();
connect(tutorialOverlay, &TutorialOverlay::nextStep, this, &TutorialController::progressTutorial);
connect(tutorialOverlay, &TutorialOverlay::nextStep, this, &TutorialController::nextStep);
connect(tutorialOverlay, &TutorialOverlay::skipTutorial, this, [this]() { tutorialOverlay->hide(); });
}
void TutorialController::addStep(const TutorialStep &step)
void TutorialController::addSequence(const TutorialSequence &seq)
{
steps.append(step);
sequences.append(seq);
}
void TutorialController::start()
{
if (steps.isEmpty()) {
if (sequences.isEmpty()) {
return;
}
// Align ourselves and show
QPoint topLeft = tutorializedWidget->mapToGlobal(QPoint(0, 0));
QSize windowSize = tutorializedWidget->size();
QTimer::singleShot(0, this, [this]() {
QWidget *win = tutorializedWidget->window();
tutorialOverlay->parentResized();
tutorialOverlay->setGeometry(QRect(QPoint(0, 0), win->size()));
tutorialOverlay->show();
tutorialOverlay->raise();
tutorialOverlay->parentResized();
tutorialOverlay->setGeometry(QRect(topLeft, windowSize));
tutorialOverlay->show();
tutorialOverlay->raise();
currentSequence = 0;
currentStep = 0;
showStep();
});
}
void TutorialController::nextStep()
{
// advance within sequence
currentStep++;
if (currentSequence < 0) {
return; // defensive in case we haven't started yet
}
if (currentStep >= sequences[currentSequence].steps.size()) {
// advance to next sequence
nextSequence();
return;
}
// Start the tutorial
currentStep = 0;
showStep();
}
void TutorialController::progressTutorial()
void TutorialController::prevStep()
{
currentStep++;
if (currentStep >= steps.size()) {
tutorialOverlay->hide();
if (currentSequence < 0) {
return; // defensive in case we haven't started yet
}
if (currentStep == 0) {
prevSequence();
return;
}
currentStep--;
showStep();
}
void TutorialController::nextSequence()
{
if (currentSequence < 0) {
return;
}
// run exit for the last step of the current sequence (showStep handles previous onExit,
// but ensure we run it here because we're jumping sequence)
// We'll increment sequence and then call showStep which will call the onEnter for the new step.
currentSequence++;
currentStep = 0;
if (currentSequence >= sequences.size()) {
exitTutorial();
return;
}
showStep();
}
void TutorialController::prevSequence()
{
if (currentSequence <= 0) {
// already at first sequence -> stay
return;
}
// go to last step of previous sequence
currentSequence--;
currentStep = sequences[currentSequence].steps.size() - 1;
showStep();
}
void TutorialController::exitTutorial()
{
// Run onExit for the current step if present
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();
}
}
tutorialOverlay->hide();
// reset indices so start() can be called again cleanly
currentSequence = -1;
currentStep = -1;
}
void TutorialController::showStep()
{
const auto &step = steps[currentStep];
// bounds checks
if (currentSequence < 0 || currentSequence >= sequences.size()) {
return;
}
const auto &seq = sequences[currentSequence];
if (currentStep < 0 || currentStep >= seq.steps.size()) {
return;
}
// Run any action associated with previous steps
if (currentStep < 0) {
const auto &previousStep = steps[currentStep - 1];
// run onExit for the previous step (including if previous step was in previous sequence)
if (!(currentSequence == 0 && currentStep == 0)) {
int prevSeq = currentSequence;
int prevStepIndex = currentStep - 1;
if (prevStepIndex < 0) {
// previous is last step of previous sequence
prevSeq = currentSequence - 1;
if (prevSeq >= 0) {
prevStepIndex = sequences[prevSeq].steps.size() - 1;
} else {
prevStepIndex = -1;
}
}
if (previousStep.onExit) {
previousStep.onExit();
if (prevSeq >= 0 && prevStepIndex >= 0) {
const auto &previousStep = sequences[prevSeq].steps[prevStepIndex];
if (previousStep.onExit) {
previousStep.onExit();
}
}
}
// current step
const auto &step = seq.steps[currentStep];
// Run any action associated with this step
if (step.onEnter) {
step.onEnter();

View File

@@ -11,8 +11,19 @@ struct TutorialStep
{
QWidget *targetWidget;
QString text;
std::function<void()> onEnter; // Optional function to run when this step starts
std::function<void()> onExit; // Optional function to run when step ends
std::function<void()> onEnter = nullptr; // Optional function to run when this step starts
std::function<void()> onExit = nullptr; // Optional function to run when step ends
};
struct TutorialSequence
{
QString name;
QVector<TutorialStep> steps;
void addStep(const TutorialStep &step)
{
steps.append(step);
}
};
class TutorialController : public QObject
@@ -20,17 +31,22 @@ class TutorialController : public QObject
Q_OBJECT
public slots:
void progressTutorial();
void start();
void nextStep();
void prevStep();
void nextSequence();
void prevSequence();
void exitTutorial();
public:
explicit TutorialController(QWidget *_tutorializedWidget);
void addStep(const TutorialStep &step);
void start();
void addSequence(const TutorialSequence &step);
private:
QWidget *tutorializedWidget;
QVector<TutorialStep> steps;
QVector<TutorialSequence> sequences;
int currentSequence = -1;
int currentStep = -1;
TutorialOverlay *tutorialOverlay;

View File

@@ -7,7 +7,7 @@
#include <QPushButton>
#include <QVBoxLayout>
TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent)
TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent, Qt::Window)
{
setAttribute(Qt::WA_TransparentForMouseEvents, false);
setAttribute(Qt::WA_NoSystemBackground, true);
@@ -16,7 +16,13 @@ TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent)
// This ensures the overlay stays exactly over the parent
if (parent) {
setGeometry(parent->rect());
QRect r = parent->rect();
// convert the parents rect to screen coordinates
QPoint globalTopLeft = parent->mapToGlobal(QPoint(0, 0));
r.moveTopLeft(globalTopLeft);
setGeometry(r);
}
}
@@ -99,7 +105,10 @@ QRect TutorialOverlay::computeBubbleRect(const QRect &hole) const
void TutorialOverlay::parentResized()
{
if (parentWidget()) {
setGeometry(parentWidget()->rect());
QRect r = parentWidget()->rect();
QPoint globalTopLeft = parentWidget()->mapToGlobal(QPoint(0, 0));
r.moveTopLeft(globalTopLeft);
setGeometry(r);
}
}