Full keyboard mode, Ctrl/Alt/Super, dead keys, shift levels

- Full (iPad Pro) mode: every typing key from the keymap, number row,
  tab, caps lock, ctrl/super/alt; shifted symbol shown above and flickable
- Dead keys compose with the locale's XKB compose table
- Ctrl/Alt/Super stick like shift and turn keys into shortcuts; a tapped
  or held shift joins shortcuts and non-text keys, auto-shift and caps
  lock don't
- Auto-capitalization and caps lock only capitalize letters; the user's
  shift picks the keymap's shifted level
- Mode switch in settings keeps key size by resizing the window

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 21:29:24 +02:00
co-authored by Claude Opus 5.5
parent 88338fde22
commit ba2f17d665
36 changed files with 948 additions and 202 deletions
+88 -18
View File
@@ -4,7 +4,7 @@
#include "engine/InputEngine.h"
#include "layout/LayoutModel.h"
#include <xkbcommon/xkbcommon-keysyms.h>
#include <xkbcommon/xkbcommon.h>
// ShiftMode mirrors ShiftState::Mode for QML.
static_assert(int(KeyboardController::ShiftMode::Off) == int(ShiftState::Mode::Off));
@@ -39,16 +39,27 @@ void KeyboardController::keyDown(const KeyDef &key, qint64 timeMs)
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:
deleteBackward(false);
m_backspaceRepeater.start();
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_engine.sendKeysym(key.keysym);
m_repeatingModifiers = shortcutModifiers();
sendKey(key.keysym);
if (key.repeat) {
m_repeatingKeysym = key.keysym;
m_keysymRepeater.start();
@@ -65,6 +76,13 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
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;
@@ -72,18 +90,12 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
stopRepeating(key);
break;
case KeyType::Character:
if (output == KeyOutput::Alternate && !alternate.isEmpty())
typeText(alternate);
else if (output == KeyOutput::Flick && !key.flick.isEmpty())
typeText(key.flick);
else
typeText(key.text(m_shift.isUppercase()));
break;
case KeyType::Space:
typeText(QStringLiteral(" "));
typeCharacter(key, output, alternate);
break;
case KeyType::Enter:
m_engine.sendKeysym(XKB_KEY_Return);
m_deadKeys.cancel();
sendKey(XKB_KEY_Return);
m_textBefore += u'\n';
autoCapitalize();
break;
@@ -102,7 +114,10 @@ void KeyboardController::keyCancelled(const KeyDef &key)
{
if (key.type == KeyType::Shift)
changeShift([&] { m_shift.release(); });
else if (key.type == KeyType::Backspace)
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);
@@ -128,6 +143,7 @@ void KeyboardController::moveCursor(QPoint steps)
void KeyboardController::resetContext()
{
m_deadKeys.cancel();
m_context = {};
m_textBefore.clear();
autoCapitalize();
@@ -147,6 +163,52 @@ void KeyboardController::setOptions(const TypingOptions &options)
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()
@@ -181,7 +243,7 @@ void KeyboardController::stopRepeating(const KeyDef &key)
void KeyboardController::sendRepeatedKeysym()
{
m_engine.sendKeysym(m_repeatingKeysym);
m_engine.sendKeysym(m_repeatingKeysym, m_repeatingModifiers);
}
void KeyboardController::autoCapitalize()
@@ -191,13 +253,21 @@ void KeyboardController::autoCapitalize()
changeShift([&] { m_shift.setAutomatic(capitalize); });
}
ShiftLevel KeyboardController::shiftLevel() const
{
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 before = m_shift.mode();
const ShiftState::Mode mode = m_shift.mode();
const ShiftLevel level = shiftLevel();
change();
if (m_shift.mode() == before)
if (m_shift.mode() == mode && shiftLevel() == level)
return;
m_model.setUppercase(m_shift.isUppercase());
m_model.setShiftLevel(shiftLevel());
Q_EMIT shiftChanged();
}