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:
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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<KWinInputContext>(context);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "HardwareKeys.h"
|
||||
|
||||
#include "engine/InputEngine.h"
|
||||
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "layout/Layout.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QVariantMap>
|
||||
|
||||
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<quint32, int> m_down; // keysym → fingers holding it
|
||||
bool m_capsLock = false;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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, ...
|
||||
|
||||
@@ -99,6 +99,8 @@ void KeyboardSurface::pointerDown(int id, QPointF pos, qint64 timeMs)
|
||||
{
|
||||
if (!m_model || !m_controller)
|
||||
return;
|
||||
// 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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "engine/InputEngine.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
|
||||
// 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<quint32> keysyms;
|
||||
Qt::KeyboardModifiers lastModifiers;
|
||||
QList<KeyEvent> keyEvents; // setKeyPressed calls
|
||||
QSet<quint32> held;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<quint32>{XKB_KEY_w, XKB_KEY_e}));
|
||||
QTest::touchEvent(&window, touchscreen).release(0, w).stationary(1);
|
||||
QCOMPARE(engine.held, QSet<quint32>{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);
|
||||
|
||||
Reference in New Issue
Block a user