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
+3 -1
View File
@@ -6,6 +6,8 @@ qt_add_library(sloposk_core STATIC
engine/InputEngine.h
engine/DirectEngine.h engine/DirectEngine.cpp
input/ShiftState.h input/ShiftState.cpp
input/Modifiers.h input/Modifiers.cpp
input/DeadKeys.h input/DeadKeys.cpp
input/PointerTracker.h input/PointerTracker.cpp
input/AlternatesPicker.h input/AlternatesPicker.cpp
input/KeyRepeater.h input/KeyRepeater.cpp
@@ -15,7 +17,7 @@ qt_add_library(sloposk_core STATIC
input/KeyboardSurface.h input/KeyboardSurface.cpp
layout/Layout.h
layout/LayoutLoader.h layout/LayoutLoader.cpp
layout/KeymapLetters.h layout/KeymapLetters.cpp
layout/KeymapKeys.h layout/KeymapKeys.cpp
layout/LayoutBuilder.h layout/LayoutBuilder.cpp
layout/KeyGeometry.h layout/KeyGeometry.cpp
layout/LayoutModel.h layout/LayoutModel.cpp
+68
View File
@@ -0,0 +1,68 @@
#include "DeadKeys.h"
#include "layout/KeymapKeys.h"
#include <xkbcommon/xkbcommon-compose.h>
#include <xkbcommon/xkbcommon.h>
namespace {
QByteArray systemLocale()
{
for (const char *name : {"LC_ALL", "LC_CTYPE", "LANG"}) {
if (const QByteArray value = qgetenv(name); !value.isEmpty())
return value;
}
return "C";
}
} // namespace
DeadKeys::DeadKeys(const QString &locale)
: m_context(xkb_context_new(XKB_CONTEXT_NO_FLAGS), xkb_context_unref)
, m_table(nullptr, xkb_compose_table_unref)
, m_state(nullptr, xkb_compose_state_unref)
{
// The C locale has no compose table; en_US.UTF-8's is the common base of all.
const QByteArray name = locale.isEmpty() ? systemLocale() : locale.toUtf8();
for (const QByteArray &candidate : {name, QByteArrayLiteral("en_US.UTF-8")}) {
m_table.reset(xkb_compose_table_new_from_locale(m_context.get(), candidate.constData(),
XKB_COMPOSE_COMPILE_NO_FLAGS));
if (m_table)
break;
}
if (m_table)
m_state.reset(xkb_compose_state_new(m_table.get(), XKB_COMPOSE_STATE_NO_FLAGS));
}
DeadKeys::~DeadKeys() = default;
void DeadKeys::press(quint32 deadKeysym)
{
m_pending += deadKeyText(deadKeysym);
if (m_state)
xkb_compose_state_feed(m_state.get(), deadKeysym);
}
QString DeadKeys::compose(const QString &text)
{
QString result = m_pending + text;
if (m_state && !text.isEmpty()) {
const char32_t ucs = text.toUcs4().first();
xkb_compose_state_feed(m_state.get(), xkb_utf32_to_keysym(ucs));
if (xkb_compose_state_get_status(m_state.get()) == XKB_COMPOSE_COMPOSED) {
char utf8[64];
xkb_compose_state_get_utf8(m_state.get(), utf8, sizeof utf8);
result = QString::fromUtf8(utf8) + text.mid(QString::fromUcs4(&ucs, 1).size());
}
}
cancel();
return result;
}
void DeadKeys::cancel()
{
m_pending.clear();
if (m_state)
xkb_compose_state_reset(m_state.get());
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <QString>
#include <memory>
struct xkb_context;
struct xkb_compose_table;
struct xkb_compose_state;
// Dead-key composition (´ then e → é) with the locale's XKB compose table,
// the same one a physical keyboard uses.
class DeadKeys
{
public:
// `locale` e.g. "sk_SK.UTF-8"; empty picks LC_ALL / LC_CTYPE / LANG.
explicit DeadKeys(const QString &locale = {});
~DeadKeys();
bool isPending() const { return !m_pending.isEmpty(); }
// A dead key was typed: starts or extends the sequence.
void press(quint32 deadKeysym);
// `text` typed while pending: the composed character, or the accents
// followed by `text` when nothing composes. Ends the sequence.
QString compose(const QString &text);
void cancel();
private:
std::unique_ptr<xkb_context, void (*)(xkb_context *)> m_context;
std::unique_ptr<xkb_compose_table, void (*)(xkb_compose_table *)> m_table;
std::unique_ptr<xkb_compose_state, void (*)(xkb_compose_state *)> m_state;
QString m_pending; // spacing accents, typed if nothing composes
};
+88 -18
View File
@@ -4,7 +4,7 @@
#include "engine/InputEngine.h"
#include "layout/LayoutModel.h"
#include <xkbcommon/xkbcommon-keysyms.h>
#include <xkbcommon/xkbcommon.h>
// ShiftMode mirrors ShiftState::Mode for QML.
static_assert(int(KeyboardController::ShiftMode::Off) == int(ShiftState::Mode::Off));
@@ -39,16 +39,27 @@ void KeyboardController::keyDown(const KeyDef &key, qint64 timeMs)
case KeyType::Shift:
changeShift([&] { m_shift.press(timeMs); });
break;
case KeyType::Modifier:
m_modifiers.press(key.modifier, timeMs);
Q_EMIT modifiersChanged();
break;
case KeyType::Backspace:
deleteBackward(false);
m_backspaceRepeater.start();
if (m_deadKeys.isPending()) {
m_deadKeys.cancel(); // backspace takes back the accent
} else if (m_modifiers.active()) {
sendKey(XKB_KEY_BackSpace);
} else {
deleteBackward(false);
m_backspaceRepeater.start();
}
break;
case KeyType::LayerSwitch:
m_layerBeforeSwitch = m_model.layerName();
m_model.setLayer(key.target);
break;
case KeyType::Keysym:
m_engine.sendKeysym(key.keysym);
m_repeatingModifiers = shortcutModifiers();
sendKey(key.keysym);
if (key.repeat) {
m_repeatingKeysym = key.keysym;
m_keysymRepeater.start();
@@ -65,6 +76,13 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
case KeyType::Shift:
changeShift([&] { m_shift.release(); });
break;
case KeyType::CapsLock:
changeShift([&] { m_shift.toggleLock(); });
break;
case KeyType::Modifier:
m_modifiers.release(key.modifier);
Q_EMIT modifiersChanged();
break;
case KeyType::Backspace:
m_backspaceRepeater.stop();
break;
@@ -72,18 +90,12 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
stopRepeating(key);
break;
case KeyType::Character:
if (output == KeyOutput::Alternate && !alternate.isEmpty())
typeText(alternate);
else if (output == KeyOutput::Flick && !key.flick.isEmpty())
typeText(key.flick);
else
typeText(key.text(m_shift.isUppercase()));
break;
case KeyType::Space:
typeText(QStringLiteral(" "));
typeCharacter(key, output, alternate);
break;
case KeyType::Enter:
m_engine.sendKeysym(XKB_KEY_Return);
m_deadKeys.cancel();
sendKey(XKB_KEY_Return);
m_textBefore += u'\n';
autoCapitalize();
break;
@@ -102,7 +114,10 @@ void KeyboardController::keyCancelled(const KeyDef &key)
{
if (key.type == KeyType::Shift)
changeShift([&] { m_shift.release(); });
else if (key.type == KeyType::Backspace)
else if (key.type == KeyType::Modifier) {
m_modifiers.release(key.modifier);
Q_EMIT modifiersChanged();
} else if (key.type == KeyType::Backspace)
m_backspaceRepeater.stop();
else if (key.type == KeyType::Keysym)
stopRepeating(key);
@@ -128,6 +143,7 @@ void KeyboardController::moveCursor(QPoint steps)
void KeyboardController::resetContext()
{
m_deadKeys.cancel();
m_context = {};
m_textBefore.clear();
autoCapitalize();
@@ -147,6 +163,52 @@ void KeyboardController::setOptions(const TypingOptions &options)
autoCapitalize();
}
void KeyboardController::typeCharacter(const KeyDef &key, KeyOutput output, const QString &alternate)
{
const QString base = key.type == KeyType::Space ? QStringLiteral(" ") : key.label;
if (m_modifiers.active()) {
// A shortcut such as Ctrl+C: the key's unshifted symbol, with modifiers.
m_deadKeys.cancel();
sendKey(xkb_utf32_to_keysym(base.toLower().toUcs4().value(0)));
return;
}
const ShiftLevel level = shiftLevel();
if (const quint32 dead = key.deadAt(level); dead && output == KeyOutput::Primary) {
m_deadKeys.press(dead);
changeShift([&] { m_shift.characterTyped(); });
return;
}
QString text = key.type == KeyType::Space ? base : key.text(level);
if (output == KeyOutput::Alternate && !alternate.isEmpty())
text = alternate;
else if (output == KeyOutput::Flick && !key.flick.isEmpty())
text = key.flick;
if (m_deadKeys.isPending())
text = m_deadKeys.compose(text);
typeText(text);
}
void KeyboardController::sendKey(quint32 keysym)
{
m_engine.sendKeysym(keysym, shortcutModifiers());
useModifiers();
}
Qt::KeyboardModifiers KeyboardController::shortcutModifiers() const
{
return m_modifiers.active() | (m_shift.appliesToShortcuts() ? Qt::ShiftModifier : Qt::NoModifier);
}
void KeyboardController::useModifiers()
{
m_modifiers.keyTyped();
Q_EMIT modifiersChanged();
if (m_shift.appliesToShortcuts())
changeShift([&] { m_shift.characterTyped(); });
}
void KeyboardController::typeText(const QString &text)
{
if (text == u" " && m_options.doubleSpacePeriod && m_context.isProse()
@@ -181,7 +243,7 @@ void KeyboardController::stopRepeating(const KeyDef &key)
void KeyboardController::sendRepeatedKeysym()
{
m_engine.sendKeysym(m_repeatingKeysym);
m_engine.sendKeysym(m_repeatingKeysym, m_repeatingModifiers);
}
void KeyboardController::autoCapitalize()
@@ -191,13 +253,21 @@ void KeyboardController::autoCapitalize()
changeShift([&] { m_shift.setAutomatic(capitalize); });
}
ShiftLevel KeyboardController::shiftLevel() const
{
if (m_shift.appliesToShortcuts())
return ShiftLevel::Shifted;
return m_shift.isActive() ? ShiftLevel::Capitals : ShiftLevel::None;
}
template<typename Change>
void KeyboardController::changeShift(Change change)
{
const ShiftState::Mode before = m_shift.mode();
const ShiftState::Mode mode = m_shift.mode();
const ShiftLevel level = shiftLevel();
change();
if (m_shift.mode() == before)
if (m_shift.mode() == mode && shiftLevel() == level)
return;
m_model.setUppercase(m_shift.isUppercase());
m_model.setShiftLevel(shiftLevel());
Q_EMIT shiftChanged();
}
+24 -4
View File
@@ -1,6 +1,8 @@
#pragma once
#include "DeadKeys.h"
#include "KeyRepeater.h"
#include "Modifiers.h"
#include "ShiftState.h"
#include "TypingOptions.h"
#include "backend/TextContext.h"
@@ -16,16 +18,23 @@ class LayoutModel;
// Which of a key's outputs the gesture selected.
enum class KeyOutput { Primary, Flick, Alternate };
// Keyboard rules: what each key does, shift, layers and the
// iOS conveniences (auto-capitalization, double-space period, backspace
// repeat). Knows nothing about touch or Wayland: it receives key presses and
// text-field context, and talks to an InputEngine.
// Keyboard rules: what each key does, shift and the other modifiers, dead
// keys, layers and the iOS conveniences (auto-capitalization, double-space
// period, backspace repeat). Knows nothing about touch or Wayland: it
// receives key presses and text-field context, and talks to an InputEngine.
//
// With Ctrl, Alt or Super active, keys become shortcuts (keysyms with those
// modifiers) instead of text. Shift joins shortcuts and non-text keys
// (Shift+F5, Shift+Tab, Shift+arrows) only when the user turned it on, not
// when auto-capitalization did.
class KeyboardController : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("Created in C++")
Q_PROPERTY(ShiftMode shiftMode READ shiftMode NOTIFY shiftChanged)
// "ctrl"/"alt"/"super" → ShiftMode
Q_PROPERTY(QVariantMap modifierModes READ modifierModes NOTIFY modifiersChanged)
public:
enum class ShiftMode { Off, Once, Locked };
@@ -34,6 +43,7 @@ public:
KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent = nullptr);
ShiftMode shiftMode() const;
QVariantMap modifierModes() const { return m_modifiers.modes(); }
void keyDown(const KeyDef &key, qint64 timeMs);
void keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64 timeMs);
@@ -51,11 +61,18 @@ public:
Q_SIGNALS:
void shiftChanged();
void modifiersChanged();
void dismissRequested();
void nextLayoutRequested(); // the globe key: switching is the system's job
private:
void typeCharacter(const KeyDef &key, KeyOutput output, const QString &alternate);
void typeText(const QString &text);
// Sends a non-text key or shortcut with the active modifiers, then uses them up.
void sendKey(quint32 keysym);
Qt::KeyboardModifiers shortcutModifiers() const;
ShiftLevel shiftLevel() const;
void useModifiers();
void deleteBackward(bool wholeWord);
void sendRepeatedKeysym();
void stopRepeating(const KeyDef &key);
@@ -67,9 +84,12 @@ private:
LayoutModel &m_model;
TypingOptions m_options;
ShiftState m_shift;
Modifiers m_modifiers;
DeadKeys m_deadKeys;
KeyRepeater m_backspaceRepeater;
KeyRepeater m_keysymRepeater; // arrows, Delete, ...
quint32 m_repeatingKeysym = 0;
Qt::KeyboardModifiers m_repeatingModifiers;
QString m_layerBeforeSwitch;
TextContext m_context;
+52
View File
@@ -0,0 +1,52 @@
#include "Modifiers.h"
ShiftState *Modifiers::find(const QString &name)
{
for (Entry &entry : m_entries) {
if (name == QLatin1StringView(entry.name))
return &entry.state;
}
return nullptr;
}
void Modifiers::press(const QString &name, qint64 timeMs)
{
if (ShiftState *state = find(name))
state->press(timeMs);
}
void Modifiers::release(const QString &name)
{
if (ShiftState *state = find(name))
state->release();
}
void Modifiers::keyTyped()
{
for (Entry &entry : m_entries)
entry.state.characterTyped();
}
void Modifiers::clear()
{
for (Entry &entry : m_entries)
entry.state = {};
}
Qt::KeyboardModifiers Modifiers::active() const
{
Qt::KeyboardModifiers flags;
for (const Entry &entry : m_entries) {
if (entry.state.isActive())
flags |= entry.flag;
}
return flags;
}
QVariantMap Modifiers::modes() const
{
QVariantMap modes;
for (const Entry &entry : m_entries)
modes.insert(QString::fromLatin1(entry.name), int(entry.state.mode()));
return modes;
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include "ShiftState.h"
#include <QString>
#include <QVariantMap>
#include <qnamespace.h>
#include <array>
// Ctrl, Alt and Super, each sticky like shift (see ShiftState): tap for the
// next key, double-tap to lock, hold while typing with another finger.
class Modifiers
{
public:
// Names as used in layouts: "ctrl", "alt", "super".
void press(const QString &name, qint64 timeMs);
void release(const QString &name);
// A key was typed: one-shot modifiers are used up.
void keyTyped();
void clear();
Qt::KeyboardModifiers active() const;
// name → ShiftState::Mode as int, for the view.
QVariantMap modes() const;
private:
struct Entry {
const char *name;
Qt::KeyboardModifier flag;
ShiftState state;
};
ShiftState *find(const QString &name);
std::array<Entry, 3> m_entries{{
{"ctrl", Qt::ControlModifier, {}},
{"alt", Qt::AltModifier, {}},
{"super", Qt::MetaModifier, {}},
}};
};
+8
View File
@@ -9,6 +9,7 @@ void ShiftState::press(qint64 timeMs)
m_lastPressUnlocked = m_mode == Mode::Locked;
m_held = true;
m_typedWhileHeld = false;
m_automatic = false;
switch (m_mode) {
case Mode::Off: m_mode = doubleTap ? Mode::Locked : Mode::Once; break;
@@ -38,4 +39,11 @@ void ShiftState::setAutomatic(bool capitalize)
if (m_held || m_mode == Mode::Locked)
return;
m_mode = capitalize ? Mode::Once : Mode::Off;
m_automatic = capitalize;
}
void ShiftState::toggleLock()
{
m_mode = m_mode == Mode::Locked ? Mode::Off : Mode::Locked;
m_automatic = false;
}
+9 -1
View File
@@ -7,6 +7,7 @@
// - tap: shift the next character only
// - double-tap: caps lock; tap again to unlock
// - hold while typing: every character shifted, off on release
// Also used for Ctrl/Alt/Super (see Modifiers), which are never automatic.
// Times are in milliseconds and passed in, so this stays deterministic.
class ShiftState
{
@@ -16,7 +17,11 @@ public:
static constexpr qint64 DoubleTapMs = 300;
Mode mode() const { return m_mode; }
bool isUppercase() const { return m_mode != Mode::Off; }
bool isActive() const { return m_mode != Mode::Off; }
// Whether shift joins shortcuts and non-text keys (Shift+F5): only when
// tapped or held by the user. Neither auto-capitalization nor caps lock
// does, as on a PC.
bool appliesToShortcuts() const { return m_held || (m_mode == Mode::Once && !m_automatic); }
void press(qint64 timeMs);
void release();
@@ -24,9 +29,12 @@ public:
// Auto-capitalization hint; ignored while shift is held or locked.
void setAutomatic(bool capitalize);
// Caps lock key: locks, or unlocks if locked.
void toggleLock();
private:
Mode m_mode = Mode::Off;
bool m_automatic = false;
bool m_held = false;
bool m_typedWhileHeld = false;
bool m_lastPressUnlocked = false; // a tap leaving caps lock doesn't start a double tap
+9 -3
View File
@@ -42,13 +42,19 @@ qreal totalUnits(const QList<Block> &blocks)
return units;
}
qsizetype totalRows(const QList<Block> &blocks)
{
qsizetype rows = 0;
for (const Block &block : blocks)
rows = std::max(rows, block.size());
return rows;
}
QList<QRectF> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardMetrics metrics)
{
QList<QRectF> rects;
const qreal units = totalUnits(blocks);
qsizetype maxRows = 0;
for (const Block &block : blocks)
maxRows = std::max(maxRows, block.size());
const qsizetype maxRows = totalRows(blocks);
if (units <= 0 || maxRows == 0)
return rects;
+2
View File
@@ -29,3 +29,5 @@ inline QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMe
qreal rowUnits(const Row &row);
// Width of all blocks in key units, ignoring gaps.
qreal totalUnits(const QList<Block> &blocks);
// Rows of the tallest block.
qsizetype totalRows(const QList<Block> &blocks);
+116
View File
@@ -0,0 +1,116 @@
#include "KeymapKeys.h"
#include <QHash>
#include <xkbcommon/xkbcommon.h>
#include <optional>
#include <ranges>
namespace {
// XKB keycodes are evdev codes + 8.
constexpr xkb_keycode_t fromEvdev(int code) { return xkb_keycode_t(code + 8); }
constexpr int Tilde = 41, Backslash = 43, LessGreater = 86;
struct PhysicalRow {
int before; // a key left of the range, or 0
int first;
int last;
int after; // a key right of the range, or 0
};
constexpr PhysicalRow Rows[] = {
{Tilde, 2, 13, 0}, // TLDE, AE01–AE12
{0, 16, 27, Backslash}, // AD01–AD12, BKSL
{0, 30, 40, 0}, // AC01–AC11
{LessGreater, 44, 53, 0}, // LSGT, AB01–AB10
};
xkb_keysym_t symbolAt(xkb_keymap *keymap, int code, xkb_layout_index_t layout, xkb_level_index_t level)
{
const xkb_keysym_t *syms = nullptr;
const int count = xkb_keymap_key_get_syms_by_level(keymap, fromEvdev(code), layout, level, &syms);
return count == 1 ? syms[0] : XKB_KEY_NoSymbol;
}
bool isDead(xkb_keysym_t sym)
{
return sym >= XKB_KEY_dead_grave && sym <= XKB_KEY_dead_longsolidusoverlay;
}
// Fills one level; false if it types nothing.
bool readLevel(xkb_keysym_t sym, QString &text, quint32 &dead)
{
if (isDead(sym)) {
dead = sym;
text = deadKeyText(sym);
return true;
}
const char32_t ucs = xkb_keysym_to_utf32(sym); // 0 for functions and NoSymbol
if (!ucs || QChar::isNonCharacter(ucs) || ucs < 0x20)
return false;
text = QString::fromUcs4(&ucs, 1);
return true;
}
std::optional<KeymapKey> keyAt(xkb_keymap *keymap, int code, xkb_layout_index_t layout)
{
KeymapKey key;
if (code == Backslash)
key.position = KeymapKey::Position::Backslash;
else if (code == LessGreater)
key.position = KeymapKey::Position::LessGreater;
if (!readLevel(symbolAt(keymap, code, layout, 0), key.lower, key.deadLower))
return std::nullopt;
if (!readLevel(symbolAt(keymap, code, layout, 1), key.upper, key.deadUpper))
key.upper = key.lower.toUpper();
return key;
}
} // namespace
bool KeymapKey::isLetter() const
{
return !deadLower && !lower.isEmpty() && QChar::isLetter(lower.toUcs4().first());
}
KeymapKeys readKeymapKeys(xkb_keymap *keymap, quint32 layoutIndex)
{
KeymapKeys result;
if (!keymap || layoutIndex >= xkb_keymap_num_layouts(keymap))
return result;
result.name = QString::fromUtf8(xkb_keymap_layout_get_name(keymap, layoutIndex));
for (const PhysicalRow &physical : Rows) {
QList<KeymapKey> &row = result.rows.emplace_back();
const auto add = [&](int code) {
if (const auto key = keyAt(keymap, code, layoutIndex))
row.append(*key);
};
if (physical.before)
add(physical.before);
for (int code : std::views::iota(physical.first, physical.last + 1))
add(code);
if (physical.after)
add(physical.after);
}
return result;
}
QString deadKeyText(quint32 keysym)
{
static const QHash<quint32, QString> accents = {
{XKB_KEY_dead_grave, QStringLiteral("`")}, {XKB_KEY_dead_acute, QStringLiteral("´")},
{XKB_KEY_dead_circumflex, QStringLiteral("^")}, {XKB_KEY_dead_tilde, QStringLiteral("~")},
{XKB_KEY_dead_macron, QStringLiteral("¯")}, {XKB_KEY_dead_breve, QStringLiteral("˘")},
{XKB_KEY_dead_abovedot, QStringLiteral("˙")}, {XKB_KEY_dead_diaeresis, QStringLiteral("¨")},
{XKB_KEY_dead_abovering, QStringLiteral("˚")}, {XKB_KEY_dead_doubleacute, QStringLiteral("˝")},
{XKB_KEY_dead_caron, QStringLiteral("ˇ")}, {XKB_KEY_dead_cedilla, QStringLiteral("¸")},
{XKB_KEY_dead_ogonek, QStringLiteral("˛")}, {XKB_KEY_dead_belowdot, QStringLiteral(".")},
{XKB_KEY_dead_hook, QStringLiteral("ˀ")}, {XKB_KEY_dead_horn, QStringLiteral("ʼ")},
{XKB_KEY_dead_greek, QStringLiteral("µ")}, {XKB_KEY_dead_stroke, QStringLiteral("/")},
};
return accents.value(keysym, QStringLiteral("◌"));
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <QList>
#include <QString>
struct xkb_keymap;
// One typing key of a keymap layout, at its two shift levels. A level is
// either text or a dead key (then `text` is its spacing accent, for labels).
struct KeymapKey {
enum class Position { Regular, Backslash, LessGreater }; // BKSL, LSGT: not on every keyboard
Position position = Position::Regular;
QString lower;
QString upper;
quint32 deadLower = 0; // dead keysym, 0 if the level types text
quint32 deadUpper = 0;
bool isLetter() const;
};
// The typing keys one layout of an XKB keymap has on a physical keyboard,
// in its four rows: 0 number row (TLDE, AE01–AE12), 1 top (AD01–AD12, BKSL),
// 2 home (AC01–AC11), 3 bottom (LSGT, AB01–AB10). Keys that type nothing on
// either level are left out.
struct KeymapKeys {
QString name; // e.g. "Slovak"
QList<QList<KeymapKey>> rows;
};
KeymapKeys readKeymapKeys(xkb_keymap *keymap, quint32 layoutIndex);
// What a dead key shows and types when nothing composes: ´ for dead_acute.
QString deadKeyText(quint32 keysym);
-66
View File
@@ -1,66 +0,0 @@
#include "KeymapLetters.h"
#include <xkbcommon/xkbcommon.h>
#include <optional>
#include <ranges>
namespace {
// XKB keycodes are evdev codes + 8.
constexpr xkb_keycode_t fromEvdev(int code) { return xkb_keycode_t(code + 8); }
struct KeyRange {
int first;
int last;
};
// AD01–AD12, AC01–AC11, AB01–AB10 in evdev codes.
constexpr KeyRange LetterRows[] = {{16, 27}, {30, 40}, {44, 53}};
// AE01–AE12 (number row), BKSL, LSGT: letters here become extras.
constexpr KeyRange ExtraKeys[] = {{2, 13}, {43, 43}, {86, 86}};
QString symbolText(xkb_keymap *keymap, xkb_keycode_t key, xkb_layout_index_t layout, xkb_level_index_t level)
{
const xkb_keysym_t *syms = nullptr;
if (xkb_keymap_key_get_syms_by_level(keymap, key, layout, level, &syms) != 1)
return {};
const char32_t ucs = xkb_keysym_to_utf32(syms[0]); // 0 for dead keys and functions
return ucs ? QString::fromUcs4(&ucs, 1) : QString();
}
bool isLetter(const QString &text)
{
return !text.isEmpty() && QChar::isLetter(text.toUcs4().first());
}
std::optional<Letter> letterAt(xkb_keymap *keymap, xkb_keycode_t key, xkb_layout_index_t layout)
{
const QString lower = symbolText(keymap, key, layout, 0);
if (!isLetter(lower))
return std::nullopt;
const QString shifted = symbolText(keymap, key, layout, 1);
return Letter{lower, isLetter(shifted) ? shifted : lower.toUpper()};
}
} // namespace
KeymapLetters readKeymapLetters(xkb_keymap *keymap, quint32 layoutIndex)
{
KeymapLetters result;
if (!keymap || layoutIndex >= xkb_keymap_num_layouts(keymap))
return result;
result.name = QString::fromUtf8(xkb_keymap_layout_get_name(keymap, layoutIndex));
const auto collect = [&](KeyRange range, QList<Letter> &into) {
for (int code : std::views::iota(range.first, range.last + 1)) {
if (const auto letter = letterAt(keymap, fromEvdev(code), layoutIndex))
into.append(*letter);
}
};
for (KeyRange range : LetterRows)
collect(range, result.rows.emplace_back());
for (KeyRange range : ExtraKeys)
collect(range, result.extras);
return result;
}
-22
View File
@@ -1,22 +0,0 @@
#pragma once
#include <QList>
#include <QString>
struct xkb_keymap;
struct Letter {
QString lower;
QString upper;
};
// The letters one layout of an XKB keymap puts where a physical keyboard has
// its three letter rows (Q…, A…, Z… positions). Letters on other keys, e.g.
// Slovak ľščťž on the number row, are collected as extras.
struct KeymapLetters {
QString name; // e.g. "Slovak"
QList<QList<Letter>> rows;
QList<Letter> extras;
};
KeymapLetters readKeymapLetters(xkb_keymap *keymap, quint32 layoutIndex);
+22 -5
View File
@@ -11,14 +11,22 @@ enum class KeyType {
Backspace,
Enter,
Shift,
CapsLock,
Modifier, // ctrl, alt or super: sticky like shift, applied to the next key
LayerSwitch,
Globe,
Dismiss,
Spacer, // empty room, e.g. the half-key indent of the home row
Letters, // template only: a row of letters from the system keymap
Keys, // template only: a whole row of typing keys from the system keymap
Keysym, // a non-text key (Home, F5, arrows...): sends `keysym`
};
// What shift does to a key. Auto-capitalization and caps lock only capitalize
// letters; a shift the user taps or holds picks the keymap's shifted level
// (Shift+1 = !), as on a PC.
enum class ShiftLevel { None, Capitals, Shifted };
struct KeyDef {
KeyType type = KeyType::Character;
QString label; // shown and typed (for character keys)
@@ -26,22 +34,30 @@ struct KeyDef {
QString flick; // secondary symbol, typed by flicking down
QStringList alternates; // long-press choices
QString target; // layer to switch to (LayerSwitch only)
QString modifier; // Modifier only: "ctrl", "alt" or "super"
quint32 dead = 0; // character keys: dead keysym typed instead of `label`, 0 if none
quint32 deadShifted = 0; // same for the shifted level
qreal width = 1.0; // in key units
bool stretch = false; // grows so the row is as wide as its block's widest
quint32 keysym = 0; // Keysym keys only
bool repeat = false; // Keysym keys only: auto-repeat while held
// Letters placeholders only.
int lettersRow = -1; // which keymap row (0 = top)
// Letters/Keys placeholders only.
int lettersRow = -1; // which keymap row (Letters: 0 = top; Keys: 0 = number row)
QStringList flicks; // flick symbol per letter, left to right
// Text this key shows/types; only character keys change case.
QString text(bool uppercase) const
// Text this key shows/types; only character keys change.
QString text(ShiftLevel level) const
{
if (!uppercase || type != KeyType::Character)
if (level == ShiftLevel::None || type != KeyType::Character)
return label;
if (level == ShiftLevel::Capitals)
return isLetter() ? label.toUpper() : label;
return shifted.isEmpty() ? label.toUpper() : shifted;
}
// The dead keysym typed at `level`, 0 if it types text.
quint32 deadAt(ShiftLevel level) const { return level == ShiftLevel::Shifted ? deadShifted : dead; }
bool isLetter() const { return !dead && label.size() == 1 && label.front().isLetter(); }
};
using Row = QList<KeyDef>;
@@ -49,6 +65,7 @@ using Row = QList<KeyDef>;
struct Layer {
QString name;
QList<Row> rows;
QList<Row> fullRows; // replaces `rows` in full-keyboard mode, if present
};
// An optional block of keys next to the main one, shown if the user wants it.
+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;
}
+10 -4
View File
@@ -1,8 +1,14 @@
#pragma once
#include "KeymapLetters.h"
#include "KeymapKeys.h"
#include "Layout.h"
// Turns the template into the layout shown: fills its letters placeholders
// from the keymap and gives each letter its flick and long-press choices.
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters);
enum class KeyboardMode {
Compact, // iPad: letters only, symbols by flick and layers
Full, // iPad Pro / PC: number row, punctuation, tab, modifiers
};
// Turns the template into the layout shown: picks each layer's rows for the
// mode, fills the keymap placeholders, and gives letters their long-press
// choices (letters of the keymap without a key of their own come first).
Layout buildLayout(const Layout &tmpl, const KeymapKeys &keymap, KeyboardMode mode = KeyboardMode::Compact);
+18 -2
View File
@@ -11,6 +11,8 @@ namespace LayoutLoader {
namespace {
constexpr int LetterRows = 3;
constexpr int KeyRows = 4;
const QStringList ModifierNames = {QStringLiteral("ctrl"), QStringLiteral("alt"), QStringLiteral("super")};
QStringList toStringList(const QJsonValue &value)
{
@@ -28,11 +30,14 @@ const QHash<QString, KeyType> &keyTypesByName()
{QStringLiteral("backspace"), KeyType::Backspace},
{QStringLiteral("enter"), KeyType::Enter},
{QStringLiteral("shift"), KeyType::Shift},
{QStringLiteral("capslock"), KeyType::CapsLock},
{QStringLiteral("modifier"), KeyType::Modifier},
{QStringLiteral("layer"), KeyType::LayerSwitch},
{QStringLiteral("globe"), KeyType::Globe},
{QStringLiteral("dismiss"), KeyType::Dismiss},
{QStringLiteral("spacer"), KeyType::Spacer},
{QStringLiteral("letters"), KeyType::Letters},
{QStringLiteral("keys"), KeyType::Keys},
{QStringLiteral("keysym"), KeyType::Keysym},
};
return types;
@@ -85,10 +90,20 @@ std::optional<KeyDef> parseKey(const QJsonValue &value, QString *error)
*error = QStringLiteral("character key without a label");
return std::nullopt;
}
key.modifier = obj.value(u"modifier").toString();
if (key.type == KeyType::Letters && (key.lettersRow < 0 || key.lettersRow >= LetterRows)) {
*error = QStringLiteral("letters need a row from 0 to %1").arg(LetterRows - 1);
return std::nullopt;
}
if (key.type == KeyType::Keys && (key.lettersRow < 0 || key.lettersRow >= KeyRows)) {
*error = QStringLiteral("keys need a row from 0 to %1").arg(KeyRows - 1);
return std::nullopt;
}
if (key.type == KeyType::Modifier && !ModifierNames.contains(key.modifier)) {
*error = QStringLiteral("modifier must be one of %1").arg(ModifierNames.join(u", "));
return std::nullopt;
}
if (key.width <= 0) {
*error = QStringLiteral("key \"%1\" has a non-positive width").arg(key.label);
return std::nullopt;
@@ -123,7 +138,8 @@ std::optional<Layer> parseLayer(const QJsonObject &obj, QString *error)
*error = QStringLiteral("layer without a name");
return std::nullopt;
}
if (!parseRows(obj.value(u"rows"), &layer.rows, error)) {
if (!parseRows(obj.value(u"rows"), &layer.rows, error)
|| (obj.contains(u"full") && !parseRows(obj.value(u"full"), &layer.fullRows, error))) {
*error = QStringLiteral("layer \"%1\": %2").arg(layer.name, *error);
return std::nullopt;
}
@@ -210,7 +226,7 @@ std::optional<Layout> load(const QString &path, QString *error)
bool validate(const Layout &layout, QString *error)
{
for (const Layer &layer : layout.layers) {
for (const Row &row : layer.rows) {
for (const Row &row : layer.rows + layer.fullRows) {
for (const KeyDef &key : row) {
if (key.type == KeyType::LayerSwitch && !layout.layer(key.target)) {
*error = QStringLiteral("layer \"%1\" switches to unknown layer \"%2\"")
+18 -7
View File
@@ -12,12 +12,15 @@ QString keyTypeName(KeyType type)
case KeyType::Backspace: return QStringLiteral("backspace");
case KeyType::Enter: return QStringLiteral("enter");
case KeyType::Shift: return QStringLiteral("shift");
case KeyType::CapsLock: return QStringLiteral("capslock");
case KeyType::Modifier: return QStringLiteral("modifier");
case KeyType::LayerSwitch: return QStringLiteral("layer");
case KeyType::Globe: return QStringLiteral("globe");
case KeyType::Dismiss: return QStringLiteral("dismiss");
case KeyType::Spacer: return QStringLiteral("spacer");
case KeyType::Keysym: return QStringLiteral("keysym");
case KeyType::Letters: break; // expanded before it reaches the model
case KeyType::Letters:
case KeyType::Keys: break; // expanded before it reaches the model
}
return {};
}
@@ -44,8 +47,10 @@ QVariant LayoutModel::data(const QModelIndex &index, int role) const
const KeyDef &key = m_keys.at(index.row());
const QRectF rect = m_rects.value(index.row());
switch (role) {
case LabelRole: return key.text(m_uppercase);
case FlickLabelRole: return key.flick;
case LabelRole: return key.text(m_shiftLevel);
// A shifted key already shows its shifted symbol (! on 1): don't repeat it.
case FlickLabelRole: return m_shiftLevel == ShiftLevel::Shifted && key.flick == key.shifted ? QString() : key.flick;
case ModifierRole: return key.modifier;
case KeyTypeRole: return keyTypeName(key.type);
case KeyXRole: return rect.x();
case KeyYRole: return rect.y();
@@ -69,6 +74,7 @@ QHash<int, QByteArray> LayoutModel::roleNames() const
{KeyHeightRole, "keyHeight"},
{PressedRole, "pressed"},
{FlickProgressRole, "flickProgress"},
{ModifierRole, "modifier"},
};
}
@@ -111,6 +117,11 @@ qreal LayoutModel::totalUnits() const
return ::totalUnits(m_blocks);
}
qsizetype LayoutModel::totalRows() const
{
return ::totalRows(m_blocks);
}
void LayoutModel::setSize(QSizeF size)
{
if (size == m_size)
@@ -121,13 +132,13 @@ void LayoutModel::setSize(QSizeF size)
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {KeyXRole, KeyYRole, KeyWidthRole, KeyHeightRole});
}
void LayoutModel::setUppercase(bool uppercase)
void LayoutModel::setShiftLevel(ShiftLevel level)
{
if (uppercase == m_uppercase)
if (level == m_shiftLevel)
return;
m_uppercase = uppercase;
m_shiftLevel = level;
if (!m_keys.isEmpty())
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {LabelRole});
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {LabelRole, FlickLabelRole});
}
void LayoutModel::setPressed(int row, bool pressed)
+5 -3
View File
@@ -30,6 +30,7 @@ public:
KeyHeightRole,
PressedRole,
FlickProgressRole,
ModifierRole,
};
using QAbstractListModel::QAbstractListModel;
@@ -43,13 +44,14 @@ public:
QVariantList sections() const;
// Width in key units of everything shown.
qreal totalUnits() const;
qsizetype totalRows() const;
void setLayout(const Layout &layout);
void setLayer(const QString &layerName);
// Section ids to show; MainSectionId stands for the layers.
void setVisibleSections(const QStringList &ids);
void setSize(QSizeF size);
void setUppercase(bool uppercase);
void setShiftLevel(ShiftLevel level);
void setPressed(int index, bool pressed);
void setFlickProgress(int index, qreal progress);
@@ -57,7 +59,7 @@ public:
int keyAt(QPointF pos) const;
const KeyDef &key(int index) const { return m_keys.at(index); }
QRectF keyRect(int index) const { return m_rects.value(index); }
bool isUppercase() const { return m_uppercase; }
bool isUppercase() const { return m_shiftLevel != ShiftLevel::None; }
Q_SIGNALS:
void layoutChanged();
@@ -73,7 +75,7 @@ private:
QStringList m_visibleSections{MainSectionId};
QList<Block> m_blocks;
QSizeF m_size;
bool m_uppercase = false;
ShiftLevel m_shiftLevel = ShiftLevel::None;
QList<KeyDef> m_keys;
QList<QRectF> m_rects;
+31 -17
View File
@@ -19,6 +19,8 @@
#include <QQuickWindow>
#include <QStandardPaths>
#include <functional>
Q_IMPORT_QML_PLUGIN(SloposkPlugin)
namespace {
@@ -38,29 +40,41 @@ std::optional<Layout> loadTemplate()
return std::nullopt;
}
// The shown layout is the template filled with the active system layout's letters.
void connectLayouts(const Layout &tmpl, WaylandKeymap &keymap, SystemLayouts &systemLayouts,
KeyboardController &controller, LayoutModel &model)
// The shown layout is the template filled with the active system layout's
// keys. Returns the rebuild, for settings that change the result.
std::function<void()> connectLayouts(const Layout &tmpl, WaylandKeymap &keymap, SystemLayouts &systemLayouts,
const Settings &settings, KeyboardController &controller, LayoutModel &model)
{
const auto rebuild = [&] {
model.setLayout(buildLayout(tmpl, readKeymapLetters(keymap.keymap(), systemLayouts.current())));
const KeyboardMode mode = settings.fullKeyboard() ? KeyboardMode::Full : KeyboardMode::Compact;
model.setLayout(buildLayout(tmpl, readKeymapKeys(keymap.keymap(), systemLayouts.current()), mode));
};
QObject::connect(&keymap, &WaylandKeymap::changed, &model, rebuild);
QObject::connect(&systemLayouts, &SystemLayouts::currentChanged, &model, rebuild);
QObject::connect(&controller, &KeyboardController::nextLayoutRequested, &systemLayouts,
[&] { systemLayouts.next(keymap.layoutCount()); });
rebuild();
return rebuild;
}
// Showing or hiding a block resizes the window so keys keep their width.
void connectSections(Settings &settings, LayoutModel &model, FloatingWindow &placement)
// Settings that add or remove keys resize the window so keys keep their size.
void connectKeySettings(Settings &settings, LayoutModel &model, FloatingWindow &placement, const QQuickWindow &window,
const std::function<void()> &rebuild)
{
QObject::connect(&settings, &Settings::visibleSectionsChanged, &model, [&] {
const qreal before = model.totalUnits();
model.setVisibleSections(settings.visibleSections());
if (before > 0)
placement.resizeBy(placement.geometry().width() * (model.totalUnits() / before - 1), 0);
});
const auto keepingKeySize = [&](const std::function<void()> &change) {
const qreal units = model.totalUnits();
const auto rows = qreal(model.totalRows());
change();
if (units <= 0 || rows <= 0)
return;
const qreal keysHeight = window.property("keysHeight").toReal();
placement.resizeBy(placement.geometry().width() * (model.totalUnits() / units - 1),
keysHeight * (qreal(model.totalRows()) / rows - 1));
};
QObject::connect(&settings, &Settings::visibleSectionsChanged, &model,
[&, keepingKeySize] { keepingKeySize([&] { model.setVisibleSections(settings.visibleSections()); }); });
QObject::connect(&settings, &Settings::fullKeyboardChanged, &model,
[keepingKeySize, rebuild] { keepingKeySize(rebuild); });
}
void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings)
@@ -114,13 +128,13 @@ int main(int argc, char *argv[])
KeyboardController controller(engine, model);
connectTextContext(backend, controller);
WaylandKeymap keymap;
SystemLayouts systemLayouts;
connectLayouts(*layoutTemplate, keymap, systemLayouts, controller, model);
Settings settings;
controller.setOptions(settings.typingOptions());
model.setVisibleSections(settings.visibleSections());
WaylandKeymap keymap;
SystemLayouts systemLayouts;
const auto rebuildLayout = connectLayouts(*layoutTemplate, keymap, systemLayouts, settings, controller, model);
QObject::connect(&settings, &Settings::typingOptionsChanged, &controller,
[&] { controller.setOptions(settings.typingOptions()); });
@@ -141,7 +155,7 @@ int main(int argc, char *argv[])
window->setProperty("placement", QVariant::fromValue(&placement));
WindowSettings windowSettings;
restoreAndPersistGeometry(placement, windowSettings);
connectSections(settings, model, placement);
connectKeySettings(settings, model, placement, *window, rebuildLayout);
VisibilityController visibility;
connectVisibility(visibility, backend, controller, placement);
+11
View File
@@ -8,6 +8,7 @@ namespace {
constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
constexpr auto VisibleSectionsKey = "layout/visibleSections";
constexpr auto FullKeyboardKey = "layout/fullKeyboard";
}
Settings::Settings(QObject *parent)
@@ -20,6 +21,16 @@ Settings::Settings(QObject *parent)
m_visibleSections = store.value(VisibleSectionsKey, QStringList{MainSectionId}).toStringList();
if (m_visibleSections.isEmpty())
m_visibleSections = {MainSectionId};
m_fullKeyboard = store.value(FullKeyboardKey, false).toBool();
}
void Settings::setFullKeyboard(bool full)
{
if (full == m_fullKeyboard)
return;
m_fullKeyboard = full;
QSettings().setValue(FullKeyboardKey, full);
Q_EMIT fullKeyboardChanged();
}
void Settings::setSectionVisible(const QString &id, bool visible)
+7
View File
@@ -17,6 +17,8 @@ class Settings : public QObject
Q_PROPERTY(bool doubleSpacePeriod READ doubleSpacePeriod WRITE setDoubleSpacePeriod NOTIFY typingOptionsChanged)
// Which key blocks are shown: "main" (the typing keys) and template section ids.
Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged)
// iPad Pro-style full keyboard (number row, tab, ctrl/alt/super) instead of the compact one.
Q_PROPERTY(bool fullKeyboard READ fullKeyboard WRITE setFullKeyboard NOTIFY fullKeyboardChanged)
public:
explicit Settings(QObject *parent = nullptr);
@@ -32,13 +34,18 @@ public:
// Hiding the last visible section is refused: something must stay on screen.
Q_INVOKABLE void setSectionVisible(const QString &id, bool visible);
bool fullKeyboard() const { return m_fullKeyboard; }
void setFullKeyboard(bool full);
Q_SIGNALS:
void typingOptionsChanged();
void visibleSectionsChanged();
void fullKeyboardChanged();
private:
void setTypingOptions(const TypingOptions &options);
TypingOptions m_typing;
QStringList m_visibleSections;
bool m_fullKeyboard = false;
};