1 /*
2 KBruch - exercise to convert mixed numbers in ratios and vice versa
3 SPDX-FileCopyrightText: 2011 Sebastian Stein <seb.kde@hpfsc.de>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "ExerciseMixedNumbers.h"
9
10 /* these includes are needed for KDE support */
11 #include <QLineEdit>
12 #include <KLocalizedString>
13 #include <KMessageBox>
14
15 /* these includes are needed for Qt support */
16 #include <QApplication>
17 #include <QGridLayout>
18 #include <QIntValidator>
19 #include <QPushButton>
20 #include <QWidget>
21
22 #ifdef DEBUG
23 #include <QDebug>
24 #endif
25
26 /* KBruch includes */
27 #include "settingsclass.h"
28
29
30 /* constructor; build UI for this exercise */
ExerciseMixedNumbers(QWidget * parent)31 ExerciseMixedNumbers::ExerciseMixedNumbers(QWidget * parent) :
32 ExerciseBase(parent)
33 {
34 #ifdef DEBUG
35 qDebug() << QStringLiteral("constructor ExerciseMixedNumbers()");
36 #endif
37
38 // define initial state
39 m_isMixedTask = true;
40 m_currentState = _CHECK_TASK;
41
42 // create a new task
43 QApplication::setOverrideCursor(Qt::WaitCursor); // show the sand clock
44 createTask();
45 QApplication::restoreOverrideCursor(); // show the normal cursor
46
47 // create layout
48 //
49 // create 2 base widgets
50 m_tmpTaskWidget = new QWidget(this);
51 m_tmpTaskWidget->setObjectName(QStringLiteral("m_tmpTaskWidget"));
52 m_checkWidget = new QWidget(this);
53 m_checkWidget->setObjectName(QStringLiteral("m_checkWidget"));
54
55 // add base widgets to base grid layout
56 m_baseGrid = new QGridLayout(this);
57 m_baseGrid->setObjectName(QStringLiteral("m_baseGrid"));
58 m_baseGrid->setColumnStretch(0, 1);
59 m_baseGrid->addWidget(m_tmpTaskWidget, 0, 0);
60 m_baseGrid->addWidget(m_checkWidget, 0, 1);
61
62 // prepare task layout
63 m_taskLayout = new QGridLayout(m_tmpTaskWidget);
64 m_taskLayout->setObjectName(QStringLiteral("m_taskLayout"));
65 m_taskLayout->setRowStretch(0, 1);
66 m_taskLayout->setRowStretch(4, 1);
67 m_taskLayout->setColumnStretch(0, 1);
68 m_taskLayout->setColumnStretch(5, 1);
69
70 // prepare check layout
71 m_checkLayout = new QGridLayout(m_checkWidget);
72 m_checkLayout->setObjectName(QStringLiteral("m_checkLayout"));
73
74 // set up task layout
75 //
76 // create task widget
77 m_taskWidget = new TaskWidget(m_tmpTaskWidget, m_task);
78 m_taskWidget->setObjectName(QStringLiteral("m_taskWidget"));
79 m_taskLayout->addWidget(m_taskWidget, 1, 1, 3, 1);
80
81 // int input validator
82 QIntValidator * intValidator = new QIntValidator(this);
83
84 // default font
85 QFont defaultFont = SettingsClass::taskFont();
86 defaultFont.setBold(true);
87 defaultFont.setPointSize(18);
88
89 // input fields for solution
90 //
91 // integer input
92 m_integerEdit = new QLineEdit(m_tmpTaskWidget);
93 m_integerEdit->setObjectName(QStringLiteral("m_integerEdit"));
94 m_integerEdit->setValidator(intValidator);
95 m_integerEdit->setToolTip(i18n("Enter the integer part of the fraction"));
96 m_integerEdit->setFont(defaultFont);
97 m_integerEdit->setFixedSize(85, 42);
98 m_integerEdit->setAlignment(Qt::AlignHCenter);
99 QObject::connect(m_integerEdit, &QLineEdit::returnPressed, this, &ExerciseMixedNumbers::integerReturnPressed);
100 m_taskLayout->addWidget(m_integerEdit, 1, 3, 3, 1, Qt::AlignVCenter |
101 Qt::AlignRight);
102 m_integerEdit->setEnabled(false);
103 m_integerEdit->hide();
104
105 // numerator input
106 m_numerEdit = new QLineEdit(m_tmpTaskWidget);
107 m_numerEdit->setObjectName(QStringLiteral("m_numerEdit"));
108 m_numerEdit->setValidator(intValidator);
109 m_numerEdit->setToolTip(i18n("Enter the numerator of the fraction"));
110 m_numerEdit->setFont(defaultFont);
111 m_numerEdit->setFixedSize(85, 42);
112 m_numerEdit->setAlignment(Qt::AlignHCenter);
113 QObject::connect(m_numerEdit, &QLineEdit::returnPressed, this, &ExerciseMixedNumbers::numerReturnPressed);
114 m_taskLayout->addWidget(m_numerEdit, 1, 4);
115
116 // add a line between the input boxes
117 m_editLine = new QFrame(m_tmpTaskWidget);
118 m_editLine->setGeometry(QRect(100, 100, 20, 20));
119 m_editLine->setFrameStyle(QFrame::HLine | QFrame::Sunken);
120 m_taskLayout->addWidget(m_editLine, 2, 4);
121
122 // denominator input
123 m_denoEdit = new QLineEdit(m_tmpTaskWidget);
124 m_denoEdit->setObjectName(QStringLiteral("m_numerEdit"));
125 m_denoEdit->setValidator(intValidator);
126 m_denoEdit->setToolTip(i18n("Enter the denominator of the fraction"));
127 m_denoEdit->setFont(defaultFont);
128 m_denoEdit->setFixedSize(85, 42);
129 m_denoEdit->setAlignment(Qt::AlignHCenter);
130 QObject::connect(m_denoEdit, &QLineEdit::returnPressed, this, &ExerciseMixedNumbers::denoReturnPressed);
131 m_taskLayout->addWidget(m_denoEdit, 3, 4);
132
133
134 // set up check layout
135 //
136 // add result widget
137 m_resultWidget = new ResultWidget(m_checkWidget, Ratio());
138 m_resultWidget->setObjectName(QStringLiteral("m_resultWidget"));
139 m_checkLayout->addWidget(m_resultWidget, 0, 0, 1, 2);
140
141 // new font size
142 defaultFont.setPointSize(10);
143
144 // check button
145 m_checkButton = new QPushButton(m_checkWidget);
146 m_checkButton->setObjectName(QStringLiteral("m_checkButton"));
147 m_checkButton->setText(i18n("&Check"));
148 m_checkButton->setDefault(true); // is the default button of the dialog
149 m_checkButton->setToolTip(i18n("Click on this button to check your result. The button will not work if you have not entered a result yet."));
150 m_checkButton->setFont(defaultFont);
151 QObject::connect(m_checkButton, &QPushButton::clicked, this, &ExerciseMixedNumbers::slotCheckButtonClicked);
152 m_checkLayout->addWidget(m_checkButton, 1, 0);
153
154 // skip button
155 m_skipButton = new QPushButton(m_checkWidget);
156 m_skipButton->setObjectName(QStringLiteral("m_skipButton"));
157 m_skipButton->setText(i18n("&Skip"));
158 m_skipButton->setToolTip(i18n("Click on this button to skip this question."));
159 m_skipButton->setFont(defaultFont);
160 QObject::connect(m_skipButton, &QPushButton::clicked, this, &ExerciseMixedNumbers::slotSkipButtonClicked);
161 m_checkLayout->addWidget(m_skipButton, 1, 1);
162
163 // add tooltip and qwhatsthis help to the exercise widget
164 setToolTip(i18n("In this exercise you have to convert a mixed number into a ratio and vice versa."));
165 setWhatsThis(i18n("In this exercise you have to convert a mixed number into a ratio and vice versa. Do not forget to reduce the result."));
166 }
167
168 /* destructor */
~ExerciseMixedNumbers()169 ExerciseMixedNumbers::~ExerciseMixedNumbers()
170 {
171 #ifdef DEBUG
172 qDebug() << QStringLiteral("destructor ExerciseMixedNumbers()");
173 #endif
174 }
175
176 /* handle user action to show a new task */
forceNewTask()177 void ExerciseMixedNumbers::forceNewTask()
178 {
179 #ifdef DEBUG
180 qDebug() << QStringLiteral("forceNewTask ExerciseMixedNumbers()");
181 #endif
182
183 if (m_currentState == _CHECK_TASK) {
184 // emit the signal for skipped
185 Q_EMIT signalExerciseSkipped();
186 }
187 m_currentState = _CHECK_TASK;
188 m_checkButton->setText(i18n("&Check"));
189
190 // generate next task
191 (void) nextTask();
192 }
193
194 /* generate new task */
createTask()195 void ExerciseMixedNumbers::createTask()
196 {
197 // generate ratio; constraints:
198 // - reduced
199 // - numerator is larger than denominator
200 // - denominator is not 1
201 Ratio tmpRatio = Ratio();
202 int numerator = 0;
203 int denominator = 1;
204 do {
205 // numerator should be between 1..15
206 numerator = int ((double(qrand()) / RAND_MAX) * 15 + 1);
207
208 // denominator should be between 1..(numerator-1)
209 denominator = int ((double(qrand()) / RAND_MAX) * numerator);
210
211 // eventually make ratio negative
212 if (double(qrand()) / RAND_MAX >= 0.5) {
213 numerator *= -1;
214 }
215 tmpRatio.setRatio(numerator, denominator);
216 } while (tmpRatio.denominator() == 1);
217
218 // store new task
219 m_task = Task();
220 m_task.add_ratio(tmpRatio);
221
222 return;
223 }
224
225 /* show next task to user */
nextTask()226 void ExerciseMixedNumbers::nextTask()
227 {
228 // create a new task
229 //
230 // alternate between converting ratio into mixed number and vice versa
231 m_isMixedTask = ! m_isMixedTask;
232 QApplication::setOverrideCursor(Qt::WaitCursor); // show the sand clock
233 createTask();
234 QApplication::restoreOverrideCursor(); // show the normal cursor
235
236 // update the task widget
237 m_taskWidget->setQuestionMixed(m_isMixedTask);
238 m_taskWidget->setTask((const Task) m_task);
239
240 // hide result widget
241 m_resultWidget->setResult(Ratio(), -1);
242
243 // change check button
244 m_checkButton->setToolTip(i18n("Click this button to check your result. The button will not work if you have not entered a result yet."));
245 m_checkButton->setText(i18n("&Check"));
246
247 // clear user input and restore input fields
248 m_denoEdit->clear();
249 m_numerEdit->clear();
250 m_integerEdit->clear();
251 m_numerEdit->setEnabled(true);
252 m_denoEdit->setEnabled(true);
253 m_skipButton->setEnabled(true);
254 if (m_isMixedTask) {
255 m_integerEdit->hide();
256 m_integerEdit->setEnabled(false);
257 m_numerEdit->setFocus();
258 } else {
259 m_integerEdit->setEnabled(true);
260 m_integerEdit->show();
261 m_integerEdit->setFocus();
262 }
263
264 return;
265 }
266
267 /* exercise gets shown */
showEvent(QShowEvent *)268 void ExerciseMixedNumbers::showEvent(QShowEvent *)
269 {
270 // that the user can start typing without moving the focus
271 if (m_isMixedTask) {
272 m_numerEdit->setFocus();
273 } else {
274 m_integerEdit->setFocus();
275 }
276 m_taskWidget->setQuestionMixed(m_isMixedTask);
277 }
278
279 /* check entered result and show solution */
showResult()280 void ExerciseMixedNumbers::showResult()
281 {
282 bool wrong = false;
283
284 // update UI while solution is shown
285 m_checkButton->setToolTip(i18n("Click this button to get the next question."));
286 m_checkButton->setText(i18n("N&ext"));
287 m_integerEdit->setEnabled(false);
288 m_numerEdit->setEnabled(false);
289 m_denoEdit->setEnabled(false);
290 m_skipButton->setEnabled(false);
291
292 // an empty numerator field is interpreted as 0
293 if (m_numerEdit->text().isEmpty()) {
294 m_numerEdit->setText(QStringLiteral("0"));
295 }
296 int resultNumerator = m_numerEdit->text().toInt();
297
298 // an empty denominator field is interpreted as 1
299 if (m_denoEdit->text().isEmpty()) {
300 m_denoEdit->setText(QStringLiteral("1"));
301 }
302 int resultDenominator = m_denoEdit->text().toInt();
303 if (resultDenominator == 0) {
304 // don't allow denominator to be 0
305 wrong = true;
306 }
307
308 // get integer if user had to input it
309 int resultInteger = 0;
310 if (! m_isMixedTask) {
311 resultInteger = m_integerEdit->text().toInt();
312
313 // in mixed notation, numerator must be smaller than denominator
314 if (qAbs(resultNumerator) >= qAbs(resultDenominator)) {
315 wrong = true;
316 }
317 }
318
319 // create result ratio, but don't try to reduce it yet
320 Ratio resultRatio = Ratio();
321 resultRatio.setRatio(resultInteger, resultNumerator, resultDenominator,
322 false);
323
324 // check for correct solution
325 Ratio solutionRatio = m_task.get_ratio_n(0);
326 if (!(resultRatio == solutionRatio)) {
327 wrong = true;
328 }
329
330 // wrong solution, try to give some hints what might be wrong
331 if (wrong) {
332 // emit the signal for wrong
333 Q_EMIT signalExerciseSolvedWrong();
334 m_resultWidget->setAnswerMixed(! m_isMixedTask);
335 m_resultWidget->setResult(solutionRatio, 0);
336
337 // check if user entered 0 as denominator
338 if (resultDenominator == 0) {
339 KMessageBox::information(this, i18n("You entered 0 as the denominator. This means division by zero, which is not allowed. This question will be counted as not correctly solved."));
340 } else {
341 // first reduce entered result to identify more problems
342 resultRatio.reduce();
343 if ((! m_isMixedTask) && (resultRatio == solutionRatio) &&
344 (qAbs(resultNumerator) >= qAbs(resultDenominator))) {
345 // maybe didn't enter mixed number notation
346 KMessageBox::information(this, i18n("You entered the correct result, but not in the mixed number notation. This question will be counted as not correctly solved."));
347 } else if (resultRatio == solutionRatio) {
348 // maybe the user entered the correct result but not reduced
349 KMessageBox::information(this, i18n("You entered the correct result, but not reduced. This question will be counted as not correctly solved."));
350 }
351 }
352 } else {
353 // emit the signal for correct
354 Q_EMIT signalExerciseSolvedCorrect();
355 m_resultWidget->setResult(solutionRatio, 1);
356 }
357
358 return;
359 }
360
361 /* handle check button */
slotCheckButtonClicked()362 void ExerciseMixedNumbers::slotCheckButtonClicked()
363 {
364 // button is used to check result and get to next task
365 if (m_currentState == _CHECK_TASK) {
366 // don't check result if nothing was entered yet
367 if (m_numerEdit->text().isEmpty() && m_denoEdit->text().isEmpty()) {
368 return;
369 }
370
371 m_currentState = _NEXT_TASK;
372 (void) showResult();
373 } else {
374 m_currentState = _CHECK_TASK;
375 (void) nextTask();
376 }
377
378 return;
379 }
380
381 /* handle skip button */
slotSkipButtonClicked()382 void ExerciseMixedNumbers::slotSkipButtonClicked()
383 {
384 forceNewTask();
385 }
386
387 /* handle integer edit */
integerReturnPressed()388 void ExerciseMixedNumbers::integerReturnPressed()
389 {
390 m_numerEdit->setFocus();
391 }
392
393 /* handle numerator edit */
numerReturnPressed()394 void ExerciseMixedNumbers::numerReturnPressed()
395 {
396 m_denoEdit->setFocus();
397 }
398
399 /* handle denominator edit */
denoReturnPressed()400 void ExerciseMixedNumbers::denoReturnPressed()
401 {
402 slotCheckButtonClicked();
403 }
404