diff --git a/README.md b/README.md index 62b2379..9b4cd64 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML U - Flick a key down for its secondary symbol; long-press for accented alternates - iOS shift: tap = one letter, double-tap = caps lock, hold = shift while held - Works everywhere, not just in text fields: with none focused, keys reach the focused window like a hardware keyboard (shortcuts, terminals, the desktop); "Always Show" in ⚙ keeps it up +- "Hold Keys" (⚙): keys stay down while touched, like a hardware keyboard (games, Shift+click, system key repeat); modifiers are plain keys, dead keys compose in the app; touch conveniences off - Letters follow the system keyboard layout; the globe key switches it - Auto-capitalization, double-space → ". " (both optional, ⚙ on the bottom bar), backspace repeat → word delete - Long-press space = trackpad; slide from `.?123` onto a symbol and release @@ -61,7 +62,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso ## Settings -`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `layout/visibleSections` (`main` plus section ids), `layout/fullKeyboard`, `window/alwaysVisible`. +`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `typing/holdKeys`, `layout/visibleSections` (`main` plus section ids), `layout/fullKeyboard`, `window/alwaysVisible`. ## Architecture diff --git a/qml/SettingsPanel.qml b/qml/SettingsPanel.qml index efc29c0..a6e686a 100644 --- a/qml/SettingsPanel.qml +++ b/qml/SettingsPanel.qml @@ -32,6 +32,14 @@ Rectangle { onToggled: checked => panel.settings.autoCapitalize = checked } + ToggleRow { + width: parent.width + text: qsTr("Hold Keys") + detail: qsTr("Keys stay down while touched, like a hardware keyboard; no accents or flicks") + checked: panel.settings.holdKeys + onToggled: checked => panel.settings.holdKeys = checked + } + ToggleRow { width: parent.width text: qsTr("Always Show") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 65d15dd..64a0d14 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ qt_add_library(sloposk_core STATIC input/ShiftState.h input/ShiftState.cpp input/Modifiers.h input/Modifiers.cpp input/DeadKeys.h input/DeadKeys.cpp + input/HardwareKeys.h input/HardwareKeys.cpp input/PointerTracker.h input/PointerTracker.cpp input/AlternatesPicker.h input/AlternatesPicker.cpp input/KeyRepeater.h input/KeyRepeater.cpp diff --git a/src/backend/KWinFakeInput.cpp b/src/backend/KWinFakeInput.cpp index 5e58a50..a0472ed 100644 --- a/src/backend/KWinFakeInput.cpp +++ b/src/backend/KWinFakeInput.cpp @@ -53,6 +53,12 @@ void KWinFakeInput::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) } } +void KWinFakeInput::setKeyPressed(quint32 keysym, bool pressed) +{ + if (isActive()) + keyboard_keysym(keysym, pressed ? Pressed : Released); +} + void KWinFakeInput::typeText(const QString &text) { for (const char32_t ucs : text.toUcs4()) diff --git a/src/backend/KWinFakeInput.h b/src/backend/KWinFakeInput.h index 7acb1a6..7f62a64 100644 --- a/src/backend/KWinFakeInput.h +++ b/src/backend/KWinFakeInput.h @@ -19,6 +19,9 @@ public: // Presses and releases `keysym` with `modifiers` held. KWin finds the key // (and any shift level) in the current keymap. void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier); + // Presses or releases one key, for keys held as long as the finger is + // down. Modifiers are keysyms too (Shift_L...): KWin holds the real key. + void setKeyPressed(quint32 keysym, bool pressed); // Types each character as its keysym. void typeText(const QString &text); }; diff --git a/src/backend/KWinInputMethod.cpp b/src/backend/KWinInputMethod.cpp index a2a4bab..578ef05 100644 --- a/src/backend/KWinInputMethod.cpp +++ b/src/backend/KWinInputMethod.cpp @@ -78,6 +78,13 @@ void KWinInputMethod::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers m_fakeInput.sendKeysym(keysym, modifiers); } +// Always fake input, even in a text field: held keys are key events, and the +// input method's keysyms can't hold a modifier across keys. +void KWinInputMethod::setKeyPressed(quint32 keysym, bool pressed) +{ + m_fakeInput.setKeyPressed(keysym, pressed); +} + void KWinInputMethod::activate(::zwp_input_method_context_v1 *context) { m_context = std::make_unique(context); diff --git a/src/backend/KWinInputMethod.h b/src/backend/KWinInputMethod.h index b2999ca..47afe4b 100644 --- a/src/backend/KWinInputMethod.h +++ b/src/backend/KWinInputMethod.h @@ -50,6 +50,7 @@ public: void replaceBeforeCursor(const QString &before, const QString &text) override; void setPreedit(const QString &text) override; void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override; + void setKeyPressed(quint32 keysym, bool pressed) override; Q_SIGNALS: void availableChanged(); diff --git a/src/backend/TextBackend.h b/src/backend/TextBackend.h index 783c68c..f0eb954 100644 --- a/src/backend/TextBackend.h +++ b/src/backend/TextBackend.h @@ -29,6 +29,9 @@ public: virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0; Q_INVOKABLE virtual void setPreedit(const QString &text) = 0; Q_INVOKABLE virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0; + // Presses or releases one key, held in between like a hardware key, to + // the focused window. Modifiers are keys too (Shift_L, Control_L...). + virtual void setKeyPressed(quint32 keysym, bool pressed) = 0; Q_SIGNALS: void activeChanged(); diff --git a/src/engine/DirectEngine.cpp b/src/engine/DirectEngine.cpp index 45ed709..b27f8b8 100644 --- a/src/engine/DirectEngine.cpp +++ b/src/engine/DirectEngine.cpp @@ -21,3 +21,8 @@ void DirectEngine::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) { m_backend.sendKeysym(keysym, modifiers); } + +void DirectEngine::setKeyPressed(quint32 keysym, bool pressed) +{ + m_backend.setKeyPressed(keysym, pressed); +} diff --git a/src/engine/DirectEngine.h b/src/engine/DirectEngine.h index b6e857e..709e55a 100644 --- a/src/engine/DirectEngine.h +++ b/src/engine/DirectEngine.h @@ -12,6 +12,7 @@ public: void commitText(const QString &text) override; void replaceBeforeCursor(const QString &before, const QString &text) override; void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override; + void setKeyPressed(quint32 keysym, bool pressed) override; private: TextBackend &m_backend; diff --git a/src/engine/InputEngine.h b/src/engine/InputEngine.h index 65fda88..aaf5484 100644 --- a/src/engine/InputEngine.h +++ b/src/engine/InputEngine.h @@ -15,4 +15,6 @@ public: // See TextBackend::replaceBeforeCursor. virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0; virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0; + // See TextBackend::setKeyPressed. + virtual void setKeyPressed(quint32 keysym, bool pressed) = 0; }; diff --git a/src/input/HardwareKeys.cpp b/src/input/HardwareKeys.cpp new file mode 100644 index 0000000..782d18a --- /dev/null +++ b/src/input/HardwareKeys.cpp @@ -0,0 +1,98 @@ +#include "HardwareKeys.h" + +#include "engine/InputEngine.h" + +#include + +namespace { + +quint32 modifierKeysym(const QString &name) +{ + if (name == u"ctrl") + return XKB_KEY_Control_L; + if (name == u"alt") + return XKB_KEY_Alt_L; + return XKB_KEY_Super_L; +} + +} // namespace + +HardwareKeys::HardwareKeys(InputEngine &engine) + : m_engine(engine) +{ +} + +bool HardwareKeys::handles(const KeyDef &key) +{ + return keysymOf(key) != XKB_KEY_NoSymbol; +} + +// The key's unshifted symbol: a held Shift (or caps lock) makes the app pick +// the shifted one, as with a hardware key. +quint32 HardwareKeys::keysymOf(const KeyDef &key) +{ + switch (key.type) { + case KeyType::Character: + return key.dead ? key.dead : xkb_utf32_to_keysym(key.label.toUcs4().value(0)); + case KeyType::Space: return XKB_KEY_space; + case KeyType::Backspace: return XKB_KEY_BackSpace; + case KeyType::Enter: return XKB_KEY_Return; + case KeyType::Shift: return XKB_KEY_Shift_L; + case KeyType::CapsLock: return XKB_KEY_Caps_Lock; + case KeyType::Modifier: return modifierKeysym(key.modifier); + case KeyType::Keysym: return key.keysym; + default: return XKB_KEY_NoSymbol; + } +} + +void HardwareKeys::press(const KeyDef &key) +{ + const quint32 keysym = keysymOf(key); + if (keysym == XKB_KEY_NoSymbol) + return; + if (m_down[keysym]++ == 0) + m_engine.setKeyPressed(keysym, true); + if (key.type == KeyType::CapsLock) + m_capsLock = !m_capsLock; +} + +void HardwareKeys::release(const KeyDef &key) +{ + const quint32 keysym = keysymOf(key); + const auto it = m_down.find(keysym); + if (it == m_down.end()) + return; + if (--*it == 0) { + m_down.erase(it); + m_engine.setKeyPressed(keysym, false); + } +} + +void HardwareKeys::releaseAll() +{ + for (auto it = m_down.cbegin(); it != m_down.cend(); ++it) + m_engine.setKeyPressed(it.key(), false); + m_down.clear(); +} + +bool HardwareKeys::isShiftHeld() const +{ + return m_down.contains(XKB_KEY_Shift_L); +} + +ShiftLevel HardwareKeys::shiftLevel() const +{ + if (isShiftHeld()) + return ShiftLevel::Shifted; + return m_capsLock ? ShiftLevel::Capitals : ShiftLevel::None; +} + +QVariantMap HardwareKeys::modifierModes() const +{ + QVariantMap modes; + for (const char *name : {"ctrl", "alt", "super"}) { + const QString key = QString::fromLatin1(name); + modes.insert(key, m_down.contains(modifierKeysym(key)) ? 1 : 0); + } + return modes; +} diff --git a/src/input/HardwareKeys.h b/src/input/HardwareKeys.h new file mode 100644 index 0000000..d571e19 --- /dev/null +++ b/src/input/HardwareKeys.h @@ -0,0 +1,41 @@ +#pragma once + +#include "layout/Layout.h" + +#include +#include + +class InputEngine; + +// The "hold keys" mode: the keyboard acts like a hardware one. A key is down +// while the finger is, so apps see held keys (games, Shift+click, the +// system's key repeat). Shift, Ctrl, Alt and Super are plain keys, caps lock +// is the system's, and dead keys compose in the app; none of the touch +// conveniences (sticky shift, auto-capitalization, accents) apply. +class HardwareKeys +{ +public: + explicit HardwareKeys(InputEngine &engine); + + // True if `key` is one this mode presses (not a layer, globe or dismiss key). + static bool handles(const KeyDef &key); + + void press(const KeyDef &key); + void release(const KeyDef &key); + // Lets go of everything still down, e.g. when the mode is turned off. + void releaseAll(); + + // For the view: shift shown while held, caps lock while on. + ShiftLevel shiftLevel() const; + bool isShiftHeld() const; + bool isCapsLockOn() const { return m_capsLock; } + // "ctrl"/"alt"/"super" → 1 while held (ShiftState::Mode::Once), else 0. + QVariantMap modifierModes() const; + +private: + static quint32 keysymOf(const KeyDef &key); + + InputEngine &m_engine; + QHash m_down; // keysym → fingers holding it + bool m_capsLock = false; +}; diff --git a/src/input/KeyboardController.cpp b/src/input/KeyboardController.cpp index 8ae6643..de9dbb3 100644 --- a/src/input/KeyboardController.cpp +++ b/src/input/KeyboardController.cpp @@ -22,6 +22,7 @@ KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model, : QObject(parent) , m_engine(engine) , m_model(model) + , m_hardware(engine) , m_keysymRepeater(KeysymRepeat) { connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward); @@ -30,11 +31,34 @@ KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model, KeyboardController::ShiftMode KeyboardController::shiftMode() const { + if (m_options.holdKeys) { + if (m_hardware.isCapsLockOn()) + return ShiftMode::Locked; + return m_hardware.isShiftHeld() ? ShiftMode::Once : ShiftMode::Off; + } return ShiftMode(m_shift.mode()); } +QVariantMap KeyboardController::modifierModes() const +{ + return m_options.holdKeys ? m_hardware.modifierModes() : m_modifiers.modes(); +} + +bool KeyboardController::holdKey(const KeyDef &key, bool pressed) +{ + if (!m_options.holdKeys || !HardwareKeys::handles(key)) + return false; + pressed ? m_hardware.press(key) : m_hardware.release(key); + m_model.setShiftLevel(shiftLevel()); + Q_EMIT shiftChanged(); + Q_EMIT modifiersChanged(); + return true; +} + void KeyboardController::keyDown(const KeyDef &key, qint64 timeMs) { + if (holdKey(key, true)) + return; switch (key.type) { case KeyType::Shift: changeShift([&] { m_shift.press(timeMs); }); @@ -72,6 +96,8 @@ void KeyboardController::keyDown(const KeyDef &key, qint64 timeMs) void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64) { + if (holdKey(key, false)) + return; switch (key.type) { case KeyType::Shift: changeShift([&] { m_shift.release(); }); @@ -112,6 +138,8 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin void KeyboardController::keyCancelled(const KeyDef &key) { + if (holdKey(key, false)) + return; if (key.type == KeyType::Shift) changeShift([&] { m_shift.release(); }); else if (key.type == KeyType::Modifier) { @@ -125,6 +153,7 @@ void KeyboardController::keyCancelled(const KeyDef &key) void KeyboardController::slideReleased(const KeyDef &target, qint64 timeMs) { + holdKey(target, true); // only in hold-keys mode: the slide never pressed it keyUp(target, KeyOutput::Primary, {}, timeMs); m_model.setLayer(m_layerBeforeSwitch); } @@ -158,7 +187,12 @@ void KeyboardController::setContext(const TextContext &context) void KeyboardController::setOptions(const TypingOptions &options) { + if (m_options.holdKeys && !options.holdKeys) + m_hardware.releaseAll(); m_options = options; + m_model.setShiftLevel(shiftLevel()); + Q_EMIT shiftChanged(); + Q_EMIT modifiersChanged(); autoCapitalize(); } @@ -254,6 +288,8 @@ void KeyboardController::autoCapitalize() ShiftLevel KeyboardController::shiftLevel() const { + if (m_options.holdKeys) + return m_hardware.shiftLevel(); if (m_shift.appliesToShortcuts()) return ShiftLevel::Shifted; return m_shift.isActive() ? ShiftLevel::Capitals : ShiftLevel::None; diff --git a/src/input/KeyboardController.h b/src/input/KeyboardController.h index 92f83f5..149a061 100644 --- a/src/input/KeyboardController.h +++ b/src/input/KeyboardController.h @@ -1,6 +1,7 @@ #pragma once #include "DeadKeys.h" +#include "HardwareKeys.h" #include "KeyRepeater.h" #include "Modifiers.h" #include "ShiftState.h" @@ -43,7 +44,9 @@ public: KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent = nullptr); ShiftMode shiftMode() const; - QVariantMap modifierModes() const { return m_modifiers.modes(); } + QVariantMap modifierModes() const; + // Hold-keys mode: no long-press, flick or trackpad gestures, no rollover. + bool holdsKeys() const { return m_options.holdKeys; } void keyDown(const KeyDef &key, qint64 timeMs); void keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64 timeMs); @@ -73,6 +76,8 @@ private: void sendKey(quint32 keysym); Qt::KeyboardModifiers shortcutModifiers() const; ShiftLevel shiftLevel() const; + // Hold-keys mode: forwards the key to m_hardware; false if it isn't one it presses. + bool holdKey(const KeyDef &key, bool pressed); void useModifiers(); void deleteBackward(bool wholeWord); void sendRepeatedKeysym(); @@ -86,6 +91,7 @@ private: TypingOptions m_options; ShiftState m_shift; Modifiers m_modifiers; + HardwareKeys m_hardware; DeadKeys m_deadKeys; KeyRepeater m_backspaceRepeater; KeyRepeater m_keysymRepeater; // arrows, Delete, ... diff --git a/src/input/KeyboardSurface.cpp b/src/input/KeyboardSurface.cpp index db5c989..bd5786e 100644 --- a/src/input/KeyboardSurface.cpp +++ b/src/input/KeyboardSurface.cpp @@ -99,7 +99,9 @@ void KeyboardSurface::pointerDown(int id, QPointF pos, qint64 timeMs) { if (!m_model || !m_controller) return; - commitPendingTaps(timeMs); + // Rollover would let go of keys still held, which hold-keys mode must not. + if (!m_controller->holdsKeys()) + commitPendingTaps(timeMs); const int index = m_model->keyAt(pos); if (index < 0) @@ -107,6 +109,7 @@ void KeyboardSurface::pointerDown(int id, QPointF pos, qint64 timeMs) PointerTracker::Config config; config.flickDistance = m_model->keyRect(index).height() * FlickDistanceRatio; + config.gestures = !m_controller->holdsKeys(); const Pointer pointer{PointerTracker(m_model->key(index), pos, config), index, -1, m_nextSerial++}; m_pointers.insert(id, pointer); m_model->setPressed(index, true); diff --git a/src/input/PointerTracker.cpp b/src/input/PointerTracker.cpp index a1081a7..1b462ac 100644 --- a/src/input/PointerTracker.cpp +++ b/src/input/PointerTracker.cpp @@ -12,7 +12,7 @@ PointerTracker::PointerTracker(const KeyDef &key, QPointF start, Config config) bool PointerTracker::wantsLongPress() const { - return m_key.type == KeyType::Space || !m_key.alternates.isEmpty(); + return m_config.gestures && (m_key.type == KeyType::Space || !m_key.alternates.isEmpty()); } void PointerTracker::moveTo(QPointF pos) @@ -28,13 +28,13 @@ void PointerTracker::moveTo(QPointF pos) if (m_key.type == KeyType::LayerSwitch) m_gesture = Gesture::Slide; - else if (!m_key.flick.isEmpty() && delta.y() > qAbs(delta.x())) + else if (m_config.gestures && !m_key.flick.isEmpty() && delta.y() > qAbs(delta.x())) m_gesture = Gesture::Flick; } bool PointerTracker::longPressElapsed() { - if (m_gesture != Gesture::Tap || m_moved) + if (!m_config.gestures || m_gesture != Gesture::Tap || m_moved) return false; if (m_key.type == KeyType::Space) { diff --git a/src/input/PointerTracker.h b/src/input/PointerTracker.h index 8c1ad73..94a3ae6 100644 --- a/src/input/PointerTracker.h +++ b/src/input/PointerTracker.h @@ -24,6 +24,7 @@ public: int longPressMs = 450; qreal trackpadStep = 14; // horizontal travel per cursor step qreal trackpadRowStep = 40; // vertical travel per line + bool gestures = true; // false: only taps and layer slides (hold-keys mode) }; PointerTracker(const KeyDef &key, QPointF start, Config config); diff --git a/src/input/TypingOptions.h b/src/input/TypingOptions.h index 710f185..fa81f22 100644 --- a/src/input/TypingOptions.h +++ b/src/input/TypingOptions.h @@ -5,6 +5,7 @@ struct TypingOptions { bool autoCapitalize = true; bool doubleSpacePeriod = true; + bool holdKeys = false; // keys stay down while touched, like a hardware keyboard (see HardwareKeys) bool operator==(const TypingOptions &) const = default; }; diff --git a/src/settings/Settings.cpp b/src/settings/Settings.cpp index 40f6c3b..68723d2 100644 --- a/src/settings/Settings.cpp +++ b/src/settings/Settings.cpp @@ -7,6 +7,7 @@ namespace { constexpr auto AutoCapitalizeKey = "typing/autoCapitalize"; constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod"; +constexpr auto HoldKeysKey = "typing/holdKeys"; constexpr auto VisibleSectionsKey = "layout/visibleSections"; constexpr auto FullKeyboardKey = "layout/fullKeyboard"; constexpr auto AlwaysVisibleKey = "window/alwaysVisible"; @@ -19,6 +20,7 @@ Settings::Settings(QObject *parent) const TypingOptions defaults; m_typing.autoCapitalize = store.value(AutoCapitalizeKey, defaults.autoCapitalize).toBool(); m_typing.doubleSpacePeriod = store.value(DoubleSpacePeriodKey, defaults.doubleSpacePeriod).toBool(); + m_typing.holdKeys = store.value(HoldKeysKey, defaults.holdKeys).toBool(); m_visibleSections = store.value(VisibleSectionsKey, QStringList{MainSectionId}).toStringList(); if (m_visibleSections.isEmpty()) m_visibleSections = {MainSectionId}; @@ -72,6 +74,13 @@ void Settings::setDoubleSpacePeriod(bool enabled) setTypingOptions(options); } +void Settings::setHoldKeys(bool enabled) +{ + TypingOptions options = m_typing; + options.holdKeys = enabled; + setTypingOptions(options); +} + void Settings::setTypingOptions(const TypingOptions &options) { if (options == m_typing) @@ -80,5 +89,6 @@ void Settings::setTypingOptions(const TypingOptions &options) QSettings store; store.setValue(AutoCapitalizeKey, m_typing.autoCapitalize); store.setValue(DoubleSpacePeriodKey, m_typing.doubleSpacePeriod); + store.setValue(HoldKeysKey, m_typing.holdKeys); Q_EMIT typingOptionsChanged(); } diff --git a/src/settings/Settings.h b/src/settings/Settings.h index d2a2e3b..0bacb5f 100644 --- a/src/settings/Settings.h +++ b/src/settings/Settings.h @@ -15,6 +15,7 @@ class Settings : public QObject QML_UNCREATABLE("Created in C++") Q_PROPERTY(bool autoCapitalize READ autoCapitalize WRITE setAutoCapitalize NOTIFY typingOptionsChanged) Q_PROPERTY(bool doubleSpacePeriod READ doubleSpacePeriod WRITE setDoubleSpacePeriod NOTIFY typingOptionsChanged) + Q_PROPERTY(bool holdKeys READ holdKeys WRITE setHoldKeys NOTIFY typingOptionsChanged) // Which key blocks are shown: "main" (the typing keys) and template section ids. Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged) // iPad Pro-style full keyboard (number row, tab, ctrl/alt/super) instead of the compact one. @@ -31,6 +32,8 @@ public: void setAutoCapitalize(bool enabled); bool doubleSpacePeriod() const { return m_typing.doubleSpacePeriod; } void setDoubleSpacePeriod(bool enabled); + bool holdKeys() const { return m_typing.holdKeys; } + void setHoldKeys(bool enabled); QStringList visibleSections() const { return m_visibleSections; } // Hiding the last visible section is refused: something must stay on screen. diff --git a/tests/FakeEngine.h b/tests/FakeEngine.h index fab536f..b96f103 100644 --- a/tests/FakeEngine.h +++ b/tests/FakeEngine.h @@ -3,6 +3,7 @@ #include "engine/InputEngine.h" #include +#include #include // Records what the keyboard sends and applies it to a simulated field, so @@ -27,8 +28,25 @@ public: lastModifiers = modifiers; } + void setKeyPressed(quint32 keysym, bool pressed) override + { + keyEvents.append({keysym, pressed}); + if (pressed) + held.insert(keysym); + else + held.remove(keysym); + } + + struct KeyEvent { + quint32 keysym; + bool pressed; + bool operator==(const KeyEvent &) const = default; + }; + QString output; // the field's text int mismatchedReplacements = 0; // replaced text that wasn't before the cursor QList keysyms; Qt::KeyboardModifiers lastModifiers; + QList keyEvents; // setKeyPressed calls + QSet held; }; diff --git a/tests/tst_controller.cpp b/tests/tst_controller.cpp index e347ec3..3fc8a00 100644 --- a/tests/tst_controller.cpp +++ b/tests/tst_controller.cpp @@ -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{{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 diff --git a/tests/tst_keyboardsurface.cpp b/tests/tst_keyboardsurface.cpp index 0ae0c8c..e05f549 100644 --- a/tests/tst_keyboardsurface.cpp +++ b/tests/tst_keyboardsurface.cpp @@ -135,6 +135,22 @@ private Q_SLOTS: QCOMPARE(engine.output, u"as"); } + void holdKeysKeepsEveryFingerDown() + { + controller->setOptions({.holdKeys = true}); + const QPoint w = keyCenter(u"w"_s), e = keyCenter(u"e"_s); + QTest::touchEvent(&window, touchscreen).press(0, w); + QTest::touchEvent(&window, touchscreen).stationary(0).press(1, e); + QTest::qWait(600); // past the long-press delay: e has accents, but no popup + QCOMPARE(engine.held, (QSet{XKB_KEY_w, XKB_KEY_e})); + QTest::touchEvent(&window, touchscreen).release(0, w).stationary(1); + QCOMPARE(engine.held, QSet{XKB_KEY_e}); + QTest::touchEvent(&window, touchscreen).release(1, e); + QVERIFY(engine.held.isEmpty()); + QVERIFY(engine.output.isEmpty()); // key events only, no text + controller->setOptions({}); + } + void slideFromNumbersKeyTypesAndReturns() { const QPoint layerKey = keyCenter(u".?123"_s, u"layer"_s);