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>
This commit is contained in:
2026-10-14 20:28:39 +02:00
co-authored by Claude Opus 5.5
parent c6c977eb4c
commit 5277ab16ff
49 changed files with 1162 additions and 359 deletions
+110
View File
@@ -0,0 +1,110 @@
#include "LayoutBuilder.h"
#include "KeyGeometry.h"
#include <QSet>
#include <algorithm>
namespace {
// Letters without a Unicode decomposition, and the key they belong under.
const QHash<QString, QString> &baseOverrides()
{
static const QHash<QString, QString> bases = {
{QStringLiteral("ß"), QStringLiteral("s")}, {QStringLiteral("ł"), QStringLiteral("l")},
{QStringLiteral("ø"), QStringLiteral("o")}, {QStringLiteral("æ"), QStringLiteral("a")},
{QStringLiteral("œ"), QStringLiteral("o")}, {QStringLiteral("đ"), QStringLiteral("d")},
{QStringLiteral("ð"), QStringLiteral("d")}, {QStringLiteral("þ"), QStringLiteral("t")},
{QStringLiteral("ħ"), QStringLiteral("h")}, {QStringLiteral("ı"), QStringLiteral("i")},
};
return bases;
}
// "š" → "s": the letter an accented letter is a variant of.
QString baseLetter(const QString &letter)
{
if (const auto it = baseOverrides().constFind(letter); it != baseOverrides().cend())
return *it;
return letter.normalized(QString::NormalizationForm_D).left(1);
}
// Long-press choices per key letter: the keymap's extras first (they're what
// this language types), then the template's, minus letters that have a key.
QHash<QString, QStringList> alternatesByLetter(const Layout &tmpl, const KeymapLetters &letters)
{
QSet<QString> onKeys;
for (const auto &row : letters.rows) {
for (const Letter &letter : row)
onKeys.insert(letter.lower);
}
QHash<QString, QStringList> result;
const auto add = [&](const QString &base, const QString &choice) {
QStringList &list = result[base];
if (!onKeys.contains(choice) && !list.contains(choice))
list.append(choice);
};
for (const Letter &extra : letters.extras)
add(baseLetter(extra.lower), extra.lower);
for (auto it = tmpl.alternates.cbegin(); it != tmpl.alternates.cend(); ++it) {
for (const QString &choice : *it)
add(it.key(), choice);
}
return result;
}
Row expandLetters(const Row &row, const KeymapLetters &letters, const QHash<QString, QStringList> &alternates)
{
Row result;
for (const KeyDef &key : row) {
if (key.type != KeyType::Letters) {
result.append(key);
continue;
}
const QList<Letter> source = letters.rows.value(key.lettersRow);
for (qsizetype i = 0; i < source.size(); ++i) {
KeyDef letter;
letter.label = source[i].lower;
letter.shifted = source[i].upper;
letter.flick = key.flicks.value(i);
letter.alternates = alternates.value(source[i].lower);
result.append(letter);
}
}
return result;
}
void stretchRows(Layer &layer)
{
qreal widest = 0;
for (const Row &row : layer.rows)
widest = std::max(widest, rowUnits(row));
for (Row &row : layer.rows) {
const auto stretchy = std::ranges::count_if(row, &KeyDef::stretch);
if (stretchy == 0)
continue;
const qreal extra = (widest - rowUnits(row)) / qreal(stretchy);
for (KeyDef &key : row) {
if (key.stretch)
key.width += extra;
}
}
}
} // namespace
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters)
{
Layout layout = tmpl;
layout.name = letters.name.isEmpty() ? tmpl.name : letters.name;
layout.alternates.clear();
const QHash<QString, QStringList> alternates = alternatesByLetter(tmpl, letters);
for (Layer &layer : layout.layers) {
for (Row &row : layer.rows)
row = expandLetters(row, letters, alternates);
stretchRows(layer);
}
return layout;
}