Files
sloposk/src/layout/LayoutBuilder.cpp
T
ericek111andClaude Opus 5.5 ac67ceac18 Full keyboard: stagger the letter rows like a hardware keyboard
- The ISO key left of Z takes its room from left shift (2.25 → 1.25 + 1),
  so ISO layouts no longer make the shift row the widest
- Caps lock and left shift keep their width; only right-hand keys stretch,
  so A no longer lines up with the key below it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:42:11 +02:00

157 lines
5.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "LayoutBuilder.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);
}
KeyDef characterKey(const KeymapKey &source)
{
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.
// `withLessGreater` tells whether the ISO key left of Z is among them.
QList<KeyDef> rowKeys(const KeymapKeys &keymap, int row, bool *withLessGreater)
{
*withLessGreater = false;
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;
}
}
QList<KeyDef> keys;
for (const KeymapKey &source : keymap.rows.value(row)) {
if (source.position == KeymapKey::Position::LessGreater && elsewhere.contains(source.lower))
continue;
*withLessGreater |= source.position == KeymapKey::Position::LessGreater;
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) {
bool withLessGreater = false;
const QList<KeyDef> keys = rowKeys(keymap, key.lettersRow, &withLessGreater);
// As on ISO keyboards, that key takes its room from the one before it (left shift).
if (withLessGreater && !result.isEmpty())
result.back().width = std::max(1.0, result.back().width - 1);
result += keys;
}
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 = choices[base];
if (!onKeys.contains(choice) && !list.contains(choice))
list.append(choice);
};
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);
}
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);
}
}
}
} // namespace
Layout buildLayout(const Layout &tmpl, const KeymapKeys &keymap, KeyboardMode mode)
{
Layout layout = tmpl;
layout.name = keymap.name.isEmpty() ? tmpl.name : keymap.name;
layout.alternates.clear();
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 = expandPlaceholders(row, keymap);
addAlternates(layer, tmpl, keymap);
}
return layout;
}