Hold Keys mode: keys stay down while touched, like a hardware keyboard

- HardwareKeys presses a key's unshifted keysym on touch and releases it
  on lift, through fake input; held keys repeat and combine as on a
  physical keyboard (games, Shift+click)
- Shift/Ctrl/Alt/Super are plain held keys, caps lock is the system's,
  dead keys compose in the app
- No long-press, flick, trackpad or rollover in this mode; nothing is
  left held when the mode is turned off or touches are cancelled
- Settings switch; default stays the touch (virtual keyboard) mode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 22:17:11 +02:00
co-authored by Claude Opus 5.5
parent f816135162
commit e02b1a8237
24 changed files with 319 additions and 6 deletions
+42
View File
@@ -330,6 +330,48 @@ private Q_SLOTS:
QVERIFY(engine.output.endsWith(u"é"_s));
}
void holdKeysPressesWhileDown()
{
controller->setOptions({.holdKeys = true});
const KeyDef shift = makeKey(KeyType::Shift);
const KeyDef a = makeKey(KeyType::Character, u"a"_s);
controller->keyDown(shift, 0);
QCOMPARE(model.isUppercase(), true); // labels follow the held shift
controller->keyDown(a, 10);
controller->keyUp(a, KeyOutput::Primary, {}, 20);
controller->keyUp(shift, KeyOutput::Primary, {}, 30);
using E = FakeEngine::KeyEvent;
QCOMPARE(engine.keyEvents, (QList<E>{{XKB_KEY_Shift_L, true}, {XKB_KEY_a, true},
{XKB_KEY_a, false}, {XKB_KEY_Shift_L, false}}));
QVERIFY(engine.output.isEmpty());
QVERIFY(engine.keysyms.isEmpty());
QCOMPARE(model.isUppercase(), false);
}
void holdKeysLeavesNoKeyStuck()
{
controller->setOptions({.holdKeys = true});
const KeyDef ctrl = modifierKey(u"ctrl"_s);
controller->keyDown(ctrl, 0);
QCOMPARE(controller->modifierModes().value(u"ctrl"_s).toInt(), 1);
controller->setOptions({}); // turned off while held
QVERIFY(engine.held.isEmpty());
controller->keyUp(ctrl, KeyOutput::Primary, {}, 10); // back to sticky rules: harmless
QVERIFY(engine.held.isEmpty());
}
void holdKeysSendsDeadKeysAndCapsLockToTheSystem()
{
controller->setOptions({.holdKeys = true});
KeyDef acute = makeKey(KeyType::Character, u"´"_s);
acute.dead = XKB_KEY_dead_acute;
tap(acute);
tap(makeKey(KeyType::CapsLock));
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Locked);
QCOMPARE(engine.keyEvents.first().keysym, quint32(XKB_KEY_dead_acute));
QCOMPARE(engine.keyEvents[2].keysym, quint32(XKB_KEY_Caps_Lock));
}
void noTextRulesWithoutAField()
{
controller->resetContext({}); // keys go to a window, not a text field