Text rules: auto-capitalization, double-space period, backspace repeat
- TextContext: field purpose/hints and text before the cursor, reported by the backend (KWin content_type + surrounding_text) - TextRules: sentence-start and double-space heuristics, deletion helpers - KeyRepeater: held backspace repeats, then deletes whole words (Ctrl+BS) - Controller keeps a best-guess of the text before the cursor so rules work instantly and in fields that don't report surrounding text - No auto-capitalization or smart punctuation in password/URL/email fields Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,8 @@ sloposk_add_test(tst_keygeometry)
|
||||
sloposk_add_test(tst_controller)
|
||||
sloposk_add_test(tst_pointertracker)
|
||||
sloposk_add_test(tst_alternatespicker)
|
||||
sloposk_add_test(tst_textrules)
|
||||
sloposk_add_test(tst_keyrepeater)
|
||||
|
||||
# Needs the QML module itself, not just the C++ backing library.
|
||||
sloposk_add_test(tst_keyboardsurface)
|
||||
|
||||
@@ -14,10 +14,15 @@ class FakeEngine : public InputEngine
|
||||
{
|
||||
public:
|
||||
void commitText(const QString &text) override { output += text; }
|
||||
void sendKeysym(quint32 keysym) override { keysyms.append(keysym); }
|
||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override
|
||||
{
|
||||
keysyms.append(keysym);
|
||||
lastModifiers = modifiers;
|
||||
}
|
||||
|
||||
QString output;
|
||||
QList<quint32> keysyms;
|
||||
Qt::KeyboardModifiers lastModifiers;
|
||||
};
|
||||
|
||||
KeyDef makeKey(KeyType type, const QString &label = {})
|
||||
@@ -105,6 +110,57 @@ private Q_SLOTS:
|
||||
QCOMPARE(model.layerName(), u"numbers");
|
||||
}
|
||||
|
||||
void backspaceStopsOnRelease()
|
||||
{
|
||||
const KeyDef backspace = makeKey(KeyType::Backspace);
|
||||
controller->keyDown(backspace, 0);
|
||||
controller->keyUp(backspace, KeyOutput::Primary, {}, 50);
|
||||
QTest::qWait(700); // past the repeat delay
|
||||
QCOMPARE(engine.keysyms.size(), 1);
|
||||
}
|
||||
|
||||
void capitalizesAtSentenceStart()
|
||||
{
|
||||
controller->resetContext();
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
|
||||
for (const char *c : {"h", "i", ".", " "})
|
||||
tap(makeKey(KeyType::Character, QString::fromLatin1(c)));
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
|
||||
tap(makeKey(KeyType::Character, u"o"_s));
|
||||
QCOMPARE(engine.output, u"Hi. O");
|
||||
}
|
||||
|
||||
void noCapitalizationInPasswordFields()
|
||||
{
|
||||
TextContext context;
|
||||
context.purpose = TextContext::Purpose::Password;
|
||||
controller->setContext(context);
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
|
||||
}
|
||||
|
||||
void fieldTextDrivesCapitalization()
|
||||
{
|
||||
TextContext context;
|
||||
context.hasSurroundingText = true;
|
||||
context.textBeforeCursor = u"Done! "_s;
|
||||
controller->setContext(context);
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
|
||||
context.textBeforeCursor = u"Done, "_s;
|
||||
controller->setContext(context);
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
|
||||
}
|
||||
|
||||
void doubleSpaceTypesPeriod()
|
||||
{
|
||||
const KeyDef space = makeKey(KeyType::Space);
|
||||
tap(makeKey(KeyType::Character, u"a"_s));
|
||||
tap(space);
|
||||
tap(space);
|
||||
QCOMPARE(engine.output, u"a . ");
|
||||
QCOMPARE(engine.keysyms, QList<quint32>{XKB_KEY_BackSpace}); // removed the first space
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
|
||||
}
|
||||
|
||||
void slideTypesSymbolAndSwitchesBack()
|
||||
{
|
||||
KeyDef layer = makeKey(KeyType::LayerSwitch);
|
||||
|
||||
@@ -25,10 +25,15 @@ class FakeEngine : public InputEngine
|
||||
{
|
||||
public:
|
||||
void commitText(const QString &text) override { output += text; }
|
||||
void sendKeysym(quint32 keysym) override { keysyms.append(keysym); }
|
||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override
|
||||
{
|
||||
keysyms.append(keysym);
|
||||
lastModifiers = modifiers;
|
||||
}
|
||||
|
||||
QString output;
|
||||
QList<quint32> keysyms;
|
||||
Qt::KeyboardModifiers lastModifiers;
|
||||
};
|
||||
|
||||
constexpr QPoint KeyboardOffset(0, 100); // room above for popups
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "input/KeyRepeater.h"
|
||||
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
class TestKeyRepeater : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void repeatsThenSwitchesToWords()
|
||||
{
|
||||
KeyRepeater repeater({.initialDelayMs = 50, .intervalMs = 10, .wordModeAfterMs = 150, .wordIntervalMs = 20});
|
||||
QSignalSpy spy(&repeater, &KeyRepeater::repeat);
|
||||
repeater.start();
|
||||
|
||||
QTest::qWait(40);
|
||||
QCOMPARE(spy.count(), 0); // still in the initial delay
|
||||
QTRY_VERIFY(spy.count() >= 3);
|
||||
QCOMPARE(spy.first().first().toBool(), false);
|
||||
QTRY_VERIFY(spy.last().first().toBool()); // word mode eventually
|
||||
|
||||
repeater.stop();
|
||||
const auto count = spy.count();
|
||||
QTest::qWait(60);
|
||||
QCOMPARE(spy.count(), count);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestKeyRepeater)
|
||||
#include "tst_keyrepeater.moc"
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "input/TextRules.h"
|
||||
|
||||
#include <QTest>
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
class TestTextRules : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void startsSentence_data()
|
||||
{
|
||||
QTest::addColumn<QString>("text");
|
||||
QTest::addColumn<bool>("expected");
|
||||
QTest::newRow("empty") << QString() << true;
|
||||
QTest::newRow("after period and space") << u"Hi. "_s << true;
|
||||
QTest::newRow("after question, two spaces") << u"Why? "_s << true;
|
||||
QTest::newRow("after newline") << u"Hi\n"_s << true;
|
||||
QTest::newRow("only spaces") << u" "_s << true;
|
||||
QTest::newRow("period without space") << u"Hi."_s << false;
|
||||
QTest::newRow("mid sentence") << u"Hi there "_s << false;
|
||||
QTest::newRow("after comma") << u"Hi, "_s << false;
|
||||
QTest::newRow("mid word") << u"Hel"_s << false;
|
||||
}
|
||||
|
||||
void startsSentence()
|
||||
{
|
||||
QFETCH(QString, text);
|
||||
QFETCH(bool, expected);
|
||||
QCOMPARE(TextRules::startsSentence(text), expected);
|
||||
}
|
||||
|
||||
void doubleSpacePeriod()
|
||||
{
|
||||
QVERIFY(TextRules::wantsDoubleSpacePeriod(u"word "_s));
|
||||
QVERIFY(TextRules::wantsDoubleSpacePeriod(u"42 "_s));
|
||||
QVERIFY(!TextRules::wantsDoubleSpacePeriod(u"word. "_s));
|
||||
QVERIFY(!TextRules::wantsDoubleSpacePeriod(u"word "_s));
|
||||
QVERIFY(!TextRules::wantsDoubleSpacePeriod(u" "_s));
|
||||
}
|
||||
|
||||
void deletion()
|
||||
{
|
||||
QCOMPARE(TextRules::withoutLastCharacter(u"ab"_s), u"a");
|
||||
QCOMPARE(TextRules::withoutLastCharacter(u"a👍"_s), u"a");
|
||||
QCOMPARE(TextRules::withoutLastWord(u"one two "_s), u"one ");
|
||||
QCOMPARE(TextRules::withoutLastWord(u"one"_s), QString());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestTextRules)
|
||||
#include "tst_textrules.moc"
|
||||
Reference in New Issue
Block a user