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