Files
sloposk/src/input/KeyboardController.cpp
T
ericek111andClaude Opus 5.5 d44aab7772 Keep the user's shift; trust the text guess less
- 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>
2026-10-15 10:13:27 +02:00

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();
}