- A tapped shift (on or off) survives the field re-reporting its context: it lasts until a character is typed, the hint changes or a field is focused - An input-method reset forgets the guessed text and pending dead key, so double-space can't delete a character the keyboard never saw - Fields that state typing preferences only get capitals if they ask - Trackpad moves in fields without surrounding text make the guess unknown; Backspace removes a whole grapheme cluster from it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
322 lines
10 KiB
C++
322 lines
10 KiB
C++
#include "KeyboardController.h"
|
|
#include "TextRules.h"
|
|
|
|
#include "engine/InputEngine.h"
|
|
#include "layout/LayoutModel.h"
|
|
|
|
#include <xkbcommon/xkbcommon.h>
|
|
|
|
// ShiftMode mirrors ShiftState::Mode for QML.
|
|
static_assert(int(KeyboardController::ShiftMode::Off) == int(ShiftState::Mode::Off));
|
|
static_assert(int(KeyboardController::ShiftMode::Once) == int(ShiftState::Mode::Once));
|
|
static_assert(int(KeyboardController::ShiftMode::Locked) == int(ShiftState::Mode::Locked));
|
|
|
|
namespace {
|
|
// Enough history for the text rules; fields may report megabytes.
|
|
constexpr qsizetype MaxTextBefore = 256;
|
|
// Stands for text we can't see: neither a sentence start nor a word, so no
|
|
// rule fires on it. For fields that don't report text, after cursor moves.
|
|
constexpr QChar UnknownText = u'\uFFFC';
|
|
// Like a hardware key: no word mode, a faster rate than Backspace.
|
|
constexpr KeyRepeater::Timing KeysymRepeat{.intervalMs = 50, .wordModeAfterMs = std::numeric_limits<int>::max()};
|
|
}
|
|
|
|
KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent)
|
|
: QObject(parent)
|
|
, m_engine(engine)
|
|
, m_model(model)
|
|
, m_hardware(engine)
|
|
, m_keysymRepeater(KeysymRepeat)
|
|
{
|
|
connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward);
|
|
connect(&m_keysymRepeater, &KeyRepeater::repeat, this, &KeyboardController::sendRepeatedKeysym);
|
|
}
|
|
|
|
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); });
|
|
break;
|
|
case KeyType::Modifier:
|
|
m_modifiers.press(key.modifier, timeMs);
|
|
Q_EMIT modifiersChanged();
|
|
break;
|
|
case KeyType::Backspace:
|
|
if (m_deadKeys.isPending()) {
|
|
m_deadKeys.cancel(); // backspace takes back the accent
|
|
} else if (m_modifiers.active()) {
|
|
sendKey(XKB_KEY_BackSpace);
|
|
} else {
|
|
deleteBackward(false);
|
|
m_backspaceRepeater.start();
|
|
}
|
|
break;
|
|
case KeyType::LayerSwitch:
|
|
m_layerBeforeSwitch = m_model.layerName();
|
|
m_model.setLayer(key.target);
|
|
break;
|
|
case KeyType::Keysym:
|
|
m_repeatingModifiers = shortcutModifiers();
|
|
sendKey(key.keysym);
|
|
if (key.repeat) {
|
|
m_repeatingKeysym = key.keysym;
|
|
m_keysymRepeater.start();
|
|
}
|
|
break;
|
|
default:
|
|
break; // everything else acts on release
|
|
}
|
|
}
|
|
|
|
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(); });
|
|
break;
|
|
case KeyType::CapsLock:
|
|
changeShift([&] { m_shift.toggleLock(); });
|
|
break;
|
|
case KeyType::Modifier:
|
|
m_modifiers.release(key.modifier);
|
|
Q_EMIT modifiersChanged();
|
|
break;
|
|
case KeyType::Backspace:
|
|
m_backspaceRepeater.stop();
|
|
break;
|
|
case KeyType::Keysym:
|
|
stopRepeating(key);
|
|
break;
|
|
case KeyType::Character:
|
|
case KeyType::Space:
|
|
typeCharacter(key, output, alternate);
|
|
break;
|
|
case KeyType::Enter:
|
|
m_deadKeys.cancel();
|
|
sendKey(XKB_KEY_Return);
|
|
m_textBefore += u'\n';
|
|
autoCapitalize();
|
|
break;
|
|
case KeyType::Globe:
|
|
Q_EMIT nextLayoutRequested();
|
|
break;
|
|
case KeyType::Dismiss:
|
|
Q_EMIT dismissRequested();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
m_modifiers.release(key.modifier);
|
|
Q_EMIT modifiersChanged();
|
|
} else if (key.type == KeyType::Backspace)
|
|
m_backspaceRepeater.stop();
|
|
else if (key.type == KeyType::Keysym)
|
|
stopRepeating(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);
|
|
}
|
|
|
|
void KeyboardController::moveCursor(QPoint steps)
|
|
{
|
|
const auto repeat = [this](int count, quint32 keysym) {
|
|
for (int i = 0; i < count; ++i)
|
|
m_engine.sendKeysym(keysym);
|
|
};
|
|
repeat(qMax(0, steps.x()), XKB_KEY_Right);
|
|
repeat(qMax(0, -steps.x()), XKB_KEY_Left);
|
|
repeat(qMax(0, steps.y()), XKB_KEY_Down);
|
|
repeat(qMax(0, -steps.y()), XKB_KEY_Up);
|
|
if (!steps.isNull() && !m_context.hasSurroundingText) {
|
|
m_textBefore = UnknownText;
|
|
autoCapitalize();
|
|
}
|
|
}
|
|
|
|
void KeyboardController::resetContext(const TextContext &context)
|
|
{
|
|
m_deadKeys.cancel();
|
|
m_textBefore.clear();
|
|
m_shift.forgetUserChoice();
|
|
setContext(context);
|
|
}
|
|
|
|
void KeyboardController::setContext(const TextContext &context)
|
|
{
|
|
if (context.hasSurroundingText) {
|
|
m_textBefore = context.textBeforeCursor.right(MaxTextBefore);
|
|
} else if (m_context.hasSurroundingText) {
|
|
// The field stopped reporting (an input-method reset): what we knew is stale.
|
|
m_deadKeys.cancel();
|
|
m_textBefore.clear();
|
|
}
|
|
m_context = context;
|
|
autoCapitalize();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void KeyboardController::typeCharacter(const KeyDef &key, KeyOutput output, const QString &alternate)
|
|
{
|
|
const QString base = key.type == KeyType::Space ? QStringLiteral(" ") : key.label;
|
|
if (m_modifiers.active()) {
|
|
// A shortcut such as Ctrl+C: the key's unshifted symbol, with modifiers.
|
|
m_deadKeys.cancel();
|
|
sendKey(xkb_utf32_to_keysym(base.toLower().toUcs4().value(0)));
|
|
return;
|
|
}
|
|
|
|
const ShiftLevel level = shiftLevel();
|
|
if (const quint32 dead = key.deadAt(level); dead && output == KeyOutput::Primary) {
|
|
m_deadKeys.press(dead);
|
|
changeShift([&] { m_shift.characterTyped(); });
|
|
return;
|
|
}
|
|
|
|
QString text = key.type == KeyType::Space ? base : key.text(level);
|
|
if (output == KeyOutput::Alternate && !alternate.isEmpty())
|
|
text = alternate;
|
|
else if (output == KeyOutput::Flick && !key.flick.isEmpty())
|
|
text = key.flick;
|
|
if (m_deadKeys.isPending())
|
|
text = m_deadKeys.compose(text);
|
|
typeText(text);
|
|
}
|
|
|
|
void KeyboardController::sendKey(quint32 keysym)
|
|
{
|
|
m_engine.sendKeysym(keysym, shortcutModifiers());
|
|
useModifiers();
|
|
}
|
|
|
|
Qt::KeyboardModifiers KeyboardController::shortcutModifiers() const
|
|
{
|
|
return m_modifiers.active() | (m_shift.appliesToShortcuts() ? Qt::ShiftModifier : Qt::NoModifier);
|
|
}
|
|
|
|
void KeyboardController::useModifiers()
|
|
{
|
|
m_modifiers.keyTyped();
|
|
Q_EMIT modifiersChanged();
|
|
if (m_shift.appliesToShortcuts())
|
|
changeShift([&] { m_shift.characterTyped(); });
|
|
}
|
|
|
|
void KeyboardController::typeText(const QString &text)
|
|
{
|
|
if (text == u" " && m_options.doubleSpacePeriod && m_context.isProse()
|
|
&& TextRules::wantsDoubleSpacePeriod(m_textBefore)) {
|
|
// "word " + space → "word. ", replacing the first space in place.
|
|
m_engine.replaceBeforeCursor(QStringLiteral(" "), QStringLiteral(". "));
|
|
m_textBefore.chop(1);
|
|
m_textBefore += QStringLiteral(". ");
|
|
} else {
|
|
m_engine.commitText(text);
|
|
m_textBefore += text;
|
|
}
|
|
m_textBefore = m_textBefore.right(MaxTextBefore);
|
|
changeShift([&] { m_shift.characterTyped(); });
|
|
autoCapitalize();
|
|
}
|
|
|
|
void KeyboardController::deleteBackward(bool wholeWord)
|
|
{
|
|
m_engine.sendKeysym(XKB_KEY_BackSpace, wholeWord ? Qt::ControlModifier : Qt::NoModifier);
|
|
m_textBefore = wholeWord ? TextRules::withoutLastWord(m_textBefore)
|
|
: TextRules::withoutLastCharacter(m_textBefore);
|
|
autoCapitalize();
|
|
}
|
|
|
|
// Only the key that started the repeat stops it; another finger's key doesn't.
|
|
void KeyboardController::stopRepeating(const KeyDef &key)
|
|
{
|
|
if (key.keysym == m_repeatingKeysym)
|
|
m_keysymRepeater.stop();
|
|
}
|
|
|
|
void KeyboardController::sendRepeatedKeysym()
|
|
{
|
|
m_engine.sendKeysym(m_repeatingKeysym, m_repeatingModifiers);
|
|
}
|
|
|
|
void KeyboardController::autoCapitalize()
|
|
{
|
|
const bool capitalize = m_options.autoCapitalize && m_context.allowsAutoCapitalization()
|
|
&& TextRules::startsSentence(m_textBefore);
|
|
changeShift([&] { m_shift.setAutomatic(capitalize); });
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
template<typename Change>
|
|
void KeyboardController::changeShift(Change change)
|
|
{
|
|
const ShiftState::Mode mode = m_shift.mode();
|
|
const ShiftLevel level = shiftLevel();
|
|
change();
|
|
if (m_shift.mode() == mode && shiftLevel() == level)
|
|
return;
|
|
m_model.setShiftLevel(shiftLevel());
|
|
Q_EMIT shiftChanged();
|
|
}
|