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
+9 -2
View File
@@ -7,6 +7,8 @@ iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML U
- Letters follow the system keyboard layout; the globe key switches it - Letters follow the system keyboard layout; the globe key switches it
- Auto-capitalization, double-space → ". " (both optional, ⚙ on the bottom bar), backspace repeat → word delete - Auto-capitalization, double-space → ". " (both optional, ⚙ on the bottom bar), backspace repeat → word delete
- Long-press space = trackpad; slide from `.?123` onto a symbol and release - Long-press space = trackpad; slide from `.?123` onto a symbol and release
- Compact (iPad) or full (iPad Pro) keyboard: number row, punctuation, tab, caps lock, ctrl/alt/super, dead keys
- Ctrl/Alt/Super stick like shift (tap, double-tap, hold) and turn keys into shortcuts; a tapped or held shift joins them (Shift+F5, Shift+Tab, Shift+arrows), auto-shift and caps lock don't
- Optional PC key blocks: function row, navigation (ins/home/pg up, del/end/pg dn, arrows), numeric keypad; any mix, typing keys included (⚙) - Optional PC key blocks: function row, navigation (ins/home/pg up, del/end/pg dn, arrows), numeric keypad; any mix, typing keys included (⚙)
- Floating: drag the bottom bar, resize from the corner; geometry is remembered per screen - Floating: drag the bottom bar, resize from the corner; geometry is remembered per screen
@@ -37,6 +39,8 @@ Dev hooks: `SLOPOSK_AUTOTYPE=text` commits on focus; `SLOPOSK_SCREENSHOT=file.pn
Letters come from the system keymap (what KWin sends to every client): its Q/A/Z rows become the three letter rows. Letters elsewhere, e.g. Slovak ľščťž on the number row, become long-press alternates. The globe key switches the system layout over `org.kde.keyboard`, so the physical keyboard follows. Letters come from the system keymap (what KWin sends to every client): its Q/A/Z rows become the three letter rows. Letters elsewhere, e.g. Slovak ľščťž on the number row, become long-press alternates. The globe key switches the system layout over `org.kde.keyboard`, so the physical keyboard follows.
In full mode every typing key comes from the keymap (number row, `[ ] \ ; ' , . /` or the layout's equivalents), with the shifted symbol shown above. Dead keys compose with the locale's XKB compose table.
Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.json` replaces it. Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.json` replaces it.
```json ```json
@@ -46,14 +50,17 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso
- Key: a string, or an object with `type` (`char` `space` `backspace` `enter` `shift` `layer` `globe` `dismiss` `spacer` `letters` `keysym`), `label`, `shifted`, `flick`, `alternates`, `width` (key units), `target` (for `layer`), `stretch` - Key: a string, or an object with `type` (`char` `space` `backspace` `enter` `shift` `layer` `globe` `dismiss` `spacer` `letters` `keysym`), `label`, `shifted`, `flick`, `alternates`, `width` (key units), `target` (for `layer`), `stretch`
- `keysym`: sends the XKB keysym named by `keysym` (`Home`, `F5`, `Left`...) on press; `repeat` auto-repeats while held - `keysym`: sends the XKB keysym named by `keysym` (`Home`, `F5`, `Left`...) on press; `repeat` auto-repeats while held
- `letters`: the keymap row `row` (0–2), with `flicks` handed out left to right - `letters`: the keymap's letters in row `row` (0–2), with `flicks` handed out left to right
- `keys`: every typing key in keymap row `row` (0 = number row … 3 = bottom)
- `modifier`: `"modifier": "ctrl" | "alt" | "super"`; `capslock`
- Layer `full`: rows used instead of `rows` in full-keyboard mode
- `stretch`: the key grows so its row is as wide as the layer's widest (letter counts vary by layout) - `stretch`: the key grows so its row is as wide as the layer's widest (letter counts vary by layout)
- Top-level `alternates`: long-press choices per letter, after the keymap's own - Top-level `alternates`: long-press choices per letter, after the keymap's own
- Top-level `sections`: optional blocks, `{"id", "name", "placement": "above" | "right", "rows"}`; the user picks which show - Top-level `sections`: optional blocks, `{"id", "name", "placement": "above" | "right", "rows"}`; the user picks which show
## Settings ## Settings
`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `layout/visibleSections` (`main` plus section ids). `~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `layout/visibleSections` (`main` plus section ids), `layout/fullKeyboard`.
## Architecture ## Architecture
+30
View File
@@ -77,6 +77,36 @@
"layers": [ "layers": [
{ {
"name": "letters", "name": "letters",
"full": [
[
{"type": "keys", "row": 0},
{"type": "backspace", "width": 1.5, "stretch": true}
],
[
{"type": "keysym", "keysym": "Tab", "label": "tab", "width": 1.5, "stretch": true},
{"type": "keys", "row": 1}
],
[
{"type": "capslock", "label": "caps lock", "width": 1.75, "stretch": true},
{"type": "keys", "row": 2},
{"type": "enter", "width": 1.75, "stretch": true}
],
[
{"type": "shift", "width": 2.25, "stretch": true},
{"type": "keys", "row": 3},
{"type": "shift", "width": 2.25, "stretch": true}
],
[
{"type": "modifier", "modifier": "ctrl", "label": "ctrl", "width": 1.25},
{"type": "modifier", "modifier": "super", "label": "super", "width": 1.25},
{"type": "modifier", "modifier": "alt", "label": "alt", "width": 1.25},
{"type": "globe"},
{"type": "space", "width": 6, "stretch": true},
{"type": "modifier", "modifier": "alt", "label": "alt", "width": 1.25},
{"type": "layer", "label": ".?123", "target": "numbers", "width": 1.25},
{"type": "dismiss"}
]
],
"rows": [ "rows": [
[ [
{"type": "letters", "row": 0, "flicks": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]}, {"type": "letters", "row": 0, "flicks": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]},
+17 -4
View File
@@ -14,13 +14,26 @@ Item {
required property real keyHeight required property real keyHeight
required property bool pressed required property bool pressed
required property real flickProgress required property real flickProgress
required property string modifier
property int shiftMode: KeyboardController.Off property int shiftMode: KeyboardController.Off
property var modifierModes: ({})
property bool blank: false // labels hidden, e.g. in trackpad mode property bool blank: false // labels hidden, e.g. in trackpad mode
readonly property bool isCharacter: keyType === "char" || keyType === "space" readonly property bool isCharacter: keyType === "char" || keyType === "space"
readonly property bool shiftActive: keyType === "shift" && shiftMode !== KeyboardController.Off // Off / Once / Locked for shift, caps lock and ctrl/alt/super.
readonly property int latch: {
if (keyType === "shift")
return shiftMode
if (keyType === "capslock")
return shiftMode === KeyboardController.Locked ? KeyboardController.Locked : KeyboardController.Off
if (keyType === "modifier")
return modifierModes[modifier] ?? KeyboardController.Off
return KeyboardController.Off
}
// Shift shows locking with its glyph; the others turn accent-coloured.
readonly property bool locked: latch === KeyboardController.Locked && keyType !== "shift"
// Character keys darken when pressed; function keys lighten (as on iPad). // Character keys darken when pressed; function keys lighten (as on iPad).
readonly property bool lit: (isCharacter !== pressed) || shiftActive readonly property bool lit: (isCharacter !== pressed) || latch !== KeyboardController.Off
readonly property var defaultGlyphs: ({ readonly property var defaultGlyphs: ({
backspace: "⌫", backspace: "⌫",
@@ -51,7 +64,7 @@ Item {
id: face id: face
anchors.fill: parent anchors.fill: parent
radius: Theme.keyRadius radius: Theme.keyRadius
color: key.lit ? Theme.characterKey : Theme.functionKey color: key.locked ? Theme.accent : key.lit ? Theme.characterKey : Theme.functionKey
} }
// Flicking slides the secondary symbol down into the main position. // Flicking slides the secondary symbol down into the main position.
@@ -81,7 +94,7 @@ Item {
verticalCenterOffset: flick.visible ? key.height * 0.14 : 0 verticalCenterOffset: flick.visible ? key.height * 0.14 : 0
} }
font.pixelSize: key.height * (key.isWord ? 0.24 : flick.visible ? 0.34 : 0.4) font.pixelSize: key.height * (key.isWord ? 0.24 : flick.visible ? 0.34 : 0.4)
color: Theme.text color: key.locked ? Theme.accentText : Theme.text
visible: !key.blank visible: !key.blank
opacity: 1 - key.flickProgress opacity: 1 - key.flickProgress
} }
+1
View File
@@ -8,6 +8,7 @@ KeyboardSurface {
model: surface.model model: surface.model
delegate: Key { delegate: Key {
shiftMode: surface.controller ? surface.controller.shiftMode : KeyboardController.Off shiftMode: surface.controller ? surface.controller.shiftMode : KeyboardController.Off
modifierModes: surface.controller ? surface.controller.modifierModes : ({})
blank: surface.trackpadActive blank: surface.trackpadActive
} }
} }
+2
View File
@@ -9,6 +9,8 @@ Window {
required property Settings settings required property Settings settings
property var placement: null // FloatingWindow, set from C++ property var placement: null // FloatingWindow, set from C++
property bool settingsOpen: false property bool settingsOpen: false
// Height of the key area, for C++ keeping key sizes when rows change.
readonly property real keysHeight: keyboard.height
// Transparent room above the keyboard for long-press popups on the top row. // Transparent room above the keyboard for long-press popups on the top row.
readonly property real popupMargin: 90 readonly property real popupMargin: 90
+8
View File
@@ -47,6 +47,14 @@ Rectangle {
SectionHeader { text: qsTr("Keys") } SectionHeader { text: qsTr("Keys") }
ToggleRow {
width: parent.width
text: qsTr("Full Keyboard")
detail: qsTr("Number row, tab, ctrl, alt — like iPad Pro")
checked: panel.settings.fullKeyboard
onToggled: checked => panel.settings.fullKeyboard = checked
}
Repeater { Repeater {
// "main" is the typing keys; the rest come from the template. // "main" is the typing keys; the rest come from the template.
model: [{ id: "main", name: qsTr("Typing keys") }].concat(panel.layoutModel.sections) model: [{ id: "main", name: qsTr("Typing keys") }].concat(panel.layoutModel.sections)
+3 -1
View File
@@ -6,6 +6,8 @@ qt_add_library(sloposk_core STATIC
engine/InputEngine.h engine/InputEngine.h
engine/DirectEngine.h engine/DirectEngine.cpp engine/DirectEngine.h engine/DirectEngine.cpp
input/ShiftState.h input/ShiftState.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/PointerTracker.h input/PointerTracker.cpp
input/AlternatesPicker.h input/AlternatesPicker.cpp input/AlternatesPicker.h input/AlternatesPicker.cpp
input/KeyRepeater.h input/KeyRepeater.cpp input/KeyRepeater.h input/KeyRepeater.cpp
@@ -15,7 +17,7 @@ qt_add_library(sloposk_core STATIC
input/KeyboardSurface.h input/KeyboardSurface.cpp input/KeyboardSurface.h input/KeyboardSurface.cpp
layout/Layout.h layout/Layout.h
layout/LayoutLoader.h layout/LayoutLoader.cpp 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/LayoutBuilder.h layout/LayoutBuilder.cpp
layout/KeyGeometry.h layout/KeyGeometry.cpp layout/KeyGeometry.h layout/KeyGeometry.cpp
layout/LayoutModel.h layout/LayoutModel.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
};
+86 -16
View File
@@ -4,7 +4,7 @@
#include "engine/InputEngine.h" #include "engine/InputEngine.h"
#include "layout/LayoutModel.h" #include "layout/LayoutModel.h"
#include <xkbcommon/xkbcommon-keysyms.h> #include <xkbcommon/xkbcommon.h>
// ShiftMode mirrors ShiftState::Mode for QML. // ShiftMode mirrors ShiftState::Mode for QML.
static_assert(int(KeyboardController::ShiftMode::Off) == int(ShiftState::Mode::Off)); 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: case KeyType::Shift:
changeShift([&] { m_shift.press(timeMs); }); changeShift([&] { m_shift.press(timeMs); });
break; break;
case KeyType::Modifier:
m_modifiers.press(key.modifier, timeMs);
Q_EMIT modifiersChanged();
break;
case KeyType::Backspace: case KeyType::Backspace:
if (m_deadKeys.isPending()) {
m_deadKeys.cancel(); // backspace takes back the accent
} else if (m_modifiers.active()) {
sendKey(XKB_KEY_BackSpace);
} else {
deleteBackward(false); deleteBackward(false);
m_backspaceRepeater.start(); m_backspaceRepeater.start();
}
break; break;
case KeyType::LayerSwitch: case KeyType::LayerSwitch:
m_layerBeforeSwitch = m_model.layerName(); m_layerBeforeSwitch = m_model.layerName();
m_model.setLayer(key.target); m_model.setLayer(key.target);
break; break;
case KeyType::Keysym: case KeyType::Keysym:
m_engine.sendKeysym(key.keysym); m_repeatingModifiers = shortcutModifiers();
sendKey(key.keysym);
if (key.repeat) { if (key.repeat) {
m_repeatingKeysym = key.keysym; m_repeatingKeysym = key.keysym;
m_keysymRepeater.start(); m_keysymRepeater.start();
@@ -65,6 +76,13 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
case KeyType::Shift: case KeyType::Shift:
changeShift([&] { m_shift.release(); }); changeShift([&] { m_shift.release(); });
break; break;
case KeyType::CapsLock:
changeShift([&] { m_shift.toggleLock(); });
break;
case KeyType::Modifier:
m_modifiers.release(key.modifier);
Q_EMIT modifiersChanged();
break;
case KeyType::Backspace: case KeyType::Backspace:
m_backspaceRepeater.stop(); m_backspaceRepeater.stop();
break; break;
@@ -72,18 +90,12 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
stopRepeating(key); stopRepeating(key);
break; break;
case KeyType::Character: 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: case KeyType::Space:
typeText(QStringLiteral(" ")); typeCharacter(key, output, alternate);
break; break;
case KeyType::Enter: case KeyType::Enter:
m_engine.sendKeysym(XKB_KEY_Return); m_deadKeys.cancel();
sendKey(XKB_KEY_Return);
m_textBefore += u'\n'; m_textBefore += u'\n';
autoCapitalize(); autoCapitalize();
break; break;
@@ -102,7 +114,10 @@ void KeyboardController::keyCancelled(const KeyDef &key)
{ {
if (key.type == KeyType::Shift) if (key.type == KeyType::Shift)
changeShift([&] { m_shift.release(); }); 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(); m_backspaceRepeater.stop();
else if (key.type == KeyType::Keysym) else if (key.type == KeyType::Keysym)
stopRepeating(key); stopRepeating(key);
@@ -128,6 +143,7 @@ void KeyboardController::moveCursor(QPoint steps)
void KeyboardController::resetContext() void KeyboardController::resetContext()
{ {
m_deadKeys.cancel();
m_context = {}; m_context = {};
m_textBefore.clear(); m_textBefore.clear();
autoCapitalize(); autoCapitalize();
@@ -147,6 +163,52 @@ void KeyboardController::setOptions(const TypingOptions &options)
autoCapitalize(); 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) void KeyboardController::typeText(const QString &text)
{ {
if (text == u" " && m_options.doubleSpacePeriod && m_context.isProse() if (text == u" " && m_options.doubleSpacePeriod && m_context.isProse()
@@ -181,7 +243,7 @@ void KeyboardController::stopRepeating(const KeyDef &key)
void KeyboardController::sendRepeatedKeysym() void KeyboardController::sendRepeatedKeysym()
{ {
m_engine.sendKeysym(m_repeatingKeysym); m_engine.sendKeysym(m_repeatingKeysym, m_repeatingModifiers);
} }
void KeyboardController::autoCapitalize() void KeyboardController::autoCapitalize()
@@ -191,13 +253,21 @@ void KeyboardController::autoCapitalize()
changeShift([&] { m_shift.setAutomatic(capitalize); }); 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> template<typename Change>
void KeyboardController::changeShift(Change 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(); change();
if (m_shift.mode() == before) if (m_shift.mode() == mode && shiftLevel() == level)
return; return;
m_model.setUppercase(m_shift.isUppercase()); m_model.setShiftLevel(shiftLevel());
Q_EMIT shiftChanged(); Q_EMIT shiftChanged();
} }
+24 -4
View File
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include "DeadKeys.h"
#include "KeyRepeater.h" #include "KeyRepeater.h"
#include "Modifiers.h"
#include "ShiftState.h" #include "ShiftState.h"
#include "TypingOptions.h" #include "TypingOptions.h"
#include "backend/TextContext.h" #include "backend/TextContext.h"
@@ -16,16 +18,23 @@ class LayoutModel;
// Which of a key's outputs the gesture selected. // Which of a key's outputs the gesture selected.
enum class KeyOutput { Primary, Flick, Alternate }; enum class KeyOutput { Primary, Flick, Alternate };
// Keyboard rules: what each key does, shift, layers and the // Keyboard rules: what each key does, shift and the other modifiers, dead
// iOS conveniences (auto-capitalization, double-space period, backspace // keys, layers and the iOS conveniences (auto-capitalization, double-space
// repeat). Knows nothing about touch or Wayland: it receives key presses and // period, backspace repeat). Knows nothing about touch or Wayland: it
// text-field context, and talks to an InputEngine. // 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 class KeyboardController : public QObject
{ {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("Created in C++") QML_UNCREATABLE("Created in C++")
Q_PROPERTY(ShiftMode shiftMode READ shiftMode NOTIFY shiftChanged) Q_PROPERTY(ShiftMode shiftMode READ shiftMode NOTIFY shiftChanged)
// "ctrl"/"alt"/"super" → ShiftMode
Q_PROPERTY(QVariantMap modifierModes READ modifierModes NOTIFY modifiersChanged)
public: public:
enum class ShiftMode { Off, Once, Locked }; enum class ShiftMode { Off, Once, Locked };
@@ -34,6 +43,7 @@ public:
KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent = nullptr); KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent = nullptr);
ShiftMode shiftMode() const; ShiftMode shiftMode() const;
QVariantMap modifierModes() const { return m_modifiers.modes(); }
void keyDown(const KeyDef &key, qint64 timeMs); void keyDown(const KeyDef &key, qint64 timeMs);
void keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64 timeMs); void keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64 timeMs);
@@ -51,11 +61,18 @@ public:
Q_SIGNALS: Q_SIGNALS:
void shiftChanged(); void shiftChanged();
void modifiersChanged();
void dismissRequested(); void dismissRequested();
void nextLayoutRequested(); // the globe key: switching is the system's job void nextLayoutRequested(); // the globe key: switching is the system's job
private: private:
void typeCharacter(const KeyDef &key, KeyOutput output, const QString &alternate);
void typeText(const QString &text); 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 deleteBackward(bool wholeWord);
void sendRepeatedKeysym(); void sendRepeatedKeysym();
void stopRepeating(const KeyDef &key); void stopRepeating(const KeyDef &key);
@@ -67,9 +84,12 @@ private:
LayoutModel &m_model; LayoutModel &m_model;
TypingOptions m_options; TypingOptions m_options;
ShiftState m_shift; ShiftState m_shift;
Modifiers m_modifiers;
DeadKeys m_deadKeys;
KeyRepeater m_backspaceRepeater; KeyRepeater m_backspaceRepeater;
KeyRepeater m_keysymRepeater; // arrows, Delete, ... KeyRepeater m_keysymRepeater; // arrows, Delete, ...
quint32 m_repeatingKeysym = 0; quint32 m_repeatingKeysym = 0;
Qt::KeyboardModifiers m_repeatingModifiers;
QString m_layerBeforeSwitch; QString m_layerBeforeSwitch;
TextContext m_context; 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_lastPressUnlocked = m_mode == Mode::Locked;
m_held = true; m_held = true;
m_typedWhileHeld = false; m_typedWhileHeld = false;
m_automatic = false;
switch (m_mode) { switch (m_mode) {
case Mode::Off: m_mode = doubleTap ? Mode::Locked : Mode::Once; break; 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) if (m_held || m_mode == Mode::Locked)
return; return;
m_mode = capitalize ? Mode::Once : Mode::Off; 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 // - tap: shift the next character only
// - double-tap: caps lock; tap again to unlock // - double-tap: caps lock; tap again to unlock
// - hold while typing: every character shifted, off on release // - 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. // Times are in milliseconds and passed in, so this stays deterministic.
class ShiftState class ShiftState
{ {
@@ -16,7 +17,11 @@ public:
static constexpr qint64 DoubleTapMs = 300; static constexpr qint64 DoubleTapMs = 300;
Mode mode() const { return m_mode; } 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 press(qint64 timeMs);
void release(); void release();
@@ -24,9 +29,12 @@ public:
// Auto-capitalization hint; ignored while shift is held or locked. // Auto-capitalization hint; ignored while shift is held or locked.
void setAutomatic(bool capitalize); void setAutomatic(bool capitalize);
// Caps lock key: locks, or unlocks if locked.
void toggleLock();
private: private:
Mode m_mode = Mode::Off; Mode m_mode = Mode::Off;
bool m_automatic = false;
bool m_held = false; bool m_held = false;
bool m_typedWhileHeld = false; bool m_typedWhileHeld = false;
bool m_lastPressUnlocked = false; // a tap leaving caps lock doesn't start a double tap 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; 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> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardMetrics metrics)
{ {
QList<QRectF> rects; QList<QRectF> rects;
const qreal units = totalUnits(blocks); const qreal units = totalUnits(blocks);
qsizetype maxRows = 0; const qsizetype maxRows = totalRows(blocks);
for (const Block &block : blocks)
maxRows = std::max(maxRows, block.size());
if (units <= 0 || maxRows == 0) if (units <= 0 || maxRows == 0)
return rects; return rects;
+2
View File
@@ -29,3 +29,5 @@ inline QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMe
qreal rowUnits(const Row &row); qreal rowUnits(const Row &row);
// Width of all blocks in key units, ignoring gaps. // Width of all blocks in key units, ignoring gaps.
qreal totalUnits(const QList<Block> &blocks); 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, Backspace,
Enter, Enter,
Shift, Shift,
CapsLock,
Modifier, // ctrl, alt or super: sticky like shift, applied to the next key
LayerSwitch, LayerSwitch,
Globe, Globe,
Dismiss, Dismiss,
Spacer, // empty room, e.g. the half-key indent of the home row Spacer, // empty room, e.g. the half-key indent of the home row
Letters, // template only: a row of letters from the system keymap 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` 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 { struct KeyDef {
KeyType type = KeyType::Character; KeyType type = KeyType::Character;
QString label; // shown and typed (for character keys) QString label; // shown and typed (for character keys)
@@ -26,22 +34,30 @@ struct KeyDef {
QString flick; // secondary symbol, typed by flicking down QString flick; // secondary symbol, typed by flicking down
QStringList alternates; // long-press choices QStringList alternates; // long-press choices
QString target; // layer to switch to (LayerSwitch only) 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 qreal width = 1.0; // in key units
bool stretch = false; // grows so the row is as wide as its block's widest bool stretch = false; // grows so the row is as wide as its block's widest
quint32 keysym = 0; // Keysym keys only quint32 keysym = 0; // Keysym keys only
bool repeat = false; // Keysym keys only: auto-repeat while held bool repeat = false; // Keysym keys only: auto-repeat while held
// Letters placeholders only. // Letters/Keys placeholders only.
int lettersRow = -1; // which keymap row (0 = top) int lettersRow = -1; // which keymap row (Letters: 0 = top; Keys: 0 = number row)
QStringList flicks; // flick symbol per letter, left to right QStringList flicks; // flick symbol per letter, left to right
// Text this key shows/types; only character keys change case. // Text this key shows/types; only character keys change.
QString text(bool uppercase) const QString text(ShiftLevel level) const
{ {
if (!uppercase || type != KeyType::Character) if (level == ShiftLevel::None || type != KeyType::Character)
return label; return label;
if (level == ShiftLevel::Capitals)
return isLetter() ? label.toUpper() : label;
return shifted.isEmpty() ? label.toUpper() : shifted; 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>; using Row = QList<KeyDef>;
@@ -49,6 +65,7 @@ using Row = QList<KeyDef>;
struct Layer { struct Layer {
QString name; QString name;
QList<Row> rows; 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. // 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> #include <QSet>
namespace { namespace {
// Letters without a Unicode decomposition, and the key they belong under. // 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); return letter.normalized(QString::NormalizationForm_D).left(1);
} }
// Long-press choices per key letter: the keymap's extras first (they're what KeyDef characterKey(const KeymapKey &source)
// 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; KeyDef key;
for (const auto &row : letters.rows) { key.label = source.lower;
for (const Letter &letter : row) key.shifted = source.upper;
onKeys.insert(letter.lower); key.dead = source.deadLower;
key.deadShifted = source.deadUpper;
return key;
} }
QHash<QString, QStringList> result; // 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;
}
}
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) { const auto add = [&](const QString &base, const QString &choice) {
QStringList &list = result[base]; QStringList &list = choices[base];
if (!onKeys.contains(choice) && !list.contains(choice)) if (!onKeys.contains(choice) && !list.contains(choice))
list.append(choice); list.append(choice);
}; };
for (const Letter &extra : letters.extras) for (const auto &row : keymap.rows) {
add(baseLetter(extra.lower), extra.lower); 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 (auto it = tmpl.alternates.cbegin(); it != tmpl.alternates.cend(); ++it) {
for (const QString &choice : *it) for (const QString &choice : *it)
add(it.key(), choice); add(it.key(), choice);
} }
return result;
}
Row expandLetters(const Row &row, const KeymapLetters &letters, const QHash<QString, QStringList> &alternates) for (Row &row : layer.rows) {
{ for (KeyDef &key : row) {
Row result; if (key.type == KeyType::Character && key.isLetter() && key.alternates.isEmpty())
for (const KeyDef &key : row) { key.alternates = choices.value(key.label);
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;
} }
} // namespace } // namespace
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters) Layout buildLayout(const Layout &tmpl, const KeymapKeys &keymap, KeyboardMode mode)
{ {
Layout layout = tmpl; Layout layout = tmpl;
layout.name = letters.name.isEmpty() ? tmpl.name : letters.name; layout.name = keymap.name.isEmpty() ? tmpl.name : keymap.name;
layout.alternates.clear(); layout.alternates.clear();
const QHash<QString, QStringList> alternates = alternatesByLetter(tmpl, letters);
for (Layer &layer : layout.layers) { for (Layer &layer : layout.layers) {
if (mode == KeyboardMode::Full && !layer.fullRows.isEmpty())
layer.rows = layer.fullRows;
layer.fullRows.clear();
for (Row &row : layer.rows) for (Row &row : layer.rows)
row = expandLetters(row, letters, alternates); row = expandPlaceholders(row, keymap);
addAlternates(layer, tmpl, keymap);
} }
return layout; return layout;
} }
+10 -4
View File
@@ -1,8 +1,14 @@
#pragma once #pragma once
#include "KeymapLetters.h" #include "KeymapKeys.h"
#include "Layout.h" #include "Layout.h"
// Turns the template into the layout shown: fills its letters placeholders enum class KeyboardMode {
// from the keymap and gives each letter its flick and long-press choices. Compact, // iPad: letters only, symbols by flick and layers
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters); 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 { namespace {
constexpr int LetterRows = 3; constexpr int LetterRows = 3;
constexpr int KeyRows = 4;
const QStringList ModifierNames = {QStringLiteral("ctrl"), QStringLiteral("alt"), QStringLiteral("super")};
QStringList toStringList(const QJsonValue &value) QStringList toStringList(const QJsonValue &value)
{ {
@@ -28,11 +30,14 @@ const QHash<QString, KeyType> &keyTypesByName()
{QStringLiteral("backspace"), KeyType::Backspace}, {QStringLiteral("backspace"), KeyType::Backspace},
{QStringLiteral("enter"), KeyType::Enter}, {QStringLiteral("enter"), KeyType::Enter},
{QStringLiteral("shift"), KeyType::Shift}, {QStringLiteral("shift"), KeyType::Shift},
{QStringLiteral("capslock"), KeyType::CapsLock},
{QStringLiteral("modifier"), KeyType::Modifier},
{QStringLiteral("layer"), KeyType::LayerSwitch}, {QStringLiteral("layer"), KeyType::LayerSwitch},
{QStringLiteral("globe"), KeyType::Globe}, {QStringLiteral("globe"), KeyType::Globe},
{QStringLiteral("dismiss"), KeyType::Dismiss}, {QStringLiteral("dismiss"), KeyType::Dismiss},
{QStringLiteral("spacer"), KeyType::Spacer}, {QStringLiteral("spacer"), KeyType::Spacer},
{QStringLiteral("letters"), KeyType::Letters}, {QStringLiteral("letters"), KeyType::Letters},
{QStringLiteral("keys"), KeyType::Keys},
{QStringLiteral("keysym"), KeyType::Keysym}, {QStringLiteral("keysym"), KeyType::Keysym},
}; };
return types; return types;
@@ -85,10 +90,20 @@ std::optional<KeyDef> parseKey(const QJsonValue &value, QString *error)
*error = QStringLiteral("character key without a label"); *error = QStringLiteral("character key without a label");
return std::nullopt; return std::nullopt;
} }
key.modifier = obj.value(u"modifier").toString();
if (key.type == KeyType::Letters && (key.lettersRow < 0 || key.lettersRow >= LetterRows)) { if (key.type == KeyType::Letters && (key.lettersRow < 0 || key.lettersRow >= LetterRows)) {
*error = QStringLiteral("letters need a row from 0 to %1").arg(LetterRows - 1); *error = QStringLiteral("letters need a row from 0 to %1").arg(LetterRows - 1);
return std::nullopt; 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) { if (key.width <= 0) {
*error = QStringLiteral("key \"%1\" has a non-positive width").arg(key.label); *error = QStringLiteral("key \"%1\" has a non-positive width").arg(key.label);
return std::nullopt; return std::nullopt;
@@ -123,7 +138,8 @@ std::optional<Layer> parseLayer(const QJsonObject &obj, QString *error)
*error = QStringLiteral("layer without a name"); *error = QStringLiteral("layer without a name");
return std::nullopt; 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); *error = QStringLiteral("layer \"%1\": %2").arg(layer.name, *error);
return std::nullopt; return std::nullopt;
} }
@@ -210,7 +226,7 @@ std::optional<Layout> load(const QString &path, QString *error)
bool validate(const Layout &layout, QString *error) bool validate(const Layout &layout, QString *error)
{ {
for (const Layer &layer : layout.layers) { 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) { for (const KeyDef &key : row) {
if (key.type == KeyType::LayerSwitch && !layout.layer(key.target)) { if (key.type == KeyType::LayerSwitch && !layout.layer(key.target)) {
*error = QStringLiteral("layer \"%1\" switches to unknown layer \"%2\"") *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::Backspace: return QStringLiteral("backspace");
case KeyType::Enter: return QStringLiteral("enter"); case KeyType::Enter: return QStringLiteral("enter");
case KeyType::Shift: return QStringLiteral("shift"); 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::LayerSwitch: return QStringLiteral("layer");
case KeyType::Globe: return QStringLiteral("globe"); case KeyType::Globe: return QStringLiteral("globe");
case KeyType::Dismiss: return QStringLiteral("dismiss"); case KeyType::Dismiss: return QStringLiteral("dismiss");
case KeyType::Spacer: return QStringLiteral("spacer"); case KeyType::Spacer: return QStringLiteral("spacer");
case KeyType::Keysym: return QStringLiteral("keysym"); 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 {}; return {};
} }
@@ -44,8 +47,10 @@ QVariant LayoutModel::data(const QModelIndex &index, int role) const
const KeyDef &key = m_keys.at(index.row()); const KeyDef &key = m_keys.at(index.row());
const QRectF rect = m_rects.value(index.row()); const QRectF rect = m_rects.value(index.row());
switch (role) { switch (role) {
case LabelRole: return key.text(m_uppercase); case LabelRole: return key.text(m_shiftLevel);
case FlickLabelRole: return key.flick; // 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 KeyTypeRole: return keyTypeName(key.type);
case KeyXRole: return rect.x(); case KeyXRole: return rect.x();
case KeyYRole: return rect.y(); case KeyYRole: return rect.y();
@@ -69,6 +74,7 @@ QHash<int, QByteArray> LayoutModel::roleNames() const
{KeyHeightRole, "keyHeight"}, {KeyHeightRole, "keyHeight"},
{PressedRole, "pressed"}, {PressedRole, "pressed"},
{FlickProgressRole, "flickProgress"}, {FlickProgressRole, "flickProgress"},
{ModifierRole, "modifier"},
}; };
} }
@@ -111,6 +117,11 @@ qreal LayoutModel::totalUnits() const
return ::totalUnits(m_blocks); return ::totalUnits(m_blocks);
} }
qsizetype LayoutModel::totalRows() const
{
return ::totalRows(m_blocks);
}
void LayoutModel::setSize(QSizeF size) void LayoutModel::setSize(QSizeF size)
{ {
if (size == m_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}); 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; return;
m_uppercase = uppercase; m_shiftLevel = level;
if (!m_keys.isEmpty()) 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) void LayoutModel::setPressed(int row, bool pressed)
+5 -3
View File
@@ -30,6 +30,7 @@ public:
KeyHeightRole, KeyHeightRole,
PressedRole, PressedRole,
FlickProgressRole, FlickProgressRole,
ModifierRole,
}; };
using QAbstractListModel::QAbstractListModel; using QAbstractListModel::QAbstractListModel;
@@ -43,13 +44,14 @@ public:
QVariantList sections() const; QVariantList sections() const;
// Width in key units of everything shown. // Width in key units of everything shown.
qreal totalUnits() const; qreal totalUnits() const;
qsizetype totalRows() const;
void setLayout(const Layout &layout); void setLayout(const Layout &layout);
void setLayer(const QString &layerName); void setLayer(const QString &layerName);
// Section ids to show; MainSectionId stands for the layers. // Section ids to show; MainSectionId stands for the layers.
void setVisibleSections(const QStringList &ids); void setVisibleSections(const QStringList &ids);
void setSize(QSizeF size); void setSize(QSizeF size);
void setUppercase(bool uppercase); void setShiftLevel(ShiftLevel level);
void setPressed(int index, bool pressed); void setPressed(int index, bool pressed);
void setFlickProgress(int index, qreal progress); void setFlickProgress(int index, qreal progress);
@@ -57,7 +59,7 @@ public:
int keyAt(QPointF pos) const; int keyAt(QPointF pos) const;
const KeyDef &key(int index) const { return m_keys.at(index); } const KeyDef &key(int index) const { return m_keys.at(index); }
QRectF keyRect(int index) const { return m_rects.value(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: Q_SIGNALS:
void layoutChanged(); void layoutChanged();
@@ -73,7 +75,7 @@ private:
QStringList m_visibleSections{MainSectionId}; QStringList m_visibleSections{MainSectionId};
QList<Block> m_blocks; QList<Block> m_blocks;
QSizeF m_size; QSizeF m_size;
bool m_uppercase = false; ShiftLevel m_shiftLevel = ShiftLevel::None;
QList<KeyDef> m_keys; QList<KeyDef> m_keys;
QList<QRectF> m_rects; QList<QRectF> m_rects;
+31 -17
View File
@@ -19,6 +19,8 @@
#include <QQuickWindow> #include <QQuickWindow>
#include <QStandardPaths> #include <QStandardPaths>
#include <functional>
Q_IMPORT_QML_PLUGIN(SloposkPlugin) Q_IMPORT_QML_PLUGIN(SloposkPlugin)
namespace { namespace {
@@ -38,29 +40,41 @@ std::optional<Layout> loadTemplate()
return std::nullopt; return std::nullopt;
} }
// The shown layout is the template filled with the active system layout's letters. // The shown layout is the template filled with the active system layout's
void connectLayouts(const Layout &tmpl, WaylandKeymap &keymap, SystemLayouts &systemLayouts, // keys. Returns the rebuild, for settings that change the result.
KeyboardController &controller, LayoutModel &model) std::function<void()> connectLayouts(const Layout &tmpl, WaylandKeymap &keymap, SystemLayouts &systemLayouts,
const Settings &settings, KeyboardController &controller, LayoutModel &model)
{ {
const auto rebuild = [&] { 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(&keymap, &WaylandKeymap::changed, &model, rebuild);
QObject::connect(&systemLayouts, &SystemLayouts::currentChanged, &model, rebuild); QObject::connect(&systemLayouts, &SystemLayouts::currentChanged, &model, rebuild);
QObject::connect(&controller, &KeyboardController::nextLayoutRequested, &systemLayouts, QObject::connect(&controller, &KeyboardController::nextLayoutRequested, &systemLayouts,
[&] { systemLayouts.next(keymap.layoutCount()); }); [&] { systemLayouts.next(keymap.layoutCount()); });
rebuild(); rebuild();
return rebuild;
} }
// Showing or hiding a block resizes the window so keys keep their width. // Settings that add or remove keys resize the window so keys keep their size.
void connectSections(Settings &settings, LayoutModel &model, FloatingWindow &placement) void connectKeySettings(Settings &settings, LayoutModel &model, FloatingWindow &placement, const QQuickWindow &window,
const std::function<void()> &rebuild)
{ {
QObject::connect(&settings, &Settings::visibleSectionsChanged, &model, [&] { const auto keepingKeySize = [&](const std::function<void()> &change) {
const qreal before = model.totalUnits(); const qreal units = model.totalUnits();
model.setVisibleSections(settings.visibleSections()); const auto rows = qreal(model.totalRows());
if (before > 0) change();
placement.resizeBy(placement.geometry().width() * (model.totalUnits() / before - 1), 0); 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) void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings)
@@ -114,13 +128,13 @@ int main(int argc, char *argv[])
KeyboardController controller(engine, model); KeyboardController controller(engine, model);
connectTextContext(backend, controller); connectTextContext(backend, controller);
WaylandKeymap keymap;
SystemLayouts systemLayouts;
connectLayouts(*layoutTemplate, keymap, systemLayouts, controller, model);
Settings settings; Settings settings;
controller.setOptions(settings.typingOptions()); controller.setOptions(settings.typingOptions());
model.setVisibleSections(settings.visibleSections()); model.setVisibleSections(settings.visibleSections());
WaylandKeymap keymap;
SystemLayouts systemLayouts;
const auto rebuildLayout = connectLayouts(*layoutTemplate, keymap, systemLayouts, settings, controller, model);
QObject::connect(&settings, &Settings::typingOptionsChanged, &controller, QObject::connect(&settings, &Settings::typingOptionsChanged, &controller,
[&] { controller.setOptions(settings.typingOptions()); }); [&] { controller.setOptions(settings.typingOptions()); });
@@ -141,7 +155,7 @@ int main(int argc, char *argv[])
window->setProperty("placement", QVariant::fromValue(&placement)); window->setProperty("placement", QVariant::fromValue(&placement));
WindowSettings windowSettings; WindowSettings windowSettings;
restoreAndPersistGeometry(placement, windowSettings); restoreAndPersistGeometry(placement, windowSettings);
connectSections(settings, model, placement); connectKeySettings(settings, model, placement, *window, rebuildLayout);
VisibilityController visibility; VisibilityController visibility;
connectVisibility(visibility, backend, controller, placement); connectVisibility(visibility, backend, controller, placement);
+11
View File
@@ -8,6 +8,7 @@ namespace {
constexpr auto AutoCapitalizeKey = "typing/autoCapitalize"; constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod"; constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
constexpr auto VisibleSectionsKey = "layout/visibleSections"; constexpr auto VisibleSectionsKey = "layout/visibleSections";
constexpr auto FullKeyboardKey = "layout/fullKeyboard";
} }
Settings::Settings(QObject *parent) Settings::Settings(QObject *parent)
@@ -20,6 +21,16 @@ Settings::Settings(QObject *parent)
m_visibleSections = store.value(VisibleSectionsKey, QStringList{MainSectionId}).toStringList(); m_visibleSections = store.value(VisibleSectionsKey, QStringList{MainSectionId}).toStringList();
if (m_visibleSections.isEmpty()) if (m_visibleSections.isEmpty())
m_visibleSections = {MainSectionId}; 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) 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) Q_PROPERTY(bool doubleSpacePeriod READ doubleSpacePeriod WRITE setDoubleSpacePeriod NOTIFY typingOptionsChanged)
// Which key blocks are shown: "main" (the typing keys) and template section ids. // Which key blocks are shown: "main" (the typing keys) and template section ids.
Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged) 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: public:
explicit Settings(QObject *parent = nullptr); explicit Settings(QObject *parent = nullptr);
@@ -32,13 +34,18 @@ public:
// Hiding the last visible section is refused: something must stay on screen. // Hiding the last visible section is refused: something must stay on screen.
Q_INVOKABLE void setSectionVisible(const QString &id, bool visible); Q_INVOKABLE void setSectionVisible(const QString &id, bool visible);
bool fullKeyboard() const { return m_fullKeyboard; }
void setFullKeyboard(bool full);
Q_SIGNALS: Q_SIGNALS:
void typingOptionsChanged(); void typingOptionsChanged();
void visibleSectionsChanged(); void visibleSectionsChanged();
void fullKeyboardChanged();
private: private:
void setTypingOptions(const TypingOptions &options); void setTypingOptions(const TypingOptions &options);
TypingOptions m_typing; TypingOptions m_typing;
QStringList m_visibleSections; QStringList m_visibleSections;
bool m_fullKeyboard = false;
}; };
+1
View File
@@ -13,6 +13,7 @@ sloposk_add_test(tst_controller)
sloposk_add_test(tst_pointertracker) sloposk_add_test(tst_pointertracker)
sloposk_add_test(tst_alternatespicker) sloposk_add_test(tst_alternatespicker)
sloposk_add_test(tst_textrules) sloposk_add_test(tst_textrules)
sloposk_add_test(tst_deadkeys)
sloposk_add_test(tst_keyrepeater) sloposk_add_test(tst_keyrepeater)
sloposk_add_test(tst_visibility) sloposk_add_test(tst_visibility)
+5 -5
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
#include "layout/KeymapLetters.h" #include "layout/KeymapKeys.h"
#include "layout/LayoutBuilder.h" #include "layout/LayoutBuilder.h"
#include "layout/LayoutLoader.h" #include "layout/LayoutLoader.h"
@@ -26,7 +26,7 @@ inline Layout builtinTemplate()
return *layout; return *layout;
} }
inline KeymapLetters letters(const char *layout, const char *variant = nullptr) inline KeymapKeys keymapKeys(const char *layout, const char *variant = nullptr)
{ {
const std::unique_ptr<xkb_context, void (*)(xkb_context *)> context( const std::unique_ptr<xkb_context, void (*)(xkb_context *)> context(
xkb_context_new(XKB_CONTEXT_NO_ENVIRONMENT_NAMES), xkb_context_unref); xkb_context_new(XKB_CONTEXT_NO_ENVIRONMENT_NAMES), xkb_context_unref);
@@ -36,12 +36,12 @@ inline KeymapLetters letters(const char *layout, const char *variant = nullptr)
xkb_keymap_new_from_names(context.get(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref); xkb_keymap_new_from_names(context.get(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref);
if (!keymap) if (!keymap)
qFatal("no XKB keymap for %s", layout); qFatal("no XKB keymap for %s", layout);
return readKeymapLetters(keymap.get(), 0); return readKeymapKeys(keymap.get(), 0);
} }
inline Layout build(const char *layout, const char *variant = nullptr) inline Layout build(const char *layout, KeyboardMode mode = KeyboardMode::Compact)
{ {
return buildLayout(builtinTemplate(), letters(layout, variant)); return buildLayout(builtinTemplate(), keymapKeys(layout), mode);
} }
} // namespace TestLayouts } // namespace TestLayouts
+95
View File
@@ -230,6 +230,101 @@ private Q_SLOTS:
QCOMPARE(model.rowCount(), mainKeys); // never nothing QCOMPARE(model.rowCount(), mainKeys); // never nothing
} }
KeyDef keysymKey(quint32 keysym)
{
KeyDef key = makeKey(KeyType::Keysym);
key.keysym = keysym;
return key;
}
KeyDef modifierKey(const QString &name)
{
KeyDef key = makeKey(KeyType::Modifier);
key.modifier = name;
return key;
}
void ctrlTurnsKeysIntoShortcuts()
{
tap(modifierKey(u"ctrl"_s));
QCOMPARE(controller->modifierModes().value(u"ctrl"_s).toInt(), int(KeyboardController::ShiftMode::Once));
tap(makeKey(KeyType::Character, u"c"_s));
QCOMPARE(engine.keysyms, QList<quint32>{XKB_KEY_c});
QCOMPARE(engine.lastModifiers, Qt::ControlModifier);
QVERIFY(engine.output.isEmpty());
// One-shot: the next key types again.
tap(makeKey(KeyType::Character, u"c"_s));
QCOMPARE(engine.output, u"c");
}
void heldModifierAppliesToEveryKey()
{
const KeyDef alt = modifierKey(u"alt"_s);
controller->keyDown(alt, 0);
tap(keysymKey(XKB_KEY_Tab));
tap(keysymKey(XKB_KEY_Tab));
controller->keyUp(alt, KeyOutput::Primary, {}, 9000);
QCOMPARE(engine.keysyms.size(), 2);
QCOMPARE(engine.lastModifiers, Qt::AltModifier);
QCOMPARE(controller->modifierModes().value(u"alt"_s).toInt(), int(KeyboardController::ShiftMode::Off));
}
void tappedShiftAppliesToFunctionKeys()
{
tap(makeKey(KeyType::Character, u"a"_s)); // mid-sentence: no auto-shift
tap(makeKey(KeyType::Shift));
tap(keysymKey(XKB_KEY_F5));
QCOMPARE(engine.keysyms.last(), quint32(XKB_KEY_F5));
QCOMPARE(engine.lastModifiers, Qt::ShiftModifier);
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); // used up
}
void autoShiftAndCapsLockDontApplyToFunctionKeys()
{
controller->resetContext(); // sentence start: auto-shift on
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
tap(keysymKey(XKB_KEY_Left));
QCOMPARE(engine.lastModifiers, Qt::NoModifier);
tap(makeKey(KeyType::CapsLock));
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Locked);
tap(keysymKey(XKB_KEY_Left));
QCOMPARE(engine.lastModifiers, Qt::NoModifier);
tap(makeKey(KeyType::Character, u"a"_s));
tap(makeKey(KeyType::Character, u"b"_s));
QCOMPARE(engine.output, u"AB");
tap(makeKey(KeyType::CapsLock));
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
}
void autoShiftCapitalizesLettersOnly()
{
KeyDef lj = makeKey(KeyType::Character, u"ľ"_s); // sk number row: Shift+ľ = 2
lj.shifted = u"2"_s;
KeyDef one = makeKey(KeyType::Character, u"+"_s);
one.shifted = u"1"_s;
controller->resetContext(); // sentence start
tap(one);
QCOMPARE(engine.output, u"+"); // not the shifted level
controller->resetContext();
tap(lj);
QCOMPARE(engine.output, u"+Ľ");
tap(makeKey(KeyType::Shift)); // the user's shift does pick it
tap(lj);
QCOMPARE(engine.output, u"+Ľ2");
}
void deadKeyComposesWithNextCharacter()
{
KeyDef acute = makeKey(KeyType::Character, u"´"_s);
acute.dead = XKB_KEY_dead_acute;
tap(makeKey(KeyType::Character, u"x"_s));
tap(acute);
QVERIFY(engine.output == u"x"); // nothing typed yet
tap(makeKey(KeyType::Character, u"e"_s));
QVERIFY(engine.output.endsWith(u"é"_s));
}
void dismissEmitsSignal() void dismissEmitsSignal()
{ {
int dismissed = 0; int dismissed = 0;
+43
View File
@@ -0,0 +1,43 @@
#include "input/DeadKeys.h"
#include <QTest>
#include <xkbcommon/xkbcommon-keysyms.h>
using namespace Qt::StringLiterals;
class TestDeadKeys : public QObject
{
Q_OBJECT
private Q_SLOTS:
void composesWithTheNextLetter()
{
DeadKeys dead(u"en_US.UTF-8"_s);
QVERIFY(!dead.isPending());
dead.press(XKB_KEY_dead_acute);
QVERIFY(dead.isPending());
QCOMPARE(dead.compose(u"e"_s), u"é");
QVERIFY(!dead.isPending());
dead.press(XKB_KEY_dead_caron);
QCOMPARE(dead.compose(u"C"_s), u"Č");
}
void typesAccentAndLetterWhenNothingComposes()
{
DeadKeys dead(u"en_US.UTF-8"_s);
dead.press(XKB_KEY_dead_caron);
QCOMPARE(dead.compose(u"q"_s), u"ˇq");
}
void cancelForgetsTheAccent()
{
DeadKeys dead(u"en_US.UTF-8"_s);
dead.press(XKB_KEY_dead_acute);
dead.cancel();
QVERIFY(!dead.isPending());
}
};
QTEST_GUILESS_MAIN(TestDeadKeys)
#include "tst_deadkeys.moc"
+32
View File
@@ -1,6 +1,7 @@
#include "TestLayouts.h" #include "TestLayouts.h"
#include <QTest> #include <QTest>
#include <xkbcommon/xkbcommon-keysyms.h>
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
@@ -90,6 +91,37 @@ private Q_SLOTS:
QVERIFY(a.contains(u"á"_s)); QVERIFY(a.contains(u"á"_s));
} }
void fullModeHasEveryTypingKey()
{
const Layout us = TestLayouts::build("us", KeyboardMode::Full);
const auto labels = [&](int row) {
QString text;
for (const KeyDef &key : us.layers.first().rows[row]) {
if (key.type == KeyType::Character)
text += key.label;
}
return text;
};
QCOMPARE(labels(0), u"`1234567890-=");
QCOMPARE(labels(1), u"qwertyuiop[]\\");
QCOMPARE(labels(2), u"asdfghjkl;'");
QCOMPARE(labels(3), u"zxcvbnm,./"); // LSGT (< >) repeats , . shifted: left out
QCOMPARE(letterKey(us, u"1"_s).flick, u"!");
QCOMPARE(letterKey(us, u"1"_s).shifted, u"!");
}
void fullModeKeepsDeadKeysAndKeymapShift()
{
const Layout sk = TestLayouts::build("sk", KeyboardMode::Full);
const KeyDef &acute = letterKey(sk, u"´"_s);
QCOMPARE(acute.dead, quint32(XKB_KEY_dead_acute));
QCOMPARE(acute.deadShifted, quint32(XKB_KEY_dead_caron));
QCOMPARE(letterKey(sk, u"ô"_s).shifted, u"\""); // as the physical key
QCOMPARE(letterKey(sk, u"ľ"_s).shifted, u"2");
// ľ has its own key now, so it's not a long-press choice on l.
QVERIFY(!letterKey(sk, u"l"_s).alternates.contains(u"ľ"_s));
}
void noPlaceholdersRemain() void noPlaceholdersRemain()
{ {
for (const Layer &layer : TestLayouts::build("us").layers) { for (const Layer &layer : TestLayouts::build("us").layers) {
+7 -3
View File
@@ -72,14 +72,18 @@ private Q_SLOTS:
void builtLayersHaveEvenRows_data() void builtLayersHaveEvenRows_data()
{ {
QTest::addColumn<QString>("xkbLayout"); QTest::addColumn<QString>("xkbLayout");
for (const char *layout : {"us", "sk", "se", "de", "fr", "ru", "gr"}) QTest::addColumn<KeyboardMode>("mode");
QTest::newRow(layout) << QString::fromLatin1(layout); for (const char *layout : {"us", "sk", "se", "de", "fr", "ru", "gr"}) {
QTest::addRow("%s compact", layout) << QString::fromLatin1(layout) << KeyboardMode::Compact;
QTest::addRow("%s full", layout) << QString::fromLatin1(layout) << KeyboardMode::Full;
}
} }
void builtLayersHaveEvenRows() void builtLayersHaveEvenRows()
{ {
QFETCH(QString, xkbLayout); QFETCH(QString, xkbLayout);
const Layout layout = TestLayouts::build(qPrintable(xkbLayout)); QFETCH(KeyboardMode, mode);
const Layout layout = TestLayouts::build(qPrintable(xkbLayout), mode);
for (const Layer &layer : layout.layers) { for (const Layer &layer : layout.layers) {
const QList<QRectF> rects = computeKeyRects(layer, {1000, 300}); const QList<QRectF> rects = computeKeyRects(layer, {1000, 300});
qsizetype i = 0; qsizetype i = 0;