diff --git a/SyntaxTutor.pro b/SyntaxTutor.pro
index 96e50ac2..c92a0731 100644
--- a/SyntaxTutor.pro
+++ b/SyntaxTutor.pro
@@ -31,8 +31,10 @@ SOURCES += \
src/backend/lr0_item.cpp \
src/backend/slr1_parser.cpp \
src/backend/symbol_table.cpp \
+ src/widgets/automatonview.cpp \
src/widgets/customtextedit.cpp \
src/widgets/grammarview.cpp \
+ src/gui/automatonviewerdialog.cpp \
src/gui/examreportdialog.cpp \
src/gui/grammareditordialog.cpp \
src/gui/lltabledialog.cpp \
@@ -54,8 +56,10 @@ HEADERS += \
src/backend/slr1_parser.hpp \
src/backend/state.hpp \
src/backend/symbol_table.hpp \
+ src/widgets/automatonview.h \
src/widgets/customtextedit.h \
src/widgets/grammarview.h \
+ src/gui/automatonviewerdialog.h \
src/gui/examreportdialog.h \
src/gui/examsession.h \
src/gui/grammareditordialog.h \
diff --git a/resources/styles/app.qss b/resources/styles/app.qss
index 4a8eae13..0dae5828 100644
--- a/resources/styles/app.qss
+++ b/resources/styles/app.qss
@@ -846,3 +846,65 @@ QDialog[examReport="true"] QPushButton#examReportCloseButton:hover {
color: #FFFFFF;
border: 1px solid rgba(255, 255, 255, 0.12);
}
+
+QPushButton#automatonButton {
+ background-color: rgba(17, 179, 188, 0.12);
+ color: #11B3BC;
+ border: 1px solid rgba(17, 179, 188, 0.35);
+ border-radius: 8px;
+ padding: 8px 14px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+QPushButton#automatonButton:hover {
+ background-color: rgba(17, 179, 188, 0.2);
+ color: #36C5CC;
+}
+
+QPushButton#automatonButton:pressed {
+ background-color: rgba(17, 179, 188, 0.28);
+}
+
+QPushButton#automatonButton:disabled {
+ background-color: #2A2E30;
+ color: #6F7A7E;
+ border: 1px solid #353D40;
+}
+
+QGraphicsView#slrAutomatonView {
+ background-color: #212526;
+ border: 1px solid #303638;
+ border-radius: 12px;
+}
+
+QDialog[automatonViewer="true"] {
+ background-color: #1F1F1F;
+}
+
+QDialog[automatonViewer="true"] QLabel#automatonViewerHint {
+ color: #6F7A7E;
+ font-size: 12px;
+}
+
+QDialog[automatonViewer="true"] QPushButton#automatonViewerToolButton {
+ background-color: #2B3133;
+ color: #E8ECEE;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ padding: 6px 12px;
+ font-size: 13px;
+ font-weight: 600;
+ min-width: 28px;
+}
+
+QDialog[automatonViewer="true"] QPushButton#automatonViewerToolButton:hover {
+ background-color: #343B3D;
+ border: 1px solid rgba(17, 179, 188, 0.4);
+ color: #36C5CC;
+}
+
+QDialog[automatonViewer="true"] QPushButton#automatonViewerToolButton:pressed {
+ background-color: #11B3BC;
+ color: #07343A;
+}
diff --git a/src/gui/automatonviewerdialog.cpp b/src/gui/automatonviewerdialog.cpp
new file mode 100644
index 00000000..52d5dc5e
--- /dev/null
+++ b/src/gui/automatonviewerdialog.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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 .
+ */
+
+#include "automatonviewerdialog.h"
+#include "automatonview.h"
+
+#include
+#include
+#include
+#include
+
+AutomatonViewerDialog::AutomatonViewerDialog(AutomatonView* view,
+ QWidget* parent)
+ : QDialog(parent), view_(view) {
+ setObjectName("automatonViewerDialog");
+ setProperty("automatonViewer", true);
+ setWindowTitle(tr("Autómata LR(0)"));
+ setModal(false);
+ resize(820, 620);
+ setMinimumSize(520, 420);
+
+ auto* rootLayout = new QVBoxLayout(this);
+ rootLayout->setContentsMargins(16, 14, 16, 16);
+ rootLayout->setSpacing(12);
+
+ auto* toolbar = new QHBoxLayout();
+ toolbar->setSpacing(8);
+
+ auto* hint = new QLabel(
+ tr("Ctrl + rueda para acercar, arrastra para desplazar"), this);
+ hint->setObjectName("automatonViewerHint");
+ toolbar->addWidget(hint);
+ toolbar->addStretch(1);
+
+ auto makeToolButton = [this](const QString& text, const QString& tip) {
+ auto* button = new QPushButton(text, this);
+ button->setObjectName("automatonViewerToolButton");
+ button->setToolTip(tip);
+ button->setCursor(Qt::PointingHandCursor);
+ button->setAutoDefault(false);
+ button->setDefault(false);
+ return button;
+ };
+
+ auto* zoomOutButton = makeToolButton(QStringLiteral("−"), tr("Alejar"));
+ auto* zoomInButton = makeToolButton(QStringLiteral("+"), tr("Acercar"));
+ auto* fitButton = makeToolButton(tr("Ajustar"), tr("Ajustar a la vista"));
+ auto* centerButton =
+ makeToolButton(tr("Centrar estado"), tr("Centrar el estado actual"));
+
+ toolbar->addWidget(zoomOutButton);
+ toolbar->addWidget(zoomInButton);
+ toolbar->addWidget(fitButton);
+ toolbar->addWidget(centerButton);
+ rootLayout->addLayout(toolbar);
+
+ view_->setParent(this);
+ rootLayout->addWidget(view_, 1);
+ view_->show();
+
+ connect(zoomOutButton, &QPushButton::clicked, view_,
+ &AutomatonView::zoomOut);
+ connect(zoomInButton, &QPushButton::clicked, view_, &AutomatonView::zoomIn);
+ connect(fitButton, &QPushButton::clicked, view_, &AutomatonView::fitToView);
+ connect(centerButton, &QPushButton::clicked, view_,
+ &AutomatonView::centerOnCurrentState);
+}
+
+AutomatonViewerDialog::~AutomatonViewerDialog() {
+ // The view is owned by the tutor, not by this dialog. Detach it so it
+ // survives the dialog and can be embedded again next time.
+ if (view_ != nullptr && view_->parent() == this) {
+ view_->hide();
+ view_->setParent(nullptr);
+ }
+}
diff --git a/src/gui/automatonviewerdialog.h b/src/gui/automatonviewerdialog.h
new file mode 100644
index 00000000..c01f40c6
--- /dev/null
+++ b/src/gui/automatonviewerdialog.h
@@ -0,0 +1,57 @@
+/*
+ * 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 AUTOMATONVIEWERDIALOG_H
+#define AUTOMATONVIEWERDIALOG_H
+
+#include
+#include
+
+class AutomatonView;
+
+/**
+ * @class AutomatonViewerDialog
+ * @brief Non-modal floating window that hosts the LR(0) automaton view.
+ *
+ * The dialog is a thin shell: it embeds an externally-owned AutomatonView
+ * (so its progressive reveal state is preserved across open/close cycles)
+ * and adds a toolbar with zoom in/out, fit-to-view and center-current
+ * actions. It never owns the view: on destruction it reparents the view out
+ * so the tutor can keep reusing it. Being non-modal, the student can keep it
+ * open while continuing the exercise.
+ */
+class AutomatonViewerDialog : public QDialog {
+ Q_OBJECT
+ public:
+ /**
+ * @brief Builds the viewer around an existing automaton view.
+ *
+ * @param view The automaton view to embed; ownership is not taken.
+ * @param parent Parent widget.
+ */
+ explicit AutomatonViewerDialog(AutomatonView* view,
+ QWidget* parent = nullptr);
+ ~AutomatonViewerDialog() override;
+
+ private:
+ // QPointer so the dialog never dereferences the borrowed view if it is
+ // destroyed first (e.g. by the tutor during teardown).
+ QPointer view_;
+};
+
+#endif // AUTOMATONVIEWERDIALOG_H
diff --git a/src/gui/slrtutorwindow.cpp b/src/gui/slrtutorwindow.cpp
index cd7af674..e91f142f 100644
--- a/src/gui/slrtutorwindow.cpp
+++ b/src/gui/slrtutorwindow.cpp
@@ -17,6 +17,7 @@
*/
#include "slrtutorwindow.h"
+#include "automatonviewerdialog.h"
#include "examreportdialog.h"
#include "grammarview.h"
#include "slrwizard.h"
@@ -27,6 +28,7 @@
#include
#include
#include
+#include
#include
namespace {
@@ -226,6 +228,7 @@ SLRTutorWindow::SLRTutorWindow(const Grammar& g, TutorialManager* tm,
for (const Lr0Item& it : st.items_) {
if (it.IsComplete() && it.antecedent_ != slr1.gr_.axiom_) {
reduceStatesIdQueue.push(st.id_);
+ reduceStateIds.insert(st.id_);
break;
}
}
@@ -270,6 +273,8 @@ SLRTutorWindow::SLRTutorWindow(const Grammar& g, TutorialManager* tm,
ui->gr->setFixedWidth(grammarView->sizeHint().width() + 32);
ui->gr->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ setupAutomatonPanel();
+
// ====== Status, Progress & First Message ===================
ui->cntRight->setText(QString::number(cntRightAnswers));
ui->cntWrong->setText(QString::number(cntWrongAnswers));
@@ -299,6 +304,19 @@ SLRTutorWindow::SLRTutorWindow(const Grammar& g, TutorialManager* tm,
}
SLRTutorWindow::~SLRTutorWindow() {
+ // Tear down the automaton viewer (if open) and the view it borrows. The
+ // viewer's destructor detaches the view, so we then own it outright and
+ // delete it explicitly to avoid any ambiguous ownership.
+ if (automatonViewer != nullptr) {
+ disconnect(automatonViewer, nullptr, this, nullptr);
+ delete automatonViewer;
+ automatonViewer = nullptr;
+ }
+ if (automatonView != nullptr) {
+ automatonView->setParent(nullptr);
+ delete automatonView;
+ automatonView = nullptr;
+ }
delete ui;
}
@@ -986,11 +1004,144 @@ void SLRTutorWindow::updateProgressPanel() {
ui->textEdit->verticalScrollBar()->setValue(scrollPos);
}
+void SLRTutorWindow::setupAutomatonPanel() {
+ if (examMode) {
+ // No automaton (and no button) during an exam.
+ ui->automatonButton->hide();
+ return;
+ }
+
+ // The view is parented to the tutor but never placed in the main layout;
+ // it stays hidden until embedded in the floating viewer. Keeping it as a
+ // tutor child lets its progressive reveal state survive open/close.
+ automatonView = new AutomatonView(this);
+ automatonView->hide();
+
+ QVector stateInfos;
+ stateInfos.reserve(static_cast(slr1.states_.size()));
+ for (const state& st : slr1.states_) {
+ stateInfos.append(
+ {st.id_, QString::fromStdString(slr1.PrintItems(st.items_))});
+ }
+
+ QVector transitionInfos;
+ for (const auto& [from, transitions] : slr1.transitions_) {
+ for (const auto& [symbol, to] : transitions) {
+ transitionInfos.append(
+ {from, QString::fromStdString(symbol), to});
+ }
+ }
+
+ automatonView->setAutomaton(stateInfos, transitionInfos);
+
+ ui->automatonButton->setCursor(Qt::PointingHandCursor);
+ connect(ui->automatonButton, &QPushButton::clicked, this,
+ &SLRTutorWindow::openAutomatonViewer);
+ updateAutomatonButton();
+}
+
+void SLRTutorWindow::updateAutomatonButton() {
+ if (automatonView == nullptr) {
+ return;
+ }
+ // Available only once the student has constructed the initial state I0.
+ ui->automatonButton->setEnabled(automatonView->visibleStateCount() > 0);
+}
+
+void SLRTutorWindow::openAutomatonViewer() {
+ if (automatonView == nullptr) {
+ return; // exam mode
+ }
+ if (automatonViewer != nullptr) {
+ // Already open: focus/raise instead of duplicating.
+ automatonViewer->raise();
+ automatonViewer->activateWindow();
+ return;
+ }
+
+ automatonViewer = new AutomatonViewerDialog(automatonView, this);
+ connect(automatonViewer, &QDialog::finished, this, [this](int) {
+ // Reclaim the view so it survives the viewer and can be reopened.
+ if (automatonView != nullptr) {
+ automatonView->setParent(this);
+ automatonView->hide();
+ }
+ if (automatonViewer != nullptr) {
+ automatonViewer->deleteLater();
+ automatonViewer = nullptr;
+ }
+ });
+ automatonViewer->show();
+ automatonViewer->raise();
+ automatonViewer->activateWindow();
+ // Fit once the viewer has its real size.
+ QTimer::singleShot(0, automatonView, &AutomatonView::fitToView);
+}
+
+void SLRTutorWindow::updateAutomatonPanel() {
+ if (automatonView == nullptr) {
+ return;
+ }
+
+ switch (currentState) {
+ // The student is analyzing one specific state of the collection.
+ case StateSlr::C:
+ case StateSlr::CA:
+ case StateSlr::CB:
+ automatonView->setCurrentState(static_cast(currentStateId));
+ break;
+
+ // From D onwards the collection is complete: full consultation mode.
+ case StateSlr::D:
+ case StateSlr::D1:
+ case StateSlr::D2:
+ case StateSlr::D_prime:
+ case StateSlr::E:
+ case StateSlr::E1:
+ case StateSlr::E2:
+ case StateSlr::H:
+ case StateSlr::H_prime:
+ automatonView->revealAll();
+ automatonView->clearStateMarks();
+ automatonView->setCurrentState(-1);
+ break;
+
+ case StateSlr::F:
+ automatonView->revealAll();
+ automatonView->clearStateMarks();
+ automatonView->setConflictStates(solutionForF());
+ automatonView->setCurrentState(-1);
+ break;
+
+ case StateSlr::FA:
+ automatonView->revealAll();
+ automatonView->setConflictStates(solutionForF());
+ automatonView->setCurrentState(
+ static_cast(currentConflictStateId));
+ break;
+
+ case StateSlr::G:
+ automatonView->revealAll();
+ automatonView->clearStateMarks();
+ automatonView->setReduceStates(reduceStateIds);
+ automatonView->setCurrentState(static_cast(currentReduceStateId));
+ break;
+
+ default:
+ automatonView->setCurrentState(-1);
+ break;
+ }
+}
+
void SLRTutorWindow::addUserState(unsigned id) {
auto st = std::ranges::find_if(
slr1.states_, [id](const state& st) { return st.id_ == id; });
if (st != slr1.states_.end()) {
userMadeStates.insert(*st);
+ if (automatonView != nullptr) {
+ automatonView->revealState(id);
+ updateAutomatonButton();
+ }
updateProgressPanel();
}
}
@@ -999,6 +1150,9 @@ void SLRTutorWindow::addUserTransition(unsigned fromId,
const std::string& symbol,
unsigned toId) {
userMadeTransitions[fromId][symbol] = toId;
+ if (automatonView != nullptr) {
+ automatonView->revealTransition(fromId, toId);
+ }
}
void SLRTutorWindow::addMessage(const QString& text, bool isUser) {
@@ -1499,6 +1653,9 @@ void SLRTutorWindow::on_confirmButton_clicked() {
void SLRTutorWindow::postQuestion() {
currentQuestionText = generateQuestion();
+ // generateQuestion() refreshes the per-state context (currentStateId,
+ // conflict/reduce ids), so the automaton panel syncs right after it.
+ updateAutomatonPanel();
addMessage(currentQuestionText, false);
}
diff --git a/src/gui/slrtutorwindow.h b/src/gui/slrtutorwindow.h
index 9adbcc32..44fc29b5 100644
--- a/src/gui/slrtutorwindow.h
+++ b/src/gui/slrtutorwindow.h
@@ -20,6 +20,8 @@
#define SLRTUTORWINDOW_H
#include "UniqueQueue.h"
+#include "automatonview.h"
+#include "automatonviewerdialog.h"
#include "examsession.h"
#include "grammar.hpp"
#include "grammarview.h"
@@ -228,6 +230,36 @@ class SLRTutorWindow : public QWidget {
bool confirmExitToHome();
QString promptExportFilePath() const;
+ // ====== LR(0) Automaton Viewer ================================
+ /**
+ * @brief Builds the (hidden) automaton view and prepares the button.
+ *
+ * Does nothing in exam mode: the automaton would reveal the collection.
+ * The view is not embedded in the main layout; it lives in a separate
+ * floating viewer opened on demand.
+ */
+ void setupAutomatonPanel();
+
+ /**
+ * @brief Syncs the automaton view with the current tutor state.
+ *
+ * Highlights the state under analysis during the C/CA/CB loop, switches
+ * to full consultation mode from D onwards, and marks conflict (F/FA)
+ * and reduce (G) states. Updates apply whether or not the viewer is open.
+ */
+ void updateAutomatonPanel();
+
+ /**
+ * @brief Opens the floating automaton viewer, or raises it if already
+ * open.
+ */
+ void openAutomatonViewer();
+
+ /**
+ * @brief Enables the viewer button once at least one state (I0) exists.
+ */
+ void updateAutomatonButton();
+
// ====== Exam Mode =============================================
void postQuestion(); ///< Shows and remembers the next question.
QString examSolutionText(); ///< Expected answer for the current state.
@@ -318,6 +350,11 @@ class SLRTutorWindow : public QWidget {
QVector statesWithLr0Conflict; // Populated in F
std::queue conflictStatesIdQueue;
unsigned currentConflictStateId = 0;
+
+ // ====== LR(0) Automaton Viewer =================================
+ AutomatonView* automatonView = nullptr; // null in exam mode
+ AutomatonViewerDialog* automatonViewer = nullptr; // null when closed
+ QSet reduceStateIds; // States highlighted in G
state currentConflictState;
std::queue
diff --git a/src/gui/slrtutorwindow.ui b/src/gui/slrtutorwindow.ui
index 46d3da4c..261f3860 100644
--- a/src/gui/slrtutorwindow.ui
+++ b/src/gui/slrtutorwindow.ui
@@ -176,11 +176,34 @@
-
-
-
- true
+
+
+ 8
-
+
-
+
+
+ PointingHandCursor
+
+
+ Ver autómata LR(0)
+
+
+
+ -
+
+
+
+ 2
+ 0
+
+
+
+ true
+
+
+
+
diff --git a/src/widgets/automatonview.cpp b/src/widgets/automatonview.cpp
new file mode 100644
index 00000000..52594193
--- /dev/null
+++ b/src/widgets/automatonview.cpp
@@ -0,0 +1,531 @@
+/*
+ * 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 .
+ */
+
+#include "automatonview.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include