Fix double-space period, shift double-tap; settings; letters from system keymap
- Double-space replaced the space via a Backspace keysym, which clients apply after the commit: use delete_surrounding_text with the commit - Tests use a shared FakeEngine that applies ops to a simulated field - Double-tap shift locks at a sentence start; typing between taps doesn't - Organization name set: config lives in ~/.config/sloposk/ - Settings (auto-capitalization, double-space period) with a ⚙ panel - Letter rows come from the compositor's XKB keymap; other letters of the layout become long-press alternates; globe switches the system layout over org.kde.keyboard - One JSON template for non-letter layers; stretch keys even out rows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,8 @@ iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML U
|
|||||||
|
|
||||||
- Flick a key down for its secondary symbol; long-press for accented alternates
|
- Flick a key down for its secondary symbol; long-press for accented alternates
|
||||||
- iOS shift: tap = one letter, double-tap = caps lock, hold = shift while held
|
- iOS shift: tap = one letter, double-tap = caps lock, hold = shift while held
|
||||||
- Auto-capitalization, double-space → ". ", backspace repeat → word delete
|
- 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
|
||||||
- Long-press space = trackpad; slide from `.?123` onto a symbol and release
|
- Long-press space = trackpad; slide from `.?123` onto a symbol and release
|
||||||
- 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
|
||||||
|
|
||||||
@@ -33,23 +34,23 @@ Dev hooks: `SLOPOSK_AUTOTYPE=text` commits on focus; `SLOPOSK_SCREENSHOT=file.pn
|
|||||||
|
|
||||||
## Layouts
|
## Layouts
|
||||||
|
|
||||||
JSON in `layouts/` (built in) or `~/.local/share/sloposk/layouts/` (overrides by `id`). The globe key cycles them; the system locale's comes first.
|
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.
|
||||||
|
|
||||||
|
Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.json` replaces it.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{"type": "letters", "row": 0, "flicks": ["1", "2", "3"]},
|
||||||
"id": "sv-SE", "name": "Svenska", "locale": "sv_SE",
|
{"type": "backspace", "width": 1.5, "stretch": true}
|
||||||
"base": "en-US",
|
|
||||||
"layers": [{"name": "letters", "rows": [[
|
|
||||||
"q",
|
|
||||||
{"label": "e", "flick": "3", "alternates": ["é", "è"]},
|
|
||||||
{"type": "backspace", "width": 1.5}
|
|
||||||
]]}]
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Key: a string, or an object with `type` (`char` `space` `backspace` `enter` `shift` `layer` `globe` `dismiss` `spacer`), `label`, `shifted`, `flick`, `alternates`, `width` (key units), `target` (for `layer`)
|
- Key: a string, or an object with `type` (`char` `space` `backspace` `enter` `shift` `layer` `globe` `dismiss` `spacer` `letters`), `label`, `shifted`, `flick`, `alternates`, `width` (key units), `target` (for `layer`), `stretch`
|
||||||
- `base`: take layers this layout doesn't define (e.g. numbers/symbols) from another layout
|
- `letters`: the keymap row `row` (0–2), with `flicks` handed out left to right
|
||||||
- Rows in a layer should add up to the same width; `tst_layoutloader` checks the built-ins
|
- `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
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -57,10 +58,11 @@ JSON in `layouts/` (built in) or `~/.local/share/sloposk/layouts/` (overrides by
|
|||||||
qml/ view only
|
qml/ view only
|
||||||
input/ touch → PointerTracker gestures → KeyboardController rules
|
input/ touch → PointerTracker gestures → KeyboardController rules
|
||||||
engine/ key output → text ops (DirectEngine; prediction/swipe later)
|
engine/ key output → text ops (DirectEngine; prediction/swipe later)
|
||||||
backend/ text ops → compositor (KWin input-method-v1)
|
backend/ text ops → compositor (KWin input-method-v1); the compositor's keymap
|
||||||
layout/ JSON → Layout → LayoutModel (geometry, hit testing)
|
layout/ template JSON + keymap letters → Layout → LayoutModel (geometry, hit testing)
|
||||||
window/ floating layer-shell placement, visibility, saved geometry
|
window/ floating layer-shell placement, visibility, saved geometry
|
||||||
app/ D-Bus control, dev hooks
|
app/ D-Bus control and system layouts, dev hooks
|
||||||
|
settings/ persisted user preferences
|
||||||
```
|
```
|
||||||
|
|
||||||
`sloposk_core` (everything compositor-independent, plus the `Sloposk` QML module) is a static library shared by the app and the tests.
|
`sloposk_core` (everything compositor-independent, plus the `Sloposk` QML module) is a static library shared by the app and the tests.
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "sv-SE",
|
|
||||||
"name": "Svenska",
|
|
||||||
"locale": "sv_SE",
|
|
||||||
"base": "en-US",
|
|
||||||
"layers": [
|
|
||||||
{
|
|
||||||
"name": "letters",
|
|
||||||
"rows": [
|
|
||||||
[
|
|
||||||
{"label": "q", "flick": "1"},
|
|
||||||
{"label": "w", "flick": "2"},
|
|
||||||
{"label": "e", "flick": "3", "alternates": ["é", "è", "ê", "ë"]},
|
|
||||||
{"label": "r", "flick": "4"},
|
|
||||||
{"label": "t", "flick": "5"},
|
|
||||||
{"label": "y", "flick": "6", "alternates": ["ý", "ü"]},
|
|
||||||
{"label": "u", "flick": "7", "alternates": ["ü", "ú", "ù", "û"]},
|
|
||||||
{"label": "i", "flick": "8", "alternates": ["í", "ì", "î", "ï"]},
|
|
||||||
{"label": "o", "flick": "9", "alternates": ["ó", "ò", "ô", "õ", "ø", "œ"]},
|
|
||||||
{"label": "p", "flick": "0"},
|
|
||||||
{"label": "å", "flick": "^"},
|
|
||||||
{"type": "backspace", "width": 1.5}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{"label": "a", "flick": "@", "alternates": ["á", "à", "â", "ã", "æ"]},
|
|
||||||
{"label": "s", "flick": "#", "alternates": ["ß", "š"]},
|
|
||||||
{"label": "d", "flick": "$", "alternates": ["ð"]},
|
|
||||||
{"label": "f", "flick": "&"},
|
|
||||||
{"label": "g", "flick": "*"},
|
|
||||||
{"label": "h", "flick": "("},
|
|
||||||
{"label": "j", "flick": ")"},
|
|
||||||
{"label": "k", "flick": "'"},
|
|
||||||
{"label": "l", "flick": "\""},
|
|
||||||
{"label": "ö", "flick": ";", "alternates": ["ø", "œ"]},
|
|
||||||
{"label": "ä", "flick": ":", "alternates": ["æ"]},
|
|
||||||
{"type": "enter", "width": 1.5}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{"type": "shift", "width": 1.5},
|
|
||||||
{"label": "z", "flick": "%", "alternates": ["ž"]},
|
|
||||||
{"label": "x", "flick": "-"},
|
|
||||||
{"label": "c", "flick": "+", "alternates": ["ç", "č"]},
|
|
||||||
{"label": "v", "flick": "="},
|
|
||||||
{"label": "b", "flick": "/"},
|
|
||||||
{"label": "n", "flick": "_", "alternates": ["ñ"]},
|
|
||||||
{"label": "m", "flick": "~"},
|
|
||||||
{"label": ",", "flick": "!", "shifted": ","},
|
|
||||||
{"label": ".", "flick": "?", "shifted": ".", "alternates": ["…"]},
|
|
||||||
{"label": "-", "flick": "–", "shifted": "-", "alternates": ["—", "•"]},
|
|
||||||
{"type": "shift"}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{"type": "layer", "label": ".?123", "target": "numbers", "width": 1.5},
|
|
||||||
{"type": "globe"},
|
|
||||||
{"type": "space", "width": 7.5},
|
|
||||||
{"type": "layer", "label": ".?123", "target": "numbers", "width": 1.5},
|
|
||||||
{"type": "dismiss"}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,54 +1,45 @@
|
|||||||
{
|
{
|
||||||
"id": "en-US",
|
"id": "template",
|
||||||
"name": "English (US)",
|
"name": "Template",
|
||||||
"locale": "en_US",
|
"alternates": {
|
||||||
|
"a": ["á", "à", "â", "ä", "æ", "ã", "å", "ā"],
|
||||||
|
"c": ["ç", "ć", "č"],
|
||||||
|
"d": ["ð"],
|
||||||
|
"e": ["é", "è", "ê", "ë", "ē", "ė", "ę"],
|
||||||
|
"i": ["í", "ì", "î", "ï", "ī", "į"],
|
||||||
|
"l": ["ł"],
|
||||||
|
"n": ["ñ", "ń"],
|
||||||
|
"o": ["ó", "ò", "ô", "ö", "õ", "ø", "œ", "ō"],
|
||||||
|
"s": ["ß", "ś", "š"],
|
||||||
|
"t": ["þ"],
|
||||||
|
"u": ["ú", "ù", "û", "ü", "ū"],
|
||||||
|
"y": ["ý", "ÿ"],
|
||||||
|
"z": ["ž", "ź", "ż"]
|
||||||
|
},
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"name": "letters",
|
"name": "letters",
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
{"label": "q", "flick": "1"},
|
{"type": "letters", "row": 0, "flicks": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]},
|
||||||
{"label": "w", "flick": "2"},
|
{"type": "backspace", "width": 1.5, "stretch": true}
|
||||||
{"label": "e", "flick": "3", "alternates": ["é", "è", "ê", "ë", "ē", "ė", "ę"]},
|
|
||||||
{"label": "r", "flick": "4"},
|
|
||||||
{"label": "t", "flick": "5", "alternates": ["þ"]},
|
|
||||||
{"label": "y", "flick": "6", "alternates": ["ý", "ÿ"]},
|
|
||||||
{"label": "u", "flick": "7", "alternates": ["ú", "ù", "û", "ü", "ū"]},
|
|
||||||
{"label": "i", "flick": "8", "alternates": ["í", "ì", "î", "ï", "ī", "į"]},
|
|
||||||
{"label": "o", "flick": "9", "alternates": ["ó", "ò", "ô", "ö", "õ", "ø", "œ", "ō"]},
|
|
||||||
{"label": "p", "flick": "0"},
|
|
||||||
{"type": "backspace", "width": 1.5}
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{"type": "spacer", "width": 0.5},
|
{"type": "spacer", "width": 0.5},
|
||||||
{"label": "a", "flick": "@", "alternates": ["á", "à", "â", "ä", "æ", "ã", "å", "ā"]},
|
{"type": "letters", "row": 1, "flicks": ["@", "#", "$", "&", "*", "(", ")", "'", "\""]},
|
||||||
{"label": "s", "flick": "#", "alternates": ["ß", "ś", "š"]},
|
{"type": "enter", "width": 2, "stretch": true}
|
||||||
{"label": "d", "flick": "$", "alternates": ["ð"]},
|
|
||||||
{"label": "f", "flick": "&"},
|
|
||||||
{"label": "g", "flick": "*"},
|
|
||||||
{"label": "h", "flick": "("},
|
|
||||||
{"label": "j", "flick": ")"},
|
|
||||||
{"label": "k", "flick": "'"},
|
|
||||||
{"label": "l", "flick": "\"", "alternates": ["ł"]},
|
|
||||||
{"type": "enter", "width": 2}
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{"type": "shift"},
|
{"type": "shift", "stretch": true},
|
||||||
{"label": "z", "flick": "%", "alternates": ["ž", "ź", "ż"]},
|
{"type": "letters", "row": 2, "flicks": ["%", "-", "+", "=", "/", ";", ":"]},
|
||||||
{"label": "x", "flick": "-"},
|
|
||||||
{"label": "c", "flick": "+", "alternates": ["ç", "ć", "č"]},
|
|
||||||
{"label": "v", "flick": "="},
|
|
||||||
{"label": "b", "flick": "/"},
|
|
||||||
{"label": "n", "flick": ";", "alternates": ["ñ", "ń"]},
|
|
||||||
{"label": "m", "flick": ":"},
|
|
||||||
{"label": ",", "flick": "!", "shifted": ","},
|
{"label": ",", "flick": "!", "shifted": ","},
|
||||||
{"label": ".", "flick": "?", "shifted": ".", "alternates": ["…"]},
|
{"label": ".", "flick": "?", "shifted": ".", "alternates": ["…"]},
|
||||||
{"type": "shift", "width": 1.5}
|
{"type": "shift", "width": 1.5, "stretch": true}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{"type": "layer", "label": ".?123", "target": "numbers", "width": 1.5},
|
{"type": "layer", "label": ".?123", "target": "numbers", "width": 1.5},
|
||||||
{"type": "globe"},
|
{"type": "globe"},
|
||||||
{"type": "space", "width": 6.5},
|
{"type": "space", "width": 6.5, "stretch": true},
|
||||||
{"type": "layer", "label": ".?123", "target": "numbers", "width": 1.5},
|
{"type": "layer", "label": ".?123", "target": "numbers", "width": 1.5},
|
||||||
{"type": "dismiss"}
|
{"type": "dismiss"}
|
||||||
]
|
]
|
||||||
@@ -6,7 +6,9 @@ Window {
|
|||||||
|
|
||||||
required property LayoutModel layoutModel
|
required property LayoutModel layoutModel
|
||||||
required property KeyboardController controller
|
required property KeyboardController controller
|
||||||
|
required property Settings settings
|
||||||
property var placement: null // FloatingWindow, set from C++
|
property var placement: null // FloatingWindow, set from C++
|
||||||
|
property bool settingsOpen: false
|
||||||
|
|
||||||
// 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
|
||||||
@@ -35,16 +37,29 @@ Window {
|
|||||||
id: keyboard
|
id: keyboard
|
||||||
anchors { left: parent.left; right: parent.right; top: parent.top; bottom: handle.top }
|
anchors { left: parent.left; right: parent.right; top: parent.top; bottom: handle.top }
|
||||||
anchors.margins: 2
|
anchors.margins: 2
|
||||||
|
visible: !root.settingsOpen
|
||||||
model: root.layoutModel
|
model: root.layoutModel
|
||||||
controller: root.controller
|
controller: root.controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsPanel {
|
||||||
|
anchors.fill: keyboard
|
||||||
|
visible: root.settingsOpen
|
||||||
|
settings: root.settings
|
||||||
|
}
|
||||||
|
|
||||||
DragHandle {
|
DragHandle {
|
||||||
id: handle
|
id: handle
|
||||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
||||||
onMoved: (dx, dy) => root.placement?.moveBy(dx, dy)
|
onMoved: (dx, dy) => root.placement?.moveBy(dx, dy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
anchors { left: parent.left; leftMargin: 8; bottom: parent.bottom }
|
||||||
|
checked: root.settingsOpen
|
||||||
|
onClicked: root.settingsOpen = !root.settingsOpen
|
||||||
|
}
|
||||||
|
|
||||||
ResizeHandle {
|
ResizeHandle {
|
||||||
anchors { right: parent.right; bottom: parent.bottom }
|
anchors { right: parent.right; bottom: parent.bottom }
|
||||||
onResized: (dw, dh) => root.placement?.resizeBy(dw, dh)
|
onResized: (dw, dh) => root.placement?.resizeBy(dw, dh)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Gear in the grab bar; opens and closes the settings panel.
|
||||||
|
Item {
|
||||||
|
id: button
|
||||||
|
|
||||||
|
property bool checked: false
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
implicitWidth: 36
|
||||||
|
implicitHeight: 22
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: button.checked ? "⌨" : "⚙"
|
||||||
|
font.pixelSize: 15
|
||||||
|
color: tap.pressed ? Theme.text : Theme.handle
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
id: tap
|
||||||
|
onTapped: button.clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Sloposk
|
||||||
|
|
||||||
|
// Replaces the keys while open. Binds straight to Settings, which persists.
|
||||||
|
Rectangle {
|
||||||
|
id: panel
|
||||||
|
|
||||||
|
required property Settings settings
|
||||||
|
|
||||||
|
color: Theme.background
|
||||||
|
radius: Theme.windowRadius
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors { fill: parent; margins: 16 }
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
width: parent.width
|
||||||
|
text: qsTr("Auto-Capitalization")
|
||||||
|
detail: qsTr("Shift at the start of a sentence")
|
||||||
|
checked: panel.settings.autoCapitalize
|
||||||
|
onToggled: checked => panel.settings.autoCapitalize = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
width: parent.width
|
||||||
|
text: qsTr("“.” Shortcut")
|
||||||
|
detail: qsTr("Double-tap space for a period and a space")
|
||||||
|
checked: panel.settings.doubleSpacePeriod
|
||||||
|
onToggled: checked => panel.settings.doubleSpacePeriod = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ QtObject {
|
|||||||
readonly property color handle: dark ? "#8e8e93" : "#7c7c80"
|
readonly property color handle: dark ? "#8e8e93" : "#7c7c80"
|
||||||
readonly property color accent: dark ? "#0a84ff" : "#007aff"
|
readonly property color accent: dark ? "#0a84ff" : "#007aff"
|
||||||
readonly property color accentText: "#ffffff"
|
readonly property color accentText: "#ffffff"
|
||||||
|
readonly property color toggleOn: dark ? "#30d158" : "#34c759"
|
||||||
|
|
||||||
readonly property real windowRadius: 14
|
readonly property real windowRadius: 14
|
||||||
readonly property real keyRadius: 6
|
readonly property real keyRadius: 6
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// A labelled on/off switch in the iOS style.
|
||||||
|
Item {
|
||||||
|
id: row
|
||||||
|
|
||||||
|
required property string text
|
||||||
|
property string detail: ""
|
||||||
|
property bool checked: false
|
||||||
|
signal toggled(bool checked)
|
||||||
|
|
||||||
|
implicitHeight: 52
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors { left: parent.left; right: track.left; rightMargin: 12; verticalCenter: parent.verticalCenter }
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: row.text
|
||||||
|
color: Theme.text
|
||||||
|
font.pixelSize: 16
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: text !== ""
|
||||||
|
text: row.detail
|
||||||
|
color: Theme.secondaryText
|
||||||
|
font.pixelSize: 12
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: track
|
||||||
|
anchors { right: parent.right; verticalCenter: parent.verticalCenter }
|
||||||
|
width: 50
|
||||||
|
height: 30
|
||||||
|
radius: height / 2
|
||||||
|
color: row.checked ? Theme.toggleOn : Theme.functionKey
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: row.checked ? parent.width - width - 2 : 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.height - 4
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
color: "#ffffff"
|
||||||
|
Behavior on x { NumberAnimation { duration: 120 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: row.toggled(!row.checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -10,15 +10,18 @@ qt_add_library(sloposk_core STATIC
|
|||||||
input/AlternatesPicker.h input/AlternatesPicker.cpp
|
input/AlternatesPicker.h input/AlternatesPicker.cpp
|
||||||
input/KeyRepeater.h input/KeyRepeater.cpp
|
input/KeyRepeater.h input/KeyRepeater.cpp
|
||||||
input/TextRules.h input/TextRules.cpp
|
input/TextRules.h input/TextRules.cpp
|
||||||
|
input/TypingOptions.h
|
||||||
input/KeyboardController.h input/KeyboardController.cpp
|
input/KeyboardController.h input/KeyboardController.cpp
|
||||||
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/LayoutRepository.h layout/LayoutRepository.cpp
|
layout/KeymapLetters.h layout/KeymapLetters.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
|
||||||
window/VisibilityController.h window/VisibilityController.cpp
|
window/VisibilityController.h window/VisibilityController.cpp
|
||||||
window/WindowSettings.h window/WindowSettings.cpp
|
window/WindowSettings.h window/WindowSettings.cpp
|
||||||
|
settings/Settings.h settings/Settings.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# QML lives in the top-level qml/ dir; alias each file so the module sees it at its root.
|
# QML lives in the top-level qml/ dir; alias each file so the module sees it at its root.
|
||||||
@@ -29,6 +32,9 @@ set(SLOPOSK_QML_FILES
|
|||||||
AlternatesPopup.qml
|
AlternatesPopup.qml
|
||||||
DragHandle.qml
|
DragHandle.qml
|
||||||
ResizeHandle.qml
|
ResizeHandle.qml
|
||||||
|
SettingsButton.qml
|
||||||
|
SettingsPanel.qml
|
||||||
|
ToggleRow.qml
|
||||||
Theme.qml
|
Theme.qml
|
||||||
)
|
)
|
||||||
list(TRANSFORM SLOPOSK_QML_FILES PREPEND ${PROJECT_SOURCE_DIR}/qml/ OUTPUT_VARIABLE sloposk_qml_paths)
|
list(TRANSFORM SLOPOSK_QML_FILES PREPEND ${PROJECT_SOURCE_DIR}/qml/ OUTPUT_VARIABLE sloposk_qml_paths)
|
||||||
@@ -55,6 +61,7 @@ target_include_directories(sloposk_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
|||||||
target_include_directories(sloposk_core PRIVATE
|
target_include_directories(sloposk_core PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/input
|
${CMAKE_CURRENT_SOURCE_DIR}/input
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/layout
|
${CMAKE_CURRENT_SOURCE_DIR}/layout
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/settings
|
||||||
)
|
)
|
||||||
target_link_libraries(sloposk_core PUBLIC Qt6::Quick PkgConfig::XKB)
|
target_link_libraries(sloposk_core PUBLIC Qt6::Quick PkgConfig::XKB)
|
||||||
|
|
||||||
@@ -64,9 +71,11 @@ qt_add_executable(sloposk
|
|||||||
main.cpp
|
main.cpp
|
||||||
app/DevHooks.h app/DevHooks.cpp
|
app/DevHooks.h app/DevHooks.cpp
|
||||||
app/DBusControl.h app/DBusControl.cpp
|
app/DBusControl.h app/DBusControl.cpp
|
||||||
|
app/SystemLayouts.h app/SystemLayouts.cpp
|
||||||
backend/InputMethodProtocol.h
|
backend/InputMethodProtocol.h
|
||||||
backend/KWinInputMethod.h backend/KWinInputMethod.cpp
|
backend/KWinInputMethod.h backend/KWinInputMethod.cpp
|
||||||
backend/KWinInputContext.h backend/KWinInputContext.cpp
|
backend/KWinInputContext.h backend/KWinInputContext.cpp
|
||||||
|
backend/WaylandKeymap.h backend/WaylandKeymap.cpp
|
||||||
window/FloatingWindow.h window/FloatingWindow.cpp
|
window/FloatingWindow.h window/FloatingWindow.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#include "SystemLayouts.h"
|
||||||
|
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusMessage>
|
||||||
|
#include <QDBusReply>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const QString Service = QStringLiteral("org.kde.keyboard");
|
||||||
|
const QString Path = QStringLiteral("/Layouts");
|
||||||
|
const QString Interface = QStringLiteral("org.kde.KeyboardLayouts");
|
||||||
|
|
||||||
|
QDBusMessage call(const QString &method, const QVariantList &args = {})
|
||||||
|
{
|
||||||
|
QDBusMessage message = QDBusMessage::createMethodCall(Service, Path, Interface, method);
|
||||||
|
message.setArguments(args);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SystemLayouts::SystemLayouts(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
const QDBusReply<uint> reply = bus.call(call(QStringLiteral("getLayout")));
|
||||||
|
m_system = reply.isValid();
|
||||||
|
if (!m_system)
|
||||||
|
return;
|
||||||
|
m_current = reply.value();
|
||||||
|
bus.connect(Service, Path, Interface, QStringLiteral("layoutChanged"), this, SLOT(setCurrent(uint)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemLayouts::next(quint32 count)
|
||||||
|
{
|
||||||
|
if (count < 2)
|
||||||
|
return;
|
||||||
|
const quint32 index = (m_current + 1) % count;
|
||||||
|
if (m_system)
|
||||||
|
QDBusConnection::sessionBus().asyncCall(call(QStringLiteral("setLayout"), {index})); // echoes layoutChanged
|
||||||
|
else
|
||||||
|
setCurrent(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemLayouts::setCurrent(uint index)
|
||||||
|
{
|
||||||
|
if (index == m_current)
|
||||||
|
return;
|
||||||
|
m_current = index;
|
||||||
|
Q_EMIT currentChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
// Which of the system's keyboard layouts is active, followed and switched
|
||||||
|
// over KWin's org.kde.keyboard D-Bus interface, so the on-screen and the
|
||||||
|
// physical keyboard always agree. Without that service it switches locally.
|
||||||
|
class SystemLayouts : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SystemLayouts(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
quint32 current() const { return m_current; }
|
||||||
|
// Switches to the next of `count` layouts.
|
||||||
|
void next(quint32 count);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void currentChanged();
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void setCurrent(uint index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_system = false;
|
||||||
|
quint32 m_current = 0;
|
||||||
|
};
|
||||||
@@ -53,6 +53,14 @@ void KWinInputContext::commit(const QString &text)
|
|||||||
commit_string(m_serial, text);
|
commit_string(m_serial, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void KWinInputContext::replaceBeforeCursor(const QString &before, const QString &text)
|
||||||
|
{
|
||||||
|
// Offsets are UTF-8 bytes relative to the cursor; applied with the commit.
|
||||||
|
const auto bytes = uint32_t(before.toUtf8().size());
|
||||||
|
delete_surrounding_text(-int32_t(bytes), bytes);
|
||||||
|
commit_string(m_serial, text);
|
||||||
|
}
|
||||||
|
|
||||||
void KWinInputContext::preedit(const QString &text)
|
void KWinInputContext::preedit(const QString &text)
|
||||||
{
|
{
|
||||||
preedit_string(m_serial, text, text);
|
preedit_string(m_serial, text, text);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public:
|
|||||||
TextContext context() const { return m_context; }
|
TextContext context() const { return m_context; }
|
||||||
|
|
||||||
void commit(const QString &text);
|
void commit(const QString &text);
|
||||||
|
void replaceBeforeCursor(const QString &before, const QString &text);
|
||||||
void preedit(const QString &text);
|
void preedit(const QString &text);
|
||||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers);
|
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers);
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ void KWinInputMethod::commitText(const QString &text)
|
|||||||
m_context->commit(text);
|
m_context->commit(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void KWinInputMethod::replaceBeforeCursor(const QString &before, const QString &text)
|
||||||
|
{
|
||||||
|
if (m_context)
|
||||||
|
m_context->replaceBeforeCursor(before, text);
|
||||||
|
}
|
||||||
|
|
||||||
void KWinInputMethod::setPreedit(const QString &text)
|
void KWinInputMethod::setPreedit(const QString &text)
|
||||||
{
|
{
|
||||||
if (m_context)
|
if (m_context)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public:
|
|||||||
TextContext context() const override;
|
TextContext context() const override;
|
||||||
|
|
||||||
void commitText(const QString &text) override;
|
void commitText(const QString &text) override;
|
||||||
|
void replaceBeforeCursor(const QString &before, const QString &text) override;
|
||||||
void setPreedit(const QString &text) override;
|
void setPreedit(const QString &text) override;
|
||||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ public:
|
|||||||
virtual TextContext context() const { return {}; }
|
virtual TextContext context() const { return {}; }
|
||||||
|
|
||||||
Q_INVOKABLE virtual void commitText(const QString &text) = 0;
|
Q_INVOKABLE virtual void commitText(const QString &text) = 0;
|
||||||
|
// Replaces `before`, the text right before the cursor, with `text` in one
|
||||||
|
// step. Don't emulate this with a Backspace keysym: clients may apply key
|
||||||
|
// events after commits, so the deletion would hit the new text.
|
||||||
|
virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0;
|
||||||
Q_INVOKABLE virtual void setPreedit(const QString &text) = 0;
|
Q_INVOKABLE virtual void setPreedit(const QString &text) = 0;
|
||||||
Q_INVOKABLE virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0;
|
Q_INVOKABLE virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#include "WaylandKeymap.h"
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <wayland-client.h>
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
// Plain libwayland on Qt's display: Qt's own seat wrappers are private API.
|
||||||
|
// Our proxies live on the default queue, which Qt dispatches.
|
||||||
|
class WaylandKeymap::Seat
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Seat(wl_display *display, WaylandKeymap &owner)
|
||||||
|
: m_owner(owner)
|
||||||
|
, m_registry(wl_display_get_registry(display))
|
||||||
|
{
|
||||||
|
wl_registry_add_listener(m_registry, &RegistryListener, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~Seat()
|
||||||
|
{
|
||||||
|
if (m_keyboard)
|
||||||
|
wl_keyboard_release(m_keyboard);
|
||||||
|
if (m_seat)
|
||||||
|
wl_seat_release(m_seat);
|
||||||
|
wl_registry_destroy(m_registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t SeatVersion = 5; // wl_keyboard.release, wl_seat.release
|
||||||
|
|
||||||
|
static void global(void *data, wl_registry *registry, uint32_t name, const char *interface, uint32_t version)
|
||||||
|
{
|
||||||
|
auto *self = static_cast<Seat *>(data);
|
||||||
|
if (self->m_seat || std::strcmp(interface, wl_seat_interface.name) != 0 || version < SeatVersion)
|
||||||
|
return;
|
||||||
|
self->m_seat = static_cast<wl_seat *>(wl_registry_bind(registry, name, &wl_seat_interface, SeatVersion));
|
||||||
|
wl_seat_add_listener(self->m_seat, &SeatListener, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void capabilities(void *data, wl_seat *seat, uint32_t caps)
|
||||||
|
{
|
||||||
|
auto *self = static_cast<Seat *>(data);
|
||||||
|
const bool hasKeyboard = caps & WL_SEAT_CAPABILITY_KEYBOARD;
|
||||||
|
if (hasKeyboard && !self->m_keyboard) {
|
||||||
|
self->m_keyboard = wl_seat_get_keyboard(seat);
|
||||||
|
wl_keyboard_add_listener(self->m_keyboard, &KeyboardListener, self);
|
||||||
|
} else if (!hasKeyboard && self->m_keyboard) {
|
||||||
|
wl_keyboard_release(std::exchange(self->m_keyboard, nullptr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void keymap(void *data, wl_keyboard *, uint32_t format, int32_t fd, uint32_t size)
|
||||||
|
{
|
||||||
|
if (format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)
|
||||||
|
static_cast<Seat *>(data)->m_owner.loadKeymap(fd, size);
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr wl_registry_listener RegistryListener = {
|
||||||
|
.global = global,
|
||||||
|
.global_remove = [](void *, wl_registry *, uint32_t) {},
|
||||||
|
};
|
||||||
|
static constexpr wl_seat_listener SeatListener = {
|
||||||
|
.capabilities = capabilities,
|
||||||
|
.name = [](void *, wl_seat *, const char *) {},
|
||||||
|
};
|
||||||
|
// The window never has keyboard focus; only the keymap matters.
|
||||||
|
static constexpr wl_keyboard_listener KeyboardListener = {
|
||||||
|
.keymap = keymap,
|
||||||
|
.enter = [](void *, wl_keyboard *, uint32_t, wl_surface *, wl_array *) {},
|
||||||
|
.leave = [](void *, wl_keyboard *, uint32_t, wl_surface *) {},
|
||||||
|
.key = [](void *, wl_keyboard *, uint32_t, uint32_t, uint32_t, uint32_t) {},
|
||||||
|
.modifiers = [](void *, wl_keyboard *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) {},
|
||||||
|
.repeat_info = [](void *, wl_keyboard *, int32_t, int32_t) {},
|
||||||
|
};
|
||||||
|
|
||||||
|
WaylandKeymap &m_owner;
|
||||||
|
wl_registry *m_registry;
|
||||||
|
wl_seat *m_seat = nullptr;
|
||||||
|
wl_keyboard *m_keyboard = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
WaylandKeymap::WaylandKeymap(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_context(xkb_context_new(XKB_CONTEXT_NO_FLAGS), xkb_context_unref)
|
||||||
|
, m_keymap(nullptr, xkb_keymap_unref)
|
||||||
|
{
|
||||||
|
setKeymap(xkb_keymap_new_from_names(m_context.get(), nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS));
|
||||||
|
|
||||||
|
if (auto *wayland = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>())
|
||||||
|
m_seat = std::make_unique<Seat>(wayland->display(), *this);
|
||||||
|
}
|
||||||
|
|
||||||
|
WaylandKeymap::~WaylandKeymap() = default;
|
||||||
|
|
||||||
|
quint32 WaylandKeymap::layoutCount() const
|
||||||
|
{
|
||||||
|
return m_keymap ? xkb_keymap_num_layouts(m_keymap.get()) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaylandKeymap::loadKeymap(int fd, uint32_t size)
|
||||||
|
{
|
||||||
|
void *data = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||||
|
if (data == MAP_FAILED)
|
||||||
|
return;
|
||||||
|
const auto *text = static_cast<const char *>(data);
|
||||||
|
// `size` counts the terminating NUL; the buffer API wants the text length.
|
||||||
|
setKeymap(xkb_keymap_new_from_buffer(m_context.get(), text, strnlen(text, size), XKB_KEYMAP_FORMAT_TEXT_V1,
|
||||||
|
XKB_KEYMAP_COMPILE_NO_FLAGS));
|
||||||
|
munmap(data, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaylandKeymap::setKeymap(xkb_keymap *keymap)
|
||||||
|
{
|
||||||
|
if (!keymap) {
|
||||||
|
qWarning("Failed to compile the keyboard keymap");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_keymap.reset(keymap);
|
||||||
|
Q_EMIT changed();
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
struct xkb_context;
|
||||||
|
struct xkb_keymap;
|
||||||
|
|
||||||
|
// The compositor's keymap, received on a wl_keyboard of our own (the keyboard
|
||||||
|
// window never has focus, but the keymap is sent regardless). Until it
|
||||||
|
// arrives, or without a keyboard on the seat, XKB's default is used
|
||||||
|
// (XKB_DEFAULT_LAYOUT etc., else "us").
|
||||||
|
class WaylandKeymap : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WaylandKeymap(QObject *parent = nullptr);
|
||||||
|
~WaylandKeymap() override;
|
||||||
|
|
||||||
|
// Valid until the next changed().
|
||||||
|
xkb_keymap *keymap() const { return m_keymap.get(); }
|
||||||
|
quint32 layoutCount() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void changed();
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Seat;
|
||||||
|
|
||||||
|
void loadKeymap(int fd, uint32_t size);
|
||||||
|
void setKeymap(xkb_keymap *keymap);
|
||||||
|
|
||||||
|
std::unique_ptr<xkb_context, void (*)(xkb_context *)> m_context;
|
||||||
|
std::unique_ptr<xkb_keymap, void (*)(xkb_keymap *)> m_keymap;
|
||||||
|
std::unique_ptr<Seat> m_seat;
|
||||||
|
};
|
||||||
@@ -12,6 +12,11 @@ void DirectEngine::commitText(const QString &text)
|
|||||||
m_backend.commitText(text);
|
m_backend.commitText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DirectEngine::replaceBeforeCursor(const QString &before, const QString &text)
|
||||||
|
{
|
||||||
|
m_backend.replaceBeforeCursor(before, text);
|
||||||
|
}
|
||||||
|
|
||||||
void DirectEngine::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
|
void DirectEngine::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
|
||||||
{
|
{
|
||||||
m_backend.sendKeysym(keysym, modifiers);
|
m_backend.sendKeysym(keysym, modifiers);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public:
|
|||||||
explicit DirectEngine(TextBackend &backend);
|
explicit DirectEngine(TextBackend &backend);
|
||||||
|
|
||||||
void commitText(const QString &text) override;
|
void commitText(const QString &text) override;
|
||||||
|
void replaceBeforeCursor(const QString &before, const QString &text) override;
|
||||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -12,5 +12,7 @@ public:
|
|||||||
virtual ~InputEngine() = default;
|
virtual ~InputEngine() = default;
|
||||||
|
|
||||||
virtual void commitText(const QString &text) = 0;
|
virtual void commitText(const QString &text) = 0;
|
||||||
|
// See TextBackend::replaceBeforeCursor.
|
||||||
|
virtual void replaceBeforeCursor(const QString &before, const QString &text) = 0;
|
||||||
virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0;
|
virtual void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier) = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#include "engine/InputEngine.h"
|
#include "engine/InputEngine.h"
|
||||||
#include "layout/LayoutModel.h"
|
#include "layout/LayoutModel.h"
|
||||||
#include "layout/LayoutRepository.h"
|
|
||||||
|
|
||||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||||
|
|
||||||
@@ -17,15 +16,11 @@ namespace {
|
|||||||
constexpr qsizetype MaxTextBefore = 256;
|
constexpr qsizetype MaxTextBefore = 256;
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model,
|
KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent)
|
||||||
const LayoutRepository &layouts, QObject *parent)
|
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_engine(engine)
|
, m_engine(engine)
|
||||||
, m_model(model)
|
, m_model(model)
|
||||||
, m_layouts(layouts)
|
|
||||||
{
|
{
|
||||||
if (!m_layouts.layouts().isEmpty())
|
|
||||||
m_model.setLayout(m_layouts.layouts().first());
|
|
||||||
connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward);
|
connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +74,7 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
|
|||||||
autoCapitalize();
|
autoCapitalize();
|
||||||
break;
|
break;
|
||||||
case KeyType::Globe:
|
case KeyType::Globe:
|
||||||
nextLayout();
|
Q_EMIT nextLayoutRequested();
|
||||||
break;
|
break;
|
||||||
case KeyType::Dismiss:
|
case KeyType::Dismiss:
|
||||||
Q_EMIT dismissRequested();
|
Q_EMIT dismissRequested();
|
||||||
@@ -130,26 +125,25 @@ void KeyboardController::setContext(const TextContext &context)
|
|||||||
autoCapitalize();
|
autoCapitalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardController::nextLayout()
|
void KeyboardController::setOptions(const TypingOptions &options)
|
||||||
{
|
{
|
||||||
const auto &layouts = m_layouts.layouts();
|
m_options = options;
|
||||||
if (layouts.size() < 2)
|
autoCapitalize();
|
||||||
return;
|
|
||||||
m_layoutIndex = int((m_layoutIndex + 1) % layouts.size());
|
|
||||||
m_model.setLayout(layouts[m_layoutIndex]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardController::typeText(const QString &text)
|
void KeyboardController::typeText(const QString &text)
|
||||||
{
|
{
|
||||||
if (text == u" " && m_context.isProse() && TextRules::wantsDoubleSpacePeriod(m_textBefore)) {
|
if (text == u" " && m_options.doubleSpacePeriod && m_context.isProse()
|
||||||
m_engine.sendKeysym(XKB_KEY_BackSpace);
|
&& TextRules::wantsDoubleSpacePeriod(m_textBefore)) {
|
||||||
|
// "word " + space → "word. ", replacing the first space in place.
|
||||||
|
m_engine.replaceBeforeCursor(QStringLiteral(" "), QStringLiteral(". "));
|
||||||
m_textBefore.chop(1);
|
m_textBefore.chop(1);
|
||||||
typeText(QStringLiteral(". "));
|
m_textBefore += QStringLiteral(". ");
|
||||||
return;
|
} else {
|
||||||
|
m_engine.commitText(text);
|
||||||
|
m_textBefore += text;
|
||||||
}
|
}
|
||||||
|
m_textBefore = m_textBefore.right(MaxTextBefore);
|
||||||
m_engine.commitText(text);
|
|
||||||
m_textBefore = (m_textBefore + text).right(MaxTextBefore);
|
|
||||||
changeShift([&] { m_shift.characterTyped(); });
|
changeShift([&] { m_shift.characterTyped(); });
|
||||||
autoCapitalize();
|
autoCapitalize();
|
||||||
}
|
}
|
||||||
@@ -164,7 +158,8 @@ void KeyboardController::deleteBackward(bool wholeWord)
|
|||||||
|
|
||||||
void KeyboardController::autoCapitalize()
|
void KeyboardController::autoCapitalize()
|
||||||
{
|
{
|
||||||
const bool capitalize = m_context.allowsAutoCapitalization() && TextRules::startsSentence(m_textBefore);
|
const bool capitalize = m_options.autoCapitalize && m_context.allowsAutoCapitalization()
|
||||||
|
&& TextRules::startsSentence(m_textBefore);
|
||||||
changeShift([&] { m_shift.setAutomatic(capitalize); });
|
changeShift([&] { m_shift.setAutomatic(capitalize); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "KeyRepeater.h"
|
#include "KeyRepeater.h"
|
||||||
#include "ShiftState.h"
|
#include "ShiftState.h"
|
||||||
|
#include "TypingOptions.h"
|
||||||
#include "backend/TextContext.h"
|
#include "backend/TextContext.h"
|
||||||
#include "layout/Layout.h"
|
#include "layout/Layout.h"
|
||||||
|
|
||||||
@@ -11,12 +12,11 @@
|
|||||||
|
|
||||||
class InputEngine;
|
class InputEngine;
|
||||||
class LayoutModel;
|
class LayoutModel;
|
||||||
class LayoutRepository;
|
|
||||||
|
|
||||||
// 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, layout switching and the
|
// Keyboard rules: what each key does, shift, layers and the
|
||||||
// iOS conveniences (auto-capitalization, double-space period, backspace
|
// iOS conveniences (auto-capitalization, double-space period, backspace
|
||||||
// repeat). Knows nothing about touch or Wayland: it receives key presses and
|
// repeat). Knows nothing about touch or Wayland: it receives key presses and
|
||||||
// text-field context, and talks to an InputEngine.
|
// text-field context, and talks to an InputEngine.
|
||||||
@@ -31,8 +31,7 @@ public:
|
|||||||
enum class ShiftMode { Off, Once, Locked };
|
enum class ShiftMode { Off, Once, Locked };
|
||||||
Q_ENUM(ShiftMode)
|
Q_ENUM(ShiftMode)
|
||||||
|
|
||||||
KeyboardController(InputEngine &engine, LayoutModel &model, const LayoutRepository &layouts,
|
KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent = nullptr);
|
||||||
QObject *parent = nullptr);
|
|
||||||
|
|
||||||
ShiftMode shiftMode() const;
|
ShiftMode shiftMode() const;
|
||||||
|
|
||||||
@@ -48,12 +47,12 @@ public:
|
|||||||
void resetContext();
|
void resetContext();
|
||||||
// The focused field reported new text/content type.
|
// The focused field reported new text/content type.
|
||||||
void setContext(const TextContext &context);
|
void setContext(const TextContext &context);
|
||||||
|
void setOptions(const TypingOptions &options);
|
||||||
Q_INVOKABLE void nextLayout();
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void shiftChanged();
|
void shiftChanged();
|
||||||
void dismissRequested();
|
void dismissRequested();
|
||||||
|
void nextLayoutRequested(); // the globe key: switching is the system's job
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void typeText(const QString &text);
|
void typeText(const QString &text);
|
||||||
@@ -64,10 +63,9 @@ private:
|
|||||||
|
|
||||||
InputEngine &m_engine;
|
InputEngine &m_engine;
|
||||||
LayoutModel &m_model;
|
LayoutModel &m_model;
|
||||||
const LayoutRepository &m_layouts;
|
TypingOptions m_options;
|
||||||
ShiftState m_shift;
|
ShiftState m_shift;
|
||||||
KeyRepeater m_backspaceRepeater;
|
KeyRepeater m_backspaceRepeater;
|
||||||
int m_layoutIndex = 0;
|
|
||||||
QString m_layerBeforeSwitch;
|
QString m_layerBeforeSwitch;
|
||||||
|
|
||||||
TextContext m_context;
|
TextContext m_context;
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
void ShiftState::press(qint64 timeMs)
|
void ShiftState::press(qint64 timeMs)
|
||||||
{
|
{
|
||||||
const bool doubleTap = timeMs - m_lastPress <= DoubleTapMs;
|
// Locks whatever the first tap did: at a sentence start that tap turns
|
||||||
|
// auto-shift off, and the second must still lock.
|
||||||
|
const bool doubleTap = timeMs - m_lastPress <= DoubleTapMs && !m_lastPressUnlocked;
|
||||||
m_lastPress = timeMs;
|
m_lastPress = timeMs;
|
||||||
|
m_lastPressUnlocked = m_mode == Mode::Locked;
|
||||||
m_held = true;
|
m_held = true;
|
||||||
m_typedWhileHeld = false;
|
m_typedWhileHeld = false;
|
||||||
|
|
||||||
switch (m_mode) {
|
switch (m_mode) {
|
||||||
case Mode::Off: m_mode = Mode::Once; break;
|
case Mode::Off: m_mode = doubleTap ? Mode::Locked : Mode::Once; break;
|
||||||
case Mode::Once: m_mode = doubleTap ? Mode::Locked : Mode::Off; break;
|
case Mode::Once: m_mode = doubleTap ? Mode::Locked : Mode::Off; break;
|
||||||
case Mode::Locked: m_mode = Mode::Off; break;
|
case Mode::Locked: m_mode = Mode::Off; break;
|
||||||
}
|
}
|
||||||
@@ -23,6 +26,7 @@ void ShiftState::release()
|
|||||||
|
|
||||||
void ShiftState::characterTyped()
|
void ShiftState::characterTyped()
|
||||||
{
|
{
|
||||||
|
m_lastPress = NoPress; // shift, type, shift is not a double tap
|
||||||
if (m_held)
|
if (m_held)
|
||||||
m_typedWhileHeld = true;
|
m_typedWhileHeld = true;
|
||||||
else if (m_mode == Mode::Once)
|
else if (m_mode == Mode::Once)
|
||||||
|
|||||||
@@ -29,5 +29,7 @@ private:
|
|||||||
Mode m_mode = Mode::Off;
|
Mode m_mode = Mode::Off;
|
||||||
bool m_held = false;
|
bool m_held = false;
|
||||||
bool m_typedWhileHeld = false;
|
bool m_typedWhileHeld = false;
|
||||||
qint64 m_lastPress = std::numeric_limits<qint64>::min() / 2;
|
bool m_lastPressUnlocked = false; // a tap leaving caps lock doesn't start a double tap
|
||||||
|
static constexpr qint64 NoPress = std::numeric_limits<qint64>::min() / 2;
|
||||||
|
qint64 m_lastPress = NoPress;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// User choices for the typing conveniences. Plain data: the controller reads
|
||||||
|
// it, Settings persists it.
|
||||||
|
struct TypingOptions {
|
||||||
|
bool autoCapitalize = true;
|
||||||
|
bool doubleSpacePeriod = true;
|
||||||
|
|
||||||
|
bool operator==(const TypingOptions &) const = default;
|
||||||
|
};
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
qreal rowUnits(const Row &row)
|
qreal rowUnits(const Row &row)
|
||||||
{
|
{
|
||||||
qreal units = 0;
|
qreal units = 0;
|
||||||
@@ -12,8 +10,6 @@ qreal rowUnits(const Row &row)
|
|||||||
return units;
|
return units;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics)
|
QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics)
|
||||||
{
|
{
|
||||||
QList<QRectF> rects;
|
QList<QRectF> rects;
|
||||||
|
|||||||
@@ -14,3 +14,6 @@ struct KeyboardMetrics {
|
|||||||
// order (spacers included). One key unit has the same width in every row, so
|
// order (spacers included). One key unit has the same width in every row, so
|
||||||
// rows with equal unit totals line up; shorter rows are centered.
|
// rows with equal unit totals line up; shorter rows are centered.
|
||||||
QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {});
|
QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {});
|
||||||
|
|
||||||
|
// Total width of a row in key units.
|
||||||
|
qreal rowUnits(const Row &row);
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#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);
|
||||||
+11
-4
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
@@ -14,6 +15,7 @@ enum class KeyType {
|
|||||||
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
|
||||||
};
|
};
|
||||||
|
|
||||||
struct KeyDef {
|
struct KeyDef {
|
||||||
@@ -24,6 +26,11 @@ struct KeyDef {
|
|||||||
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)
|
||||||
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 the layer's widest
|
||||||
|
|
||||||
|
// Letters placeholders only.
|
||||||
|
int lettersRow = -1; // which keymap row (0 = top)
|
||||||
|
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 case.
|
||||||
QString text(bool uppercase) const
|
QString text(bool uppercase) const
|
||||||
@@ -42,11 +49,11 @@ struct Layer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct Layout {
|
struct Layout {
|
||||||
QString id; // e.g. "en-US"
|
QString id;
|
||||||
QString name; // human readable
|
QString name; // human readable, e.g. "Slovak"
|
||||||
QString locale; // e.g. "en_US"
|
|
||||||
QString base; // id of a layout to take missing layers from
|
|
||||||
QList<Layer> layers; // the first one is shown by default
|
QList<Layer> layers; // the first one is shown by default
|
||||||
|
// Template only: long-press choices for keymap letters, by lowercase letter.
|
||||||
|
QHash<QString, QStringList> alternates;
|
||||||
|
|
||||||
const Layer *layer(const QString &layerName) const
|
const Layer *layer(const QString &layerName) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#include "LayoutBuilder.h"
|
||||||
|
#include "KeyGeometry.h"
|
||||||
|
|
||||||
|
#include <QSet>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Letters without a Unicode decomposition, and the key they belong under.
|
||||||
|
const QHash<QString, QString> &baseOverrides()
|
||||||
|
{
|
||||||
|
static const QHash<QString, QString> bases = {
|
||||||
|
{QStringLiteral("ß"), QStringLiteral("s")}, {QStringLiteral("ł"), QStringLiteral("l")},
|
||||||
|
{QStringLiteral("ø"), QStringLiteral("o")}, {QStringLiteral("æ"), QStringLiteral("a")},
|
||||||
|
{QStringLiteral("œ"), QStringLiteral("o")}, {QStringLiteral("đ"), QStringLiteral("d")},
|
||||||
|
{QStringLiteral("ð"), QStringLiteral("d")}, {QStringLiteral("þ"), QStringLiteral("t")},
|
||||||
|
{QStringLiteral("ħ"), QStringLiteral("h")}, {QStringLiteral("ı"), QStringLiteral("i")},
|
||||||
|
};
|
||||||
|
return bases;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "š" → "s": the letter an accented letter is a variant of.
|
||||||
|
QString baseLetter(const QString &letter)
|
||||||
|
{
|
||||||
|
if (const auto it = baseOverrides().constFind(letter); it != baseOverrides().cend())
|
||||||
|
return *it;
|
||||||
|
return letter.normalized(QString::NormalizationForm_D).left(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Long-press choices per key letter: the keymap's extras first (they're what
|
||||||
|
// this language types), then the template's, minus letters that have a key.
|
||||||
|
QHash<QString, QStringList> alternatesByLetter(const Layout &tmpl, const KeymapLetters &letters)
|
||||||
|
{
|
||||||
|
QSet<QString> onKeys;
|
||||||
|
for (const auto &row : letters.rows) {
|
||||||
|
for (const Letter &letter : row)
|
||||||
|
onKeys.insert(letter.lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<QString, QStringList> result;
|
||||||
|
const auto add = [&](const QString &base, const QString &choice) {
|
||||||
|
QStringList &list = result[base];
|
||||||
|
if (!onKeys.contains(choice) && !list.contains(choice))
|
||||||
|
list.append(choice);
|
||||||
|
};
|
||||||
|
for (const Letter &extra : letters.extras)
|
||||||
|
add(baseLetter(extra.lower), extra.lower);
|
||||||
|
for (auto it = tmpl.alternates.cbegin(); it != tmpl.alternates.cend(); ++it) {
|
||||||
|
for (const QString &choice : *it)
|
||||||
|
add(it.key(), choice);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Row expandLetters(const Row &row, const KeymapLetters &letters, const QHash<QString, QStringList> &alternates)
|
||||||
|
{
|
||||||
|
Row result;
|
||||||
|
for (const KeyDef &key : row) {
|
||||||
|
if (key.type != KeyType::Letters) {
|
||||||
|
result.append(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QList<Letter> source = letters.rows.value(key.lettersRow);
|
||||||
|
for (qsizetype i = 0; i < source.size(); ++i) {
|
||||||
|
KeyDef letter;
|
||||||
|
letter.label = source[i].lower;
|
||||||
|
letter.shifted = source[i].upper;
|
||||||
|
letter.flick = key.flicks.value(i);
|
||||||
|
letter.alternates = alternates.value(source[i].lower);
|
||||||
|
result.append(letter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stretchRows(Layer &layer)
|
||||||
|
{
|
||||||
|
qreal widest = 0;
|
||||||
|
for (const Row &row : layer.rows)
|
||||||
|
widest = std::max(widest, rowUnits(row));
|
||||||
|
|
||||||
|
for (Row &row : layer.rows) {
|
||||||
|
const auto stretchy = std::ranges::count_if(row, &KeyDef::stretch);
|
||||||
|
if (stretchy == 0)
|
||||||
|
continue;
|
||||||
|
const qreal extra = (widest - rowUnits(row)) / qreal(stretchy);
|
||||||
|
for (KeyDef &key : row) {
|
||||||
|
if (key.stretch)
|
||||||
|
key.width += extra;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters)
|
||||||
|
{
|
||||||
|
Layout layout = tmpl;
|
||||||
|
layout.name = letters.name.isEmpty() ? tmpl.name : letters.name;
|
||||||
|
layout.alternates.clear();
|
||||||
|
|
||||||
|
const QHash<QString, QStringList> alternates = alternatesByLetter(tmpl, letters);
|
||||||
|
for (Layer &layer : layout.layers) {
|
||||||
|
for (Row &row : layer.rows)
|
||||||
|
row = expandLetters(row, letters, alternates);
|
||||||
|
stretchRows(layer);
|
||||||
|
}
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "KeymapLetters.h"
|
||||||
|
#include "Layout.h"
|
||||||
|
|
||||||
|
// Turns the template into the layout shown: fills its letters placeholders
|
||||||
|
// from the keymap, gives each letter its flick and long-press choices, and
|
||||||
|
// stretches the marked keys so every row of a layer is equally wide.
|
||||||
|
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters);
|
||||||
@@ -9,6 +9,16 @@ namespace LayoutLoader {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr int LetterRows = 3;
|
||||||
|
|
||||||
|
QStringList toStringList(const QJsonValue &value)
|
||||||
|
{
|
||||||
|
QStringList list;
|
||||||
|
for (const QJsonValue &item : value.toArray())
|
||||||
|
list.append(item.toString());
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
const QHash<QString, KeyType> &keyTypesByName()
|
const QHash<QString, KeyType> &keyTypesByName()
|
||||||
{
|
{
|
||||||
static const QHash<QString, KeyType> types = {
|
static const QHash<QString, KeyType> types = {
|
||||||
@@ -21,6 +31,7 @@ const QHash<QString, KeyType> &keyTypesByName()
|
|||||||
{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},
|
||||||
};
|
};
|
||||||
return types;
|
return types;
|
||||||
}
|
}
|
||||||
@@ -51,13 +62,19 @@ std::optional<KeyDef> parseKey(const QJsonValue &value, QString *error)
|
|||||||
key.flick = obj.value(u"flick").toString();
|
key.flick = obj.value(u"flick").toString();
|
||||||
key.target = obj.value(u"target").toString();
|
key.target = obj.value(u"target").toString();
|
||||||
key.width = obj.value(u"width").toDouble(1.0);
|
key.width = obj.value(u"width").toDouble(1.0);
|
||||||
for (const QJsonValue &alt : obj.value(u"alternates").toArray())
|
key.stretch = obj.value(u"stretch").toBool();
|
||||||
key.alternates.append(alt.toString());
|
key.alternates = toStringList(obj.value(u"alternates"));
|
||||||
|
key.lettersRow = obj.value(u"row").toInt(-1);
|
||||||
|
key.flicks = toStringList(obj.value(u"flicks"));
|
||||||
|
|
||||||
if (key.type == KeyType::Character && key.label.isEmpty()) {
|
if (key.type == KeyType::Character && key.label.isEmpty()) {
|
||||||
*error = QStringLiteral("character key without a label");
|
*error = QStringLiteral("character key without a label");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
if (key.type == KeyType::Letters && (key.lettersRow < 0 || key.lettersRow >= LetterRows)) {
|
||||||
|
*error = QStringLiteral("letters need a row from 0 to %1").arg(LetterRows - 1);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
if (key.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;
|
||||||
@@ -110,8 +127,9 @@ std::optional<Layout> parse(const QByteArray &json, QString *error)
|
|||||||
Layout layout;
|
Layout layout;
|
||||||
layout.id = root.value(u"id").toString();
|
layout.id = root.value(u"id").toString();
|
||||||
layout.name = root.value(u"name").toString(layout.id);
|
layout.name = root.value(u"name").toString(layout.id);
|
||||||
layout.locale = root.value(u"locale").toString();
|
const QJsonObject alternates = root.value(u"alternates").toObject();
|
||||||
layout.base = root.value(u"base").toString();
|
for (auto it = alternates.begin(); it != alternates.end(); ++it)
|
||||||
|
layout.alternates.insert(it.key(), toStringList(it.value()));
|
||||||
if (layout.id.isEmpty()) {
|
if (layout.id.isEmpty()) {
|
||||||
*error = QStringLiteral("layout without an id");
|
*error = QStringLiteral("layout without an id");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -124,8 +142,8 @@ std::optional<Layout> parse(const QByteArray &json, QString *error)
|
|||||||
layout.layers.append(*layer);
|
layout.layers.append(*layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layout.layers.isEmpty() && layout.base.isEmpty()) {
|
if (layout.layers.isEmpty()) {
|
||||||
*error = QStringLiteral("layout has no layers and no base");
|
*error = QStringLiteral("layout has no layers");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return layout;
|
return layout;
|
||||||
|
|||||||
@@ -6,16 +6,17 @@
|
|||||||
|
|
||||||
class QJsonValue;
|
class QJsonValue;
|
||||||
|
|
||||||
// Parses the JSON layout format (see layouts/en-US.json).
|
// Parses the JSON template format (see layouts/template.json).
|
||||||
// A key is either a plain string (a character key) or an object:
|
// A key is either a plain string (a character key) or an object:
|
||||||
// {"type": "backspace", "width": 1.5}
|
// {"type": "backspace", "width": 1.5, "stretch": true}
|
||||||
// {"label": "e", "flick": "3", "alternates": ["é", "è"]}
|
// {"label": ",", "flick": "!", "alternates": ["…"]}
|
||||||
|
// {"type": "letters", "row": 0, "flicks": ["1", "2"]} (filled from the keymap)
|
||||||
namespace LayoutLoader {
|
namespace LayoutLoader {
|
||||||
|
|
||||||
std::optional<Layout> parse(const QByteArray &json, QString *error);
|
std::optional<Layout> parse(const QByteArray &json, QString *error);
|
||||||
std::optional<Layout> load(const QString &path, QString *error);
|
std::optional<Layout> load(const QString &path, QString *error);
|
||||||
|
|
||||||
// Checks references that can only be resolved once bases are merged in.
|
// Checks references between layers.
|
||||||
bool validate(const Layout &layout, QString *error);
|
bool validate(const Layout &layout, QString *error);
|
||||||
|
|
||||||
} // namespace LayoutLoader
|
} // namespace LayoutLoader
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ QString keyTypeName(KeyType type)
|
|||||||
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::Letters: break; // expanded before it reaches the model
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
#include "LayoutRepository.h"
|
|
||||||
#include "LayoutLoader.h"
|
|
||||||
|
|
||||||
#include <QDir>
|
|
||||||
#include <QLoggingCategory>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(lcLayouts, "sloposk.layouts")
|
|
||||||
|
|
||||||
void LayoutRepository::loadDirectory(const QString &dir)
|
|
||||||
{
|
|
||||||
const QDir directory(dir);
|
|
||||||
for (const QString &file : directory.entryList({QStringLiteral("*.json")}, QDir::Files, QDir::Name)) {
|
|
||||||
QString error;
|
|
||||||
const auto layout = LayoutLoader::load(directory.filePath(file), &error);
|
|
||||||
if (layout)
|
|
||||||
add(*layout);
|
|
||||||
else
|
|
||||||
qCWarning(lcLayouts) << "Skipping" << directory.filePath(file) << ":" << error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void LayoutRepository::finalize(const QString &preferredLocale)
|
|
||||||
{
|
|
||||||
resolveBases();
|
|
||||||
|
|
||||||
m_layouts.removeIf([](const Layout &layout) {
|
|
||||||
QString error;
|
|
||||||
if (LayoutLoader::validate(layout, &error))
|
|
||||||
return false;
|
|
||||||
qCWarning(lcLayouts) << "Dropping layout" << layout.id << ":" << error;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
std::stable_partition(m_layouts.begin(), m_layouts.end(), [&](const Layout &layout) {
|
|
||||||
return layout.locale == preferredLocale;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
int LayoutRepository::indexOf(const QString &id) const
|
|
||||||
{
|
|
||||||
for (qsizetype i = 0; i < m_layouts.size(); ++i) {
|
|
||||||
if (m_layouts[i].id == id)
|
|
||||||
return int(i);
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void LayoutRepository::add(const Layout &layout)
|
|
||||||
{
|
|
||||||
const int existing = indexOf(layout.id);
|
|
||||||
if (existing >= 0)
|
|
||||||
m_layouts[existing] = layout;
|
|
||||||
else
|
|
||||||
m_layouts.append(layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LayoutRepository::resolveBases()
|
|
||||||
{
|
|
||||||
// One level of inheritance keeps this simple and cycle-free.
|
|
||||||
for (Layout &layout : m_layouts) {
|
|
||||||
if (layout.base.isEmpty())
|
|
||||||
continue;
|
|
||||||
const int baseIndex = indexOf(layout.base);
|
|
||||||
if (baseIndex < 0) {
|
|
||||||
qCWarning(lcLayouts) << "Layout" << layout.id << "has unknown base" << layout.base;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const Layer &layer : m_layouts[baseIndex].layers) {
|
|
||||||
if (!layout.layer(layer.name))
|
|
||||||
layout.layers.append(layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Layout.h"
|
|
||||||
|
|
||||||
// All usable layouts: built-ins from resources plus user overrides on disk.
|
|
||||||
// A later layout with the same id replaces an earlier one.
|
|
||||||
class LayoutRepository
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
// Loads every *.json in dir. Invalid files are skipped with a warning.
|
|
||||||
void loadDirectory(const QString &dir);
|
|
||||||
|
|
||||||
// Merges base layers in, drops invalid layouts, puts `locale` first.
|
|
||||||
void finalize(const QString &preferredLocale);
|
|
||||||
|
|
||||||
const QList<Layout> &layouts() const { return m_layouts; }
|
|
||||||
int indexOf(const QString &id) const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void add(const Layout &layout);
|
|
||||||
void resolveBases();
|
|
||||||
|
|
||||||
QList<Layout> m_layouts;
|
|
||||||
};
|
|
||||||
+48
-15
@@ -1,16 +1,19 @@
|
|||||||
#include "app/DBusControl.h"
|
#include "app/DBusControl.h"
|
||||||
#include "app/DevHooks.h"
|
#include "app/DevHooks.h"
|
||||||
|
#include "app/SystemLayouts.h"
|
||||||
#include "backend/KWinInputMethod.h"
|
#include "backend/KWinInputMethod.h"
|
||||||
|
#include "backend/WaylandKeymap.h"
|
||||||
#include "engine/DirectEngine.h"
|
#include "engine/DirectEngine.h"
|
||||||
#include "input/KeyboardController.h"
|
#include "input/KeyboardController.h"
|
||||||
|
#include "layout/LayoutBuilder.h"
|
||||||
|
#include "layout/LayoutLoader.h"
|
||||||
#include "layout/LayoutModel.h"
|
#include "layout/LayoutModel.h"
|
||||||
#include "layout/LayoutRepository.h"
|
#include "settings/Settings.h"
|
||||||
#include "window/FloatingWindow.h"
|
#include "window/FloatingWindow.h"
|
||||||
#include "window/VisibilityController.h"
|
#include "window/VisibilityController.h"
|
||||||
#include "window/WindowSettings.h"
|
#include "window/WindowSettings.h"
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QLocale>
|
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQmlExtensionPlugin>
|
#include <QQmlExtensionPlugin>
|
||||||
#include <QQuickWindow>
|
#include <QQuickWindow>
|
||||||
@@ -20,13 +23,33 @@ Q_IMPORT_QML_PLUGIN(SloposkPlugin)
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
void loadLayouts(LayoutRepository &layouts)
|
std::optional<Layout> loadTemplate()
|
||||||
{
|
{
|
||||||
layouts.loadDirectory(QStringLiteral(":/layouts"));
|
// ~/.local/share/sloposk/template.json replaces the built-in one.
|
||||||
// User layouts (~/.local/share/sloposk/layouts) override built-ins by id.
|
const QString user = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("template.json"));
|
||||||
layouts.loadDirectory(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)
|
for (const QString &path : {user, QStringLiteral(":/layouts/template.json")}) {
|
||||||
+ QStringLiteral("/layouts"));
|
if (path.isEmpty())
|
||||||
layouts.finalize(QLocale::system().name());
|
continue;
|
||||||
|
QString error;
|
||||||
|
if (auto layout = LayoutLoader::load(path, &error); layout && LayoutLoader::validate(*layout, &error))
|
||||||
|
return layout;
|
||||||
|
qWarning("Skipping layout template %s: %s", qPrintable(path), qPrintable(error));
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shown layout is the template filled with the active system layout's letters.
|
||||||
|
void connectLayouts(const Layout &tmpl, WaylandKeymap &keymap, SystemLayouts &systemLayouts,
|
||||||
|
KeyboardController &controller, LayoutModel &model)
|
||||||
|
{
|
||||||
|
const auto rebuild = [&] {
|
||||||
|
model.setLayout(buildLayout(tmpl, readKeymapLetters(keymap.keymap(), systemLayouts.current())));
|
||||||
|
};
|
||||||
|
QObject::connect(&keymap, &WaylandKeymap::changed, &model, rebuild);
|
||||||
|
QObject::connect(&systemLayouts, &SystemLayouts::currentChanged, &model, rebuild);
|
||||||
|
QObject::connect(&controller, &KeyboardController::nextLayoutRequested, &systemLayouts,
|
||||||
|
[&] { systemLayouts.next(keymap.layoutCount()); });
|
||||||
|
rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings)
|
void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings)
|
||||||
@@ -65,25 +88,35 @@ int main(int argc, char *argv[])
|
|||||||
{
|
{
|
||||||
QGuiApplication app(argc, argv);
|
QGuiApplication app(argc, argv);
|
||||||
QGuiApplication::setApplicationName(QStringLiteral("sloposk"));
|
QGuiApplication::setApplicationName(QStringLiteral("sloposk"));
|
||||||
|
QGuiApplication::setOrganizationName(QStringLiteral("sloposk")); // ~/.config/sloposk/sloposk.conf
|
||||||
QGuiApplication::setDesktopFileName(QStringLiteral("org.sloposk.keyboard"));
|
QGuiApplication::setDesktopFileName(QStringLiteral("org.sloposk.keyboard"));
|
||||||
|
|
||||||
LayoutRepository layouts;
|
const std::optional<Layout> layoutTemplate = loadTemplate();
|
||||||
loadLayouts(layouts);
|
if (!layoutTemplate) {
|
||||||
if (layouts.layouts().isEmpty()) {
|
qCritical("No usable layout template found");
|
||||||
qCritical("No usable keyboard layouts found");
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
KWinInputMethod backend;
|
KWinInputMethod backend;
|
||||||
DirectEngine engine(backend);
|
DirectEngine engine(backend);
|
||||||
LayoutModel model;
|
LayoutModel model;
|
||||||
KeyboardController controller(engine, model, layouts);
|
KeyboardController controller(engine, model);
|
||||||
connectTextContext(backend, controller);
|
connectTextContext(backend, controller);
|
||||||
|
|
||||||
|
WaylandKeymap keymap;
|
||||||
|
SystemLayouts systemLayouts;
|
||||||
|
connectLayouts(*layoutTemplate, keymap, systemLayouts, controller, model);
|
||||||
|
|
||||||
|
Settings settings;
|
||||||
|
controller.setOptions(settings.typingOptions());
|
||||||
|
QObject::connect(&settings, &Settings::typingOptionsChanged, &controller,
|
||||||
|
[&] { controller.setOptions(settings.typingOptions()); });
|
||||||
|
|
||||||
QQmlApplicationEngine qml;
|
QQmlApplicationEngine qml;
|
||||||
qml.setInitialProperties({
|
qml.setInitialProperties({
|
||||||
{QStringLiteral("layoutModel"), QVariant::fromValue(&model)},
|
{QStringLiteral("layoutModel"), QVariant::fromValue(&model)},
|
||||||
{QStringLiteral("controller"), QVariant::fromValue(&controller)},
|
{QStringLiteral("controller"), QVariant::fromValue(&controller)},
|
||||||
|
{QStringLiteral("settings"), QVariant::fromValue(&settings)},
|
||||||
});
|
});
|
||||||
qml.loadFromModule("Sloposk", "Main");
|
qml.loadFromModule("Sloposk", "Main");
|
||||||
auto *window = qml.rootObjects().isEmpty() ? nullptr : qobject_cast<QQuickWindow *>(qml.rootObjects().first());
|
auto *window = qml.rootObjects().isEmpty() ? nullptr : qobject_cast<QQuickWindow *>(qml.rootObjects().first());
|
||||||
@@ -94,8 +127,8 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
FloatingWindow placement(window);
|
FloatingWindow placement(window);
|
||||||
window->setProperty("placement", QVariant::fromValue(&placement));
|
window->setProperty("placement", QVariant::fromValue(&placement));
|
||||||
WindowSettings settings;
|
WindowSettings windowSettings;
|
||||||
restoreAndPersistGeometry(placement, settings);
|
restoreAndPersistGeometry(placement, windowSettings);
|
||||||
|
|
||||||
VisibilityController visibility;
|
VisibilityController visibility;
|
||||||
connectVisibility(visibility, backend, controller, placement);
|
connectVisibility(visibility, backend, controller, placement);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#include "Settings.h"
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
|
||||||
|
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings::Settings(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
const QSettings store;
|
||||||
|
const TypingOptions defaults;
|
||||||
|
m_typing.autoCapitalize = store.value(AutoCapitalizeKey, defaults.autoCapitalize).toBool();
|
||||||
|
m_typing.doubleSpacePeriod = store.value(DoubleSpacePeriodKey, defaults.doubleSpacePeriod).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setAutoCapitalize(bool enabled)
|
||||||
|
{
|
||||||
|
TypingOptions options = m_typing;
|
||||||
|
options.autoCapitalize = enabled;
|
||||||
|
setTypingOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setDoubleSpacePeriod(bool enabled)
|
||||||
|
{
|
||||||
|
TypingOptions options = m_typing;
|
||||||
|
options.doubleSpacePeriod = enabled;
|
||||||
|
setTypingOptions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setTypingOptions(const TypingOptions &options)
|
||||||
|
{
|
||||||
|
if (options == m_typing)
|
||||||
|
return;
|
||||||
|
m_typing = options;
|
||||||
|
QSettings store;
|
||||||
|
store.setValue(AutoCapitalizeKey, m_typing.autoCapitalize);
|
||||||
|
store.setValue(DoubleSpacePeriodKey, m_typing.doubleSpacePeriod);
|
||||||
|
Q_EMIT typingOptionsChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "input/TypingOptions.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QtQml/qqmlregistration.h>
|
||||||
|
|
||||||
|
// User preferences, persisted with QSettings (~/.config/sloposk/sloposk.conf)
|
||||||
|
// and editable from the keyboard's settings panel.
|
||||||
|
class Settings : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_UNCREATABLE("Created in C++")
|
||||||
|
Q_PROPERTY(bool autoCapitalize READ autoCapitalize WRITE setAutoCapitalize NOTIFY typingOptionsChanged)
|
||||||
|
Q_PROPERTY(bool doubleSpacePeriod READ doubleSpacePeriod WRITE setDoubleSpacePeriod NOTIFY typingOptionsChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Settings(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
TypingOptions typingOptions() const { return m_typing; }
|
||||||
|
|
||||||
|
bool autoCapitalize() const { return m_typing.autoCapitalize; }
|
||||||
|
void setAutoCapitalize(bool enabled);
|
||||||
|
bool doubleSpacePeriod() const { return m_typing.doubleSpacePeriod; }
|
||||||
|
void setDoubleSpacePeriod(bool enabled);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void typingOptionsChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setTypingOptions(const TypingOptions &options);
|
||||||
|
|
||||||
|
TypingOptions m_typing;
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ endfunction()
|
|||||||
|
|
||||||
sloposk_add_test(tst_shiftstate)
|
sloposk_add_test(tst_shiftstate)
|
||||||
sloposk_add_test(tst_layoutloader)
|
sloposk_add_test(tst_layoutloader)
|
||||||
|
sloposk_add_test(tst_layoutbuilder)
|
||||||
sloposk_add_test(tst_keygeometry)
|
sloposk_add_test(tst_keygeometry)
|
||||||
sloposk_add_test(tst_controller)
|
sloposk_add_test(tst_controller)
|
||||||
sloposk_add_test(tst_pointertracker)
|
sloposk_add_test(tst_pointertracker)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "engine/InputEngine.h"
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// Records what the keyboard sends and applies it to a simulated field, so
|
||||||
|
// tests check the text a user would see rather than the calls made.
|
||||||
|
class FakeEngine : public InputEngine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void commitText(const QString &text) override { output += text; }
|
||||||
|
|
||||||
|
void replaceBeforeCursor(const QString &before, const QString &text) override
|
||||||
|
{
|
||||||
|
if (output.endsWith(before))
|
||||||
|
output.chop(before.size());
|
||||||
|
else
|
||||||
|
mismatchedReplacements++;
|
||||||
|
output += text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override
|
||||||
|
{
|
||||||
|
keysyms.append(keysym);
|
||||||
|
lastModifiers = modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString output; // the field's text
|
||||||
|
int mismatchedReplacements = 0; // replaced text that wasn't before the cursor
|
||||||
|
QList<quint32> keysyms;
|
||||||
|
Qt::KeyboardModifiers lastModifiers;
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "layout/KeymapLetters.h"
|
||||||
|
#include "layout/LayoutBuilder.h"
|
||||||
|
#include "layout/LayoutLoader.h"
|
||||||
|
|
||||||
|
#include <QTest>
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// Builds layouts the way the app does: the built-in template filled from a
|
||||||
|
// keymap, here compiled from XKB names instead of received from a compositor.
|
||||||
|
// Q_INIT_RESOURCE can't be used inside a namespace.
|
||||||
|
inline void initLayoutResources() { Q_INIT_RESOURCE(layouts); }
|
||||||
|
|
||||||
|
namespace TestLayouts {
|
||||||
|
|
||||||
|
inline Layout builtinTemplate()
|
||||||
|
{
|
||||||
|
initLayoutResources();
|
||||||
|
QString error;
|
||||||
|
const auto layout = LayoutLoader::load(QStringLiteral(":/layouts/template.json"), &error);
|
||||||
|
if (!layout)
|
||||||
|
qFatal("template.json: %s", qPrintable(error));
|
||||||
|
return *layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline KeymapLetters letters(const char *layout, const char *variant = nullptr)
|
||||||
|
{
|
||||||
|
const std::unique_ptr<xkb_context, void (*)(xkb_context *)> context(
|
||||||
|
xkb_context_new(XKB_CONTEXT_NO_ENVIRONMENT_NAMES), xkb_context_unref);
|
||||||
|
const xkb_rule_names names{.rules = "evdev", .model = "pc105", .layout = layout, .variant = variant,
|
||||||
|
.options = nullptr};
|
||||||
|
const std::unique_ptr<xkb_keymap, void (*)(xkb_keymap *)> keymap(
|
||||||
|
xkb_keymap_new_from_names(context.get(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS), xkb_keymap_unref);
|
||||||
|
if (!keymap)
|
||||||
|
qFatal("no XKB keymap for %s", layout);
|
||||||
|
return readKeymapLetters(keymap.get(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Layout build(const char *layout, const char *variant = nullptr)
|
||||||
|
{
|
||||||
|
return buildLayout(builtinTemplate(), letters(layout, variant));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace TestLayouts
|
||||||
+43
-30
@@ -1,7 +1,7 @@
|
|||||||
#include "engine/InputEngine.h"
|
#include "FakeEngine.h"
|
||||||
#include "input/KeyboardController.h"
|
#include "input/KeyboardController.h"
|
||||||
#include "layout/LayoutModel.h"
|
#include "layout/LayoutModel.h"
|
||||||
#include "layout/LayoutRepository.h"
|
#include "TestLayouts.h"
|
||||||
|
|
||||||
#include <QTest>
|
#include <QTest>
|
||||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||||
@@ -10,21 +10,6 @@ using namespace Qt::StringLiterals;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class FakeEngine : public InputEngine
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void commitText(const QString &text) override { output += text; }
|
|
||||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override
|
|
||||||
{
|
|
||||||
keysyms.append(keysym);
|
|
||||||
lastModifiers = modifiers;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString output;
|
|
||||||
QList<quint32> keysyms;
|
|
||||||
Qt::KeyboardModifiers lastModifiers;
|
|
||||||
};
|
|
||||||
|
|
||||||
KeyDef makeKey(KeyType type, const QString &label = {})
|
KeyDef makeKey(KeyType type, const QString &label = {})
|
||||||
{
|
{
|
||||||
KeyDef k;
|
KeyDef k;
|
||||||
@@ -41,7 +26,6 @@ class TestController : public QObject
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
FakeEngine engine;
|
FakeEngine engine;
|
||||||
LayoutRepository repo;
|
|
||||||
LayoutModel model;
|
LayoutModel model;
|
||||||
std::unique_ptr<KeyboardController> controller;
|
std::unique_ptr<KeyboardController> controller;
|
||||||
qint64 now = 0;
|
qint64 now = 0;
|
||||||
@@ -57,11 +41,8 @@ private Q_SLOTS:
|
|||||||
void init()
|
void init()
|
||||||
{
|
{
|
||||||
engine = {};
|
engine = {};
|
||||||
repo = {};
|
model.setLayout(TestLayouts::build("us"));
|
||||||
Q_INIT_RESOURCE(layouts);
|
controller = std::make_unique<KeyboardController>(engine, model);
|
||||||
repo.loadDirectory(u":/layouts"_s);
|
|
||||||
repo.finalize(u"en_US"_s);
|
|
||||||
controller = std::make_unique<KeyboardController>(engine, model, repo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void typesLowercase()
|
void typesLowercase()
|
||||||
@@ -156,11 +137,44 @@ private Q_SLOTS:
|
|||||||
tap(makeKey(KeyType::Character, u"a"_s));
|
tap(makeKey(KeyType::Character, u"a"_s));
|
||||||
tap(space);
|
tap(space);
|
||||||
tap(space);
|
tap(space);
|
||||||
QCOMPARE(engine.output, u"a . ");
|
QCOMPARE(engine.output, u"a. ");
|
||||||
QCOMPARE(engine.keysyms, QList<quint32>{XKB_KEY_BackSpace}); // removed the first space
|
QCOMPARE(engine.mismatchedReplacements, 0);
|
||||||
|
// A Backspace keysym may reach the client after the commit and eat the new space.
|
||||||
|
QVERIFY(engine.keysyms.isEmpty());
|
||||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
|
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void doubleSpacePeriodCanBeTurnedOff()
|
||||||
|
{
|
||||||
|
controller->setOptions({.doubleSpacePeriod = false});
|
||||||
|
const KeyDef space = makeKey(KeyType::Space);
|
||||||
|
tap(makeKey(KeyType::Character, u"a"_s));
|
||||||
|
tap(space);
|
||||||
|
tap(space);
|
||||||
|
QCOMPARE(engine.output, u"A ");
|
||||||
|
}
|
||||||
|
|
||||||
|
void autoCapitalizationCanBeTurnedOff()
|
||||||
|
{
|
||||||
|
controller->resetContext();
|
||||||
|
controller->setOptions({.autoCapitalize = false});
|
||||||
|
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
|
||||||
|
tap(makeKey(KeyType::Character, u"a"_s));
|
||||||
|
QCOMPARE(engine.output, u"a");
|
||||||
|
}
|
||||||
|
|
||||||
|
void doubleTapShiftLocksAtSentenceStart()
|
||||||
|
{
|
||||||
|
controller->resetContext(); // a field was focused
|
||||||
|
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); // auto-shift
|
||||||
|
const KeyDef shift = makeKey(KeyType::Shift);
|
||||||
|
controller->keyDown(shift, 5000);
|
||||||
|
controller->keyUp(shift, KeyOutput::Primary, {}, 5050);
|
||||||
|
controller->keyDown(shift, 5200);
|
||||||
|
controller->keyUp(shift, KeyOutput::Primary, {}, 5250);
|
||||||
|
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Locked);
|
||||||
|
}
|
||||||
|
|
||||||
void slideTypesSymbolAndSwitchesBack()
|
void slideTypesSymbolAndSwitchesBack()
|
||||||
{
|
{
|
||||||
KeyDef layer = makeKey(KeyType::LayerSwitch);
|
KeyDef layer = makeKey(KeyType::LayerSwitch);
|
||||||
@@ -177,13 +191,12 @@ private Q_SLOTS:
|
|||||||
QCOMPARE(engine.keysyms, (QList<quint32>{XKB_KEY_Right, XKB_KEY_Right, XKB_KEY_Up}));
|
QCOMPARE(engine.keysyms, (QList<quint32>{XKB_KEY_Right, XKB_KEY_Right, XKB_KEY_Up}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void globeCyclesLayouts()
|
void globeRequestsNextLayout()
|
||||||
{
|
{
|
||||||
QCOMPARE(model.layoutName(), u"English (US)");
|
int requested = 0;
|
||||||
|
connect(controller.get(), &KeyboardController::nextLayoutRequested, this, [&] { ++requested; });
|
||||||
tap(makeKey(KeyType::Globe));
|
tap(makeKey(KeyType::Globe));
|
||||||
QCOMPARE(model.layoutName(), u"Svenska");
|
QCOMPARE(requested, 1);
|
||||||
tap(makeKey(KeyType::Globe));
|
|
||||||
QCOMPARE(model.layoutName(), u"English (US)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void dismissEmitsSignal()
|
void dismissEmitsSignal()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// End-to-end gesture tests: real Keyboard.qml, synthetic touch events.
|
// End-to-end gesture tests: real Keyboard.qml, synthetic touch events.
|
||||||
// Set SLOPOSK_SNAPSHOT_DIR to also save what the keyboard looks like mid-gesture.
|
// Set SLOPOSK_SNAPSHOT_DIR to also save what the keyboard looks like mid-gesture.
|
||||||
|
|
||||||
#include "engine/InputEngine.h"
|
#include "FakeEngine.h"
|
||||||
#include "input/KeyboardController.h"
|
#include "input/KeyboardController.h"
|
||||||
#include "input/KeyboardSurface.h"
|
#include "input/KeyboardSurface.h"
|
||||||
#include "layout/LayoutModel.h"
|
#include "layout/LayoutModel.h"
|
||||||
#include "layout/LayoutRepository.h"
|
#include "TestLayouts.h"
|
||||||
|
|
||||||
#include <QQmlComponent>
|
#include <QQmlComponent>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
@@ -21,21 +21,6 @@ using namespace Qt::StringLiterals;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class FakeEngine : public InputEngine
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void commitText(const QString &text) override { output += text; }
|
|
||||||
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers) override
|
|
||||||
{
|
|
||||||
keysyms.append(keysym);
|
|
||||||
lastModifiers = modifiers;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString output;
|
|
||||||
QList<quint32> keysyms;
|
|
||||||
Qt::KeyboardModifiers lastModifiers;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr QPoint KeyboardOffset(0, 100); // room above for popups
|
constexpr QPoint KeyboardOffset(0, 100); // room above for popups
|
||||||
constexpr int LongPressWaitMs = 600;
|
constexpr int LongPressWaitMs = 600;
|
||||||
|
|
||||||
@@ -47,7 +32,6 @@ class TestKeyboardSurface : public QObject
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
FakeEngine engine;
|
FakeEngine engine;
|
||||||
LayoutRepository repo;
|
|
||||||
LayoutModel model;
|
LayoutModel model;
|
||||||
std::unique_ptr<KeyboardController> controller;
|
std::unique_ptr<KeyboardController> controller;
|
||||||
QQmlEngine qml;
|
QQmlEngine qml;
|
||||||
@@ -81,10 +65,8 @@ private:
|
|||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void initTestCase()
|
void initTestCase()
|
||||||
{
|
{
|
||||||
Q_INIT_RESOURCE(layouts);
|
model.setLayout(TestLayouts::build("us"));
|
||||||
repo.loadDirectory(u":/layouts"_s);
|
controller = std::make_unique<KeyboardController>(engine, model);
|
||||||
repo.finalize(u"en_US"_s);
|
|
||||||
controller = std::make_unique<KeyboardController>(engine, model, repo);
|
|
||||||
|
|
||||||
QQmlComponent component(&qml, "Sloposk", "Keyboard");
|
QQmlComponent component(&qml, "Sloposk", "Keyboard");
|
||||||
keyboard = qobject_cast<QQuickItem *>(component.createWithInitialProperties({
|
keyboard = qobject_cast<QQuickItem *>(component.createWithInitialProperties({
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#include "TestLayouts.h"
|
||||||
|
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QString rowLetters(const Layout &layout, int row)
|
||||||
|
{
|
||||||
|
QString letters;
|
||||||
|
for (const KeyDef &key : layout.layers.first().rows[row]) {
|
||||||
|
if (key.type == KeyType::Character && key.label.front().isLetter())
|
||||||
|
letters += key.label;
|
||||||
|
}
|
||||||
|
return letters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyDef &letterKey(const Layout &layout, const QString &label)
|
||||||
|
{
|
||||||
|
for (const Row &row : layout.layers.first().rows) {
|
||||||
|
for (const KeyDef &key : row) {
|
||||||
|
if (key.label == label)
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qFatal("no key %s", qPrintable(label));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class TestLayoutBuilder : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void readsLetterRowsFromKeymap()
|
||||||
|
{
|
||||||
|
const Layout us = TestLayouts::build("us");
|
||||||
|
QCOMPARE(rowLetters(us, 0), u"qwertyuiop");
|
||||||
|
QCOMPARE(rowLetters(us, 1), u"asdfghjkl");
|
||||||
|
QCOMPARE(rowLetters(us, 2), u"zxcvbnm");
|
||||||
|
|
||||||
|
const Layout sk = TestLayouts::build("sk");
|
||||||
|
QCOMPARE(sk.name, u"Slovak");
|
||||||
|
QCOMPARE(rowLetters(sk, 0), u"qwertzuiopúä");
|
||||||
|
QCOMPARE(rowLetters(sk, 1), u"asdfghjklô");
|
||||||
|
QCOMPARE(rowLetters(sk, 2), u"yxcvbnm");
|
||||||
|
|
||||||
|
const Layout se = TestLayouts::build("se");
|
||||||
|
QCOMPARE(rowLetters(se, 0), u"qwertyuiopå");
|
||||||
|
QCOMPARE(rowLetters(se, 1), u"asdfghjklöä");
|
||||||
|
}
|
||||||
|
|
||||||
|
void shiftedFormComesFromKeymap()
|
||||||
|
{
|
||||||
|
QCOMPARE(letterKey(TestLayouts::build("sk"), u"ô"_s).shifted, u"Ô");
|
||||||
|
}
|
||||||
|
|
||||||
|
void flicksFollowColumns()
|
||||||
|
{
|
||||||
|
const Layout sk = TestLayouts::build("sk");
|
||||||
|
QCOMPARE(letterKey(sk, u"q"_s).flick, u"1");
|
||||||
|
QCOMPARE(letterKey(sk, u"p"_s).flick, u"0");
|
||||||
|
QVERIFY(letterKey(sk, u"ú"_s).flick.isEmpty()); // more letters than flicks
|
||||||
|
}
|
||||||
|
|
||||||
|
void numberRowLettersBecomeAlternates()
|
||||||
|
{
|
||||||
|
const Layout sk = TestLayouts::build("sk");
|
||||||
|
// The keymap's own letters come first: they're what this language types.
|
||||||
|
QCOMPARE(letterKey(sk, u"s"_s).alternates.first(), u"š");
|
||||||
|
QCOMPARE(letterKey(sk, u"l"_s).alternates.first(), u"ľ");
|
||||||
|
QCOMPARE(letterKey(sk, u"n"_s).alternates.first(), u"ň"); // BKSL on sk
|
||||||
|
QCOMPARE(letterKey(sk, u"e"_s).alternates.first(), u"é");
|
||||||
|
QCOMPARE(letterKey(sk, u"y"_s).alternates.first(), u"ý");
|
||||||
|
}
|
||||||
|
|
||||||
|
void lettersWithoutDecompositionFindTheirBase()
|
||||||
|
{
|
||||||
|
QCOMPARE(letterKey(TestLayouts::build("de"), u"s"_s).alternates.first(), u"ß");
|
||||||
|
}
|
||||||
|
|
||||||
|
void alternatesSkipLettersThatHaveKeys()
|
||||||
|
{
|
||||||
|
const Layout se = TestLayouts::build("se");
|
||||||
|
const QStringList a = letterKey(se, u"a"_s).alternates;
|
||||||
|
QVERIFY(!a.contains(u"ä"_s));
|
||||||
|
QVERIFY(!a.contains(u"å"_s));
|
||||||
|
QVERIFY(a.contains(u"á"_s));
|
||||||
|
}
|
||||||
|
|
||||||
|
void noPlaceholdersRemain()
|
||||||
|
{
|
||||||
|
for (const Layer &layer : TestLayouts::build("us").layers) {
|
||||||
|
for (const Row &row : layer.rows) {
|
||||||
|
for (const KeyDef &key : row)
|
||||||
|
QVERIFY(key.type != KeyType::Letters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(TestLayoutBuilder)
|
||||||
|
#include "tst_layoutbuilder.moc"
|
||||||
+29
-29
@@ -1,22 +1,11 @@
|
|||||||
|
#include "TestLayouts.h"
|
||||||
|
#include "layout/KeyGeometry.h"
|
||||||
#include "layout/LayoutLoader.h"
|
#include "layout/LayoutLoader.h"
|
||||||
#include "layout/LayoutRepository.h"
|
|
||||||
|
|
||||||
#include <QTest>
|
#include <QTest>
|
||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
qreal rowUnits(const Row &row)
|
|
||||||
{
|
|
||||||
qreal units = 0;
|
|
||||||
for (const KeyDef &key : row)
|
|
||||||
units += key.width;
|
|
||||||
return units;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
class TestLayoutLoader : public QObject
|
class TestLayoutLoader : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -65,24 +54,35 @@ private Q_SLOTS:
|
|||||||
QVERIFY(!LayoutLoader::validate(*layout, &error));
|
QVERIFY(!LayoutLoader::validate(*layout, &error));
|
||||||
}
|
}
|
||||||
|
|
||||||
void builtinLayoutsAreConsistent()
|
void templateIsValid()
|
||||||
{
|
{
|
||||||
Q_INIT_RESOURCE(layouts);
|
QString error;
|
||||||
LayoutRepository repo;
|
QVERIFY2(LayoutLoader::validate(TestLayouts::builtinTemplate(), &error), qPrintable(error));
|
||||||
repo.loadDirectory(u":/layouts"_s);
|
}
|
||||||
repo.finalize({});
|
|
||||||
QVERIFY(repo.indexOf(u"en-US"_s) >= 0);
|
|
||||||
QVERIFY(repo.indexOf(u"sv-SE"_s) >= 0);
|
|
||||||
|
|
||||||
for (const Layout &layout : repo.layouts()) {
|
void rejectsLettersWithoutRow()
|
||||||
QVERIFY2(layout.layer(u"numbers"_s), qPrintable(layout.id)); // sv-SE gets it from its base
|
{
|
||||||
for (const Layer &layer : layout.layers) {
|
QString error;
|
||||||
const qreal expected = rowUnits(layer.rows.first());
|
QVERIFY(!LayoutLoader::parse(R"({"id": "t", "layers": [{"name": "l", "rows": [[{"type": "letters"}]]}]})",
|
||||||
for (const Row &row : layer.rows) {
|
&error));
|
||||||
QVERIFY2(qFuzzyCompare(rowUnits(row), expected),
|
}
|
||||||
qPrintable(u"%1/%2: rows differ in width"_s.arg(layout.id, layer.name)));
|
|
||||||
}
|
// Stretching must even out rows whatever the keymap's letter counts.
|
||||||
}
|
void builtLayersHaveEqualRows_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QString>("xkbLayout");
|
||||||
|
for (const char *layout : {"us", "sk", "se", "de", "fr", "ru", "gr"})
|
||||||
|
QTest::newRow(layout) << QString::fromLatin1(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void builtLayersHaveEqualRows()
|
||||||
|
{
|
||||||
|
QFETCH(QString, xkbLayout);
|
||||||
|
const Layout layout = TestLayouts::build(qPrintable(xkbLayout));
|
||||||
|
for (const Layer &layer : layout.layers) {
|
||||||
|
const qreal expected = rowUnits(layer.rows.first());
|
||||||
|
for (const Row &row : layer.rows)
|
||||||
|
QVERIFY2(qFuzzyCompare(rowUnits(row), expected), qPrintable(layer.name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,37 @@ private Q_SLOTS:
|
|||||||
QCOMPARE(shift.mode(), Mode::Locked);
|
QCOMPARE(shift.mode(), Mode::Locked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void doubleTapLocksAfterAutomaticShift()
|
||||||
|
{
|
||||||
|
ShiftState shift;
|
||||||
|
shift.setAutomatic(true);
|
||||||
|
shift.press(0); // turns auto-shift off...
|
||||||
|
shift.release();
|
||||||
|
shift.press(200); // ...and the second tap locks
|
||||||
|
shift.release();
|
||||||
|
QCOMPARE(shift.mode(), Mode::Locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tapAfterUnlockIsNotDoubleTap()
|
||||||
|
{
|
||||||
|
ShiftState shift;
|
||||||
|
shift.press(0);
|
||||||
|
shift.press(100); // locked
|
||||||
|
shift.press(1000); // unlocks
|
||||||
|
shift.press(1100);
|
||||||
|
QCOMPARE(shift.mode(), Mode::Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
void typingBetweenTapsIsNotDoubleTap()
|
||||||
|
{
|
||||||
|
ShiftState shift;
|
||||||
|
shift.press(0);
|
||||||
|
shift.release();
|
||||||
|
shift.characterTyped();
|
||||||
|
shift.press(200);
|
||||||
|
QCOMPARE(shift.mode(), Mode::Once);
|
||||||
|
}
|
||||||
|
|
||||||
void tapUnlocks()
|
void tapUnlocks()
|
||||||
{
|
{
|
||||||
ShiftState shift;
|
ShiftState shift;
|
||||||
|
|||||||
Reference in New Issue
Block a user