Full keyboard mode, Ctrl/Alt/Super, dead keys, shift levels
- Full (iPad Pro) mode: every typing key from the keymap, number row, tab, caps lock, ctrl/super/alt; shifted symbol shown above and flickable - Dead keys compose with the locale's XKB compose table - Ctrl/Alt/Super stick like shift and turn keys into shortcuts; a tapped or held shift joins shortcuts and non-text keys, auto-shift and caps lock don't - Auto-capitalization and caps lock only capitalize letters; the user's shift picks the keymap's shifted level - Mode switch in settings keeps key size by resizing the window Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ sloposk_add_test(tst_controller)
|
||||
sloposk_add_test(tst_pointertracker)
|
||||
sloposk_add_test(tst_alternatespicker)
|
||||
sloposk_add_test(tst_textrules)
|
||||
sloposk_add_test(tst_deadkeys)
|
||||
sloposk_add_test(tst_keyrepeater)
|
||||
sloposk_add_test(tst_visibility)
|
||||
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "layout/KeymapLetters.h"
|
||||
#include "layout/KeymapKeys.h"
|
||||
#include "layout/LayoutBuilder.h"
|
||||
#include "layout/LayoutLoader.h"
|
||||
|
||||
@@ -26,7 +26,7 @@ inline Layout builtinTemplate()
|
||||
return *layout;
|
||||
}
|
||||
|
||||
inline KeymapLetters letters(const char *layout, const char *variant = nullptr)
|
||||
inline KeymapKeys keymapKeys(const char *layout, const char *variant = nullptr)
|
||||
{
|
||||
const std::unique_ptr<xkb_context, void (*)(xkb_context *)> context(
|
||||
xkb_context_new(XKB_CONTEXT_NO_ENVIRONMENT_NAMES), xkb_context_unref);
|
||||
@@ -36,12 +36,12 @@ inline KeymapLetters letters(const char *layout, const char *variant = nullptr)
|
||||
xkb_keymap_new_from_names(context.get(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref);
|
||||
if (!keymap)
|
||||
qFatal("no XKB keymap for %s", layout);
|
||||
return readKeymapLetters(keymap.get(), 0);
|
||||
return readKeymapKeys(keymap.get(), 0);
|
||||
}
|
||||
|
||||
inline Layout build(const char *layout, const char *variant = nullptr)
|
||||
inline Layout build(const char *layout, KeyboardMode mode = KeyboardMode::Compact)
|
||||
{
|
||||
return buildLayout(builtinTemplate(), letters(layout, variant));
|
||||
return buildLayout(builtinTemplate(), keymapKeys(layout), mode);
|
||||
}
|
||||
|
||||
} // namespace TestLayouts
|
||||
|
||||
@@ -230,6 +230,101 @@ private Q_SLOTS:
|
||||
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(); // 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(); // sentence start
|
||||
tap(one);
|
||||
QCOMPARE(engine.output, u"+"); // not the shifted level
|
||||
controller->resetContext();
|
||||
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 dismissEmitsSignal()
|
||||
{
|
||||
int dismissed = 0;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "input/DeadKeys.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
class TestDeadKeys : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void composesWithTheNextLetter()
|
||||
{
|
||||
DeadKeys dead(u"en_US.UTF-8"_s);
|
||||
QVERIFY(!dead.isPending());
|
||||
dead.press(XKB_KEY_dead_acute);
|
||||
QVERIFY(dead.isPending());
|
||||
QCOMPARE(dead.compose(u"e"_s), u"é");
|
||||
QVERIFY(!dead.isPending());
|
||||
|
||||
dead.press(XKB_KEY_dead_caron);
|
||||
QCOMPARE(dead.compose(u"C"_s), u"Č");
|
||||
}
|
||||
|
||||
void typesAccentAndLetterWhenNothingComposes()
|
||||
{
|
||||
DeadKeys dead(u"en_US.UTF-8"_s);
|
||||
dead.press(XKB_KEY_dead_caron);
|
||||
QCOMPARE(dead.compose(u"q"_s), u"ˇq");
|
||||
}
|
||||
|
||||
void cancelForgetsTheAccent()
|
||||
{
|
||||
DeadKeys dead(u"en_US.UTF-8"_s);
|
||||
dead.press(XKB_KEY_dead_acute);
|
||||
dead.cancel();
|
||||
QVERIFY(!dead.isPending());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestDeadKeys)
|
||||
#include "tst_deadkeys.moc"
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TestLayouts.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
@@ -90,6 +91,37 @@ private Q_SLOTS:
|
||||
QVERIFY(a.contains(u"á"_s));
|
||||
}
|
||||
|
||||
void fullModeHasEveryTypingKey()
|
||||
{
|
||||
const Layout us = TestLayouts::build("us", KeyboardMode::Full);
|
||||
const auto labels = [&](int row) {
|
||||
QString text;
|
||||
for (const KeyDef &key : us.layers.first().rows[row]) {
|
||||
if (key.type == KeyType::Character)
|
||||
text += key.label;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
QCOMPARE(labels(0), u"`1234567890-=");
|
||||
QCOMPARE(labels(1), u"qwertyuiop[]\\");
|
||||
QCOMPARE(labels(2), u"asdfghjkl;'");
|
||||
QCOMPARE(labels(3), u"zxcvbnm,./"); // LSGT (< >) repeats , . shifted: left out
|
||||
QCOMPARE(letterKey(us, u"1"_s).flick, u"!");
|
||||
QCOMPARE(letterKey(us, u"1"_s).shifted, u"!");
|
||||
}
|
||||
|
||||
void fullModeKeepsDeadKeysAndKeymapShift()
|
||||
{
|
||||
const Layout sk = TestLayouts::build("sk", KeyboardMode::Full);
|
||||
const KeyDef ´ = letterKey(sk, u"´"_s);
|
||||
QCOMPARE(acute.dead, quint32(XKB_KEY_dead_acute));
|
||||
QCOMPARE(acute.deadShifted, quint32(XKB_KEY_dead_caron));
|
||||
QCOMPARE(letterKey(sk, u"ô"_s).shifted, u"\""); // as the physical key
|
||||
QCOMPARE(letterKey(sk, u"ľ"_s).shifted, u"2");
|
||||
// ľ has its own key now, so it's not a long-press choice on l.
|
||||
QVERIFY(!letterKey(sk, u"l"_s).alternates.contains(u"ľ"_s));
|
||||
}
|
||||
|
||||
void noPlaceholdersRemain()
|
||||
{
|
||||
for (const Layer &layer : TestLayouts::build("us").layers) {
|
||||
|
||||
@@ -72,14 +72,18 @@ private Q_SLOTS:
|
||||
void builtLayersHaveEvenRows_data()
|
||||
{
|
||||
QTest::addColumn<QString>("xkbLayout");
|
||||
for (const char *layout : {"us", "sk", "se", "de", "fr", "ru", "gr"})
|
||||
QTest::newRow(layout) << QString::fromLatin1(layout);
|
||||
QTest::addColumn<KeyboardMode>("mode");
|
||||
for (const char *layout : {"us", "sk", "se", "de", "fr", "ru", "gr"}) {
|
||||
QTest::addRow("%s compact", layout) << QString::fromLatin1(layout) << KeyboardMode::Compact;
|
||||
QTest::addRow("%s full", layout) << QString::fromLatin1(layout) << KeyboardMode::Full;
|
||||
}
|
||||
}
|
||||
|
||||
void builtLayersHaveEvenRows()
|
||||
{
|
||||
QFETCH(QString, xkbLayout);
|
||||
const Layout layout = TestLayouts::build(qPrintable(xkbLayout));
|
||||
QFETCH(KeyboardMode, mode);
|
||||
const Layout layout = TestLayouts::build(qPrintable(xkbLayout), mode);
|
||||
for (const Layer &layer : layout.layers) {
|
||||
const QList<QRectF> rects = computeKeyRects(layer, {1000, 300});
|
||||
qsizetype i = 0;
|
||||
|
||||
Reference in New Issue
Block a user