Type into any window: fake input when no text field is focused

- KWinFakeInput (org_kde_kwin_fake_input v6, offered by KWin to the
  input-method process): keysyms with real modifier keys held
- The KWin backend uses the input method in text fields and fake input
  everywhere else, so shortcuts, terminals and the desktop get keys
- Text rules (auto-capitalization, double-space) only apply in fields
- Always Show setting keeps the keyboard up without a text field

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 21:59:32 +02:00
co-authored by Claude Opus 5.5
parent ba2f17d665
commit f816135162
20 changed files with 303 additions and 23 deletions
+25 -9
View File
@@ -10,6 +10,10 @@ using namespace Qt::StringLiterals;
namespace {
const TextContext Field{.field = true};
// A field with some text already: no auto-shift until a test asks for it.
const TextContext MidSentence{.field = true, .hasSurroundingText = true, .textBeforeCursor = u"so"_s};
KeyDef makeKey(KeyType type, const QString &label = {})
{
KeyDef k;
@@ -44,6 +48,7 @@ private Q_SLOTS:
model.setLayout(TestLayouts::build("us"));
model.setVisibleSections({MainSectionId});
controller = std::make_unique<KeyboardController>(engine, model);
controller->resetContext(MidSentence); // most tests type into a text field
}
void typesLowercase()
@@ -103,7 +108,7 @@ private Q_SLOTS:
void capitalizesAtSentenceStart()
{
controller->resetContext();
controller->resetContext(Field);
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
for (const char *c : {"h", "i", ".", " "})
tap(makeKey(KeyType::Character, QString::fromLatin1(c)));
@@ -114,7 +119,7 @@ private Q_SLOTS:
void noCapitalizationInPasswordFields()
{
TextContext context;
TextContext context = Field;
context.purpose = TextContext::Purpose::Password;
controller->setContext(context);
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
@@ -122,7 +127,7 @@ private Q_SLOTS:
void fieldTextDrivesCapitalization()
{
TextContext context;
TextContext context = Field;
context.hasSurroundingText = true;
context.textBeforeCursor = u"Done! "_s;
controller->setContext(context);
@@ -152,12 +157,12 @@ private Q_SLOTS:
tap(makeKey(KeyType::Character, u"a"_s));
tap(space);
tap(space);
QCOMPARE(engine.output, u"A ");
QCOMPARE(engine.output, u"a ");
}
void autoCapitalizationCanBeTurnedOff()
{
controller->resetContext();
controller->resetContext(Field);
controller->setOptions({.autoCapitalize = false});
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
tap(makeKey(KeyType::Character, u"a"_s));
@@ -166,7 +171,7 @@ private Q_SLOTS:
void doubleTapShiftLocksAtSentenceStart()
{
controller->resetContext(); // a field was focused
controller->resetContext(Field); // a field was focused
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); // auto-shift
const KeyDef shift = makeKey(KeyType::Shift);
controller->keyDown(shift, 5000);
@@ -281,7 +286,7 @@ private Q_SLOTS:
void autoShiftAndCapsLockDontApplyToFunctionKeys()
{
controller->resetContext(); // sentence start: auto-shift on
controller->resetContext(Field); // sentence start: auto-shift on
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
tap(keysymKey(XKB_KEY_Left));
QCOMPARE(engine.lastModifiers, Qt::NoModifier);
@@ -303,10 +308,10 @@ private Q_SLOTS:
lj.shifted = u"2"_s;
KeyDef one = makeKey(KeyType::Character, u"+"_s);
one.shifted = u"1"_s;
controller->resetContext(); // sentence start
controller->resetContext(Field); // sentence start
tap(one);
QCOMPARE(engine.output, u"+"); // not the shifted level
controller->resetContext();
controller->resetContext(Field);
tap(lj);
QCOMPARE(engine.output, u"+Ľ");
tap(makeKey(KeyType::Shift)); // the user's shift does pick it
@@ -325,6 +330,17 @@ private Q_SLOTS:
QVERIFY(engine.output.endsWith(u"é"_s));
}
void noTextRulesWithoutAField()
{
controller->resetContext({}); // keys go to a window, not a text field
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
const KeyDef space = makeKey(KeyType::Space);
tap(makeKey(KeyType::Character, u"a"_s));
tap(space);
tap(space);
QCOMPARE(engine.output, u"a ");
}
void dismissEmitsSignal()
{
int dismissed = 0;
+12
View File
@@ -27,6 +27,18 @@ private Q_SLOTS:
QVERIFY(v.isVisible());
}
void alwaysVisibleShowsWithoutField()
{
VisibilityController v;
v.setAlwaysVisible(true);
QVERIFY(v.isVisible());
v.hide(); // dismiss key still works...
QVERIFY(!v.isVisible());
v.setFieldActive(true); // ...until the focus changes
v.setFieldActive(false);
QVERIFY(v.isVisible());
}
void toggleShowsWithoutField()
{
VisibilityController v;