Files
sloposk/tests/tst_controller.cpp
T
ericek111andClaude Opus 5.5 f816135162 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>
2026-10-14 21:59:32 +02:00

355 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "FakeEngine.h"
#include "input/KeyboardController.h"
#include "layout/LayoutModel.h"
#include "TestLayouts.h"
#include <QTest>
#include <xkbcommon/xkbcommon-keysyms.h>
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;
k.type = type;
k.label = label;
return k;
}
} // namespace
class TestController : public QObject
{
Q_OBJECT
private:
FakeEngine engine;
LayoutModel model;
std::unique_ptr<KeyboardController> controller;
qint64 now = 0;
void tap(const KeyDef &key, KeyOutput output = KeyOutput::Primary, const QString &alternate = {})
{
now += 1000; // far apart: never a double tap
controller->keyDown(key, now);
controller->keyUp(key, output, alternate, now + 50);
}
private Q_SLOTS:
void init()
{
engine = {};
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()
{
tap(makeKey(KeyType::Character, u"a"_s));
QCOMPARE(engine.output, u"a");
}
void shiftCapitalizesOneCharacter()
{
const KeyDef a = makeKey(KeyType::Character, u"a"_s);
tap(makeKey(KeyType::Shift));
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
tap(a);
tap(a);
QCOMPARE(engine.output, u"Aa");
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
}
void flickAndAlternateOutputs()
{
KeyDef e = makeKey(KeyType::Character, u"e"_s);
e.flick = u"3"_s;
tap(e, KeyOutput::Flick);
tap(e, KeyOutput::Alternate, u"é"_s);
QCOMPARE(engine.output, u"3é");
}
void backspaceActsOnPress()
{
controller->keyDown(makeKey(KeyType::Backspace), 0);
QCOMPARE(engine.keysyms, QList<quint32>{XKB_KEY_BackSpace});
}
void enterSendsReturn()
{
tap(makeKey(KeyType::Enter));
QCOMPARE(engine.keysyms, QList<quint32>{XKB_KEY_Return});
}
void layerSwitchChangesModelLayer()
{
KeyDef layer = makeKey(KeyType::LayerSwitch);
layer.target = u"numbers"_s;
controller->keyDown(layer, 0);
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(Field);
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 = Field;
context.purpose = TextContext::Purpose::Password;
controller->setContext(context);
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
}
void fieldTextDrivesCapitalization()
{
TextContext context = Field;
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.mismatchedReplacements, 0);
// A Backspace keysym may reach the client after the commit and eat the new space.
QVERIFY(engine.keysyms.isEmpty());
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
}
void doubleSpacePeriodCanBeTurnedOff()
{
controller->setOptions({.doubleSpacePeriod = false});
const KeyDef space = makeKey(KeyType::Space);
tap(makeKey(KeyType::Character, u"a"_s));
tap(space);
tap(space);
QCOMPARE(engine.output, u"a ");
}
void autoCapitalizationCanBeTurnedOff()
{
controller->resetContext(Field);
controller->setOptions({.autoCapitalize = false});
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
tap(makeKey(KeyType::Character, u"a"_s));
QCOMPARE(engine.output, u"a");
}
void doubleTapShiftLocksAtSentenceStart()
{
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);
controller->keyUp(shift, KeyOutput::Primary, {}, 5050);
controller->keyDown(shift, 5200);
controller->keyUp(shift, KeyOutput::Primary, {}, 5250);
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Locked);
}
void slideTypesSymbolAndSwitchesBack()
{
KeyDef layer = makeKey(KeyType::LayerSwitch);
layer.target = u"numbers"_s;
controller->keyDown(layer, 0);
controller->slideReleased(makeKey(KeyType::Character, u"5"_s), 100);
QCOMPARE(engine.output, u"5");
QCOMPARE(model.layerName(), u"letters");
}
void trackpadMovesCursor()
{
controller->moveCursor({2, -1});
QCOMPARE(engine.keysyms, (QList<quint32>{XKB_KEY_Right, XKB_KEY_Right, XKB_KEY_Up}));
}
void globeRequestsNextLayout()
{
int requested = 0;
connect(controller.get(), &KeyboardController::nextLayoutRequested, this, [&] { ++requested; });
tap(makeKey(KeyType::Globe));
QCOMPARE(requested, 1);
}
void keysymKeyActsOnPressAndRepeats()
{
KeyDef left = makeKey(KeyType::Keysym, u"←"_s);
left.keysym = XKB_KEY_Left;
left.repeat = true;
controller->keyDown(left, 0);
QCOMPARE(engine.keysyms, QList<quint32>{XKB_KEY_Left});
QTRY_VERIFY_WITH_TIMEOUT(engine.keysyms.size() >= 3, 2000); // held: repeats
controller->keyUp(left, KeyOutput::Primary, {}, 0);
const auto sent = engine.keysyms.size();
QTest::qWait(200);
QCOMPARE(engine.keysyms.size(), sent);
QVERIFY(engine.output.isEmpty()); // no text
}
void sectionsAreComposedAroundTheMainKeys()
{
const qsizetype mainKeys = model.rowCount();
const qreal mainUnits = model.totalUnits();
model.setVisibleSections({MainSectionId, u"function"_s});
QCOMPARE(model.rowCount(), mainKeys + 13);
QCOMPARE(model.totalUnits(), mainUnits); // above, not beside
model.setVisibleSections({MainSectionId, u"navigation"_s});
QCOMPARE(model.totalUnits(), mainUnits + 3);
model.setVisibleSections({u"numpad"_s});
QCOMPARE(model.rowCount(), 19);
model.setVisibleSections({});
QCOMPARE(model.rowCount(), mainKeys); // never nothing
}
KeyDef keysymKey(quint32 keysym)
{
KeyDef key = makeKey(KeyType::Keysym);
key.keysym = keysym;
return key;
}
KeyDef modifierKey(const QString &name)
{
KeyDef key = makeKey(KeyType::Modifier);
key.modifier = name;
return key;
}
void ctrlTurnsKeysIntoShortcuts()
{
tap(modifierKey(u"ctrl"_s));
QCOMPARE(controller->modifierModes().value(u"ctrl"_s).toInt(), int(KeyboardController::ShiftMode::Once));
tap(makeKey(KeyType::Character, u"c"_s));
QCOMPARE(engine.keysyms, QList<quint32>{XKB_KEY_c});
QCOMPARE(engine.lastModifiers, Qt::ControlModifier);
QVERIFY(engine.output.isEmpty());
// One-shot: the next key types again.
tap(makeKey(KeyType::Character, u"c"_s));
QCOMPARE(engine.output, u"c");
}
void heldModifierAppliesToEveryKey()
{
const KeyDef alt = modifierKey(u"alt"_s);
controller->keyDown(alt, 0);
tap(keysymKey(XKB_KEY_Tab));
tap(keysymKey(XKB_KEY_Tab));
controller->keyUp(alt, KeyOutput::Primary, {}, 9000);
QCOMPARE(engine.keysyms.size(), 2);
QCOMPARE(engine.lastModifiers, Qt::AltModifier);
QCOMPARE(controller->modifierModes().value(u"alt"_s).toInt(), int(KeyboardController::ShiftMode::Off));
}
void tappedShiftAppliesToFunctionKeys()
{
tap(makeKey(KeyType::Character, u"a"_s)); // mid-sentence: no auto-shift
tap(makeKey(KeyType::Shift));
tap(keysymKey(XKB_KEY_F5));
QCOMPARE(engine.keysyms.last(), quint32(XKB_KEY_F5));
QCOMPARE(engine.lastModifiers, Qt::ShiftModifier);
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); // used up
}
void autoShiftAndCapsLockDontApplyToFunctionKeys()
{
controller->resetContext(Field); // sentence start: auto-shift on
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
tap(keysymKey(XKB_KEY_Left));
QCOMPARE(engine.lastModifiers, Qt::NoModifier);
tap(makeKey(KeyType::CapsLock));
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Locked);
tap(keysymKey(XKB_KEY_Left));
QCOMPARE(engine.lastModifiers, Qt::NoModifier);
tap(makeKey(KeyType::Character, u"a"_s));
tap(makeKey(KeyType::Character, u"b"_s));
QCOMPARE(engine.output, u"AB");
tap(makeKey(KeyType::CapsLock));
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
}
void autoShiftCapitalizesLettersOnly()
{
KeyDef lj = makeKey(KeyType::Character, u"ľ"_s); // sk number row: Shift+ľ = 2
lj.shifted = u"2"_s;
KeyDef one = makeKey(KeyType::Character, u"+"_s);
one.shifted = u"1"_s;
controller->resetContext(Field); // sentence start
tap(one);
QCOMPARE(engine.output, u"+"); // not the shifted level
controller->resetContext(Field);
tap(lj);
QCOMPARE(engine.output, u"+Ľ");
tap(makeKey(KeyType::Shift)); // the user's shift does pick it
tap(lj);
QCOMPARE(engine.output, u"+Ľ2");
}
void deadKeyComposesWithNextCharacter()
{
KeyDef acute = makeKey(KeyType::Character, u"´"_s);
acute.dead = XKB_KEY_dead_acute;
tap(makeKey(KeyType::Character, u"x"_s));
tap(acute);
QVERIFY(engine.output == u"x"); // nothing typed yet
tap(makeKey(KeyType::Character, u"e"_s));
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;
connect(controller.get(), &KeyboardController::dismissRequested, this, [&] { ++dismissed; });
tap(makeKey(KeyType::Dismiss));
QCOMPARE(dismissed, 1);
}
};
QTEST_GUILESS_MAIN(TestController)
#include "tst_controller.moc"