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:
2026-10-14 14:59:21 +02:00
co-authored by Claude Opus 5.5
parent 2b5edeb365
commit 92c132c66f
22 changed files with 452 additions and 39 deletions
+2
View File
@@ -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)
+57 -1
View File
@@ -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);
+6 -1
View File
@@ -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
+31
View File
@@ -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"
+53
View File
@@ -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"