#include "KeyboardController.h" #include "TextRules.h" #include "engine/InputEngine.h" #include "layout/LayoutModel.h" #include // 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::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 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(); }