From c31face8dd735df70fb0de67da965328be31673d Mon Sep 17 00:00:00 2001
From: jose-rZM <100773386+jose-rZM@users.noreply.github.com>
Date: Fri, 12 Jun 2026 19:36:16 +0200
Subject: [PATCH 1/3] feat: add ll guided mode
---
SyntaxTutor.pro | 2 +
src/gui/llwizard.h | 240 +++++++++++++++++++++++++++++++++++++++++
src/gui/llwizardpage.h | 156 +++++++++++++++++++++++++++
3 files changed, 398 insertions(+)
create mode 100644 src/gui/llwizard.h
create mode 100644 src/gui/llwizardpage.h
diff --git a/SyntaxTutor.pro b/SyntaxTutor.pro
index 4e25def..96e50ac 100644
--- a/SyntaxTutor.pro
+++ b/SyntaxTutor.pro
@@ -61,6 +61,8 @@ HEADERS += \
src/gui/grammareditordialog.h \
src/gui/lltabledialog.h \
src/gui/lltutorwindow.h \
+ src/gui/llwizard.h \
+ src/gui/llwizardpage.h \
src/gui/mainwindow.h \
src/gui/slrtabledialog.h \
src/gui/slrtutorwindow.h \
diff --git a/src/gui/llwizard.h b/src/gui/llwizard.h
new file mode 100644
index 0000000..8f89809
--- /dev/null
+++ b/src/gui/llwizard.h
@@ -0,0 +1,240 @@
+/*
+ * SyntaxTutor - Interactive Tutorial About Syntax Analyzers
+ * Copyright (C) 2025 Jose R. (jose-rzm)
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef LLWIZARD_H
+#define LLWIZARD_H
+
+#include "ll1_parser.hpp"
+#include "llwizardpage.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/**
+ * @class LLWizard
+ * @brief Interactive assistant that guides the student step-by-step through
+ * the LL(1) parsing table.
+ *
+ * This dialog presents one non-empty cell of the LL(1) table at a time,
+ * asking the user to deduce the production that belongs in it from the
+ * prediction symbols (símbolos directores): the cell (A, b) holds the
+ * production A → α with b ∈ SD(A → α). It mirrors the SLR(1) guided dialog
+ * and intentionally avoids the native wizard widget to keep a consistent
+ * look across platforms.
+ */
+class LLWizard : public QDialog {
+ Q_OBJECT
+ public:
+ /**
+ * @brief Constructs the LL(1) guided dialog from the computed table.
+ *
+ * @param parser The LL(1) parser instance holding the reference table.
+ * @param rowHeaders Non-terminal symbols in display order.
+ * @param colHeaders Terminal symbols in display order.
+ * @param parent Parent widget.
+ */
+ LLWizard(LL1Parser& parser, const QStringList& rowHeaders,
+ const QStringList& colHeaders, QWidget* parent = nullptr)
+ : QDialog(parent) {
+ setProperty("guidedDialog", "ll");
+ setWindowTitle(tr("Completar tabla LL(1)"));
+ setWindowFlag(Qt::WindowCloseButtonHint, true);
+ setModal(true);
+ resize(640, 440);
+ setMinimumSize(560, 400);
+
+ auto* rootLayout = new QVBoxLayout(this);
+ rootLayout->setContentsMargins(0, 0, 0, 0);
+ rootLayout->setSpacing(0);
+
+ auto* panel = new QFrame(this);
+ panel->setObjectName("llWizardPanel");
+
+ auto* panelLayout = new QVBoxLayout(panel);
+ panelLayout->setContentsMargins(28, 24, 28, 24);
+ panelLayout->setSpacing(20);
+
+ auto* headerLayout = new QHBoxLayout();
+ headerLayout->setSpacing(12);
+
+ auto* titleColumn = new QVBoxLayout();
+ titleColumn->setContentsMargins(0, 0, 0, 0);
+ titleColumn->setSpacing(6);
+
+ m_titleLabel = new QLabel(panel);
+ m_titleLabel->setObjectName("llWizardTitle");
+ titleColumn->addWidget(m_titleLabel);
+
+ m_stepCounterLabel = new QLabel(panel);
+ m_stepCounterLabel->setObjectName("llWizardStepCounter");
+ titleColumn->addWidget(m_stepCounterLabel);
+
+ m_progressBar = new QProgressBar(panel);
+ m_progressBar->setObjectName("llWizardProgress");
+ m_progressBar->setTextVisible(false);
+ m_progressBar->setRange(0, 100);
+ m_progressBar->setFixedHeight(4);
+ titleColumn->addWidget(m_progressBar);
+
+ headerLayout->addLayout(titleColumn, 1);
+
+ m_closeButton = new QPushButton(tr("Salir"), panel);
+ m_closeButton->setObjectName("llWizardCloseButton");
+ m_closeButton->setAutoDefault(false);
+ m_closeButton->setDefault(false);
+ m_closeButton->setCursor(Qt::PointingHandCursor);
+ headerLayout->addWidget(m_closeButton, 0, Qt::AlignTop);
+
+ panelLayout->addLayout(headerLayout);
+
+ m_stack = new QStackedWidget(panel);
+ m_stack->setObjectName("llWizardStack");
+ panelLayout->addWidget(m_stack, 1);
+
+ auto* footerLayout = new QHBoxLayout();
+ footerLayout->setContentsMargins(0, 0, 0, 0);
+ footerLayout->setSpacing(12);
+ footerLayout->addStretch(1);
+
+ m_nextButton = new QPushButton(panel);
+ m_nextButton->setObjectName("llWizardNextButton");
+ m_nextButton->setProperty("role", "primary");
+ m_nextButton->setAutoDefault(false);
+ m_nextButton->setDefault(false);
+ footerLayout->addWidget(m_nextButton);
+ panelLayout->addLayout(footerLayout);
+
+ rootLayout->addWidget(panel);
+
+ for (const QString& nonTerminal : rowHeaders) {
+ const auto rowIt = parser.ll1_t_.find(nonTerminal.toStdString());
+ if (rowIt == parser.ll1_t_.end()) {
+ continue;
+ }
+ for (const QString& symbol : colHeaders) {
+ const auto cellIt = rowIt->second.find(symbol.toStdString());
+ if (cellIt == rowIt->second.end() || cellIt->second.empty() ||
+ cellIt->second[0].empty()) {
+ continue;
+ }
+
+ QStringList production;
+ for (const std::string& s : cellIt->second[0]) {
+ production.append(QString::fromStdString(s));
+ }
+ const QString expected = production.join(' ');
+
+ const QString explanation =
+ tr("La celda (%1, '%2') no está vacía: existe una "
+ "producción %1 → α tal que '%2' ∈ SD(%1 → α).\n"
+ "¿Qué producción α escribes en esta celda?")
+ .arg(nonTerminal, symbol);
+
+ auto* page = new LLWizardPage(nonTerminal, symbol, explanation,
+ expected, m_stack);
+ connect(page, &LLWizardPage::completionChanged, this,
+ &LLWizard::updateCurrentPage);
+ connect(page, &LLWizardPage::submitRequested, this,
+ &LLWizard::advance);
+ m_stack->addWidget(page);
+ }
+ }
+
+ connect(m_closeButton, &QPushButton::clicked, this, &QDialog::reject);
+ connect(m_nextButton, &QPushButton::clicked, this, &LLWizard::advance);
+
+ updateCurrentPage();
+ QTimer::singleShot(0, this, [this]() {
+ if (auto* page = currentPage()) {
+ page->focusAnswerField();
+ }
+ });
+ }
+
+ LLWizardPage* currentPage() const {
+ return qobject_cast(m_stack->currentWidget());
+ }
+
+ QPushButton* nextButton() const { return m_nextButton; }
+ QPushButton* closeButton() const { return m_closeButton; }
+
+ bool isOnLastPage() const {
+ return m_stack->count() > 0 &&
+ m_stack->currentIndex() == m_stack->count() - 1;
+ }
+
+ private slots:
+ void advance() {
+ auto* page = currentPage();
+ if (page == nullptr || !page->isComplete()) {
+ return;
+ }
+
+ if (isOnLastPage()) {
+ accept();
+ return;
+ }
+
+ m_stack->setCurrentIndex(m_stack->currentIndex() + 1);
+ updateCurrentPage();
+ if (auto* nextPage = currentPage()) {
+ nextPage->focusAnswerField();
+ }
+ }
+
+ void updateCurrentPage() {
+ auto* page = currentPage();
+ if (page == nullptr) {
+ m_titleLabel->setText(tr("Modo guiado"));
+ m_stepCounterLabel->clear();
+ m_progressBar->setValue(0);
+ m_nextButton->setEnabled(false);
+ m_nextButton->setText(tr("Finalizar"));
+ return;
+ }
+
+ const int currentStep = m_stack->currentIndex() + 1;
+ const int totalSteps = m_stack->count();
+ m_titleLabel->setText(page->titleText());
+ m_stepCounterLabel->setText(
+ tr("Paso %1 de %2").arg(currentStep).arg(totalSteps));
+ m_progressBar->setValue((100 * currentStep) / qMax(1, totalSteps));
+ m_nextButton->setEnabled(page->isComplete());
+ m_nextButton->setCursor(page->isComplete() ? Qt::PointingHandCursor
+ : Qt::ArrowCursor);
+ m_nextButton->setText(isOnLastPage() ? tr("Finalizar")
+ : tr("Continuar"));
+ }
+
+ private:
+ QLabel* m_titleLabel = nullptr;
+ QLabel* m_stepCounterLabel = nullptr;
+ QProgressBar* m_progressBar = nullptr;
+ QPushButton* m_closeButton = nullptr;
+ QStackedWidget* m_stack = nullptr;
+ QPushButton* m_nextButton = nullptr;
+};
+
+#endif // LLWIZARD_H
diff --git a/src/gui/llwizardpage.h b/src/gui/llwizardpage.h
new file mode 100644
index 0000000..d4006ff
--- /dev/null
+++ b/src/gui/llwizardpage.h
@@ -0,0 +1,156 @@
+/*
+ * SyntaxTutor - Interactive Tutorial About Syntax Analyzers
+ * Copyright (C) 2025 Jose R. (jose-rzm)
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef LLWIZARDPAGE_H
+#define LLWIZARDPAGE_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+/**
+ * @class LLWizardPage
+ * @brief A single guided step for constructing the LL(1) table.
+ *
+ * This widget presents one (non-terminal, terminal) cell at a time with a
+ * compact explanation card and an answer card. The expected answer is the
+ * production that belongs in the cell; the input is validated ignoring
+ * whitespace so "a B", "a B" and "aB" are all accepted. It emits a signal
+ * whenever the completion state changes.
+ */
+class LLWizardPage : public QWidget {
+ Q_OBJECT
+ public:
+ /**
+ * @brief Constructs a page for a specific cell in the LL(1) table.
+ *
+ * @param nonTerminal The non-terminal symbol (row header).
+ * @param symbol The terminal symbol (column header).
+ * @param explanation A pedagogical explanation shown to the user.
+ * @param expected The expected production, space-separated (e.g. "a B"
+ * or "EPSILON").
+ * @param parent The parent widget.
+ */
+ LLWizardPage(const QString& nonTerminal, const QString& symbol,
+ const QString& explanation, const QString& expected,
+ QWidget* parent = nullptr)
+ : QWidget(parent),
+ m_title(tr("Fila %1, columna '%2'").arg(nonTerminal).arg(symbol)),
+ m_expected(expected) {
+ auto* rootLayout = new QVBoxLayout(this);
+ rootLayout->setContentsMargins(0, 0, 0, 0);
+ rootLayout->setSpacing(22);
+
+ auto* explanationTitle = new QLabel(tr("Pista"), this);
+ explanationTitle->setObjectName("llWizardSectionTitle");
+ rootLayout->addWidget(explanationTitle);
+
+ auto* explanationLabel = new QLabel(explanation, this);
+ explanationLabel->setObjectName("llWizardExplanationLabel");
+ explanationLabel->setWordWrap(true);
+ rootLayout->addWidget(explanationLabel);
+
+ auto* answerTitle = new QLabel(tr("Tu respuesta"), this);
+ answerTitle->setObjectName("llWizardSectionTitle");
+ rootLayout->addWidget(answerTitle);
+
+ m_edit = new QLineEdit(this);
+ m_edit->setObjectName("llWizardAnswerEdit");
+ m_edit->setMinimumHeight(48);
+ m_edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+ m_edit->setPlaceholderText(
+ tr("Escribe la producción (EPSILON si es vacía)"));
+ rootLayout->addWidget(m_edit);
+
+ m_feedback = new QLabel(this);
+ m_feedback->setObjectName("llWizardFeedbackLabel");
+ m_feedback->setWordWrap(true);
+ m_feedback->setMinimumHeight(40);
+ m_feedback->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
+ rootLayout->addWidget(m_feedback);
+
+ rootLayout->addStretch(1);
+
+ connect(m_edit, &QLineEdit::textChanged, this,
+ &LLWizardPage::onTextChanged);
+ connect(m_edit, &QLineEdit::returnPressed, this,
+ &LLWizardPage::submitRequested);
+ }
+
+ QString titleText() const { return m_title; }
+ bool isComplete() const { return m_isComplete; }
+ void focusAnswerField() { m_edit->setFocus(); }
+
+#ifdef SYNTAXTUTOR_TESTING
+ QString expectedForTest() const { return m_expected; }
+#endif
+
+ signals:
+ void completionChanged(bool complete);
+ void submitRequested();
+
+ private slots:
+ /**
+ * @brief Checks the user's input and updates inline feedback.
+ *
+ * The comparison ignores whitespace, matching the tolerant parsing the
+ * table cells already accept.
+ *
+ * @param text The current user input.
+ */
+ void onTextChanged(const QString& text) {
+ const QString trimmed = text.trimmed();
+ const bool correct = (compact(trimmed) == compact(m_expected));
+
+ if (trimmed.isEmpty()) {
+ m_feedback->clear();
+ } else if (correct) {
+ m_feedback->setText(
+ tr("✔ Respuesta correcta, pasa a la siguiente pregunta"));
+ } else {
+ m_feedback->setText(tr("✘ Incorrecto, repasa los símbolos "
+ "directores (SD) en el panel del tutor."));
+ }
+
+ if (m_isComplete == correct) {
+ return;
+ }
+
+ m_isComplete = correct;
+ emit completionChanged(m_isComplete);
+ }
+
+ private:
+ static QString compact(const QString& text) {
+ QString result = text;
+ result.remove(QRegularExpression("\\s+"));
+ return result;
+ }
+
+ QString m_title; ///< Header title for this guided step.
+ QString m_expected; ///< Expected production, space-separated.
+ QLabel* m_feedback; ///< Inline feedback label for answer validation.
+ QLineEdit* m_edit; ///< Input field for the user's answer.
+ bool m_isComplete =
+ false; ///< Whether the user has entered the correct response.
+};
+
+#endif // LLWIZARDPAGE_H
From b5f5049d055002385d1800921e1f5a18f8225cea Mon Sep 17 00:00:00 2001
From: jose-rZM <100773386+jose-rZM@users.noreply.github.com>
Date: Fri, 12 Jun 2026 19:36:50 +0200
Subject: [PATCH 2/3] feat: add guided mode
---
resources/styles/app.qss | 48 ++++++++++++++++++++++++++-------------
src/gui/lltabledialog.cpp | 29 ++++++++++++++++++++++-
src/gui/lltabledialog.h | 23 +++++++++++++++++++
src/gui/lltutorwindow.cpp | 33 +++++++++++++++++++++++++++
src/gui/lltutorwindow.h | 13 +++++++++++
5 files changed, 129 insertions(+), 17 deletions(-)
diff --git a/resources/styles/app.qss b/resources/styles/app.qss
index 10ec484..4a8eae1 100644
--- a/resources/styles/app.qss
+++ b/resources/styles/app.qss
@@ -519,41 +519,48 @@ QTextBrowser#infoDialogContent a {
color: #36C5CC;
}
-QDialog[guidedDialog="slr"] {
+QDialog[guidedDialog="slr"],
+QDialog[guidedDialog="ll"] {
background-color: #1F1F1F;
border: none;
}
-QDialog[guidedDialog="slr"] QFrame#slrWizardPanel {
+QDialog[guidedDialog="slr"] QFrame#slrWizardPanel,
+QDialog[guidedDialog="ll"] QFrame#llWizardPanel {
background: transparent;
border: none;
border-radius: 0;
}
-QDialog[guidedDialog="slr"] QLabel#slrWizardTitle {
+QDialog[guidedDialog="slr"] QLabel#slrWizardTitle,
+QDialog[guidedDialog="ll"] QLabel#llWizardTitle {
color: #F4F7F8;
font-size: 28px;
font-weight: 700;
}
-QDialog[guidedDialog="slr"] QLabel#slrWizardStepCounter {
+QDialog[guidedDialog="slr"] QLabel#slrWizardStepCounter,
+QDialog[guidedDialog="ll"] QLabel#llWizardStepCounter {
color: #9AA5A8;
font-size: 12px;
font-weight: 600;
}
-QDialog[guidedDialog="slr"] QProgressBar#slrWizardProgress {
+QDialog[guidedDialog="slr"] QProgressBar#slrWizardProgress,
+QDialog[guidedDialog="ll"] QProgressBar#llWizardProgress {
background-color: rgba(255, 255, 255, 0.05);
border: none;
border-radius: 2px;
}
-QDialog[guidedDialog="slr"] QProgressBar#slrWizardProgress::chunk {
+QDialog[guidedDialog="slr"] QProgressBar#slrWizardProgress::chunk,
+QDialog[guidedDialog="ll"] QProgressBar#llWizardProgress::chunk {
background-color: #11B3BC;
border-radius: 2px;
}
-QDialog[guidedDialog="slr"] QPushButton#slrWizardCloseButton {
+QDialog[guidedDialog="slr"] QPushButton#slrWizardCloseButton,
+QDialog[guidedDialog="ll"] QPushButton#llWizardCloseButton {
background: transparent;
color: #AAB5B8;
border: 1px solid rgba(255, 255, 255, 0.08);
@@ -563,17 +570,20 @@ QDialog[guidedDialog="slr"] QPushButton#slrWizardCloseButton {
font-weight: 600;
}
-QDialog[guidedDialog="slr"] QPushButton#slrWizardCloseButton:hover {
+QDialog[guidedDialog="slr"] QPushButton#slrWizardCloseButton:hover,
+QDialog[guidedDialog="ll"] QPushButton#llWizardCloseButton:hover {
background-color: rgba(255, 255, 255, 0.03);
color: #FFFFFF;
border: 1px solid rgba(255, 255, 255, 0.12);
}
-QDialog[guidedDialog="slr"] QPushButton#slrWizardCloseButton:pressed {
+QDialog[guidedDialog="slr"] QPushButton#slrWizardCloseButton:pressed,
+QDialog[guidedDialog="ll"] QPushButton#llWizardCloseButton:pressed {
background-color: rgba(255, 255, 255, 0.06);
}
-QDialog[guidedDialog="slr"] QLabel#slrWizardSectionTitle {
+QDialog[guidedDialog="slr"] QLabel#slrWizardSectionTitle,
+QDialog[guidedDialog="ll"] QLabel#llWizardSectionTitle {
color: #9FCFD2;
font-size: 12px;
font-weight: 700;
@@ -581,13 +591,15 @@ QDialog[guidedDialog="slr"] QLabel#slrWizardSectionTitle {
text-transform: uppercase;
}
-QDialog[guidedDialog="slr"] QLabel#slrWizardExplanationLabel {
+QDialog[guidedDialog="slr"] QLabel#slrWizardExplanationLabel,
+QDialog[guidedDialog="ll"] QLabel#llWizardExplanationLabel {
color: #DEE5E7;
font-size: 15px;
line-height: 1.5;
}
-QDialog[guidedDialog="slr"] QLineEdit#slrWizardAnswerEdit {
+QDialog[guidedDialog="slr"] QLineEdit#slrWizardAnswerEdit,
+QDialog[guidedDialog="ll"] QLineEdit#llWizardAnswerEdit {
background-color: #2B3133;
color: #F1F4F5;
border: 1px solid rgba(255, 255, 255, 0.10);
@@ -597,23 +609,27 @@ QDialog[guidedDialog="slr"] QLineEdit#slrWizardAnswerEdit {
selection-background-color: #35515A;
}
-QDialog[guidedDialog="slr"] QLineEdit#slrWizardAnswerEdit:focus {
+QDialog[guidedDialog="slr"] QLineEdit#slrWizardAnswerEdit:focus,
+QDialog[guidedDialog="ll"] QLineEdit#llWizardAnswerEdit:focus {
border: 1px solid #11B3BC;
}
-QDialog[guidedDialog="slr"] QLabel#slrWizardFeedbackLabel {
+QDialog[guidedDialog="slr"] QLabel#slrWizardFeedbackLabel,
+QDialog[guidedDialog="ll"] QLabel#llWizardFeedbackLabel {
color: #AEB9BC;
min-height: 40px;
padding-top: 2px;
}
-QDialog[guidedDialog="slr"] QPushButton#slrWizardNextButton {
+QDialog[guidedDialog="slr"] QPushButton#slrWizardNextButton,
+QDialog[guidedDialog="ll"] QPushButton#llWizardNextButton {
min-width: 136px;
padding: 11px 18px;
border-radius: 10px;
}
-QDialog[guidedDialog="slr"] QPushButton#slrWizardNextButton:disabled {
+QDialog[guidedDialog="slr"] QPushButton#slrWizardNextButton:disabled,
+QDialog[guidedDialog="ll"] QPushButton#llWizardNextButton:disabled {
background-color: #2A2E30;
color: #6F7A7E;
border: 1px solid #353D40;
diff --git a/src/gui/lltabledialog.cpp b/src/gui/lltabledialog.cpp
index aa7ddcd..c18ebd4 100644
--- a/src/gui/lltabledialog.cpp
+++ b/src/gui/lltabledialog.cpp
@@ -69,9 +69,22 @@ LLTableDialog::LLTableDialog(const QStringList& rowHeaders,
submitButton->setCursor(Qt::PointingHandCursor);
submitButton->setProperty("role", "primary");
+ guidedButton = new QPushButton(tr("Modo guiado"), this);
+ guidedButton->setObjectName("llTableGuidedButton");
+ guidedButton->setCursor(Qt::PointingHandCursor);
+ guidedButton->setProperty("role", "primary");
+
QVBoxLayout* layout = new QVBoxLayout;
layout->addWidget(table);
- layout->addWidget(submitButton);
+
+ auto* buttons = new QHBoxLayout;
+ buttons->setContentsMargins(0, 0, 0, 0);
+ buttons->setSpacing(10);
+ buttons->addStretch();
+ buttons->addWidget(guidedButton);
+ buttons->addWidget(submitButton);
+
+ layout->addLayout(buttons);
layout->setContentsMargins(10, 10, 10, 10);
setLayout(layout);
@@ -104,6 +117,20 @@ LLTableDialog::LLTableDialog(const QStringList& rowHeaders,
commitPendingEdit();
emit submitted(getTableData());
});
+ connect(guidedButton, &QPushButton::clicked, this,
+ [this]() {
+ commitPendingEdit();
+ emit guidedRequested(getTableData());
+ });
+}
+
+void LLTableDialog::setGuidedModeActive(bool active) {
+ guidedButton->setEnabled(!active);
+ submitButton->setEnabled(!active);
+}
+
+void LLTableDialog::setGuidedButtonVisible(bool visible) {
+ guidedButton->setVisible(visible);
}
void LLTableDialog::commitPendingEdit() {
diff --git a/src/gui/lltabledialog.h b/src/gui/lltabledialog.h
index 8efd7e6..543d81a 100644
--- a/src/gui/lltabledialog.h
+++ b/src/gui/lltabledialog.h
@@ -75,6 +75,22 @@ class LLTableDialog : public QDialog {
*/
void highlightIncorrectCells(const QList>& coords);
+ /**
+ * @brief Enables or disables the dialog buttons while the guided mode
+ * dialog is open.
+ * @param active Whether the guided mode is currently active.
+ */
+ void setGuidedModeActive(bool active);
+
+ /**
+ * @brief Shows or hides the guided mode button.
+ *
+ * Exam mode hides it: the guided walkthrough would reveal the answers.
+ *
+ * @param visible Whether the button should be visible.
+ */
+ void setGuidedButtonVisible(bool visible);
+
signals:
/**
* @brief Signal emitted when the user submits the table.
@@ -82,11 +98,18 @@ class LLTableDialog : public QDialog {
*/
void submitted(const QVector>& data);
+ /**
+ * @brief Signal emitted when the user asks for the guided mode.
+ * @param data The current table data, restored when the wizard closes.
+ */
+ void guidedRequested(const QVector>& data);
+
private:
void commitPendingEdit();
QTableWidget* table; ///< The widget representing the LL(1) parsing table.
QPushButton* submitButton; ///< Button to submit the completed table.
+ QPushButton* guidedButton; ///< Button to launch guided mode.
};
#endif // LLTABLEDIALOG_H
diff --git a/src/gui/lltutorwindow.cpp b/src/gui/lltutorwindow.cpp
index 36284b5..5b17918 100644
--- a/src/gui/lltutorwindow.cpp
+++ b/src/gui/lltutorwindow.cpp
@@ -19,6 +19,7 @@
#include "lltutorwindow.h"
#include "examreportdialog.h"
#include "grammarview.h"
+#include "llwizard.h"
#include "tutorialmanager.h"
#include "ui_lltutorwindow.h"
#include
@@ -611,6 +612,26 @@ void LLTutorWindow::addMessage(const QString& text, bool isUser) {
ui->listWidget->scrollToBottom();
}
+void LLTutorWindow::connectGuidedMode(LLTableDialog* dialog,
+ const QStringList& colHeaders) {
+ connect(dialog, &LLTableDialog::guidedRequested, this,
+ [this, dialog, colHeaders](const QVector>& data) {
+ const QVector> snapshot = data;
+ auto* wizard =
+ new LLWizard(ll1, sortedNonTerminals, colHeaders, dialog);
+ wizard->setAttribute(Qt::WA_DeleteOnClose);
+ wizard->setWindowModality(Qt::WindowModal);
+ dialog->setGuidedModeActive(true);
+
+ connect(wizard, &QDialog::finished, dialog,
+ [dialog, snapshot](int) {
+ dialog->setGuidedModeActive(false);
+ dialog->setInitialData(snapshot);
+ });
+ wizard->show();
+ });
+}
+
void LLTutorWindow::showTableForCPrime() {
QStringList colHeaders;
@@ -623,6 +644,12 @@ void LLTutorWindow::showTableForCPrime() {
colHeaders.sort();
auto* dialog =
new LLTableDialog(sortedNonTerminals, colHeaders, this, &rawTable);
+ if (examMode) {
+ // The guided walkthrough reveals the answers; not during an exam.
+ dialog->setGuidedButtonVisible(false);
+ }
+
+ connectGuidedMode(dialog, colHeaders);
connect(dialog, &LLTableDialog::submitted, this,
[this, dialog, colHeaders](const QVector>& data) {
@@ -676,6 +703,12 @@ void LLTutorWindow::showTable() {
auto* dialog =
new LLTableDialog(sortedNonTerminals, colHeaders, this, &rawTable);
currentDlg = dialog;
+ if (examMode) {
+ // The guided walkthrough reveals the answers; not during an exam.
+ dialog->setGuidedButtonVisible(false);
+ }
+
+ connectGuidedMode(dialog, colHeaders);
connect(dialog, &LLTableDialog::submitted, this,
[this, colHeaders](const QVector>& data) {
diff --git a/src/gui/lltutorwindow.h b/src/gui/lltutorwindow.h
index 9d021f2..9541e84 100644
--- a/src/gui/lltutorwindow.h
+++ b/src/gui/lltutorwindow.h
@@ -206,6 +206,19 @@ class LLTutorWindow : public QWidget {
void handleTableSubmission(const QVector>& raw,
const QStringList& colHeaders);
+
+ /**
+ * @brief Hooks the table dialog's guided mode button to the LL wizard.
+ *
+ * Opens the step-by-step LL(1) table assistant when requested, freezing
+ * the table dialog while it is active and restoring the user's snapshot
+ * when it closes.
+ *
+ * @param dialog Table dialog to connect.
+ * @param colHeaders Terminal symbols in the dialog's column order.
+ */
+ void connectGuidedMode(LLTableDialog* dialog,
+ const QStringList& colHeaders);
void updatePlaceholder();
bool confirmExitToHome();
QString promptExportFilePath() const;
From e9dc8b4db6f7da41e13adf5f691566226a1cc988 Mon Sep 17 00:00:00 2001
From: jose-rZM <100773386+jose-rZM@users.noreply.github.com>
Date: Fri, 12 Jun 2026 19:37:07 +0200
Subject: [PATCH 3/3] test: add ll guided mode tests
---
tests/helpers/qt_modal_test_utils.h | 11 ++
tests/tests.pro | 2 +
tests/tutor/ll_tutor_window_test.cpp | 257 +++++++++++++++++++++++++++
tests/tutor/tutor_window_test.h | 3 +
4 files changed, 273 insertions(+)
diff --git a/tests/helpers/qt_modal_test_utils.h b/tests/helpers/qt_modal_test_utils.h
index 6af54cc..06267b6 100644
--- a/tests/helpers/qt_modal_test_utils.h
+++ b/tests/helpers/qt_modal_test_utils.h
@@ -252,4 +252,15 @@ inline void requestSlrGuidedMode(SLRTableDialog* dialog,
QTest::mouseClick(button, Qt::LeftButton);
}
+inline void requestLlGuidedMode(LLTableDialog* dialog,
+ const QVector>& raw) {
+ auto* table = dialog->findChild("llTableWidget");
+ auto* button = dialog->findChild("llTableGuidedButton");
+ QVERIFY(table != nullptr);
+ QVERIFY(button != nullptr);
+
+ setTableData(table, raw);
+ QTest::mouseClick(button, Qt::LeftButton);
+}
+
} // namespace QtModalTestUtils
diff --git a/tests/tests.pro b/tests/tests.pro
index 7fe54f3..b8e4f1f 100644
--- a/tests/tests.pro
+++ b/tests/tests.pro
@@ -63,6 +63,8 @@ HEADERS += \
../src/gui/grammareditordialog.h \
../src/gui/lltabledialog.h \
../src/gui/lltutorwindow.h \
+ ../src/gui/llwizard.h \
+ ../src/gui/llwizardpage.h \
../src/gui/mainwindow.h \
../src/gui/slrtabledialog.h \
../src/gui/slrtutorwindow.h \
diff --git a/tests/tutor/ll_tutor_window_test.cpp b/tests/tutor/ll_tutor_window_test.cpp
index 455ebfa..ef532cc 100644
--- a/tests/tutor/ll_tutor_window_test.cpp
+++ b/tests/tutor/ll_tutor_window_test.cpp
@@ -3,6 +3,7 @@
#include "examreportdialog.h"
#include "ll1_tutor_test_utils.h"
#include "lltutorwindow.h"
+#include "llwizard.h"
#include "qt_modal_test_utils.h"
#include "tutor_grammar_fixtures.h"
#include "tutor_scenario.h"
@@ -12,6 +13,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -872,3 +875,257 @@ void TutorWindowTest::llExamModeAllCorrectScoresTen() {
QTest::qWait(20);
});
}
+
+namespace {
+
+LLWizard* waitForLlWizard() {
+ return QtModalTestUtils::waitForVisibleTopLevelWidget();
+}
+
+void finishLlWizard(LLWizard* wizard) {
+ QPointer wizardGuard(wizard);
+ QVERIFY(wizardGuard != nullptr);
+ int guard = 0;
+ while (wizardGuard != nullptr && wizardGuard->isVisible() &&
+ ++guard < 200) {
+ auto* page = wizardGuard->currentPage();
+ QVERIFY(page != nullptr);
+ auto* edit = page->findChild("llWizardAnswerEdit");
+ QVERIFY(edit != nullptr);
+ edit->setText(page->expectedForTest());
+ QApplication::processEvents();
+
+ const bool isFinalPage = wizardGuard->isOnLastPage();
+ QPushButton* nextButton = wizardGuard->nextButton();
+ QVERIFY(nextButton != nullptr);
+ QTest::mouseClick(nextButton, Qt::LeftButton);
+ QApplication::processEvents();
+
+ if (isFinalPage) {
+ break;
+ }
+ }
+}
+
+} // namespace
+
+// -----------------------------------------------------------------------------
+// Case: LL1-TC-19
+// Summary:
+// Exercises the LL(1) guided mode chrome: opening from the table dialog,
+// inline feedback, whitespace-tolerant validation, and exiting early.
+//
+// Situation:
+// LL(1) tutor in C with the table dialog open on the simple fixture.
+//
+// Action:
+// The user opens guided mode, types a wrong answer, then the expected
+// production with extra spaces, advances one page and exits.
+//
+// Expected:
+// The wizard freezes the table dialog while open, shows feedback for wrong
+// input, accepts the spaced variant, and returns to the table on exit.
+// -----------------------------------------------------------------------------
+void TutorWindowTest::llGuidedModeWizardUsesCustomNavigationAndAllowsExit() {
+ const Grammar grammar = TutorGrammarFixtures::makeLl1SimpleGrammar();
+ LLTutorWindow tutor(grammar, nullptr);
+
+ runScenario(tutor, {{Ll1TutorTestUtils::tableSizeAnswer(grammar), "B", 1,
+ 0}});
+ answerRemainingRulesUntilStateC(tutor, grammar);
+
+ LLTableDialog* dialog = waitForTableDialog();
+ auto* table = dialog->findChild("llTableWidget");
+ auto* guidedButton = dialog->findChild("llTableGuidedButton");
+ auto* submitButton = dialog->findChild("llTableSubmitButton");
+ QVERIFY(table != nullptr);
+ QVERIFY(guidedButton != nullptr);
+ QVERIFY(submitButton != nullptr);
+
+ QtModalTestUtils::requestLlGuidedMode(
+ dialog, QVector>(
+ table->rowCount(), QVector(table->columnCount())));
+ LLWizard* wizard = waitForLlWizard();
+ QVERIFY(wizard != nullptr);
+ QPointer wizardGuard(wizard);
+ auto* page = wizard->currentPage();
+ QVERIFY(page != nullptr);
+
+ QVERIFY(!guidedButton->isEnabled());
+ QVERIFY(!submitButton->isEnabled());
+
+ auto* titleLabel = wizard->findChild("llWizardTitle");
+ QVERIFY(titleLabel != nullptr);
+ QVERIFY(!titleLabel->text().isEmpty());
+
+ auto* stepCounter = wizard->findChild("llWizardStepCounter");
+ QVERIFY(stepCounter != nullptr);
+ QVERIFY(!stepCounter->text().isEmpty());
+
+ auto* feedbackLabel = page->findChild("llWizardFeedbackLabel");
+ QVERIFY(feedbackLabel != nullptr);
+ QVERIFY(feedbackLabel->text().isEmpty());
+
+ auto* edit = page->findChild("llWizardAnswerEdit");
+ QVERIFY(edit != nullptr);
+ edit->setText(QStringLiteral("wrong"));
+ QApplication::processEvents();
+ QVERIFY(!feedbackLabel->text().isEmpty());
+ QVERIFY(!wizard->nextButton()->isEnabled());
+
+ // Whitespace-tolerant validation: pad the expected production.
+ const QString spaced =
+ QStringLiteral(" %1 ").arg(page->expectedForTest()).replace(' ',
+ " ");
+ edit->setText(spaced);
+ QApplication::processEvents();
+ QVERIFY(wizard->nextButton()->isEnabled());
+
+ QPointer firstPage(page);
+ QTest::keyClick(edit, Qt::Key_Return);
+ QTRY_VERIFY(firstPage == nullptr || wizard->currentPage() != firstPage);
+
+ QTest::mouseClick(wizard->closeButton(), Qt::LeftButton);
+ QTRY_VERIFY(wizardGuard == nullptr || !wizardGuard->isVisible());
+ QVERIFY(guidedButton->isEnabled());
+ QVERIFY(submitButton->isEnabled());
+ QVERIFY(dialog->isVisible());
+ QCOMPARE(tutor.currentStateForTest(), QString("C"));
+}
+
+// -----------------------------------------------------------------------------
+// Case: LL1-TC-20
+// Summary:
+// Completes the LL(1) guided mode and verifies the table dialog is restored
+// with the user's snapshot.
+//
+// Situation:
+// LL(1) tutor in C with the table dialog open on each fixture.
+//
+// Action:
+// The user opens guided mode and answers every cell correctly.
+//
+// Expected:
+// The wizard walks every non-empty cell of the LL(1) table, closes after
+// the last page, and control returns to the table dialog still in C.
+// -----------------------------------------------------------------------------
+void TutorWindowTest::llGuidedModeWizardCompletesAndReturnsToTable() {
+ forEachLlFixture([](const auto& fixture) {
+ const Grammar grammar = fixture.grammar;
+ LLTutorWindow tutor(grammar, nullptr);
+
+ runScenario(tutor, {{Ll1TutorTestUtils::tableSizeAnswer(grammar), "B",
+ 1, 0}});
+ answerRemainingRulesUntilStateC(tutor, grammar);
+
+ LLTableDialog* dialog = waitForTableDialog();
+ auto* table = dialog->findChild("llTableWidget");
+ auto* guidedButton =
+ dialog->findChild("llTableGuidedButton");
+ auto* submitButton =
+ dialog->findChild("llTableSubmitButton");
+ QVERIFY(table != nullptr);
+ QVERIFY(guidedButton != nullptr);
+ QVERIFY(submitButton != nullptr);
+
+ QtModalTestUtils::requestLlGuidedMode(
+ dialog,
+ QVector>(table->rowCount(),
+ QVector(table->columnCount())));
+ LLWizard* wizard = waitForLlWizard();
+ QVERIFY(wizard != nullptr);
+ QPointer wizardGuard(wizard);
+
+ QVERIFY(!guidedButton->isEnabled());
+ QVERIFY(!submitButton->isEnabled());
+
+ finishLlWizard(wizard);
+ QTRY_VERIFY(wizardGuard == nullptr || !wizardGuard->isVisible());
+ QVERIFY(guidedButton->isEnabled());
+ QVERIFY(submitButton->isEnabled());
+ QVERIFY(dialog->isVisible());
+ QCOMPARE(tutor.currentStateForTest(), QString("C"));
+
+ // The exercise still finishes normally after using the wizard.
+ QtModalTestUtils::submitLlTableDialog(
+ dialog, QtModalTestUtils::buildExpectedTable(grammar, table));
+ QTRY_COMPARE(tutor.currentStateForTest(), QString("fin"));
+ });
+}
+
+// -----------------------------------------------------------------------------
+// Case: LL1-TC-21
+// Summary:
+// Verifies the guided mode is reachable from the C' retry table and that
+// exam mode hides the guided button.
+//
+// Situation:
+// LL(1) tutor driven to C' on the simple fixture; a second tutor in exam
+// mode at the C table.
+//
+// Action:
+// The test opens guided mode from the C' dialog, then checks the exam
+// tutor's table dialog.
+//
+// Expected:
+// The wizard opens in C'; in exam mode the guided button is hidden.
+// -----------------------------------------------------------------------------
+void TutorWindowTest::llGuidedModeAvailableInCPrimeAndHiddenInExam() {
+ const Grammar grammar = TutorGrammarFixtures::makeLl1SimpleGrammar();
+ {
+ LLTutorWindow tutor(grammar, nullptr);
+ runScenario(tutor, {{Ll1TutorTestUtils::tableSizeAnswer(grammar), "B",
+ 1, 0}});
+ answerRemainingRulesUntilStateC(tutor, grammar);
+ driveTutorToCPrime(tutor, grammar);
+
+ LLTableDialog* dialog = waitForTableDialog();
+ auto* table = dialog->findChild("llTableWidget");
+ auto* guidedButton =
+ dialog->findChild("llTableGuidedButton");
+ QVERIFY(table != nullptr);
+ QVERIFY(guidedButton != nullptr);
+ QVERIFY(guidedButton->isVisible());
+
+ QtModalTestUtils::requestLlGuidedMode(
+ dialog,
+ QVector>(table->rowCount(),
+ QVector(table->columnCount())));
+ LLWizard* wizard = waitForLlWizard();
+ QVERIFY(wizard != nullptr);
+ QPointer wizardGuard(wizard);
+ QTest::mouseClick(wizard->closeButton(), Qt::LeftButton);
+ QTRY_VERIFY(wizardGuard == nullptr || !wizardGuard->isVisible());
+ QVERIFY(dialog->isVisible());
+
+ // Leave the exercise cleanly.
+ QtModalTestUtils::submitLlTableDialog(
+ dialog, QtModalTestUtils::buildExpectedTable(grammar, table));
+ QTRY_COMPARE(tutor.currentStateForTest(), QString("fin"));
+ }
+
+ LLTutorWindow examTutor(grammar, nullptr, nullptr, true);
+ examTutor.setAnswerForTest(Ll1TutorTestUtils::tableSizeAnswer(grammar));
+ examTutor.submitForTest();
+ answerRemainingRulesUntilStateC(examTutor, grammar);
+ QCOMPARE(examTutor.currentStateForTest(), QString("C"));
+
+ LLTableDialog* examDialog = waitForTableDialog();
+ QVERIFY(examDialog != nullptr);
+ auto* examGuidedButton =
+ examDialog->findChild("llTableGuidedButton");
+ QVERIFY(examGuidedButton != nullptr);
+ QVERIFY(!examGuidedButton->isVisible());
+
+ auto* examTable = examDialog->findChild("llTableWidget");
+ QVERIFY(examTable != nullptr);
+ QtModalTestUtils::submitLlTableDialog(
+ examDialog,
+ QtModalTestUtils::buildExpectedTable(grammar, examTable));
+ QTRY_COMPARE(examTutor.currentStateForTest(), QString("fin"));
+ auto* report =
+ QtModalTestUtils::waitForVisibleTopLevelWidget();
+ QVERIFY(report != nullptr);
+ QTest::mouseClick(report->findChild("examReportCloseButton"),
+ Qt::LeftButton);
+}
diff --git a/tests/tutor/tutor_window_test.h b/tests/tutor/tutor_window_test.h
index d8ffee3..d8869cd 100644
--- a/tests/tutor/tutor_window_test.h
+++ b/tests/tutor/tutor_window_test.h
@@ -24,6 +24,9 @@ class TutorWindowTest : public QObject {
void llAcceptsFlexibleTableCellFormatting();
void llExamModeWrongAnswersFollowMainPathAndGradeZero();
void llExamModeAllCorrectScoresTen();
+ void llGuidedModeWizardUsesCustomNavigationAndAllowsExit();
+ void llGuidedModeWizardCompletesAndReturnsToTable();
+ void llGuidedModeAvailableInCPrimeAndHiddenInExam();
void slrCreatesTutorWithNullTutorialManager();
void slrStateAErrorPathAdvancesThroughAprime();