- 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>
176 lines
5.1 KiB
C++
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();
|
|
}
|