wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ExerciseOverlay.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QApplication>
7#include <QFontInfo>
8#include <QHBoxLayout>
9#include <QLabel>
10#include <QPainter>
11#include <QPushButton>
12#include <QVBoxLayout>
13
16
18 : QWidget(parent)
19 , m_engine(engine)
20{
21 setFixedWidth(480);
22 setAttribute(Qt::WA_TranslucentBackground);
23
24 setupUi();
25
27 this, &ExerciseOverlay::applyTheme);
28}
29
30void ExerciseOverlay::setupUi()
31{
32 auto *outerLayout = new QVBoxLayout(this);
33 outerLayout->setContentsMargins(12, 12, 12, 12);
34 outerLayout->setSpacing(8);
35
36 // Step counter row
37 auto *topRow = new QHBoxLayout;
38 m_stepCounter = new QLabel(this);
39 topRow->addWidget(m_stepCounter);
40 topRow->addStretch();
41 outerLayout->addLayout(topRow);
42
43 // Instruction text
44 m_instructionLabel = new QLabel(this);
45 m_instructionLabel->setWordWrap(true);
46 outerLayout->addWidget(m_instructionLabel);
47
48 // Hint text (hidden by default)
49 m_hintLabel = new QLabel(this);
50 m_hintLabel->setWordWrap(true);
51 m_hintLabel->hide();
52 outerLayout->addWidget(m_hintLabel);
53
54 // Navigation row: [Exit] [Hint] ←stretch→ [← Back] [Next →]
55 auto *btnRow = new QHBoxLayout;
56 btnRow->setSpacing(6);
57
58 m_closeButton = new QPushButton(tr("Exit"), this);
59 m_closeButton->setToolTip(tr("Close exercise"));
60
61 m_hintButton = new QPushButton(tr("Hint"), this);
62
63 m_prevButton = new QPushButton(tr("← Back"), this);
64
65 m_nextButton = new QPushButton(tr("Next →"), this);
66 m_nextButton->setEnabled(false);
67
68 btnRow->addWidget(m_closeButton);
69 btnRow->addWidget(m_hintButton);
70 btnRow->addStretch();
71 btnRow->addWidget(m_prevButton);
72 btnRow->addWidget(m_nextButton);
73 outerLayout->addLayout(btnRow);
74
75 adjustSize();
76
77 connect(m_closeButton, &QPushButton::clicked, this, &ExerciseOverlay::closeRequested);
78 connect(m_hintButton, &QPushButton::clicked, this, [this] {
79 m_hintVisible = !m_hintVisible;
80 m_hintLabel->setVisible(m_hintVisible);
81 m_hintButton->setText(m_hintVisible ? tr("Hide hint") : tr("Hint"));
82 adjustSize();
84 });
85 connect(m_prevButton, &QPushButton::clicked, this, [this] {
86 m_engine->goToPreviousStep();
87 });
88 connect(m_nextButton, &QPushButton::clicked, this, [this] {
89 m_engine->advanceStep();
90 });
91
92 applyTheme();
93}
94
95int ExerciseOverlay::scaledFontPx(int basePx)
96{
97 constexpr int baselinePx = 13;
98 const int currentPx = QFontInfo(QApplication::font()).pixelSize();
99 return qRound(static_cast<double>(basePx) * currentPx / baselinePx);
100}
101
102void ExerciseOverlay::applyTheme()
103{
104 const bool dark = (ThemeManager::effectiveTheme() == Theme::Dark);
105 const int navFontPx = scaledFontPx(13);
106
107 if (dark) {
108 m_bgColor = QColor(30, 30, 30, 200);
109 m_textColor = QColor(255, 255, 255);
110 m_hintColor = QColor(255, 255, 180, 230);
111 m_counterColor = QColor(255, 255, 255, 200);
112
113 const QString navStyle =
114 QString("QPushButton { color: white; background: rgba(255,255,255,40);"
115 " border: 1px solid rgba(255,255,255,80); border-radius: 4px;"
116 " padding: 3px 10px; font-size: %1px; }"
117 "QPushButton:hover { background: rgba(255,255,255,60); }"
118 "QPushButton:disabled { color: rgba(255,255,255,80); background: rgba(80,80,80,60); }")
119 .arg(navFontPx);
120 m_closeButton->setStyleSheet(navStyle);
121 m_hintButton->setStyleSheet(navStyle);
122 m_prevButton->setStyleSheet(navStyle);
123 m_nextButton->setStyleSheet(
124 QString("QPushButton { color: white; background: rgba(60,150,60,180);"
125 " border: 1px solid rgba(255,255,255,80); border-radius: 4px;"
126 " padding: 3px 10px; font-size: %1px; }"
127 "QPushButton:hover { background: rgba(60,180,60,200); }"
128 "QPushButton:disabled { background: rgba(100,100,100,80);"
129 " color: rgba(255,255,255,100); }")
130 .arg(navFontPx));
131 } else {
132 m_bgColor = QColor(245, 245, 245, 230);
133 m_textColor = QColor(20, 20, 20);
134 m_hintColor = QColor(110, 90, 0, 230);
135 m_counterColor = QColor(80, 80, 80, 200);
136
137 const QString navStyle =
138 QString("QPushButton { color: rgb(20,20,20); background: rgba(0,0,0,20);"
139 " border: 1px solid rgba(0,0,0,80); border-radius: 4px;"
140 " padding: 3px 10px; font-size: %1px; }"
141 "QPushButton:hover { background: rgba(0,0,0,40); }"
142 "QPushButton:disabled { color: rgba(0,0,0,60); background: rgba(0,0,0,10); }")
143 .arg(navFontPx);
144 m_closeButton->setStyleSheet(navStyle);
145 m_hintButton->setStyleSheet(navStyle);
146 m_prevButton->setStyleSheet(navStyle);
147 m_nextButton->setStyleSheet(
148 QString("QPushButton { color: white; background: rgba(40,130,40,220);"
149 " border: 1px solid rgba(0,100,0,150); border-radius: 4px;"
150 " padding: 3px 10px; font-size: %1px; }"
151 "QPushButton:hover { background: rgba(40,160,40,230); }"
152 "QPushButton:disabled { background: rgba(150,150,150,80);"
153 " color: rgba(255,255,255,120); }")
154 .arg(navFontPx));
155 }
156
157 m_stepCounter->setStyleSheet(
158 QString("color: rgba(%1,%2,%3,%4); font-size: %5px;")
159 .arg(m_counterColor.red()).arg(m_counterColor.green())
160 .arg(m_counterColor.blue()).arg(m_counterColor.alpha()).arg(scaledFontPx(12)));
161 m_instructionLabel->setStyleSheet(
162 QString("color: rgb(%1,%2,%3); font-size: %4px; font-weight: bold;")
163 .arg(m_textColor.red()).arg(m_textColor.green()).arg(m_textColor.blue())
164 .arg(scaledFontPx(14)));
165 m_hintLabel->setStyleSheet(
166 QString("color: rgba(%1,%2,%3,%4); font-size: %5px;")
167 .arg(m_hintColor.red()).arg(m_hintColor.green())
168 .arg(m_hintColor.blue()).arg(m_hintColor.alpha()).arg(scaledFontPx(13)));
169
170 update();
171}
172
174{
175 if (!parentWidget()) {
176 return;
177 }
178 adjustSize();
179 const int x = (parentWidget()->width() - width()) / 2;
180 const int y = parentWidget()->height() - height() - 12;
181 move(x, y);
182}
183
184void ExerciseOverlay::onStepChanged(int step, int total, const ExerciseStep &stepData)
185{
186 m_stepCounter->setText(tr("Step %1 of %2").arg(step + 1).arg(total));
187 m_instructionLabel->setText(stepData.instruction);
188 m_hintLabel->setText(stepData.hint);
189
190 // Reset hint state on step change
191 m_hintVisible = false;
192 m_hintLabel->hide();
193 m_hintButton->setText(tr("Hint"));
194
195 const bool isObserveStep = stepData.requiredElements.isEmpty() && stepData.requiredConnections.isEmpty();
196 const bool isLastStep = (step == total - 1);
197
198 updateNextButton(isLastStep);
199 // Next button enabled only for observe steps (auto-advance handles others)
200 m_nextButton->setEnabled(isObserveStep);
201
202 m_prevButton->setEnabled(step > 0);
203
204 adjustSize();
206}
207
209{
210 const ExerciseStep &stepData = m_engine->currentStepData();
211 const int step = m_engine->currentStep();
212 const int total = m_engine->totalSteps();
213
214 m_stepCounter->setText(tr("Step %1 of %2").arg(step + 1).arg(total));
215 m_instructionLabel->setText(stepData.instruction);
216 m_hintLabel->setText(stepData.hint);
217 updateNextButton(step == total - 1);
218
219 m_closeButton->setText(tr("Exit"));
220 m_closeButton->setToolTip(tr("Close exercise"));
221 m_hintButton->setText(m_hintVisible ? tr("Hide hint") : tr("Hint"));
222 m_prevButton->setText(tr("← Back"));
223
224 adjustSize();
226}
227
229{
230 m_stepCounter->setText({});
231 m_instructionLabel->setText(tr("Exercise complete! Well done."));
232 m_hintLabel->hide();
233 m_hintButton->hide();
234 m_prevButton->hide();
235 m_nextButton->setText(tr("Close"));
236 m_nextButton->setEnabled(true);
237 disconnect(m_nextButton, &QPushButton::clicked, nullptr, nullptr);
238 connect(m_nextButton, &QPushButton::clicked, this, &ExerciseOverlay::closeRequested);
239 adjustSize();
241}
242
243void ExerciseOverlay::updateNextButton(bool isLastStep)
244{
245 m_nextButton->setText(isLastStep ? tr("Finish") : tr("Next →"));
246}
247
248void ExerciseOverlay::paintEvent(QPaintEvent *event)
249{
250 Q_UNUSED(event)
251 QPainter painter(this);
252 painter.setRenderHint(QPainter::Antialiasing);
253 painter.setBrush(m_bgColor);
254 painter.setPen(Qt::NoPen);
255 painter.drawRoundedRect(rect(), 8, 8);
256}
Theme management types and singleton ThemeManager.
ExerciseOverlay(ExerciseEngine *engine, QWidget *parent=nullptr)
void onStepChanged(int step, int total, const ExerciseStep &stepData)
void closeRequested()
void paintEvent(QPaintEvent *event) override
static ThemeManager & instance()
Returns the singleton ThemeManager instance.
void themeChanged()
Emitted whenever the active theme changes.
static Theme effectiveTheme()
Returns the effective theme (Light or Dark), resolving System to the OS preference.
QVector< ExerciseConnectionRequirement > requiredConnections
QVector< ExerciseElementRequirement > requiredElements
QString instruction