Layouts, iPad-style rendering, taps, shift and layers
- JSON layouts (en-US, sv-SE inheriting number/symbol layers via base) - LayoutModel + KeyGeometry: proportional rows, nearest-key hit testing - KeyboardSurface: multi-pointer touch/mouse routing to the controller - KeyboardController + ShiftState: iOS shift/caps-lock, layers, globe, dismiss - QML view with light/dark iPad palette and drag handle - DevHooks: SLOPOSK_AUTOTYPE, SLOPOSK_SCREENSHOT for headless checks - Unit tests for shift, loader, geometry and controller Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import QtQuick
|
||||
|
||||
// The grab bar under the floating keyboard. Emits how far it was dragged;
|
||||
// moving the window is up to the owner.
|
||||
Item {
|
||||
id: handle
|
||||
|
||||
signal moved(real dx, real dy)
|
||||
|
||||
implicitHeight: 22
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 120
|
||||
height: 5
|
||||
radius: height / 2
|
||||
color: Theme.handle
|
||||
}
|
||||
|
||||
DragHandler {
|
||||
target: null
|
||||
// The window follows the pointer, so the pointer stays near its press
|
||||
// position in window coordinates: measure each step from there.
|
||||
onCentroidChanged: {
|
||||
if (active)
|
||||
handle.moved(centroid.scenePosition.x - centroid.scenePressPosition.x,
|
||||
centroid.scenePosition.y - centroid.scenePressPosition.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import QtQuick
|
||||
import Sloposk
|
||||
|
||||
// One key, drawn from the LayoutModel roles. Purely visual.
|
||||
Item {
|
||||
id: key
|
||||
|
||||
required property string label
|
||||
required property string flickLabel
|
||||
required property string keyType
|
||||
required property real keyX
|
||||
required property real keyY
|
||||
required property real keyWidth
|
||||
required property real keyHeight
|
||||
required property bool pressed
|
||||
property int shiftMode: KeyboardController.Off
|
||||
|
||||
readonly property bool isCharacter: keyType === "char" || keyType === "space"
|
||||
readonly property bool shiftActive: keyType === "shift" && shiftMode !== KeyboardController.Off
|
||||
// Character keys darken when pressed; function keys lighten (as on iPad).
|
||||
readonly property bool lit: (isCharacter !== pressed) || shiftActive
|
||||
|
||||
readonly property var defaultGlyphs: ({
|
||||
backspace: "⌫",
|
||||
globe: "🌐",
|
||||
dismiss: "⌨",
|
||||
enter: "return",
|
||||
})
|
||||
readonly property string displayLabel: {
|
||||
if (keyType === "shift")
|
||||
return ["⇧", "⬆", "⇪"][shiftMode]
|
||||
return label !== "" ? label : (defaultGlyphs[keyType] ?? "")
|
||||
}
|
||||
// Count code points, not UTF-16 units, so emoji glyphs aren't treated as words.
|
||||
readonly property bool isWord: [...displayLabel].length > 1 && !isCharacter
|
||||
|
||||
x: keyX
|
||||
y: keyY
|
||||
width: keyWidth
|
||||
height: keyHeight
|
||||
|
||||
Rectangle {
|
||||
anchors { fill: face; topMargin: 1; bottomMargin: -1 }
|
||||
radius: Theme.keyRadius
|
||||
color: Theme.keyShadow
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: face
|
||||
anchors.fill: parent
|
||||
radius: Theme.keyRadius
|
||||
color: key.lit ? Theme.characterKey : Theme.functionKey
|
||||
}
|
||||
|
||||
Text {
|
||||
id: flick
|
||||
visible: text !== ""
|
||||
text: key.flickLabel
|
||||
anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; topMargin: key.height * 0.1 }
|
||||
font.pixelSize: key.height * 0.24
|
||||
color: Theme.secondaryText
|
||||
}
|
||||
|
||||
Text {
|
||||
text: key.displayLabel
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
verticalCenter: parent.verticalCenter
|
||||
verticalCenterOffset: flick.visible ? key.height * 0.14 : 0
|
||||
}
|
||||
font.pixelSize: key.height * (key.isWord ? 0.24 : flick.visible ? 0.34 : 0.4)
|
||||
color: Theme.text
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import QtQuick
|
||||
import Sloposk
|
||||
|
||||
KeyboardSurface {
|
||||
id: surface
|
||||
|
||||
Repeater {
|
||||
model: surface.model
|
||||
delegate: Key {
|
||||
shiftMode: surface.controller ? surface.controller.shiftMode : KeyboardController.Off
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-32
@@ -1,45 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Sloposk
|
||||
|
||||
// Spike UI: proves text commits, keysyms and dragging. Replaced by the real
|
||||
// keyboard in milestone 2.
|
||||
Window {
|
||||
id: root
|
||||
|
||||
required property var backend
|
||||
property var placement: null
|
||||
required property LayoutModel layoutModel
|
||||
required property KeyboardController controller
|
||||
property var placement: null // FloatingWindow, set from C++
|
||||
|
||||
width: 720
|
||||
height: 120
|
||||
width: 1000
|
||||
height: 340
|
||||
visible: false
|
||||
color: "#d1d3d9"
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
id: handle
|
||||
anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom; bottomMargin: 6 }
|
||||
width: 120; height: 6; radius: 3
|
||||
color: "#8e8e93"
|
||||
anchors.fill: parent
|
||||
radius: Theme.windowRadius
|
||||
color: Theme.background
|
||||
|
||||
DragHandler {
|
||||
target: null
|
||||
margin: 12
|
||||
property point last
|
||||
onActiveChanged: last = centroid.scenePressPosition
|
||||
onCentroidChanged: {
|
||||
if (!active || !root.placement)
|
||||
return
|
||||
const p = centroid.scenePosition
|
||||
root.placement.moveBy(p.x - last.x, p.y - last.y)
|
||||
}
|
||||
Keyboard {
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top; bottom: handle.top }
|
||||
anchors.margins: 2
|
||||
model: root.layoutModel
|
||||
controller: root.controller
|
||||
}
|
||||
|
||||
DragHandle {
|
||||
id: handle
|
||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
||||
onMoved: (dx, dy) => root.placement?.moveBy(dx, dy)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
Button { text: "héllo👍"; focusPolicy: Qt.NoFocus; onClicked: root.backend.commitText(text) }
|
||||
Button { text: "⌫"; focusPolicy: Qt.NoFocus; onClicked: root.backend.sendKeysym(0xff08) }
|
||||
Button { text: "⏎"; focusPolicy: Qt.NoFocus; onClicked: root.backend.sendKeysym(0xff0d) }
|
||||
Label { anchors.verticalCenter: parent.verticalCenter; text: root.backend.active ? "active" : "inactive" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
// iPad keyboard palette; follows the system light/dark preference.
|
||||
QtObject {
|
||||
readonly property bool dark: Qt.styleHints.colorScheme === Qt.ColorScheme.Dark
|
||||
|
||||
readonly property color background: dark ? "#2c2c2e" : "#d1d3d9"
|
||||
readonly property color characterKey: dark ? "#6b6b6e" : "#ffffff"
|
||||
readonly property color functionKey: dark ? "#464649" : "#abb0bc"
|
||||
readonly property color keyShadow: dark ? "#141416" : "#88898d"
|
||||
readonly property color text: dark ? "#ffffff" : "#000000"
|
||||
readonly property color secondaryText: dark ? "#a5a5aa" : "#8a8a8e"
|
||||
readonly property color handle: dark ? "#8e8e93" : "#7c7c80"
|
||||
|
||||
readonly property real windowRadius: 14
|
||||
readonly property real keyRadius: 6
|
||||
}
|
||||
Reference in New Issue
Block a user