- 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>
157 lines
5.6 KiB
C++
157 lines
5.6 KiB
C++
#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;
|
||
}
|