- 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>
111 lines
3.6 KiB
C++
111 lines
3.6 KiB
C++
#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;
|
||
}
|