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
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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
|
## 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
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ Rectangle {
|
|||||||
onToggled: checked => panel.settings.autoCapitalize = checked
|
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 {
|
ToggleRow {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: qsTr("Always Show")
|
text: qsTr("Always Show")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ qt_add_library(sloposk_core STATIC
|
|||||||
input/ShiftState.h input/ShiftState.cpp
|
input/ShiftState.h input/ShiftState.cpp
|
||||||
input/Modifiers.h input/Modifiers.cpp
|
input/Modifiers.h input/Modifiers.cpp
|
||||||
input/DeadKeys.h input/DeadKeys.cpp
|
input/DeadKeys.h input/DeadKeys.cpp
|
||||||
|
input/HardwareKeys.h input/HardwareKeys.cpp
|
||||||
input/PointerTracker.h input/PointerTracker.cpp
|
input/PointerTracker.h input/PointerTracker.cpp
|
||||||
input/AlternatesPicker.h input/AlternatesPicker.cpp
|
input/AlternatesPicker.h input/AlternatesPicker.cpp
|
||||||
input/KeyRepeater.h input/KeyRepeater.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)
|
void KWinFakeInput::typeText(const QString &text)
|
||||||
{
|
{
|
||||||
for (const char32_t ucs : text.toUcs4())
|
for (const char32_t ucs : text.toUcs4())
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public:
|
|||||||
// Presses and releases `keysym` with `modifiers` held. KWin finds the key
|
// Presses and releases `keysym` with `modifiers` held. KWin finds the key
|
||||||
// (and any shift level) in the current keymap.
|
// (and any shift level) in the current keymap.
|
||||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
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.
|
// Types each character as its keysym.
|
||||||
void typeText(const QString &text);
|
void typeText(const QString &text);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ void KWinInputMethod::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers
|
|||||||
m_fakeInput.sendKeysym(keysym, 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)
|
void KWinInputMethod::activate(::zwp_input_method_context_v1 *context)
|
||||||
{
|
{
|
||||||
m_context = std::make_unique<KWinInputContext>(context);
|
m_context = std::make_unique<KWinInputContext>(context);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public:
|
|||||||
void replaceBeforeCursor(const QString &before, const QString &text) override;
|
void replaceBeforeCursor(const QString &before, const QString &text) override;
|
||||||
void setPreedit(const QString &text) override;
|
void setPreedit(const QString &text) override;
|
||||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
||||||
|
void setKeyPressed(quint32 keysym, bool pressed) override;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void availableChanged();
|
void availableChanged();
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ public:
|
|||||||
virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0;
|
virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0;
|
||||||
Q_INVOKABLE virtual void setPreedit(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;
|
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:
|
Q_SIGNALS:
|
||||||
void activeChanged();
|
void activeChanged();
|
||||||
|
|||||||
@@ -21,3 +21,8 @@ void DirectEngine::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
|
|||||||
{
|
{
|
||||||
m_backend.sendKeysym(keysym, 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 commitText(const QString &text) override;
|
||||||
void replaceBeforeCursor(const QString &before, const QString &text) override;
|
void replaceBeforeCursor(const QString &before, const QString &text) override;
|
||||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
||||||
|
void setKeyPressed(quint32 keysym, bool pressed) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TextBackend &m_backend;
|
TextBackend &m_backend;
|
||||||
|
|||||||
@@ -15,4 +15,6 @@ public:
|
|||||||
// See TextBackend::replaceBeforeCursor.
|
// See TextBackend::replaceBeforeCursor.
|
||||||
virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0;
|
virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0;
|
||||||
virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 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)
|
: QObject(parent)
|
||||||
, m_engine(engine)
|
, m_engine(engine)
|
||||||
, m_model(model)
|
, m_model(model)
|
||||||
|
, m_hardware(engine)
|
||||||
, m_keysymRepeater(KeysymRepeat)
|
, m_keysymRepeater(KeysymRepeat)
|
||||||
{
|
{
|
||||||
connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward);
|
connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward);
|
||||||
@@ -30,11 +31,34 @@ KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model,
|
|||||||
|
|
||||||
KeyboardController::ShiftMode KeyboardController::shiftMode() const
|
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());
|
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)
|
void KeyboardController::keyDown(const KeyDef &key, qint64 timeMs)
|
||||||
{
|
{
|
||||||
|
if (holdKey(key, true))
|
||||||
|
return;
|
||||||
switch (key.type) {
|
switch (key.type) {
|
||||||
case KeyType::Shift:
|
case KeyType::Shift:
|
||||||
changeShift([&] { m_shift.press(timeMs); });
|
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)
|
void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64)
|
||||||
{
|
{
|
||||||
|
if (holdKey(key, false))
|
||||||
|
return;
|
||||||
switch (key.type) {
|
switch (key.type) {
|
||||||
case KeyType::Shift:
|
case KeyType::Shift:
|
||||||
changeShift([&] { m_shift.release(); });
|
changeShift([&] { m_shift.release(); });
|
||||||
@@ -112,6 +138,8 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
|
|||||||
|
|
||||||
void KeyboardController::keyCancelled(const KeyDef &key)
|
void KeyboardController::keyCancelled(const KeyDef &key)
|
||||||
{
|
{
|
||||||
|
if (holdKey(key, false))
|
||||||
|
return;
|
||||||
if (key.type == KeyType::Shift)
|
if (key.type == KeyType::Shift)
|
||||||
changeShift([&] { m_shift.release(); });
|
changeShift([&] { m_shift.release(); });
|
||||||
else if (key.type == KeyType::Modifier) {
|
else if (key.type == KeyType::Modifier) {
|
||||||
@@ -125,6 +153,7 @@ void KeyboardController::keyCancelled(const KeyDef &key)
|
|||||||
|
|
||||||
void KeyboardController::slideReleased(const KeyDef &target, qint64 timeMs)
|
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);
|
keyUp(target, KeyOutput::Primary, {}, timeMs);
|
||||||
m_model.setLayer(m_layerBeforeSwitch);
|
m_model.setLayer(m_layerBeforeSwitch);
|
||||||
}
|
}
|
||||||
@@ -158,7 +187,12 @@ void KeyboardController::setContext(const TextContext &context)
|
|||||||
|
|
||||||
void KeyboardController::setOptions(const TypingOptions &options)
|
void KeyboardController::setOptions(const TypingOptions &options)
|
||||||
{
|
{
|
||||||
|
if (m_options.holdKeys && !options.holdKeys)
|
||||||
|
m_hardware.releaseAll();
|
||||||
m_options = options;
|
m_options = options;
|
||||||
|
m_model.setShiftLevel(shiftLevel());
|
||||||
|
Q_EMIT shiftChanged();
|
||||||
|
Q_EMIT modifiersChanged();
|
||||||
autoCapitalize();
|
autoCapitalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +288,8 @@ void KeyboardController::autoCapitalize()
|
|||||||
|
|
||||||
ShiftLevel KeyboardController::shiftLevel() const
|
ShiftLevel KeyboardController::shiftLevel() const
|
||||||
{
|
{
|
||||||
|
if (m_options.holdKeys)
|
||||||
|
return m_hardware.shiftLevel();
|
||||||
if (m_shift.appliesToShortcuts())
|
if (m_shift.appliesToShortcuts())
|
||||||
return ShiftLevel::Shifted;
|
return ShiftLevel::Shifted;
|
||||||
return m_shift.isActive() ? ShiftLevel::Capitals : ShiftLevel::None;
|
return m_shift.isActive() ? ShiftLevel::Capitals : ShiftLevel::None;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "DeadKeys.h"
|
#include "DeadKeys.h"
|
||||||
|
#include "HardwareKeys.h"
|
||||||
#include "KeyRepeater.h"
|
#include "KeyRepeater.h"
|
||||||
#include "Modifiers.h"
|
#include "Modifiers.h"
|
||||||
#include "ShiftState.h"
|
#include "ShiftState.h"
|
||||||
@@ -43,7 +44,9 @@ public:
|
|||||||
KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent = nullptr);
|
KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent = nullptr);
|
||||||
|
|
||||||
ShiftMode shiftMode() const;
|
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 keyDown(const KeyDef &key, qint64 timeMs);
|
||||||
void keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64 timeMs);
|
void keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64 timeMs);
|
||||||
@@ -73,6 +76,8 @@ private:
|
|||||||
void sendKey(quint32 keysym);
|
void sendKey(quint32 keysym);
|
||||||
Qt::KeyboardModifiers shortcutModifiers() const;
|
Qt::KeyboardModifiers shortcutModifiers() const;
|
||||||
ShiftLevel shiftLevel() 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 useModifiers();
|
||||||
void deleteBackward(bool wholeWord);
|
void deleteBackward(bool wholeWord);
|
||||||
void sendRepeatedKeysym();
|
void sendRepeatedKeysym();
|
||||||
@@ -86,6 +91,7 @@ private:
|
|||||||
TypingOptions m_options;
|
TypingOptions m_options;
|
||||||
ShiftState m_shift;
|
ShiftState m_shift;
|
||||||
Modifiers m_modifiers;
|
Modifiers m_modifiers;
|
||||||
|
HardwareKeys m_hardware;
|
||||||
DeadKeys m_deadKeys;
|
DeadKeys m_deadKeys;
|
||||||
KeyRepeater m_backspaceRepeater;
|
KeyRepeater m_backspaceRepeater;
|
||||||
KeyRepeater m_keysymRepeater; // arrows, Delete, ...
|
KeyRepeater m_keysymRepeater; // arrows, Delete, ...
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ void KeyboardSurface::pointerDown(int id, QPointF pos, qint64 timeMs)
|
|||||||
{
|
{
|
||||||
if (!m_model || !m_controller)
|
if (!m_model || !m_controller)
|
||||||
return;
|
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);
|
const int index = m_model->keyAt(pos);
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
@@ -107,6 +109,7 @@ void KeyboardSurface::pointerDown(int id, QPointF pos, qint64 timeMs)
|
|||||||
|
|
||||||
PointerTracker::Config config;
|
PointerTracker::Config config;
|
||||||
config.flickDistance = m_model->keyRect(index).height() * FlickDistanceRatio;
|
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++};
|
const Pointer pointer{PointerTracker(m_model->key(index), pos, config), index, -1, m_nextSerial++};
|
||||||
m_pointers.insert(id, pointer);
|
m_pointers.insert(id, pointer);
|
||||||
m_model->setPressed(index, true);
|
m_model->setPressed(index, true);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ PointerTracker::PointerTracker(const KeyDef &key, QPointF start, Config config)
|
|||||||
|
|
||||||
bool PointerTracker::wantsLongPress() const
|
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)
|
void PointerTracker::moveTo(QPointF pos)
|
||||||
@@ -28,13 +28,13 @@ void PointerTracker::moveTo(QPointF pos)
|
|||||||
|
|
||||||
if (m_key.type == KeyType::LayerSwitch)
|
if (m_key.type == KeyType::LayerSwitch)
|
||||||
m_gesture = Gesture::Slide;
|
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;
|
m_gesture = Gesture::Flick;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PointerTracker::longPressElapsed()
|
bool PointerTracker::longPressElapsed()
|
||||||
{
|
{
|
||||||
if (m_gesture != Gesture::Tap || m_moved)
|
if (!m_config.gestures || m_gesture != Gesture::Tap || m_moved)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (m_key.type == KeyType::Space) {
|
if (m_key.type == KeyType::Space) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public:
|
|||||||
int longPressMs = 450;
|
int longPressMs = 450;
|
||||||
qreal trackpadStep = 14; // horizontal travel per cursor step
|
qreal trackpadStep = 14; // horizontal travel per cursor step
|
||||||
qreal trackpadRowStep = 40; // vertical travel per line
|
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);
|
PointerTracker(const KeyDef &key, QPointF start, Config config);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
struct TypingOptions {
|
struct TypingOptions {
|
||||||
bool autoCapitalize = true;
|
bool autoCapitalize = true;
|
||||||
bool doubleSpacePeriod = true;
|
bool doubleSpacePeriod = true;
|
||||||
|
bool holdKeys = false; // keys stay down while touched, like a hardware keyboard (see HardwareKeys)
|
||||||
|
|
||||||
bool operator==(const TypingOptions &) const = default;
|
bool operator==(const TypingOptions &) const = default;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
|
constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
|
||||||
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
|
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
|
||||||
|
constexpr auto HoldKeysKey = "typing/holdKeys";
|
||||||
constexpr auto VisibleSectionsKey = "layout/visibleSections";
|
constexpr auto VisibleSectionsKey = "layout/visibleSections";
|
||||||
constexpr auto FullKeyboardKey = "layout/fullKeyboard";
|
constexpr auto FullKeyboardKey = "layout/fullKeyboard";
|
||||||
constexpr auto AlwaysVisibleKey = "window/alwaysVisible";
|
constexpr auto AlwaysVisibleKey = "window/alwaysVisible";
|
||||||
@@ -19,6 +20,7 @@ Settings::Settings(QObject *parent)
|
|||||||
const TypingOptions defaults;
|
const TypingOptions defaults;
|
||||||
m_typing.autoCapitalize = store.value(AutoCapitalizeKey, defaults.autoCapitalize).toBool();
|
m_typing.autoCapitalize = store.value(AutoCapitalizeKey, defaults.autoCapitalize).toBool();
|
||||||
m_typing.doubleSpacePeriod = store.value(DoubleSpacePeriodKey, defaults.doubleSpacePeriod).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();
|
m_visibleSections = store.value(VisibleSectionsKey, QStringList{MainSectionId}).toStringList();
|
||||||
if (m_visibleSections.isEmpty())
|
if (m_visibleSections.isEmpty())
|
||||||
m_visibleSections = {MainSectionId};
|
m_visibleSections = {MainSectionId};
|
||||||
@@ -72,6 +74,13 @@ void Settings::setDoubleSpacePeriod(bool enabled)
|
|||||||
setTypingOptions(options);
|
setTypingOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Settings::setHoldKeys(bool enabled)
|
||||||
|
{
|
||||||
|
TypingOptions options = m_typing;
|
||||||
|
options.holdKeys = enabled;
|
||||||
|
setTypingOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
void Settings::setTypingOptions(const TypingOptions &options)
|
void Settings::setTypingOptions(const TypingOptions &options)
|
||||||
{
|
{
|
||||||
if (options == m_typing)
|
if (options == m_typing)
|
||||||
@@ -80,5 +89,6 @@ void Settings::setTypingOptions(const TypingOptions &options)
|
|||||||
QSettings store;
|
QSettings store;
|
||||||
store.setValue(AutoCapitalizeKey, m_typing.autoCapitalize);
|
store.setValue(AutoCapitalizeKey, m_typing.autoCapitalize);
|
||||||
store.setValue(DoubleSpacePeriodKey, m_typing.doubleSpacePeriod);
|
store.setValue(DoubleSpacePeriodKey, m_typing.doubleSpacePeriod);
|
||||||
|
store.setValue(HoldKeysKey, m_typing.holdKeys);
|
||||||
Q_EMIT typingOptionsChanged();
|
Q_EMIT typingOptionsChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class Settings : public QObject
|
|||||||
QML_UNCREATABLE("Created in C++")
|
QML_UNCREATABLE("Created in C++")
|
||||||
Q_PROPERTY(bool autoCapitalize READ autoCapitalize WRITE setAutoCapitalize NOTIFY typingOptionsChanged)
|
Q_PROPERTY(bool autoCapitalize READ autoCapitalize WRITE setAutoCapitalize NOTIFY typingOptionsChanged)
|
||||||
Q_PROPERTY(bool doubleSpacePeriod READ doubleSpacePeriod WRITE setDoubleSpacePeriod 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.
|
// Which key blocks are shown: "main" (the typing keys) and template section ids.
|
||||||
Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged)
|
Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged)
|
||||||
// iPad Pro-style full keyboard (number row, tab, ctrl/alt/super) instead of the compact one.
|
// 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);
|
void setAutoCapitalize(bool enabled);
|
||||||
bool doubleSpacePeriod() const { return m_typing.doubleSpacePeriod; }
|
bool doubleSpacePeriod() const { return m_typing.doubleSpacePeriod; }
|
||||||
void setDoubleSpacePeriod(bool enabled);
|
void setDoubleSpacePeriod(bool enabled);
|
||||||
|
bool holdKeys() const { return m_typing.holdKeys; }
|
||||||
|
void setHoldKeys(bool enabled);
|
||||||
|
|
||||||
QStringList visibleSections() const { return m_visibleSections; }
|
QStringList visibleSections() const { return m_visibleSections; }
|
||||||
// Hiding the last visible section is refused: something must stay on screen.
|
// Hiding the last visible section is refused: something must stay on screen.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "engine/InputEngine.h"
|
#include "engine/InputEngine.h"
|
||||||
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QSet>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
// Records what the keyboard sends and applies it to a simulated field, so
|
// Records what the keyboard sends and applies it to a simulated field, so
|
||||||
@@ -27,8 +28,25 @@ public:
|
|||||||
lastModifiers = modifiers;
|
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
|
QString output; // the field's text
|
||||||
int mismatchedReplacements = 0; // replaced text that wasn't before the cursor
|
int mismatchedReplacements = 0; // replaced text that wasn't before the cursor
|
||||||
QList<quint32> keysyms;
|
QList<quint32> keysyms;
|
||||||
Qt::KeyboardModifiers lastModifiers;
|
Qt::KeyboardModifiers lastModifiers;
|
||||||
|
QList<KeyEvent> keyEvents; // setKeyPressed calls
|
||||||
|
QSet<quint32> held;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -330,6 +330,48 @@ private Q_SLOTS:
|
|||||||
QVERIFY(engine.output.endsWith(u"é"_s));
|
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()
|
void noTextRulesWithoutAField()
|
||||||
{
|
{
|
||||||
controller->resetContext({}); // keys go to a window, not a text field
|
controller->resetContext({}); // keys go to a window, not a text field
|
||||||
|
|||||||
@@ -135,6 +135,22 @@ private Q_SLOTS:
|
|||||||
QCOMPARE(engine.output, u"as");
|
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()
|
void slideFromNumbersKeyTypesAndReturns()
|
||||||
{
|
{
|
||||||
const QPoint layerKey = keyCenter(u".?123"_s, u"layer"_s);
|
const QPoint layerKey = keyCenter(u".?123"_s, u"layer"_s);
|
||||||
|
|||||||
Reference in New Issue
Block a user