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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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, {}},
|
||||||
|
}};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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("◌"));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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\"")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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 ´ = 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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user