/*************************************************************************** * ktouchtrainer.cpp * * ----------------- * * Copyright (C) 2000 by Håvard Frøiland, 2006 by Andreas Nicolai * * ghorwin@users.sourceforge.net * * * * 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 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "ktouchtrainer.h" #include "ktouchtrainer.moc" #include #include #include #include #include #include #include "ktouch.h" #include "ktouchstatuswidget.h" #include "ktouchtextlinewidget.h" #include "ktouchkeyboardwidget.h" #include "ktouchlecture.h" #include "ktouchdefaults.h" #include "ktouchlevelsummarydialog.h" #include "prefs.h" KTouchTrainer::KTouchTrainer(KTouchStatusWidget *status, KTouchTextLineWidget *textLine, KTouchKeyboardWidget *keyboard, KTouchLecture *lecture) : QObject(), m_trainingTimer(new QTimer), m_statusWidget(status), m_textLineWidget(textLine), m_keyboardWidget(keyboard), m_lecture(lecture) { m_level = 0; // level 1, but we're storing it zero based m_line = 0; m_wordsInCurrentLine = 0; m_trainingPaused=true; // we start in pause mode m_teacherText=m_lecture->level(0).line(0); m_studentText=""; m_decLinesCount=0; m_incLinesCount=0; // reset statistics m_levelStats.clear(); m_sessionStats.clear(); #ifdef KDEEDU_KTOUCH_BUILD_WITH_PHONON m_player = Phonon::createPlayer( Phonon::GameCategory ); m_player->setParent(this); /// \todo preload sounds and improve sound playback system m_levelUpSound = KGlobal::dirs()->findResource("appdata","up.wav"); m_levelDownSound = KGlobal::dirs()->findResource("appdata","down.wav"); m_typeWriterSound = KGlobal::dirs()->findResource("appdata","typewriter.wav"); #endif // KDEEDU_KTOUCH_BUILD_WITH_PHONON connect(m_statusWidget->levelUpBtn, SIGNAL(clicked()), this, SLOT(levelUp()) ); connect(m_statusWidget->levelDownBtn, SIGNAL(clicked()), this, SLOT(levelDown()) ); connect(m_trainingTimer, SIGNAL(timeout()), this, SLOT(timerTick()) ); } // ---------------------------------------------------------------------------- KTouchTrainer::~KTouchTrainer() { delete m_trainingTimer; } // ---------------------------------------------------------------------------- void KTouchTrainer::gotoFirstLine() { m_statusWidget->setNewChars( m_lecture->level(m_level).newChars() ); m_line=0; newLine(); // finally tell the keyboard widget, that we have a new set of new chars m_keyboardWidget->setKnownChars( m_lecture->knownCharsInLevel(m_level) ); } // ---------------------------------------------------------------------------- void KTouchTrainer::keyPressed(QChar key) { // NOTE : In this function we need to distinguish between left and right // typing. Use the config setting Prefs::right2LeftTyping() for that. if(Prefs::soundOnKeypress()){ #ifdef KDEEDU_KTOUCH_BUILD_WITH_PHONON m_player->setCurrentSource(m_typeWriterSound.url()); m_player->play(); #endif // KDEEDU_KTOUCH_BUILD_WITH_PHONON } if (m_trainingPaused) continueTraining(); if (m_teacherText==m_studentText) { // if already at end of line, don't add more chars /// \todo Flash the line when line complete if (Prefs::beepOnError()) QApplication::beep(); return; } // remember length of student text without added character int old_student_text_len = m_studentText.length(); // compose new student text depending in typing direction QString new_student_text = m_studentText; new_student_text += key; // don´t allow excessive amounts of characters per line if (new_student_text.length() > m_teacherText.length()) { if (Prefs::beepOnError()) QApplication::beep(); return; } // store the new student text m_studentText = new_student_text; // we need to find out, if the key was correct or not if (studentLineCorrect()) statsAddCorrectChar(key); // ok, all student text is correct else { // nope, the key was wrong : beep !!! if (Prefs::beepOnError()) QApplication::beep(); // check if the key is the first wrong key that was mistyped. Only then add it // to the wrong char statistics. if (m_teacherText.left(old_student_text_len)==m_studentText.left(old_student_text_len) && m_teacherText.length() > old_student_text_len) { // add the key the student ought to press to the wrong character stats int next_key_index = old_student_text_len; statsAddWrongChar( m_teacherText[next_key_index] ); } } /// \todo Implement option whether subsequent mistyped keys should be remembered as missed /// keys as well. updateWidgets(); // update all the other widgets (keyboard widget, status widget and statusbar) // automatically press enter when option selected if (m_studentText == m_teacherText) { if (!Prefs::pressEnterOnLineEnd()) enterPressed(); } } // ---------------------------------------------------------------------------- void KTouchTrainer::backspacePressed() { if(Prefs::soundOnKeypress()){ #ifdef KDEEDU_KTOUCH_BUILD_WITH_PHONON m_player->setCurrentSource(m_typeWriterSound.url()); m_player->play(); #endif // KDEEDU_KTOUCH_BUILD_WITH_PHONON } if (m_trainingPaused) continueTraining(); /// \todo Implement the "remove space character = remove word count" feature int len = m_studentText.length(); if (len) { if(Prefs::beepOnError()){ if (m_teacherText.startsWith(m_studentText)) { // we are removing a correctly typed char QApplication::beep(); } } m_studentText = m_studentText.left(--len); updateWidgets(); // update all the widgets and the word count if (m_teacherText.startsWith(m_studentText)) m_keyboardWidget->newKey(m_teacherText[len]); else m_keyboardWidget->newKey(QChar(8)); } else { if(Prefs::beepOnError()) QApplication::beep(); } } // ---------------------------------------------------------------------------- void KTouchTrainer::enterPressed() { if(Prefs::soundOnKeypress()){ #ifdef KDEEDU_KTOUCH_BUILD_WITH_PHONON m_player->setCurrentSource(m_typeWriterSound.url()); m_player->play(); #endif // KDEEDU_KTOUCH_BUILD_WITH_PHONON } if (m_trainingPaused) continueTraining(); if (m_studentText != m_teacherText) { if (Prefs::beepOnError()){ QApplication::beep(); } return; } updateWordCount(); m_levelStats.m_words += m_wordsInCurrentLine; m_sessionStats.m_words += m_wordsInCurrentLine; bool increase_level = false; bool decrease_level = false; // check if level increase criterion was fulfilled if (Prefs::upCorrectLimit() <= m_levelStats.correctness()*100 && Prefs::upSpeedLimit() <= m_levelStats.charSpeed()) { m_decLinesCount=0; ++m_incLinesCount; increase_level = true; } // otherwise check if level decrease criterion was fulfilled else if (Prefs::downCorrectLimit() > m_levelStats.correctness()*100 || Prefs::downSpeedLimit() > m_levelStats.charSpeed()) { decrease_level = true; m_incLinesCount=0; ++m_decLinesCount; } // Level completed? (are we in the last line of the level?) if (m_line+1 >= m_lecture->level(m_level).count()) { // ok, lets do all the checks at the end of the level // show the level summary dialog with the highscores //#define LEVELSUMMARYDIALOG_ENABLED #ifdef LEVELSUMMARYDIALOG_ENABLED KTouchLevelSummaryDialog dlg(m_textLineWidget); dlg.showInfo(increase_level, decrease_level, m_levelStats); #endif // LEVELSUMMARYDIALOG_ENABLED if (Prefs::autoLevelChange()) { if (increase_level) { levelUp(); return; } else if (decrease_level) { levelDown(); return; } } // restart the level m_line=0; } else { // we are still in the middle of the level // let's check for mid-level advance/fall back if (Prefs::autoLevelChange() && !Prefs::completeWholeTrainingLevel()) { // we are allowed to advance early, or go back to the last/lower level // first check if we can advance if (increase_level && m_incLinesCount >= Prefs::numberOfLinesWorkload()) { levelUp(); return; } // now check if we need to go back to the last level if (decrease_level && m_decLinesCount >= Prefs::numberOfLinesWorkload() && m_level != 0) { levelDown(); return; } } // no level change, simply go to next line ++m_line; } newLine(); } // ---------------------------------------------------------------------------- void KTouchTrainer::updateWidgets() { // update status widget m_statusWidget->updateStatus(m_level, m_levelStats.correctness()); // update slide line widget m_textLineWidget->setStudentText(m_studentText); // update keyboard widget -> show next to be pressed char. // we have to find out first whether the student text is correct or not. if (studentLineCorrect()) { // ok, all student text is correct if (m_teacherText.length()==m_studentText.length()) m_keyboardWidget->newKey(QChar(13)); // we have reached the end of the line else{ m_keyboardWidget->newKey(m_teacherText[m_studentText.length()]); } } else { m_keyboardWidget->newKey(QChar(8)); // wrong key, user must now press backspace } updateWordCount(); // here we first update the word count updateStatusBar(); // and then the status bar } // ---------------------------------------------------------------------------- void KTouchTrainer::startTraining(bool keepLevel) { // Here we start a new training session. // keep the current level if flag is set if (!keepLevel) m_level=0; // reset the level and session statistics m_levelStats.clear(); m_sessionStats.clear(); // transfer level to level statistics m_levelStats.m_levelNum = m_level; // remember level in session stats m_sessionStats.m_levelNums.insert(m_level); // go to first line in level (also resets word count) gotoFirstLine(); updateStatusBarMessage(i18n("Starting training session: Waiting for first keypress...") ); updateStatusBar(); m_statusWidget->updateStatus(m_level, 1); m_statusWidget->updateSpeed( 0 ); m_trainingPaused=true; // Go into "Pause" mode m_trainingTimer->stop(); // Training timer will be started on first keypress. updateLevelChangeButtons(); } // ---------------------------------------------------------------------------- // Pauses the current training session. // This function is called from class KTouch (when the Pause action is executed). void KTouchTrainer::pauseTraining() { m_trainingPaused=true; m_trainingTimer->stop(); m_textLineWidget->setActive(false); m_statusWidget->updateStatus(m_level, m_levelStats.correctness()); m_statusWidget->updateSpeed((int) m_levelStats.charSpeed() ); updateStatusBarMessage(i18n("Training session paused. Training continues on next keypress...") ); updateStatusBar(); } // ---------------------------------------------------------------------------- // Continues the current training session. // This function is called from class KTouch when a user presses a normal key // while the training is in pause mode. void KTouchTrainer::continueTraining() { m_trainingPaused=false; m_textLineWidget->setActive(true); m_statusWidget->updateStatus(m_level, m_levelStats.correctness() ); m_statusWidget->updateSpeed((int) m_levelStats.charSpeed() ); updateStatusBarMessage(i18n("Training session! The time is running...") ); updateStatusBar(); m_trainingTimer->start(LCD_UPDATE_INTERVAL); // start the timer } // ---------------------------------------------------------------------------- void KTouchTrainer::updateLevelChangeButtons() { if (!Prefs::disableManualLevelChange()) { m_statusWidget->levelUpBtn->setEnabled(m_level < m_lecture->levelCount() - 1); m_statusWidget->levelDownBtn->setEnabled(m_level > 0); } } void KTouchTrainer::storeTrainingStatistics() { // at first get a reference to the statistics data of the current lecture KTouchLectureStats& data = KTouchPtr->getCurrentLectureStats(); // update word count updateWordCount(); // add word count to level and session statistics m_levelStats.m_words += m_wordsInCurrentLine; m_sessionStats.m_words += m_wordsInCurrentLine; // are there level stats to be stored? if (m_levelStats.m_elapsedTime != 0) { //kDebug() << "[KTouchTrainer::storeTrainingStatistics] Storing level statistics!"; m_levelStats.m_timeRecorded = QDateTime::currentDateTime(); data.m_levelStats.push_back( m_levelStats ); } if (m_sessionStats.m_elapsedTime != 0) { //kDebug() << "[KTouchTrainer::storeTrainingStatistics] Storing session statistics!"; m_sessionStats.m_timeRecorded = QDateTime::currentDateTime(); data.m_sessionStats.push_back( m_sessionStats ); } } // ---------------------------------------------------------------------------- bool KTouchTrainer::studentLineCorrect() const { if (m_teacherText.isEmpty()) return m_studentText.isEmpty(); return m_teacherText.startsWith(m_studentText); } // ---------------------------------------------------------------------------- // *** Public slots *** void KTouchTrainer::levelUp() { if(Prefs::soundOnLevel()){ #ifdef KDEEDU_KTOUCH_BUILD_WITH_PHONON m_player->setCurrentSource(m_levelUpSound.url()); m_player->play(); #endif // KDEEDU_KTOUCH_BUILD_WITH_PHONON } if (m_level < m_lecture->levelCount() - 1) { m_level++; } else { levelAllComplete(); } m_incLinesCount = 0; m_decLinesCount = 0; // Store level statistics if level is increased statsChangeLevel(); gotoFirstLine(); updateLevelChangeButtons(); } // ---------------------------------------------------------------------------- void KTouchTrainer::levelDown() { if (m_level>0) { --m_level; if(Prefs::soundOnLevel()){ #ifdef KDEEDU_KTOUCH_BUILD_WITH_PHONON m_player->setCurrentSource(m_levelDownSound.url()); m_player->play(); #endif // KDEEDU_KTOUCH_BUILD_WITH_PHONON } } m_incLinesCount = 0; m_decLinesCount = 0; // Store level statistics if level is increased statsChangeLevel(); gotoFirstLine(); updateLevelChangeButtons(); } void KTouchTrainer::levelAllComplete() { pauseTraining(); KMessageBox::information(0, i18n("You have finished this training exercise.\n" "This training session will start from the beginning."), i18n("You rock!")); statsChangeLevel(); startTraining(false); } void KTouchTrainer::timerTick() { if (m_trainingPaused) return; // Add the timer interval. I think we can neglect the error we make when the session is // paused and continued... it's not a scientific calculation, isn't it? statsAddTime(LCD_UPDATE_INTERVAL*0.001); // update only the widgets that are affected by progressing time m_statusWidget->updateSpeed((int) m_levelStats.charSpeed() ); } // ---------------------------------------------------------------------------- // *** Private functions *** void KTouchTrainer::newLine() { m_teacherText = m_lecture->level(m_level).line(m_line); m_studentText=""; m_wordsInCurrentLine = 0; m_keyboardWidget->newKey(m_teacherText[0]); m_textLineWidget->setTeacherText(m_teacherText); updateStatusBar(); // update status bar } // ---------------------------------------------------------------------------- void KTouchTrainer::updateStatusBar() const { KTouchPtr->changeStatusbarStats(m_levelStats.m_correctChars, m_levelStats.m_totalChars, m_levelStats.m_words + m_wordsInCurrentLine, m_sessionStats.m_correctChars, m_sessionStats.m_totalChars, m_sessionStats.m_words + m_wordsInCurrentLine); } // ---------------------------------------------------------------------------- void KTouchTrainer::updateStatusBarMessage(const QString& message) const { KTouchPtr->changeStatusbarMessage(message); } // ---------------------------------------------------------------------------- void KTouchTrainer::updateWordCount() { // now update the m_wordsInCurrentLine variable if (!studentLineCorrect()) return; // if error, don't update int words = 0; bool space = true; for (int i=0; igetCurrentLectureStats(); // are there level stats to be stored? if (m_levelStats.m_elapsedTime != 0) { //kDebug() << "[KTouchTrainer::storeTrainingStatistics] Storing level statistics!"; m_levelStats.m_timeRecorded = QDateTime::currentDateTime(); data.m_levelStats.push_back( m_levelStats ); } // clear level stats m_levelStats.clear(); // transfer current level to level statistics m_levelStats.m_levelNum = m_level; // remember level in session stats m_sessionStats.m_levelNums.insert(m_level); // show new level (in status widet) and 100% correctness m_statusWidget->updateStatus(m_level, 1); } // ----------------------------------------------------------------------------