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
+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);