Files
sloposk/src/input/KeyboardController.cpp
T
ericek111andClaude Opus 5.5 5277ab16ff Fix double-space period, shift double-tap; settings; letters from system keymap
- Double-space replaced the space via a Backspace keysym, which clients
  apply after the commit: use delete_surrounding_text with the commit
- Tests use a shared FakeEngine that applies ops to a simulated field
- Double-tap shift locks at a sentence start; typing between taps doesn't
- Organization name set: config lives in ~/.config/sloposk/
- Settings (auto-capitalization, double-space period) with a ⚙ panel
- Letter rows come from the compositor's XKB keymap; other letters of the
  layout become long-press alternates; globe switches the system layout
  over org.kde.keyboard
- One JSON template for non-letter layers; stretch keys even out rows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:28:39 +02:00

176 lines
5.1 KiB
C++

#include "KeyboardController.h"
#include "TextRules.h"
#include "engine/InputEngine.h"
#include "layout/LayoutModel.h"
#include <xkbcommon/xkbcommon-keysyms.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;
}
KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent)
: QObject(parent)
, m_engine(engine)
, m_model(model)
{
connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward);
}
KeyboardController::ShiftMode KeyboardController::shiftMode() const
{
return ShiftMode(m_shift.mode());
}
void KeyboardController::keyDown(const KeyDef &key, qint64 timeMs)
{
switch (key.type) {
case KeyType::Shift:
changeShift([&] { m_shift.press(timeMs); });
break;
case KeyType::Backspace:
deleteBackward(false);
m_backspaceRepeater.start();
break;
case KeyType::LayerSwitch:
m_layerBeforeSwitch = m_model.layerName();
m_model.setLayer(key.target);
break;
default:
break; // everything else acts on release
}
}
void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64)
{
switch (key.type) {
case KeyType::Shift:
changeShift([&] { m_shift.release(); });
break;
case KeyType::Backspace:
m_backspaceRepeater.stop();
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(" "));
break;
case KeyType::Enter:
m_engine.sendKeysym(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 (key.type == KeyType::Shift)
changeShift([&] { m_shift.release(); });
else if (key.type == KeyType::Backspace)
m_backspaceRepeater.stop();
}
void KeyboardController::slideReleased(const KeyDef &target, qint64 timeMs)
{
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);
}
void KeyboardController::resetContext()
{
m_context = {};
m_textBefore.clear();
autoCapitalize();
}
void KeyboardController::setContext(const TextContext &context)
{
m_context = context;
if (context.hasSurroundingText)
m_textBefore = context.textBeforeCursor.right(MaxTextBefore);
autoCapitalize();
}
void KeyboardController::setOptions(const TypingOptions &options)
{
m_options = options;
autoCapitalize();
}
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();
}
void KeyboardController::autoCapitalize()
{
const bool capitalize = m_options.autoCapitalize && m_context.allowsAutoCapitalization()
&& TextRules::startsSentence(m_textBefore);
changeShift([&] { m_shift.setAutomatic(capitalize); });
}
template<typename Change>
void KeyboardController::changeShift(Change change)
{
const ShiftState::Mode before = m_shift.mode();
change();
if (m_shift.mode() == before)
return;
m_model.setUppercase(m_shift.isUppercase());
Q_EMIT shiftChanged();
}