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
+91 -35
View File
@@ -2,7 +2,6 @@
#include <QSet>
namespace {
// Letters without a Unicode decomposition, and the key they belong under.
@@ -26,64 +25,121 @@ QString baseLetter(const QString &letter)
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)
KeyDef characterKey(const KeymapKey &source)
{
QSet<QString> onKeys;
for (const auto &row : letters.rows) {
for (const Letter &letter : row)
onKeys.insert(letter.lower);
KeyDef key;
key.label = source.lower;
key.shifted = source.upper;
key.dead = source.deadLower;
key.deadShifted = source.deadUpper;
return key;
}
// The compact letter rows: letters only, and not the keys some keyboards lack.
QList<KeyDef> letterKeys(const KeymapKeys &keymap, int row, const QStringList &flicks)
{
QList<KeyDef> keys;
for (const KeymapKey &source : keymap.rows.value(row + 1)) {
if (!source.isLetter() || source.position != KeymapKey::Position::Regular)
continue;
KeyDef key = characterKey(source);
// iPad letters always uppercase; the keymap may shift some to a symbol (sk: ô → ").
key.shifted = source.lower.toUpper();
key.deadShifted = 0;
key.flick = flicks.value(keys.size());
keys.append(key);
}
return keys;
}
// A full row: every typing key, the shifted symbol shown (and flickable) above.
QList<KeyDef> rowKeys(const KeymapKeys &keymap, int row)
{
QSet<QString> elsewhere; // LSGT often repeats a pair that exists elsewhere (US: < >)
for (const auto &keys : keymap.rows) {
for (const KeymapKey &key : keys) {
if (key.position != KeymapKey::Position::LessGreater)
elsewhere << key.lower << key.upper;
}
}
QHash<QString, QStringList> result;
QList<KeyDef> keys;
for (const KeymapKey &source : keymap.rows.value(row)) {
if (source.position == KeymapKey::Position::LessGreater && elsewhere.contains(source.lower))
continue;
KeyDef key = characterKey(source);
// Show (and flick for) a shifted level that isn't just the capital letter: ! on 1, " on sk ô.
if (!source.deadUpper && source.upper != source.lower && source.upper != source.lower.toUpper())
key.flick = source.upper;
keys.append(key);
}
return keys;
}
Row expandPlaceholders(const Row &row, const KeymapKeys &keymap)
{
Row result;
for (const KeyDef &key : row) {
if (key.type == KeyType::Letters)
result += letterKeys(keymap, key.lettersRow, key.flicks);
else if (key.type == KeyType::Keys)
result += rowKeys(keymap, key.lettersRow);
else
result.append(key);
}
return result;
}
// Long-press choices per letter: the keymap's letters that have no key here
// first (they're what this language types), then the template's.
void addAlternates(Layer &layer, const Layout &tmpl, const KeymapKeys &keymap)
{
QSet<QString> onKeys;
for (const Row &row : layer.rows) {
for (const KeyDef &key : row)
onKeys << key.label << key.shifted.toLower();
}
QHash<QString, QStringList> choices;
const auto add = [&](const QString &base, const QString &choice) {
QStringList &list = result[base];
QStringList &list = choices[base];
if (!onKeys.contains(choice) && !list.contains(choice))
list.append(choice);
};
for (const Letter &extra : letters.extras)
add(baseLetter(extra.lower), extra.lower);
for (const auto &row : keymap.rows) {
for (const KeymapKey &key : row) {
if (key.isLetter())
add(baseLetter(key.lower), key.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);
for (Row &row : layer.rows) {
for (KeyDef &key : row) {
if (key.type == KeyType::Character && key.isLetter() && key.alternates.isEmpty())
key.alternates = choices.value(key.label);
}
}
return result;
}
} // namespace
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters)
Layout buildLayout(const Layout &tmpl, const KeymapKeys &keymap, KeyboardMode mode)
{
Layout layout = tmpl;
layout.name = letters.name.isEmpty() ? tmpl.name : letters.name;
layout.name = keymap.name.isEmpty() ? tmpl.name : keymap.name;
layout.alternates.clear();
const QHash<QString, QStringList> alternates = alternatesByLetter(tmpl, letters);
for (Layer &layer : layout.layers) {
if (mode == KeyboardMode::Full && !layer.fullRows.isEmpty())
layer.rows = layer.fullRows;
layer.fullRows.clear();
for (Row &row : layer.rows)
row = expandLetters(row, letters, alternates);
row = expandPlaceholders(row, keymap);
addAlternates(layer, tmpl, keymap);
}
return layout;
}