Optional PC key blocks: function row, navigation, numeric keypad
- Template sections placed above or to the right of the typing keys; the user picks any mix in settings, typing keys included - keysym keys send XKB keysyms on press, optionally repeating - Geometry lays blocks side by side with one key unit; stretch keys are resolved at layout time so composed blocks line up - Toggling a block resizes the window so keys keep their size Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML U
|
|||||||
- Letters follow the system keyboard layout; the globe key switches it
|
- Letters follow the system keyboard layout; the globe key switches it
|
||||||
- Auto-capitalization, double-space → ". " (both optional, ⚙ on the bottom bar), backspace repeat → word delete
|
- Auto-capitalization, double-space → ". " (both optional, ⚙ on the bottom bar), backspace repeat → word delete
|
||||||
- Long-press space = trackpad; slide from `.?123` onto a symbol and release
|
- Long-press space = trackpad; slide from `.?123` onto a symbol and release
|
||||||
|
- Optional PC key blocks: function row, navigation (ins/home/pg up, del/end/pg dn, arrows), numeric keypad; any mix, typing keys included (⚙)
|
||||||
- Floating: drag the bottom bar, resize from the corner; geometry is remembered per screen
|
- Floating: drag the bottom bar, resize from the corner; geometry is remembered per screen
|
||||||
|
|
||||||
## Build & test
|
## Build & test
|
||||||
@@ -43,14 +44,16 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso
|
|||||||
{"type": "backspace", "width": 1.5, "stretch": true}
|
{"type": "backspace", "width": 1.5, "stretch": true}
|
||||||
```
|
```
|
||||||
|
|
||||||
- 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`
|
- Key: a string, or an object with `type` (`char` `space` `backspace` `enter` `shift` `layer` `globe` `dismiss` `spacer` `letters` `keysym`), `label`, `shifted`, `flick`, `alternates`, `width` (key units), `target` (for `layer`), `stretch`
|
||||||
|
- `keysym`: sends the XKB keysym named by `keysym` (`Home`, `F5`, `Left`...) on press; `repeat` auto-repeats while held
|
||||||
- `letters`: the keymap row `row` (0–2), with `flicks` handed out left to right
|
- `letters`: the keymap row `row` (0–2), with `flicks` handed out left to right
|
||||||
- `stretch`: the key grows so its row is as wide as the layer's widest (letter counts vary by layout)
|
- `stretch`: the key grows so its row is as wide as the layer's widest (letter counts vary by layout)
|
||||||
- Top-level `alternates`: long-press choices per letter, after the keymap's own
|
- Top-level `alternates`: long-press choices per letter, after the keymap's own
|
||||||
|
- Top-level `sections`: optional blocks, `{"id", "name", "placement": "above" | "right", "rows"}`; the user picks which show
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`.
|
`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `layout/visibleSections` (`main` plus section ids).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,64 @@
|
|||||||
"y": ["ý", "ÿ"],
|
"y": ["ý", "ÿ"],
|
||||||
"z": ["ž", "ź", "ż"]
|
"z": ["ž", "ź", "ż"]
|
||||||
},
|
},
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"id": "function",
|
||||||
|
"name": "Function keys",
|
||||||
|
"placement": "above",
|
||||||
|
"rows": [[
|
||||||
|
{"type": "keysym", "keysym": "Escape", "label": "esc", "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F1", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F2", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F3", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F4", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F5", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F6", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F7", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F8", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F9", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F10", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F11", "width": 0.75, "stretch": true},
|
||||||
|
{"type": "keysym", "keysym": "F12", "width": 0.75, "stretch": true}
|
||||||
|
]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "navigation",
|
||||||
|
"name": "Navigation keys",
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
{"type": "keysym", "keysym": "Insert", "label": "ins"},
|
||||||
|
{"type": "keysym", "keysym": "Home", "label": "home"},
|
||||||
|
{"type": "keysym", "keysym": "Prior", "label": "pg up", "repeat": true}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{"type": "keysym", "keysym": "Delete", "label": "del", "repeat": true},
|
||||||
|
{"type": "keysym", "keysym": "End", "label": "end"},
|
||||||
|
{"type": "keysym", "keysym": "Next", "label": "pg dn", "repeat": true}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{"type": "spacer"},
|
||||||
|
{"type": "keysym", "keysym": "Up", "label": "↑", "repeat": true},
|
||||||
|
{"type": "spacer"}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{"type": "keysym", "keysym": "Left", "label": "←", "repeat": true},
|
||||||
|
{"type": "keysym", "keysym": "Down", "label": "↓", "repeat": true},
|
||||||
|
{"type": "keysym", "keysym": "Right", "label": "→", "repeat": true}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "numpad",
|
||||||
|
"name": "Numeric keypad",
|
||||||
|
"rows": [
|
||||||
|
["7", "8", "9", "/", {"type": "backspace"}],
|
||||||
|
["4", "5", "6", "*", "-"],
|
||||||
|
["1", "2", "3", "+", {"type": "enter"}],
|
||||||
|
[{"label": "0", "width": 2}, ".", ",", "="]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"name": "letters",
|
"name": "letters",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ Window {
|
|||||||
anchors.fill: keyboard
|
anchors.fill: keyboard
|
||||||
visible: root.settingsOpen
|
visible: root.settingsOpen
|
||||||
settings: root.settings
|
settings: root.settings
|
||||||
|
layoutModel: root.layoutModel
|
||||||
}
|
}
|
||||||
|
|
||||||
DragHandle {
|
DragHandle {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Small caps-style title above a group of settings.
|
||||||
|
Text {
|
||||||
|
color: Theme.secondaryText
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.capitalization: Font.AllUppercase
|
||||||
|
bottomPadding: 4
|
||||||
|
}
|
||||||
+46
-15
@@ -6,28 +6,59 @@ Rectangle {
|
|||||||
id: panel
|
id: panel
|
||||||
|
|
||||||
required property Settings settings
|
required property Settings settings
|
||||||
|
required property LayoutModel layoutModel
|
||||||
|
|
||||||
color: Theme.background
|
color: Theme.background
|
||||||
radius: Theme.windowRadius
|
radius: Theme.windowRadius
|
||||||
|
|
||||||
Column {
|
Row {
|
||||||
anchors { fill: parent; margins: 16 }
|
id: columns
|
||||||
spacing: 4
|
readonly property real columnWidth: (width - spacing) / 2
|
||||||
|
|
||||||
ToggleRow {
|
anchors { fill: parent; margins: 16 }
|
||||||
width: parent.width
|
spacing: 32
|
||||||
text: qsTr("Auto-Capitalization")
|
|
||||||
detail: qsTr("Shift at the start of a sentence")
|
Column {
|
||||||
checked: panel.settings.autoCapitalize
|
width: columns.columnWidth
|
||||||
onToggled: checked => panel.settings.autoCapitalize = checked
|
spacing: 4
|
||||||
|
|
||||||
|
SectionHeader { text: qsTr("Typing") }
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleRow {
|
Column {
|
||||||
width: parent.width
|
width: columns.columnWidth
|
||||||
text: qsTr("“.” Shortcut")
|
spacing: 4
|
||||||
detail: qsTr("Double-tap space for a period and a space")
|
|
||||||
checked: panel.settings.doubleSpacePeriod
|
SectionHeader { text: qsTr("Keys") }
|
||||||
onToggled: checked => panel.settings.doubleSpacePeriod = checked
|
|
||||||
|
Repeater {
|
||||||
|
// "main" is the typing keys; the rest come from the template.
|
||||||
|
model: [{ id: "main", name: qsTr("Typing keys") }].concat(panel.layoutModel.sections)
|
||||||
|
|
||||||
|
ToggleRow {
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
text: modelData.name
|
||||||
|
checked: panel.settings.visibleSections.includes(modelData.id)
|
||||||
|
onToggled: checked => panel.settings.setSectionVisible(modelData.id, checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ set(SLOPOSK_QML_FILES
|
|||||||
AlternatesPopup.qml
|
AlternatesPopup.qml
|
||||||
DragHandle.qml
|
DragHandle.qml
|
||||||
ResizeHandle.qml
|
ResizeHandle.qml
|
||||||
|
SectionHeader.qml
|
||||||
SettingsButton.qml
|
SettingsButton.qml
|
||||||
SettingsPanel.qml
|
SettingsPanel.qml
|
||||||
ToggleRow.qml
|
ToggleRow.qml
|
||||||
|
|||||||
@@ -14,14 +14,18 @@ static_assert(int(KeyboardController::ShiftMode::Locked) == int(ShiftState::Mode
|
|||||||
namespace {
|
namespace {
|
||||||
// Enough history for the text rules; fields may report megabytes.
|
// Enough history for the text rules; fields may report megabytes.
|
||||||
constexpr qsizetype MaxTextBefore = 256;
|
constexpr qsizetype MaxTextBefore = 256;
|
||||||
|
// Like a hardware key: no word mode, a faster rate than Backspace.
|
||||||
|
constexpr KeyRepeater::Timing KeysymRepeat{.intervalMs = 50, .wordModeAfterMs = std::numeric_limits<int>::max()};
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent)
|
KeyboardController::KeyboardController(InputEngine &engine, LayoutModel &model, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_engine(engine)
|
, m_engine(engine)
|
||||||
, m_model(model)
|
, m_model(model)
|
||||||
|
, m_keysymRepeater(KeysymRepeat)
|
||||||
{
|
{
|
||||||
connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward);
|
connect(&m_backspaceRepeater, &KeyRepeater::repeat, this, &KeyboardController::deleteBackward);
|
||||||
|
connect(&m_keysymRepeater, &KeyRepeater::repeat, this, &KeyboardController::sendRepeatedKeysym);
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardController::ShiftMode KeyboardController::shiftMode() const
|
KeyboardController::ShiftMode KeyboardController::shiftMode() const
|
||||||
@@ -43,6 +47,13 @@ void KeyboardController::keyDown(const KeyDef &key, qint64 timeMs)
|
|||||||
m_layerBeforeSwitch = m_model.layerName();
|
m_layerBeforeSwitch = m_model.layerName();
|
||||||
m_model.setLayer(key.target);
|
m_model.setLayer(key.target);
|
||||||
break;
|
break;
|
||||||
|
case KeyType::Keysym:
|
||||||
|
m_engine.sendKeysym(key.keysym);
|
||||||
|
if (key.repeat) {
|
||||||
|
m_repeatingKeysym = key.keysym;
|
||||||
|
m_keysymRepeater.start();
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break; // everything else acts on release
|
break; // everything else acts on release
|
||||||
}
|
}
|
||||||
@@ -57,6 +68,9 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
|
|||||||
case KeyType::Backspace:
|
case KeyType::Backspace:
|
||||||
m_backspaceRepeater.stop();
|
m_backspaceRepeater.stop();
|
||||||
break;
|
break;
|
||||||
|
case KeyType::Keysym:
|
||||||
|
stopRepeating(key);
|
||||||
|
break;
|
||||||
case KeyType::Character:
|
case KeyType::Character:
|
||||||
if (output == KeyOutput::Alternate && !alternate.isEmpty())
|
if (output == KeyOutput::Alternate && !alternate.isEmpty())
|
||||||
typeText(alternate);
|
typeText(alternate);
|
||||||
@@ -90,6 +104,8 @@ void KeyboardController::keyCancelled(const KeyDef &key)
|
|||||||
changeShift([&] { m_shift.release(); });
|
changeShift([&] { m_shift.release(); });
|
||||||
else if (key.type == KeyType::Backspace)
|
else if (key.type == KeyType::Backspace)
|
||||||
m_backspaceRepeater.stop();
|
m_backspaceRepeater.stop();
|
||||||
|
else if (key.type == KeyType::Keysym)
|
||||||
|
stopRepeating(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardController::slideReleased(const KeyDef &target, qint64 timeMs)
|
void KeyboardController::slideReleased(const KeyDef &target, qint64 timeMs)
|
||||||
@@ -156,6 +172,18 @@ void KeyboardController::deleteBackward(bool wholeWord)
|
|||||||
autoCapitalize();
|
autoCapitalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only the key that started the repeat stops it; another finger's key doesn't.
|
||||||
|
void KeyboardController::stopRepeating(const KeyDef &key)
|
||||||
|
{
|
||||||
|
if (key.keysym == m_repeatingKeysym)
|
||||||
|
m_keysymRepeater.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeyboardController::sendRepeatedKeysym()
|
||||||
|
{
|
||||||
|
m_engine.sendKeysym(m_repeatingKeysym);
|
||||||
|
}
|
||||||
|
|
||||||
void KeyboardController::autoCapitalize()
|
void KeyboardController::autoCapitalize()
|
||||||
{
|
{
|
||||||
const bool capitalize = m_options.autoCapitalize && m_context.allowsAutoCapitalization()
|
const bool capitalize = m_options.autoCapitalize && m_context.allowsAutoCapitalization()
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ Q_SIGNALS:
|
|||||||
private:
|
private:
|
||||||
void typeText(const QString &text);
|
void typeText(const QString &text);
|
||||||
void deleteBackward(bool wholeWord);
|
void deleteBackward(bool wholeWord);
|
||||||
|
void sendRepeatedKeysym();
|
||||||
|
void stopRepeating(const KeyDef &key);
|
||||||
void autoCapitalize();
|
void autoCapitalize();
|
||||||
template<typename Change>
|
template<typename Change>
|
||||||
void changeShift(Change change);
|
void changeShift(Change change);
|
||||||
@@ -66,6 +68,8 @@ private:
|
|||||||
TypingOptions m_options;
|
TypingOptions m_options;
|
||||||
ShiftState m_shift;
|
ShiftState m_shift;
|
||||||
KeyRepeater m_backspaceRepeater;
|
KeyRepeater m_backspaceRepeater;
|
||||||
|
KeyRepeater m_keysymRepeater; // arrows, Delete, ...
|
||||||
|
quint32 m_repeatingKeysym = 0;
|
||||||
QString m_layerBeforeSwitch;
|
QString m_layerBeforeSwitch;
|
||||||
|
|
||||||
TextContext m_context;
|
TextContext m_context;
|
||||||
|
|||||||
+58
-18
@@ -1,6 +1,30 @@
|
|||||||
#include "KeyGeometry.h"
|
#include "KeyGeometry.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
qreal blockUnits(const Block &block)
|
||||||
|
{
|
||||||
|
qreal units = 0;
|
||||||
|
for (const Row &row : block)
|
||||||
|
units = std::max(units, rowUnits(row));
|
||||||
|
return units;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key widths in units once stretch keys take up the row's slack.
|
||||||
|
QList<qreal> stretchedWidths(const Row &row, qreal targetUnits)
|
||||||
|
{
|
||||||
|
const auto stretchy = std::ranges::count_if(row, &KeyDef::stretch);
|
||||||
|
const qreal extra = stretchy ? (targetUnits - rowUnits(row)) / qreal(stretchy) : 0;
|
||||||
|
QList<qreal> widths;
|
||||||
|
for (const KeyDef &key : row)
|
||||||
|
widths.append(key.width + (key.stretch ? extra : 0));
|
||||||
|
return widths;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
qreal rowUnits(const Row &row)
|
qreal rowUnits(const Row &row)
|
||||||
{
|
{
|
||||||
@@ -10,33 +34,49 @@ qreal rowUnits(const Row &row)
|
|||||||
return units;
|
return units;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics)
|
qreal totalUnits(const QList<Block> &blocks)
|
||||||
|
{
|
||||||
|
qreal units = 0;
|
||||||
|
for (const Block &block : blocks)
|
||||||
|
units += blockUnits(block);
|
||||||
|
return units;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QRectF> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardMetrics metrics)
|
||||||
{
|
{
|
||||||
QList<QRectF> rects;
|
QList<QRectF> rects;
|
||||||
if (layer.rows.isEmpty())
|
const qreal units = totalUnits(blocks);
|
||||||
|
qsizetype maxRows = 0;
|
||||||
|
for (const Block &block : blocks)
|
||||||
|
maxRows = std::max(maxRows, block.size());
|
||||||
|
if (units <= 0 || maxRows == 0)
|
||||||
return rects;
|
return rects;
|
||||||
|
|
||||||
qreal maxUnits = 0;
|
|
||||||
for (const Row &row : layer.rows)
|
|
||||||
maxUnits = std::max(maxUnits, rowUnits(row));
|
|
||||||
|
|
||||||
const qreal innerWidth = size.width() - 2 * metrics.padding;
|
const qreal innerWidth = size.width() - 2 * metrics.padding;
|
||||||
const qreal innerHeight = size.height() - 2 * metrics.padding;
|
const qreal innerHeight = size.height() - 2 * metrics.padding;
|
||||||
const int rowCount = int(layer.rows.size());
|
const auto blockCount = qreal(blocks.size());
|
||||||
|
|
||||||
// Each cell includes one trailing gap; the last gap of a row is dropped.
|
// Each cell includes one trailing gap; the last gap of a row is dropped,
|
||||||
const qreal unitWidth = (innerWidth + metrics.gap) / maxUnits;
|
// and blocks are separated by blockGap instead.
|
||||||
const qreal rowHeight = (innerHeight - (rowCount - 1) * metrics.gap) / rowCount;
|
const qreal unitWidth = (innerWidth + blockCount * metrics.gap - (blockCount - 1) * metrics.blockGap) / units;
|
||||||
|
const qreal rowHeight = (innerHeight - qreal(maxRows - 1) * metrics.gap) / qreal(maxRows);
|
||||||
|
|
||||||
qreal y = metrics.padding;
|
qreal left = metrics.padding;
|
||||||
for (const Row &row : layer.rows) {
|
for (const Block &block : blocks) {
|
||||||
qreal x = metrics.padding + (maxUnits - rowUnits(row)) * unitWidth / 2;
|
const qreal width = blockUnits(block);
|
||||||
for (const KeyDef &key : row) {
|
qreal y = metrics.padding + qreal(maxRows - block.size()) * (rowHeight + metrics.gap);
|
||||||
const qreal cellWidth = key.width * unitWidth;
|
for (const Row &row : block) {
|
||||||
rects.append(QRectF(x, y, std::max<qreal>(0, cellWidth - metrics.gap), rowHeight));
|
const QList<qreal> widths = stretchedWidths(row, width);
|
||||||
x += cellWidth;
|
const qreal used = std::accumulate(widths.begin(), widths.end(), 0.0);
|
||||||
|
qreal x = left + (width - used) * unitWidth / 2;
|
||||||
|
for (qreal keyWidth : widths) {
|
||||||
|
const qreal cellWidth = keyWidth * unitWidth;
|
||||||
|
rects.append(QRectF(x, y, std::max<qreal>(0, cellWidth - metrics.gap), rowHeight));
|
||||||
|
x += cellWidth;
|
||||||
|
}
|
||||||
|
y += rowHeight + metrics.gap;
|
||||||
}
|
}
|
||||||
y += rowHeight + metrics.gap;
|
left += width * unitWidth - metrics.gap + metrics.blockGap;
|
||||||
}
|
}
|
||||||
return rects;
|
return rects;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,26 @@
|
|||||||
#include <QSizeF>
|
#include <QSizeF>
|
||||||
|
|
||||||
struct KeyboardMetrics {
|
struct KeyboardMetrics {
|
||||||
qreal padding = 6; // around the whole keyboard
|
qreal padding = 6; // around the whole keyboard
|
||||||
qreal gap = 8; // between keys, horizontally and vertically
|
qreal gap = 8; // between keys, horizontally and vertically
|
||||||
|
qreal blockGap = 24; // between side-by-side blocks (main, navigation, numpad)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lays out a layer's keys to fill `size`. Returns one rect per key, in row
|
// The keys on screen: blocks of rows, side by side.
|
||||||
// order (spacers included). One key unit has the same width in every row, so
|
using Block = QList<Row>;
|
||||||
// rows with equal unit totals line up; shorter rows are centered.
|
|
||||||
QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {});
|
|
||||||
|
|
||||||
// Total width of a row in key units.
|
// Lays out blocks to fill `size`. Returns one rect per key, block by block in
|
||||||
|
// row order (spacers included). One key unit has the same width everywhere;
|
||||||
|
// all rows share one height, and shorter blocks sit at the bottom. In each
|
||||||
|
// block, stretch keys grow to make their row as wide as the widest one, and
|
||||||
|
// remaining shorter rows are centered.
|
||||||
|
QList<QRectF> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardMetrics metrics = {});
|
||||||
|
inline QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {})
|
||||||
|
{
|
||||||
|
return computeKeyRects({layer.rows}, size, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total width of a row in key units, before stretching.
|
||||||
qreal rowUnits(const Row &row);
|
qreal rowUnits(const Row &row);
|
||||||
|
// Width of all blocks in key units, ignoring gaps.
|
||||||
|
qreal totalUnits(const QList<Block> &blocks);
|
||||||
|
|||||||
+18
-2
@@ -16,6 +16,7 @@ enum class KeyType {
|
|||||||
Dismiss,
|
Dismiss,
|
||||||
Spacer, // empty room, e.g. the half-key indent of the home row
|
Spacer, // empty room, e.g. the half-key indent of the home row
|
||||||
Letters, // template only: a row of letters from the system keymap
|
Letters, // template only: a row of letters from the system keymap
|
||||||
|
Keysym, // a non-text key (Home, F5, arrows...): sends `keysym`
|
||||||
};
|
};
|
||||||
|
|
||||||
struct KeyDef {
|
struct KeyDef {
|
||||||
@@ -26,7 +27,9 @@ 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
|
bool stretch = false; // grows so the row is as wide as its block's widest
|
||||||
|
quint32 keysym = 0; // Keysym keys only
|
||||||
|
bool repeat = false; // Keysym keys only: auto-repeat while held
|
||||||
|
|
||||||
// Letters placeholders only.
|
// Letters placeholders only.
|
||||||
int lettersRow = -1; // which keymap row (0 = top)
|
int lettersRow = -1; // which keymap row (0 = top)
|
||||||
@@ -48,10 +51,21 @@ struct Layer {
|
|||||||
QList<Row> rows;
|
QList<Row> rows;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// An optional block of keys next to the main one, shown if the user wants it.
|
||||||
|
struct Section {
|
||||||
|
enum class Placement { Above, Right }; // above the main keys, or a column to their right
|
||||||
|
|
||||||
|
QString id; // "main" is reserved for the layers
|
||||||
|
QString name; // shown in settings
|
||||||
|
Placement placement = Placement::Right;
|
||||||
|
QList<Row> rows;
|
||||||
|
};
|
||||||
|
|
||||||
struct Layout {
|
struct Layout {
|
||||||
QString id;
|
QString id;
|
||||||
QString name; // human readable, e.g. "Slovak"
|
QString name; // human readable, e.g. "Slovak"
|
||||||
QList<Layer> layers; // the first one is shown by default
|
QList<Layer> layers; // the main keys; the first one is shown by default
|
||||||
|
QList<Section> sections;
|
||||||
// Template only: long-press choices for keymap letters, by lowercase letter.
|
// Template only: long-press choices for keymap letters, by lowercase letter.
|
||||||
QHash<QString, QStringList> alternates;
|
QHash<QString, QStringList> alternates;
|
||||||
|
|
||||||
@@ -64,3 +78,5 @@ struct Layout {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inline const QString MainSectionId = QStringLiteral("main");
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#include "LayoutBuilder.h"
|
#include "LayoutBuilder.h"
|
||||||
#include "KeyGeometry.h"
|
|
||||||
|
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -74,24 +72,6 @@ Row expandLetters(const Row &row, const KeymapLetters &letters, const QHash<QStr
|
|||||||
return result;
|
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
|
} // namespace
|
||||||
|
|
||||||
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters)
|
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters)
|
||||||
@@ -104,7 +84,6 @@ Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters)
|
|||||||
for (Layer &layer : layout.layers) {
|
for (Layer &layer : layout.layers) {
|
||||||
for (Row &row : layer.rows)
|
for (Row &row : layer.rows)
|
||||||
row = expandLetters(row, letters, alternates);
|
row = expandLetters(row, letters, alternates);
|
||||||
stretchRows(layer);
|
|
||||||
}
|
}
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,5 @@
|
|||||||
#include "Layout.h"
|
#include "Layout.h"
|
||||||
|
|
||||||
// Turns the template into the layout shown: fills its letters placeholders
|
// Turns the template into the layout shown: fills its letters placeholders
|
||||||
// from the keymap, gives each letter its flick and long-press choices, and
|
// from the keymap and gives each letter its flick and long-press choices.
|
||||||
// stretches the marked keys so every row of a layer is equally wide.
|
|
||||||
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters);
|
Layout buildLayout(const Layout &tmpl, const KeymapLetters &letters);
|
||||||
|
|||||||
+64
-16
@@ -4,6 +4,7 @@
|
|||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
namespace LayoutLoader {
|
namespace LayoutLoader {
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ const QHash<QString, KeyType> &keyTypesByName()
|
|||||||
{QStringLiteral("dismiss"), KeyType::Dismiss},
|
{QStringLiteral("dismiss"), KeyType::Dismiss},
|
||||||
{QStringLiteral("spacer"), KeyType::Spacer},
|
{QStringLiteral("spacer"), KeyType::Spacer},
|
||||||
{QStringLiteral("letters"), KeyType::Letters},
|
{QStringLiteral("letters"), KeyType::Letters},
|
||||||
|
{QStringLiteral("keysym"), KeyType::Keysym},
|
||||||
};
|
};
|
||||||
return types;
|
return types;
|
||||||
}
|
}
|
||||||
@@ -66,6 +68,18 @@ std::optional<KeyDef> parseKey(const QJsonValue &value, QString *error)
|
|||||||
key.alternates = toStringList(obj.value(u"alternates"));
|
key.alternates = toStringList(obj.value(u"alternates"));
|
||||||
key.lettersRow = obj.value(u"row").toInt(-1);
|
key.lettersRow = obj.value(u"row").toInt(-1);
|
||||||
key.flicks = toStringList(obj.value(u"flicks"));
|
key.flicks = toStringList(obj.value(u"flicks"));
|
||||||
|
key.repeat = obj.value(u"repeat").toBool();
|
||||||
|
|
||||||
|
if (key.type == KeyType::Keysym) {
|
||||||
|
const QString name = obj.value(u"keysym").toString();
|
||||||
|
key.keysym = xkb_keysym_from_name(qPrintable(name), XKB_KEYSYM_NO_FLAGS);
|
||||||
|
if (key.keysym == XKB_KEY_NoSymbol) {
|
||||||
|
*error = QStringLiteral("unknown keysym \"%1\"").arg(name);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (key.label.isEmpty())
|
||||||
|
key.label = name;
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
@@ -82,6 +96,25 @@ std::optional<KeyDef> parseKey(const QJsonValue &value, QString *error)
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool parseRows(const QJsonValue &value, QList<Row> *rows, QString *error)
|
||||||
|
{
|
||||||
|
for (const QJsonValue &rowValue : value.toArray()) {
|
||||||
|
Row row;
|
||||||
|
for (const QJsonValue &keyValue : rowValue.toArray()) {
|
||||||
|
auto key = parseKey(keyValue, error);
|
||||||
|
if (!key)
|
||||||
|
return false;
|
||||||
|
row.append(*key);
|
||||||
|
}
|
||||||
|
rows->append(row);
|
||||||
|
}
|
||||||
|
if (rows->isEmpty()) {
|
||||||
|
*error = QStringLiteral("no rows");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<Layer> parseLayer(const QJsonObject &obj, QString *error)
|
std::optional<Layer> parseLayer(const QJsonObject &obj, QString *error)
|
||||||
{
|
{
|
||||||
Layer layer;
|
Layer layer;
|
||||||
@@ -90,27 +123,35 @@ std::optional<Layer> parseLayer(const QJsonObject &obj, QString *error)
|
|||||||
*error = QStringLiteral("layer without a name");
|
*error = QStringLiteral("layer without a name");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
if (!parseRows(obj.value(u"rows"), &layer.rows, error)) {
|
||||||
for (const QJsonValue &rowValue : obj.value(u"rows").toArray()) {
|
*error = QStringLiteral("layer \"%1\": %2").arg(layer.name, *error);
|
||||||
Row row;
|
|
||||||
for (const QJsonValue &keyValue : rowValue.toArray()) {
|
|
||||||
auto key = parseKey(keyValue, error);
|
|
||||||
if (!key) {
|
|
||||||
*error = QStringLiteral("layer \"%1\": %2").arg(layer.name, *error);
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
row.append(*key);
|
|
||||||
}
|
|
||||||
layer.rows.append(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layer.rows.isEmpty()) {
|
|
||||||
*error = QStringLiteral("layer \"%1\" has no rows").arg(layer.name);
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<Section> parseSection(const QJsonObject &obj, QString *error)
|
||||||
|
{
|
||||||
|
Section section;
|
||||||
|
section.id = obj.value(u"id").toString();
|
||||||
|
section.name = obj.value(u"name").toString(section.id);
|
||||||
|
if (section.id.isEmpty() || section.id == MainSectionId) {
|
||||||
|
*error = QStringLiteral("section needs an id other than \"%1\"").arg(MainSectionId);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const QString placement = obj.value(u"placement").toString(QStringLiteral("right"));
|
||||||
|
if (placement != u"right" && placement != u"above") {
|
||||||
|
*error = QStringLiteral("section \"%1\": placement must be \"above\" or \"right\"").arg(section.id);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
section.placement = placement == u"above" ? Section::Placement::Above : Section::Placement::Right;
|
||||||
|
if (!parseRows(obj.value(u"rows"), §ion.rows, error)) {
|
||||||
|
*error = QStringLiteral("section \"%1\": %2").arg(section.id, *error);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::optional<Layout> parse(const QByteArray &json, QString *error)
|
std::optional<Layout> parse(const QByteArray &json, QString *error)
|
||||||
@@ -142,6 +183,13 @@ std::optional<Layout> parse(const QByteArray &json, QString *error)
|
|||||||
layout.layers.append(*layer);
|
layout.layers.append(*layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const QJsonValue §ionValue : root.value(u"sections").toArray()) {
|
||||||
|
auto section = parseSection(sectionValue.toObject(), error);
|
||||||
|
if (!section)
|
||||||
|
return std::nullopt;
|
||||||
|
layout.sections.append(*section);
|
||||||
|
}
|
||||||
|
|
||||||
if (layout.layers.isEmpty()) {
|
if (layout.layers.isEmpty()) {
|
||||||
*error = QStringLiteral("layout has no layers");
|
*error = QStringLiteral("layout has no layers");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|||||||
+60
-10
@@ -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::Keysym: return QStringLiteral("keysym");
|
||||||
case KeyType::Letters: break; // expanded before it reaches the model
|
case KeyType::Letters: break; // expanded before it reaches the model
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
@@ -89,6 +90,27 @@ void LayoutModel::setLayer(const QString &layerName)
|
|||||||
Q_EMIT layerChanged();
|
Q_EMIT layerChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LayoutModel::setVisibleSections(const QStringList &ids)
|
||||||
|
{
|
||||||
|
if (ids == m_visibleSections)
|
||||||
|
return;
|
||||||
|
m_visibleSections = ids;
|
||||||
|
rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList LayoutModel::sections() const
|
||||||
|
{
|
||||||
|
QVariantList list;
|
||||||
|
for (const Section §ion : m_layout.sections)
|
||||||
|
list.append(QVariantMap{{QStringLiteral("id"), section.id}, {QStringLiteral("name"), section.name}});
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal LayoutModel::totalUnits() const
|
||||||
|
{
|
||||||
|
return ::totalUnits(m_blocks);
|
||||||
|
}
|
||||||
|
|
||||||
void LayoutModel::setSize(QSizeF size)
|
void LayoutModel::setSize(QSizeF size)
|
||||||
{
|
{
|
||||||
if (size == m_size)
|
if (size == m_size)
|
||||||
@@ -140,12 +162,39 @@ int LayoutModel::keyAt(QPointF pos) const
|
|||||||
return nearest;
|
return nearest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<Block> LayoutModel::composeBlocks() const
|
||||||
|
{
|
||||||
|
const auto shown = [this](const QString &id) { return m_visibleSections.contains(id); };
|
||||||
|
const Layer *layer = m_layout.layer(m_layerName);
|
||||||
|
|
||||||
|
Block main;
|
||||||
|
for (const Section §ion : m_layout.sections) {
|
||||||
|
if (section.placement == Section::Placement::Above && shown(section.id))
|
||||||
|
main += section.rows;
|
||||||
|
}
|
||||||
|
if (layer && shown(MainSectionId))
|
||||||
|
main += layer->rows;
|
||||||
|
|
||||||
|
QList<Block> blocks;
|
||||||
|
if (!main.isEmpty())
|
||||||
|
blocks.append(main);
|
||||||
|
for (const Section §ion : m_layout.sections) {
|
||||||
|
if (section.placement == Section::Placement::Right && shown(section.id))
|
||||||
|
blocks.append(section.rows);
|
||||||
|
}
|
||||||
|
// Never show nothing.
|
||||||
|
if (blocks.isEmpty() && layer)
|
||||||
|
blocks.append(layer->rows);
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
void LayoutModel::rebuild()
|
void LayoutModel::rebuild()
|
||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
m_blocks = composeBlocks();
|
||||||
m_keys.clear();
|
m_keys.clear();
|
||||||
if (const Layer *layer = m_layout.layer(m_layerName)) {
|
for (const Block &block : std::as_const(m_blocks)) {
|
||||||
for (const Row &row : layer->rows) {
|
for (const Row &row : block) {
|
||||||
for (const KeyDef &key : row) {
|
for (const KeyDef &key : row) {
|
||||||
if (key.type != KeyType::Spacer)
|
if (key.type != KeyType::Spacer)
|
||||||
m_keys.append(key);
|
m_keys.append(key);
|
||||||
@@ -161,17 +210,18 @@ void LayoutModel::rebuild()
|
|||||||
void LayoutModel::relayout()
|
void LayoutModel::relayout()
|
||||||
{
|
{
|
||||||
m_rects.clear();
|
m_rects.clear();
|
||||||
const Layer *layer = m_layout.layer(m_layerName);
|
if (m_size.isEmpty())
|
||||||
if (!layer || m_size.isEmpty())
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const QList<QRectF> all = computeKeyRects(*layer, m_size);
|
const QList<QRectF> all = computeKeyRects(m_blocks, m_size);
|
||||||
qsizetype i = 0;
|
qsizetype i = 0;
|
||||||
for (const Row &row : layer->rows) {
|
for (const Block &block : std::as_const(m_blocks)) {
|
||||||
for (const KeyDef &key : row) {
|
for (const Row &row : block) {
|
||||||
if (key.type != KeyType::Spacer)
|
for (const KeyDef &key : row) {
|
||||||
m_rects.append(all[i]);
|
if (key.type != KeyType::Spacer)
|
||||||
++i;
|
m_rects.append(all[i]);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <QtQml/qqmlregistration.h>
|
#include <QtQml/qqmlregistration.h>
|
||||||
|
|
||||||
// The keys of the current layer, positioned for the current size.
|
// The keys on screen, positioned for the current size: the current layer
|
||||||
// Spacers take room but are not exposed as items.
|
// plus the sections the user chose to show. Spacers take room but are not
|
||||||
|
// exposed as items.
|
||||||
class LayoutModel : public QAbstractListModel
|
class LayoutModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -15,6 +16,8 @@ class LayoutModel : public QAbstractListModel
|
|||||||
QML_UNCREATABLE("Created in C++")
|
QML_UNCREATABLE("Created in C++")
|
||||||
Q_PROPERTY(QString layoutName READ layoutName NOTIFY layoutChanged)
|
Q_PROPERTY(QString layoutName READ layoutName NOTIFY layoutChanged)
|
||||||
Q_PROPERTY(QString layerName READ layerName NOTIFY layerChanged)
|
Q_PROPERTY(QString layerName READ layerName NOTIFY layerChanged)
|
||||||
|
// [{id, name}] of the optional sections, for the settings panel.
|
||||||
|
Q_PROPERTY(QVariantList sections READ sections NOTIFY layoutChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum Role {
|
enum Role {
|
||||||
@@ -37,9 +40,14 @@ public:
|
|||||||
|
|
||||||
QString layoutName() const { return m_layout.name; }
|
QString layoutName() const { return m_layout.name; }
|
||||||
QString layerName() const { return m_layerName; }
|
QString layerName() const { return m_layerName; }
|
||||||
|
QVariantList sections() const;
|
||||||
|
// Width in key units of everything shown.
|
||||||
|
qreal totalUnits() const;
|
||||||
|
|
||||||
void setLayout(const Layout &layout);
|
void setLayout(const Layout &layout);
|
||||||
void setLayer(const QString &layerName);
|
void setLayer(const QString &layerName);
|
||||||
|
// Section ids to show; MainSectionId stands for the layers.
|
||||||
|
void setVisibleSections(const QStringList &ids);
|
||||||
void setSize(QSizeF size);
|
void setSize(QSizeF size);
|
||||||
void setUppercase(bool uppercase);
|
void setUppercase(bool uppercase);
|
||||||
void setPressed(int index, bool pressed);
|
void setPressed(int index, bool pressed);
|
||||||
@@ -56,11 +64,14 @@ Q_SIGNALS:
|
|||||||
void layerChanged();
|
void layerChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
QList<Block> composeBlocks() const;
|
||||||
void rebuild();
|
void rebuild();
|
||||||
void relayout();
|
void relayout();
|
||||||
|
|
||||||
Layout m_layout;
|
Layout m_layout;
|
||||||
QString m_layerName;
|
QString m_layerName;
|
||||||
|
QStringList m_visibleSections{MainSectionId};
|
||||||
|
QList<Block> m_blocks;
|
||||||
QSizeF m_size;
|
QSizeF m_size;
|
||||||
bool m_uppercase = false;
|
bool m_uppercase = false;
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ void connectLayouts(const Layout &tmpl, WaylandKeymap &keymap, SystemLayouts &sy
|
|||||||
rebuild();
|
rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Showing or hiding a block resizes the window so keys keep their width.
|
||||||
|
void connectSections(Settings &settings, LayoutModel &model, FloatingWindow &placement)
|
||||||
|
{
|
||||||
|
QObject::connect(&settings, &Settings::visibleSectionsChanged, &model, [&] {
|
||||||
|
const qreal before = model.totalUnits();
|
||||||
|
model.setVisibleSections(settings.visibleSections());
|
||||||
|
if (before > 0)
|
||||||
|
placement.resizeBy(placement.geometry().width() * (model.totalUnits() / before - 1), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings)
|
void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings)
|
||||||
{
|
{
|
||||||
if (const auto geometry = settings.load(placement.screenName()))
|
if (const auto geometry = settings.load(placement.screenName()))
|
||||||
@@ -109,6 +120,7 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
Settings settings;
|
Settings settings;
|
||||||
controller.setOptions(settings.typingOptions());
|
controller.setOptions(settings.typingOptions());
|
||||||
|
model.setVisibleSections(settings.visibleSections());
|
||||||
QObject::connect(&settings, &Settings::typingOptionsChanged, &controller,
|
QObject::connect(&settings, &Settings::typingOptionsChanged, &controller,
|
||||||
[&] { controller.setOptions(settings.typingOptions()); });
|
[&] { controller.setOptions(settings.typingOptions()); });
|
||||||
|
|
||||||
@@ -129,6 +141,7 @@ int main(int argc, char *argv[])
|
|||||||
window->setProperty("placement", QVariant::fromValue(&placement));
|
window->setProperty("placement", QVariant::fromValue(&placement));
|
||||||
WindowSettings windowSettings;
|
WindowSettings windowSettings;
|
||||||
restoreAndPersistGeometry(placement, windowSettings);
|
restoreAndPersistGeometry(placement, windowSettings);
|
||||||
|
connectSections(settings, model, placement);
|
||||||
|
|
||||||
VisibilityController visibility;
|
VisibilityController visibility;
|
||||||
connectVisibility(visibility, backend, controller, placement);
|
connectVisibility(visibility, backend, controller, placement);
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
#include "Settings.h"
|
#include "Settings.h"
|
||||||
|
|
||||||
|
#include "layout/Layout.h"
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
|
constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
|
||||||
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
|
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
|
||||||
|
constexpr auto VisibleSectionsKey = "layout/visibleSections";
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings::Settings(QObject *parent)
|
Settings::Settings(QObject *parent)
|
||||||
@@ -14,6 +17,23 @@ Settings::Settings(QObject *parent)
|
|||||||
const TypingOptions defaults;
|
const TypingOptions defaults;
|
||||||
m_typing.autoCapitalize = store.value(AutoCapitalizeKey, defaults.autoCapitalize).toBool();
|
m_typing.autoCapitalize = store.value(AutoCapitalizeKey, defaults.autoCapitalize).toBool();
|
||||||
m_typing.doubleSpacePeriod = store.value(DoubleSpacePeriodKey, defaults.doubleSpacePeriod).toBool();
|
m_typing.doubleSpacePeriod = store.value(DoubleSpacePeriodKey, defaults.doubleSpacePeriod).toBool();
|
||||||
|
m_visibleSections = store.value(VisibleSectionsKey, QStringList{MainSectionId}).toStringList();
|
||||||
|
if (m_visibleSections.isEmpty())
|
||||||
|
m_visibleSections = {MainSectionId};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::setSectionVisible(const QString &id, bool visible)
|
||||||
|
{
|
||||||
|
QStringList sections = m_visibleSections;
|
||||||
|
if (visible && !sections.contains(id))
|
||||||
|
sections.append(id);
|
||||||
|
else if (!visible)
|
||||||
|
sections.removeAll(id);
|
||||||
|
if (sections == m_visibleSections || sections.isEmpty())
|
||||||
|
return;
|
||||||
|
m_visibleSections = sections;
|
||||||
|
QSettings().setValue(VisibleSectionsKey, m_visibleSections);
|
||||||
|
Q_EMIT visibleSectionsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Settings::setAutoCapitalize(bool enabled)
|
void Settings::setAutoCapitalize(bool enabled)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "input/TypingOptions.h"
|
#include "input/TypingOptions.h"
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QStringList>
|
||||||
#include <QtQml/qqmlregistration.h>
|
#include <QtQml/qqmlregistration.h>
|
||||||
|
|
||||||
// User preferences, persisted with QSettings (~/.config/sloposk/sloposk.conf)
|
// User preferences, persisted with QSettings (~/.config/sloposk/sloposk.conf)
|
||||||
@@ -14,6 +15,8 @@ class Settings : public QObject
|
|||||||
QML_UNCREATABLE("Created in C++")
|
QML_UNCREATABLE("Created in C++")
|
||||||
Q_PROPERTY(bool autoCapitalize READ autoCapitalize WRITE setAutoCapitalize NOTIFY typingOptionsChanged)
|
Q_PROPERTY(bool autoCapitalize READ autoCapitalize WRITE setAutoCapitalize NOTIFY typingOptionsChanged)
|
||||||
Q_PROPERTY(bool doubleSpacePeriod READ doubleSpacePeriod WRITE setDoubleSpacePeriod NOTIFY typingOptionsChanged)
|
Q_PROPERTY(bool doubleSpacePeriod READ doubleSpacePeriod WRITE setDoubleSpacePeriod NOTIFY typingOptionsChanged)
|
||||||
|
// Which key blocks are shown: "main" (the typing keys) and template section ids.
|
||||||
|
Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Settings(QObject *parent = nullptr);
|
explicit Settings(QObject *parent = nullptr);
|
||||||
@@ -25,11 +28,17 @@ public:
|
|||||||
bool doubleSpacePeriod() const { return m_typing.doubleSpacePeriod; }
|
bool doubleSpacePeriod() const { return m_typing.doubleSpacePeriod; }
|
||||||
void setDoubleSpacePeriod(bool enabled);
|
void setDoubleSpacePeriod(bool enabled);
|
||||||
|
|
||||||
|
QStringList visibleSections() const { return m_visibleSections; }
|
||||||
|
// Hiding the last visible section is refused: something must stay on screen.
|
||||||
|
Q_INVOKABLE void setSectionVisible(const QString &id, bool visible);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void typingOptionsChanged();
|
void typingOptionsChanged();
|
||||||
|
void visibleSectionsChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setTypingOptions(const TypingOptions &options);
|
void setTypingOptions(const TypingOptions &options);
|
||||||
|
|
||||||
TypingOptions m_typing;
|
TypingOptions m_typing;
|
||||||
|
QStringList m_visibleSections;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ private Q_SLOTS:
|
|||||||
{
|
{
|
||||||
engine = {};
|
engine = {};
|
||||||
model.setLayout(TestLayouts::build("us"));
|
model.setLayout(TestLayouts::build("us"));
|
||||||
|
model.setVisibleSections({MainSectionId});
|
||||||
controller = std::make_unique<KeyboardController>(engine, model);
|
controller = std::make_unique<KeyboardController>(engine, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +200,36 @@ private Q_SLOTS:
|
|||||||
QCOMPARE(requested, 1);
|
QCOMPARE(requested, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void keysymKeyActsOnPressAndRepeats()
|
||||||
|
{
|
||||||
|
KeyDef left = makeKey(KeyType::Keysym, u"←"_s);
|
||||||
|
left.keysym = XKB_KEY_Left;
|
||||||
|
left.repeat = true;
|
||||||
|
controller->keyDown(left, 0);
|
||||||
|
QCOMPARE(engine.keysyms, QList<quint32>{XKB_KEY_Left});
|
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(engine.keysyms.size() >= 3, 2000); // held: repeats
|
||||||
|
controller->keyUp(left, KeyOutput::Primary, {}, 0);
|
||||||
|
const auto sent = engine.keysyms.size();
|
||||||
|
QTest::qWait(200);
|
||||||
|
QCOMPARE(engine.keysyms.size(), sent);
|
||||||
|
QVERIFY(engine.output.isEmpty()); // no text
|
||||||
|
}
|
||||||
|
|
||||||
|
void sectionsAreComposedAroundTheMainKeys()
|
||||||
|
{
|
||||||
|
const qsizetype mainKeys = model.rowCount();
|
||||||
|
const qreal mainUnits = model.totalUnits();
|
||||||
|
model.setVisibleSections({MainSectionId, u"function"_s});
|
||||||
|
QCOMPARE(model.rowCount(), mainKeys + 13);
|
||||||
|
QCOMPARE(model.totalUnits(), mainUnits); // above, not beside
|
||||||
|
model.setVisibleSections({MainSectionId, u"navigation"_s});
|
||||||
|
QCOMPARE(model.totalUnits(), mainUnits + 3);
|
||||||
|
model.setVisibleSections({u"numpad"_s});
|
||||||
|
QCOMPARE(model.rowCount(), 19);
|
||||||
|
model.setVisibleSections({});
|
||||||
|
QCOMPARE(model.rowCount(), mainKeys); // never nothing
|
||||||
|
}
|
||||||
|
|
||||||
void dismissEmitsSignal()
|
void dismissEmitsSignal()
|
||||||
{
|
{
|
||||||
int dismissed = 0;
|
int dismissed = 0;
|
||||||
|
|||||||
@@ -49,6 +49,26 @@ private Q_SLOTS:
|
|||||||
QCOMPARE(rects[4].left(), 100.0);
|
QCOMPARE(rects[4].left(), 100.0);
|
||||||
QCOMPARE(rects[5].right(), 300.0);
|
QCOMPARE(rects[5].right(), 300.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void stretchKeysFillTheRow()
|
||||||
|
{
|
||||||
|
KeyDef wide = key();
|
||||||
|
wide.stretch = true;
|
||||||
|
const Layer layer{u"l"_s, {{key(), key(), key(), key()}, {key(), wide}}};
|
||||||
|
const auto rects = computeKeyRects(layer, {400, 100}, {.padding = 0, .gap = 0});
|
||||||
|
QCOMPARE(rects[4].left(), 0.0);
|
||||||
|
QCOMPARE(rects[5].width(), 300.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void blocksShareUnitAndSitAtTheBottom()
|
||||||
|
{
|
||||||
|
const Block tall{{key(), key()}, {key(), key()}};
|
||||||
|
const Block shortBlock{{key()}};
|
||||||
|
const auto rects = computeKeyRects({tall, shortBlock}, {400, 100}, {.padding = 0, .gap = 0, .blockGap = 100});
|
||||||
|
// 3 units in 300 px: 100 each.
|
||||||
|
QCOMPARE(rects[1].right(), 200.0);
|
||||||
|
QCOMPARE(rects[4], QRectF(300, 50, 100, 50));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestKeyGeometry)
|
QTEST_GUILESS_MAIN(TestKeyGeometry)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "layout/LayoutLoader.h"
|
#include "layout/LayoutLoader.h"
|
||||||
|
|
||||||
#include <QTest>
|
#include <QTest>
|
||||||
|
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
@@ -67,24 +68,60 @@ private Q_SLOTS:
|
|||||||
&error));
|
&error));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stretching must even out rows whatever the keymap's letter counts.
|
// Stretch keys must even out rows whatever the keymap's letter counts.
|
||||||
void builtLayersHaveEqualRows_data()
|
void builtLayersHaveEvenRows_data()
|
||||||
{
|
{
|
||||||
QTest::addColumn<QString>("xkbLayout");
|
QTest::addColumn<QString>("xkbLayout");
|
||||||
for (const char *layout : {"us", "sk", "se", "de", "fr", "ru", "gr"})
|
for (const char *layout : {"us", "sk", "se", "de", "fr", "ru", "gr"})
|
||||||
QTest::newRow(layout) << QString::fromLatin1(layout);
|
QTest::newRow(layout) << QString::fromLatin1(layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
void builtLayersHaveEqualRows()
|
void builtLayersHaveEvenRows()
|
||||||
{
|
{
|
||||||
QFETCH(QString, xkbLayout);
|
QFETCH(QString, xkbLayout);
|
||||||
const Layout layout = TestLayouts::build(qPrintable(xkbLayout));
|
const Layout layout = TestLayouts::build(qPrintable(xkbLayout));
|
||||||
for (const Layer &layer : layout.layers) {
|
for (const Layer &layer : layout.layers) {
|
||||||
const qreal expected = rowUnits(layer.rows.first());
|
const QList<QRectF> rects = computeKeyRects(layer, {1000, 300});
|
||||||
for (const Row &row : layer.rows)
|
qsizetype i = 0;
|
||||||
QVERIFY2(qFuzzyCompare(rowUnits(row), expected), qPrintable(layer.name));
|
for (const Row &row : layer.rows) {
|
||||||
|
const QRectF first = rects[i];
|
||||||
|
const QRectF last = rects[i + row.size() - 1];
|
||||||
|
QVERIFY2(qAbs(first.left() - rects.first().left()) < 0.01, qPrintable(layer.name));
|
||||||
|
QVERIFY2(qAbs(last.right() - rects[layer.rows.first().size() - 1].right()) < 0.01,
|
||||||
|
qPrintable(layer.name));
|
||||||
|
i += row.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void templateHasKeySections()
|
||||||
|
{
|
||||||
|
const Layout layout = TestLayouts::builtinTemplate();
|
||||||
|
QStringList ids;
|
||||||
|
for (const Section §ion : layout.sections)
|
||||||
|
ids.append(section.id);
|
||||||
|
QCOMPARE(ids, (QStringList{u"function"_s, u"navigation"_s, u"numpad"_s}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void parsesKeysymKeys()
|
||||||
|
{
|
||||||
|
QString error;
|
||||||
|
const auto layout = LayoutLoader::parse(
|
||||||
|
R"({"id": "t", "layers": [{"name": "l", "rows": [[{"type": "keysym", "keysym": "Home", "repeat": true}]]}]})",
|
||||||
|
&error);
|
||||||
|
QVERIFY2(layout, qPrintable(error));
|
||||||
|
const KeyDef &key = layout->layers[0].rows[0][0];
|
||||||
|
QCOMPARE(key.keysym, quint32(XKB_KEY_Home));
|
||||||
|
QCOMPARE(key.label, u"Home"); // the keysym name unless labelled
|
||||||
|
QVERIFY(key.repeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rejectsUnknownKeysym()
|
||||||
|
{
|
||||||
|
QString error;
|
||||||
|
QVERIFY(!LayoutLoader::parse(
|
||||||
|
R"({"id": "t", "layers": [{"name": "l", "rows": [[{"type": "keysym", "keysym": "Nope"}]]}]})", &error));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestLayoutLoader)
|
QTEST_GUILESS_MAIN(TestLayoutLoader)
|
||||||
|
|||||||
Reference in New Issue
Block a user