Commit 1a910f17 authored by Sebastian Gottfried's avatar Sebastian Gottfried
Browse files

training screen: fix key event handling wrt dead keys

Turns the event handling in QML is too limited to properly support dead
keys. At least on X11 dead key handling is part of some input method
and one has support for them in QML. Therefore the event handling part
of the training line has been moved to C++.

While I'm at it I also made sure that the training widget acts more line
standard input widget, so Ctrl+Backspace now deletes the last word.

BUG: 309197
FIXED-IN: 2.0
parent f57bd502
......@@ -44,6 +44,7 @@ set(ktouch_SRCS
declarativeitems/lessonfontsizecalculater.cpp
declarativeitems/preferencesproxy.cpp
declarativeitems/scalebackgrounditem.cpp
declarativeitems/traininglinecore.cpp
core/resource.cpp
core/keyboardlayoutbase.cpp
core/keyboardlayout.cpp
......
......@@ -21,6 +21,7 @@
#include <QGraphicsDropShadowEffect>
#include <QScriptValue>
#include <QScriptEngine>
#include <QKeyEvent>
#include <kdeclarative.h>
......@@ -31,6 +32,7 @@
#include "declarativeitems/lessonfontsizecalculater.h"
#include "declarativeitems/preferencesproxy.h"
#include "declarativeitems/scalebackgrounditem.h"
#include "declarativeitems/traininglinecore.h"
#include "core/keyboardlayout.h"
#include "core/key.h"
#include "core/specialkey.h"
......@@ -117,4 +119,5 @@ void Application::registerQmlTypes()
qmlRegisterType<GridItem>("ktouch", 1, 0 , "Grid");
qmlRegisterType<LessonFontSizeCalculater>("ktouch", 1, 0, "LessonFontSizeCalculater");
qmlRegisterType<ScaleBackgroundItem>("ktouch", 1, 0, "ScaleBackgroundItem");
qmlRegisterType<TrainingLineCore>("ktouch", 1, 0, "TrainingLineCore");
}
/*
* Copyright 2012 Sebastian Gottfried <sebastiangottfried@web.de>
*
* 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.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "traininglinecore.h"
#include <QKeyEvent>
#include <QTextBoundaryFinder>
#include "core/trainingstats.h"
#include "preferences.h"
TrainingLineCore::TrainingLineCore(QDeclarativeItem* parent) :
QDeclarativeItem(parent),
m_active(false),
m_trainingStats(0),
m_hintKey(-1),
m_keyHintOccurrenceCount(0)
{
setFlag(QDeclarativeItem::ItemAcceptsInputMethod);
}
bool TrainingLineCore::active() const
{
return m_active;
}
void TrainingLineCore::setActive(bool active)
{
if (active != m_active)
{
m_active = active;
emit activeChanged();
}
}
TrainingStats* TrainingLineCore::trainingStats() const
{
return m_trainingStats;
}
void TrainingLineCore::setTrainingStats(TrainingStats* trainingStats)
{
if (trainingStats != m_trainingStats)
{
m_trainingStats = trainingStats;
emit trainingStatsChanged();
}
}
QString TrainingLineCore::referenceLine() const
{
return m_referenceLine;
}
void TrainingLineCore::setReferenceLine(const QString& referenceLine)
{
if (referenceLine != m_referenceLine)
{
m_referenceLine = referenceLine;
m_actualLine = "";
emit referenceLineChanged();
emit actualLineChanged();
}
}
QString TrainingLineCore::actualLine() const
{
return m_actualLine;
}
bool TrainingLineCore::isCorrect() const
{
return m_actualLine == m_referenceLine.left(m_actualLine.length());
}
QString TrainingLineCore::nextCharacter() const
{
const int actualLength = m_actualLine.length();
if (actualLength < m_referenceLine.length())
{
return m_referenceLine.at(actualLength);
}
return QString();
}
int TrainingLineCore::hintKey() const
{
return m_keyHintOccurrenceCount >= 3? m_hintKey: -1;
}
void TrainingLineCore::keyPressEvent(QKeyEvent* event)
{
QDeclarativeItem::keyPressEvent(event);
if (!m_active)
{
event->ignore();
return;
}
if (m_referenceLine == m_actualLine)
{
if (Preferences::nextLineWithReturn())
{
if (event->key() == Qt::Key_Return)
{
reset();
clearKeyHint();
emit done();
event->accept();
return;
}
else
{
giveKeyHint(Qt::Key_Return);
}
}
else if (Preferences::nextLineWithSpace())
{
if (event->key() == Qt::Key_Space)
{
reset();
clearKeyHint();
emit done();
event->accept();
return;
}
else
{
giveKeyHint(Qt::Key_Space);
}
}
}
bool unknown = false;
if (event == QKeySequence::DeleteStartOfWord)
{
deleteStartOfWord();
}
else
{
if (event->modifiers() & Qt::ControlModifier)
{
switch (event->key())
{
case Qt::Key_Backspace:
deleteStartOfWord();
break;
default:
unknown = true;
}
}
else
{
switch (event->key())
{
case Qt::Key_Backspace:
backspace();
break;
default:
unknown = true;
}
}
}
if (unknown)
{
const QString text = event->text();
if (!text.isEmpty() && text.at(0).isPrint())
{
add(text);
event->accept();
return;
}
}
if (unknown)
event->ignore();
else
event->accept();
}
void TrainingLineCore::inputMethodEvent(QInputMethodEvent *event)
{
if (!m_active)
{
event->ignore();
return;
}
const QString commitString = event->commitString();
if (!commitString.isEmpty())
{
add(commitString);
}
event->accept();
}
QVariant TrainingLineCore::inputMethodQuery(Qt::InputMethodQuery query) const
{
switch (query)
{
case Qt::ImCursorPosition:
return QVariant(m_actualLine.length());
case Qt::ImSurroundingText:
return QVariant(m_actualLine);
case Qt::ImCurrentSelection:
return QVariant("");
case Qt::ImMaximumTextLength:
return QVariant(m_referenceLine.length());
case Qt::ImAnchorPosition:
return QVariant(m_actualLine.length());
default:
return QVariant();
}
}
void TrainingLineCore::add(const QString& text)
{
const int maxLength = m_referenceLine.length();
const int actualLength = m_actualLine.length();
const QString newText = text.left(maxLength - actualLength);
bool correct = isCorrect();
for (int i = 0; i < newText.length(); i++)
{
const QString character(newText.at(i));
const bool characterIsCorrect = character == m_referenceLine.at(actualLength + i);
if (m_trainingStats)
{
m_trainingStats->logCharacter(character, characterIsCorrect? TrainingStats::CorrectCharacter: TrainingStats::IncorrectCharacter);
}
correct = correct && characterIsCorrect;
if (correct)
{
clearKeyHint();
}
else
{
giveKeyHint(Qt::Key_Backspace);
}
}
m_actualLine += text.left(maxLength - actualLength);
emit actualLineChanged();
}
void TrainingLineCore::backspace()
{
const int actualLength = m_actualLine.length();
if (actualLength > 0)
{
m_actualLine = m_actualLine.left(actualLength - 1);
emit actualLineChanged();
if (isCorrect())
{
clearKeyHint();
}
}
}
void TrainingLineCore::deleteStartOfWord()
{
const int actualLength = m_actualLine.length();
if (actualLength > 0)
{
QTextBoundaryFinder finder(QTextBoundaryFinder::Word, m_actualLine);
finder.setPosition(actualLength);
finder.toPreviousBoundary();
m_actualLine = m_actualLine.left(finder.position());
emit actualLineChanged();
}
}
void TrainingLineCore::reset()
{
m_actualLine = "";
emit actualLineChanged();
}
void TrainingLineCore::giveKeyHint(int key)
{
if (key == m_hintKey)
{
m_keyHintOccurrenceCount++;
}
else
{
m_hintKey = key;
m_keyHintOccurrenceCount = 1;
}
emit hintKeyChanged();
}
void TrainingLineCore::clearKeyHint()
{
m_hintKey = -1;
m_keyHintOccurrenceCount = 0;
emit hintKeyChanged();
}
/*
* Copyright 2012 Sebastian Gottfried <sebastiangottfried@web.de>
*
* 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.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef TRAININGLINECORE_H
#define TRAININGLINECORE_H
#include <QDeclarativeItem>
class TrainingStats;
class TrainingLineCore : public QDeclarativeItem
{
Q_OBJECT
Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged)
Q_PROPERTY(TrainingStats* trainingStats READ trainingStats WRITE setTrainingStats NOTIFY trainingStatsChanged)
Q_PROPERTY(QString referenceLine READ referenceLine WRITE setReferenceLine NOTIFY referenceLineChanged)
Q_PROPERTY(QString actualLine READ actualLine NOTIFY actualLineChanged)
Q_PROPERTY(bool isCorrect READ isCorrect NOTIFY actualLineChanged)
Q_PROPERTY(QString nextCharacter READ nextCharacter NOTIFY actualLineChanged)
Q_PROPERTY(int hintKey READ hintKey NOTIFY hintKeyChanged)
public:
explicit TrainingLineCore(QDeclarativeItem* parent = 0);
bool active() const;
void setActive(bool active);
TrainingStats* trainingStats() const;
void setTrainingStats(TrainingStats* trainingStats);
QString referenceLine() const;
void setReferenceLine(const QString& referenceLine);
QString actualLine() const;
bool isCorrect() const;
QString nextCharacter() const;
int hintKey() const;
signals:
void activeChanged();
void trainingStatsChanged();
void referenceLineChanged();
void actualLineChanged();
void hintKeyChanged();
void done();
protected:
void keyPressEvent(QKeyEvent* event);
void inputMethodEvent(QInputMethodEvent* event);
QVariant inputMethodQuery(Qt::InputMethodQuery query) const;
private:
void add(const QString& text);
void backspace();
void deleteStartOfWord();
void reset();
void giveKeyHint(int key);
void clearKeyHint();
bool m_active;
TrainingStats* m_trainingStats;
QString m_referenceLine;
QString m_actualLine;
int m_hintKey;
int m_keyHintOccurrenceCount;
};
#endif // TRAININGLINECORE_H
......@@ -18,20 +18,13 @@
import QtQuick 1.1
import ktouch 1.0
Item {
TrainingLineCore {
id: line
property string text
property real fontScale
property real charWidth
property bool active
signal done
signal keyPressed(variant event)
signal keyReleased(variant event)
signal newNextChar(string nextChar)
property string enteredText: ""
property int position: 0
property bool isCorrect: true
property int repeatedErrorCount: 0
property int repeatedErrorSolution: SpecialKey.Backspace
......@@ -39,124 +32,20 @@ Item {
height: line.fontScale * LessonFontSizeCalculater.BasePixelSize
focus: true
onTextChanged: resetLine()
onFocusChanged: {
if (!line.activeFocus) {
stats.stopTraining()
}
}
function startTraining() {
stats.startTraining()
stopTimer.restart()
}
function resetLine() {
line.enteredText = ""
line.isCorrect = true
line.position = 0
lineChars.model = 0
lineChars.model = line.text.length
line.repeatedErrorCount = 0
emitNextChar()
}
function deleteLastChar() {
if (line.position === 0) {
return
}
line.position--
var charItem = lineChars.itemAt(line.position)
charItem.text = line.text.charAt(line.position)
charItem.state = "placeholder"
line.enteredText = line.enteredText.substring(0, line.position)
line.isCorrect = line.enteredText === line.text.substring(0, line.position)
if (line.isCorrect) {
line.repeatedErrorCount = 0
trainingStats.stopTraining()
}
emitNextChar()
}
function addChar(newChar) {
if (line.position >= text.length)
return
var correctChar = text.charAt(line.position)
var isCorrect = newChar === correctChar
var charItem = lineChars.itemAt(line.position)
line.enteredText += newChar
if (line.isCorrect) {
stats.logCharacter(correctChar, isCorrect? TrainingStats.CorrectCharacter: TrainingStats.IncorrectCharacter)
}
line.isCorrect = line.isCorrect && isCorrect
if (line.isCorrect) {
line.repeatedErrorCount = 0
}
else {
line.repeatedErrorSolution = SpecialKey.Backspace
line.repeatedErrorCount++
}
charItem.text = newChar !== " " || isCorrect? newChar: "\u2423"
charItem.state = line.isCorrect? "done": "error"
line.position++
emitNextChar()
}
function emitNextChar() {
if (line.position >= line.text.length)
newNextChar(null)
else
newNextChar(line.text.charAt(line.position))
}
Keys.onPressed: {
if (!line.active)
return
if (line.position == text.length && line.isCorrect) {
line.repeatedErrorSolution = preferences.nextLineWithReturn? SpecialKey.Return: SpecialKey.Space
line.repeatedErrorCount++
}
cursorAnimation.restart()
switch(event.key)
{
case Qt.Key_Space:
startTraining()
if (preferences.nextLineWithSpace && line.position == text.length && line.isCorrect) {
resetLine()
line.done()
}
else {
addChar(event.text.charAt(0))
}
break
case Qt.Key_Return:
startTraining()
if (preferences.nextLineWithReturn && line.position == text.length && line.isCorrect) {
resetLine()
line.done()
}
break
case Qt.Key_Backspace:
startTraining()
deleteLastChar()
break
case Qt.Key_Delete:
case Qt.Key_Tab:
break
default:
startTraining()
if (event.text !== "") {
addChar(event.text.charAt(0))
}
break
}
trainingStats.startTraining()
stopTimer.restart()
if (!event.isAutoRepeat) {
line.keyPressed(event)
}
......@@ -165,8 +54,10 @@ Item {
Keys.onReleased: {
if (!line.active)
return
if (!event.isAutoRepeat)
if (!event.isAutoRepeat) {
line.keyReleased(event)
}
}
Timer {
......@@ -179,7 +70,7 @@ Item {
Repeater {
id: lineChars
model: 0
model: referenceLine.length
Item {
id: lineCharWrapper
......@@ -189,7 +80,18 @@ Item {
y: 0
width: lineChar.width * line.fontScale
height: lineChar.height * line.fontScale
state: "placeholder"
state: {
if (index < line.actualLine.length)
{
var charCorrect = line.actualLine[index] === line.referenceLine[index]
var previousCorrect = index == 0 || lineChars.itemAt(index - 1).state === "done"
return charCorrect && previousCorrect? "done": "error"
}