Numeric keypad spans the full keyboard height
- Sections take "fillHeight": their rows stretch over the whole key area instead of sitting at the bottom at the shared row height - Rows of other blocks are unaffected; the template turns it on for the numpad Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso
|
|||||||
- Layer `full`: rows used instead of `rows` in full-keyboard mode
|
- Layer `full`: rows used instead of `rows` in full-keyboard mode
|
||||||
- `stretch`: the key grows so its row is as wide as the layer's widest (letter counts vary by layout)
|
- `stretch`: the key grows so its row is as wide as the layer's widest (letter counts vary by layout)
|
||||||
- Top-level `alternates`: long-press choices per letter, after the keymap's own
|
- Top-level `alternates`: long-press choices per letter, after the keymap's own
|
||||||
- Top-level `sections`: optional blocks, `{"id", "name", "placement": "above" | "right", "rows"}`; the user picks which show
|
- Top-level `sections`: optional blocks, `{"id", "name", "placement": "above" | "right", "fillHeight", "rows"}`; the user picks which show; `fillHeight` stretches a right-hand block over the full keyboard height
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
{
|
{
|
||||||
"id": "numpad",
|
"id": "numpad",
|
||||||
"name": "Numeric keypad",
|
"name": "Numeric keypad",
|
||||||
|
"fillHeight": true,
|
||||||
"rows": [
|
"rows": [
|
||||||
["7", "8", "9", "/", {"type": "backspace"}],
|
["7", "8", "9", "/", {"type": "backspace"}],
|
||||||
["4", "5", "6", "*", "-"],
|
["4", "5", "6", "*", "-"],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace {
|
|||||||
qreal blockUnits(const Block &block)
|
qreal blockUnits(const Block &block)
|
||||||
{
|
{
|
||||||
qreal units = 0;
|
qreal units = 0;
|
||||||
for (const Row &row : block)
|
for (const Row &row : block.rows)
|
||||||
units = std::max(units, rowUnits(row));
|
units = std::max(units, rowUnits(row));
|
||||||
return units;
|
return units;
|
||||||
}
|
}
|
||||||
@@ -44,9 +44,13 @@ qreal totalUnits(const QList<Block> &blocks)
|
|||||||
|
|
||||||
qsizetype totalRows(const QList<Block> &blocks)
|
qsizetype totalRows(const QList<Block> &blocks)
|
||||||
{
|
{
|
||||||
|
// Filling blocks only count when nothing else is shown.
|
||||||
|
const bool allFill = std::ranges::all_of(blocks, &Block::fillHeight);
|
||||||
qsizetype rows = 0;
|
qsizetype rows = 0;
|
||||||
for (const Block &block : blocks)
|
for (const Block &block : blocks) {
|
||||||
rows = std::max(rows, block.size());
|
if (allFill || !block.fillHeight)
|
||||||
|
rows = std::max(rows, block.rows.size());
|
||||||
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,13 +69,17 @@ QList<QRectF> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardM
|
|||||||
// 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,
|
||||||
// and blocks are separated by blockGap instead.
|
// and blocks are separated by blockGap instead.
|
||||||
const qreal unitWidth = (innerWidth + blockCount * metrics.gap - (blockCount - 1) * metrics.blockGap) / units;
|
const qreal unitWidth = (innerWidth + blockCount * metrics.gap - (blockCount - 1) * metrics.blockGap) / units;
|
||||||
const qreal rowHeight = (innerHeight - qreal(maxRows - 1) * metrics.gap) / qreal(maxRows);
|
const auto heightFor = [&](qsizetype rows) { return (innerHeight - qreal(rows - 1) * metrics.gap) / qreal(rows); };
|
||||||
|
const qreal sharedRowHeight = heightFor(maxRows);
|
||||||
|
|
||||||
qreal left = metrics.padding;
|
qreal left = metrics.padding;
|
||||||
for (const Block &block : blocks) {
|
for (const Block &block : blocks) {
|
||||||
const qreal width = blockUnits(block);
|
const qreal width = blockUnits(block);
|
||||||
qreal y = metrics.padding + qreal(maxRows - block.size()) * (rowHeight + metrics.gap);
|
const qsizetype rows = block.rows.size();
|
||||||
for (const Row &row : block) {
|
const bool fill = block.fillHeight && rows > 0;
|
||||||
|
const qreal rowHeight = fill ? heightFor(rows) : sharedRowHeight;
|
||||||
|
qreal y = metrics.padding + (fill ? 0 : qreal(maxRows - rows) * (rowHeight + metrics.gap));
|
||||||
|
for (const Row &row : block.rows) {
|
||||||
const QList<qreal> widths = stretchedWidths(row, width);
|
const QList<qreal> widths = stretchedWidths(row, width);
|
||||||
const qreal used = std::accumulate(widths.begin(), widths.end(), 0.0);
|
const qreal used = std::accumulate(widths.begin(), widths.end(), 0.0);
|
||||||
qreal x = left + (width - used) * unitWidth / 2;
|
qreal x = left + (width - used) * unitWidth / 2;
|
||||||
|
|||||||
@@ -12,22 +12,25 @@ struct KeyboardMetrics {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The keys on screen: blocks of rows, side by side.
|
// The keys on screen: blocks of rows, side by side.
|
||||||
using Block = QList<Row>;
|
struct Block {
|
||||||
|
QList<Row> rows;
|
||||||
|
bool fillHeight = false; // rows grow to span the full height instead of sharing the row height
|
||||||
|
};
|
||||||
|
|
||||||
// Lays out blocks to fill `size`. Returns one rect per key, block by block in
|
// 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;
|
// 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
|
// rows share one height, set by the tallest block that doesn't fill, and
|
||||||
// block, stretch keys grow to make their row as wide as the widest one, and
|
// shorter blocks sit at the bottom. In each block, stretch keys grow to make
|
||||||
// remaining shorter rows are centered.
|
// 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 = {});
|
QList<QRectF> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardMetrics metrics = {});
|
||||||
inline QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {})
|
inline QList<QRectF> computeKeyRects(const Layer &layer, QSizeF size, KeyboardMetrics metrics = {})
|
||||||
{
|
{
|
||||||
return computeKeyRects({layer.rows}, size, metrics);
|
return computeKeyRects({{layer.rows}}, size, metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Total width of a row in key units, before stretching.
|
// 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.
|
// Width of all blocks in key units, ignoring gaps.
|
||||||
qreal totalUnits(const QList<Block> &blocks);
|
qreal totalUnits(const QList<Block> &blocks);
|
||||||
// Rows of the tallest block.
|
// Rows of the tallest block that sets the row height (see computeKeyRects).
|
||||||
qsizetype totalRows(const QList<Block> &blocks);
|
qsizetype totalRows(const QList<Block> &blocks);
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ struct Section {
|
|||||||
QString id; // "main" is reserved for the layers
|
QString id; // "main" is reserved for the layers
|
||||||
QString name; // shown in settings
|
QString name; // shown in settings
|
||||||
Placement placement = Placement::Right;
|
Placement placement = Placement::Right;
|
||||||
|
bool fillHeight = false; // a right-hand block whose rows span the whole keyboard height
|
||||||
QList<Row> rows;
|
QList<Row> rows;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ std::optional<Section> parseSection(const QJsonObject &obj, QString *error)
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
section.placement = placement == u"above" ? Section::Placement::Above : Section::Placement::Right;
|
section.placement = placement == u"above" ? Section::Placement::Above : Section::Placement::Right;
|
||||||
|
section.fillHeight = obj.value(u"fillHeight").toBool(false);
|
||||||
if (!parseRows(obj.value(u"rows"), §ion.rows, error)) {
|
if (!parseRows(obj.value(u"rows"), §ion.rows, error)) {
|
||||||
*error = QStringLiteral("section \"%1\": %2").arg(section.id, *error);
|
*error = QStringLiteral("section \"%1\": %2").arg(section.id, *error);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|||||||
@@ -181,21 +181,21 @@ QList<Block> LayoutModel::composeBlocks() const
|
|||||||
Block main;
|
Block main;
|
||||||
for (const Section §ion : m_layout.sections) {
|
for (const Section §ion : m_layout.sections) {
|
||||||
if (section.placement == Section::Placement::Above && shown(section.id))
|
if (section.placement == Section::Placement::Above && shown(section.id))
|
||||||
main += section.rows;
|
main.rows += section.rows;
|
||||||
}
|
}
|
||||||
if (layer && shown(MainSectionId))
|
if (layer && shown(MainSectionId))
|
||||||
main += layer->rows;
|
main.rows += layer->rows;
|
||||||
|
|
||||||
QList<Block> blocks;
|
QList<Block> blocks;
|
||||||
if (!main.isEmpty())
|
if (!main.rows.isEmpty())
|
||||||
blocks.append(main);
|
blocks.append(main);
|
||||||
for (const Section §ion : m_layout.sections) {
|
for (const Section §ion : m_layout.sections) {
|
||||||
if (section.placement == Section::Placement::Right && shown(section.id))
|
if (section.placement == Section::Placement::Right && shown(section.id))
|
||||||
blocks.append(section.rows);
|
blocks.append({section.rows, section.fillHeight});
|
||||||
}
|
}
|
||||||
// Never show nothing.
|
// Never show nothing.
|
||||||
if (blocks.isEmpty() && layer)
|
if (blocks.isEmpty() && layer)
|
||||||
blocks.append(layer->rows);
|
blocks.append({layer->rows});
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ void LayoutModel::rebuild()
|
|||||||
m_blocks = composeBlocks();
|
m_blocks = composeBlocks();
|
||||||
m_keys.clear();
|
m_keys.clear();
|
||||||
for (const Block &block : std::as_const(m_blocks)) {
|
for (const Block &block : std::as_const(m_blocks)) {
|
||||||
for (const Row &row : block) {
|
for (const Row &row : block.rows) {
|
||||||
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);
|
||||||
@@ -227,7 +227,7 @@ void LayoutModel::relayout()
|
|||||||
const QList<QRectF> all = computeKeyRects(m_blocks, m_size);
|
const QList<QRectF> all = computeKeyRects(m_blocks, m_size);
|
||||||
qsizetype i = 0;
|
qsizetype i = 0;
|
||||||
for (const Block &block : std::as_const(m_blocks)) {
|
for (const Block &block : std::as_const(m_blocks)) {
|
||||||
for (const Row &row : block) {
|
for (const Row &row : block.rows) {
|
||||||
for (const KeyDef &key : row) {
|
for (const KeyDef &key : row) {
|
||||||
if (key.type != KeyType::Spacer)
|
if (key.type != KeyType::Spacer)
|
||||||
m_rects.append(all[i]);
|
m_rects.append(all[i]);
|
||||||
|
|||||||
@@ -62,13 +62,26 @@ private Q_SLOTS:
|
|||||||
|
|
||||||
void blocksShareUnitAndSitAtTheBottom()
|
void blocksShareUnitAndSitAtTheBottom()
|
||||||
{
|
{
|
||||||
const Block tall{{key(), key()}, {key(), key()}};
|
const Block tall{{{key(), key()}, {key(), key()}}};
|
||||||
const Block shortBlock{{key()}};
|
const Block shortBlock{{{key()}}};
|
||||||
const auto rects = computeKeyRects({tall, shortBlock}, {400, 100}, {.padding = 0, .gap = 0, .blockGap = 100});
|
const auto rects = computeKeyRects({tall, shortBlock}, {400, 100}, {.padding = 0, .gap = 0, .blockGap = 100});
|
||||||
// 3 units in 300 px: 100 each.
|
// 3 units in 300 px: 100 each.
|
||||||
QCOMPARE(rects[1].right(), 200.0);
|
QCOMPARE(rects[1].right(), 200.0);
|
||||||
QCOMPARE(rects[4], QRectF(300, 50, 100, 50));
|
QCOMPARE(rects[4], QRectF(300, 50, 100, 50));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void fillingBlockSpansTheFullHeight()
|
||||||
|
{
|
||||||
|
const Block tall{{{key()}, {key()}, {key()}, {key()}}};
|
||||||
|
const Block filling{{{key()}, {key()}}, true};
|
||||||
|
const auto rects = computeKeyRects({tall, filling}, {200, 100}, {.padding = 0, .gap = 0, .blockGap = 0});
|
||||||
|
QCOMPARE(rects[0].height(), 25.0);
|
||||||
|
QCOMPARE(rects[4], QRectF(100, 0, 100, 50));
|
||||||
|
QCOMPARE(rects[5], QRectF(100, 50, 100, 50));
|
||||||
|
// A filling block taller than the rest doesn't shrink the shared rows.
|
||||||
|
QCOMPARE(totalRows({tall, filling}), 4);
|
||||||
|
QCOMPARE(totalRows({filling}), 2);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_GUILESS_MAIN(TestKeyGeometry)
|
QTEST_GUILESS_MAIN(TestKeyGeometry)
|
||||||
|
|||||||
Reference in New Issue
Block a user