Add a keyboard for text fields on the desktop

Fixes #7: SteamVR's keyboard never came up for the desktop's apps, and
opening it for our panels doesn't work well on the Frame (it's Steam's own
panel, mounted in the dashboard's scene, it follows the laser between
panels, and it takes the controllers over to SteamVR's laser).

- KWin starts input/ft-textinput as the desktop's input method. It tells
  the input relay when a text field gains or loses focus, and the relay
  asks ft-screens to open or close the keyboard. The session drops the
  QT_IM_MODULE=xim and GTK_IM_MODULE=xim that the gamescope session sets,
  or Qt and GTK apps never report text fields.
- The keyboard is ft-screens' own panel (screens/keyboard.cpp): a US laptop
  layout, typed with a controller's laser or the 3D mouse. It opens 0.7 m
  in front of you, below your eyes and facing you. It has a grab bar to
  move it, a Close key, latching Shift, Ctrl and Alt, and repeat on a held
  key. It's drawn into shared DMA-BUFs, so it doesn't flicker. Its keys
  reach the focused screen as key presses, so every app takes them.
- It steps aside while the Steam menu or Steam's own keyboard is up and
  comes back after. A layout reset closes it, and it doesn't open without
  a head pose.
- Frametop Input Settings has a Keyboard page: open it for every text
  field, only while no keyboard is connected (the default), only from a
  mapped button (the new Open/close keyboard action, for mice and
  controllers), or never. A switch keeps it open until you close it.
- The pointer helper treats every frametop.* overlay as a real panel. The
  keyboard's shared texture reports 0x0 like SteamVR's scene-graph
  controls, and the helper had given it their wide catch radius.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-09-30 13:41:47 -06:00
1 parent 3aea571496
commit 5714978e65
15 files changed
+1047 -33

No files matched your search

+53 -3
View File
@@ -19,6 +19,7 @@ Kirigami.ApplicationWindow {
Kirigami.Action { text: "Devices"; icon.name: "input-mouse"; onTriggered: root.show(devicesPage) },
Kirigami.Action { text: "Buttons"; icon.name: "input-keyboard"; onTriggered: root.show(buttonsPage) },
Kirigami.Action { text: "Controllers"; icon.name: "input-gamepad"; onTriggered: root.show(controllersPage) },
Kirigami.Action { text: "Keyboard"; icon.name: "input-keyboard-virtual"; onTriggered: root.show(keyboardPage) },
Kirigami.Action { text: "Pointer"; icon.name: "transform-move"; onTriggered: root.show(pointerPage) },
Kirigami.Action { text: "Ignored panels"; icon.name: "view-hidden"; onTriggered: root.show(ignorePage) },
Kirigami.Action { text: "Gaze"; icon.name: "view-visible"; onTriggered: root.show(gazePage) },
@@ -55,9 +56,10 @@ Kirigami.ApplicationWindow {
pageStack.push(page)
}
// FT_INPUT_PAGE=buttons|controllers|pointer|ignore|gaze|bluetooth opens the app on that page.
pageStack.initialPage: ({ buttons: buttonsPage, controllers: controllersPage, pointer: pointerPage,
ignore: ignorePage, gaze: gazePage, bluetooth: bluetoothPage })[startPage] || devicesPage
// FT_INPUT_PAGE=buttons|controllers|keyboard|pointer|ignore|gaze|bluetooth opens the app on that page.
pageStack.initialPage: ({ buttons: buttonsPage, controllers: controllersPage, keyboard: keyboardPage,
pointer: pointerPage, ignore: ignorePage, gaze: gazePage,
bluetooth: bluetoothPage })[startPage] || devicesPage
Connections {
target: backend
@@ -505,6 +507,54 @@ Kirigami.ApplicationWindow {
}
}
// ---------------------------------------------------------------- Keyboard
Component {
id: keyboardPage
Kirigami.ScrollablePage {
id: kpage
title: "Keyboard"
// Pass-through keyboards connected now: with "no_keyboard", the keyboard waits for none.
// A program's uinput keyboard (frame-voice's, say) doesn't count.
property var keyboards: backend.devices.filter(d => d.connected && d.role === "passthrough"
&& d.kinds.indexOf("keyboard") >= 0 && !d.uinput)
Kirigami.FormLayout {
Controls.ComboBox {
Kirigami.FormData.label: "Show the keyboard:"
model: backend.vrKeyboardModes
textRole: "text"
valueRole: "value"
Component.onCompleted: currentIndex = indexOfValue(backend.vrKeyboard)
onActivated: backend.setVrKeyboard(currentValue)
}
Controls.Switch {
Kirigami.FormData.label: "Keep it open:"
text: "Until you press its Close key or your keyboard button, not only while the text field has focus"
checked: backend.vrKeyboardPersist
enabled: backend.vrKeyboard !== "never"
onToggled: backend.setVrKeyboardPersist(checked)
}
Controls.Label {
Kirigami.FormData.label: "Keyboards connected:"
text: kpage.keyboards.length ? kpage.keyboards.map(d => d.name).join(", ") : "none"
}
}
footer: Controls.Label {
padding: Kirigami.Units.largeSpacing
wrapMode: Text.Wrap
opacity: 0.7
text: "Frametop's keyboard opens in front of you, below your eyes, and closes with its Close key or a layout reset "
+ "(or when the text field loses focus, with Keep it open off). It steps aside while the Steam "
+ "menu or Steam's own keyboard is up. Type on it with a laser or the 3D mouse. It types into any "
+ "app, but only Qt, GTK and Firefox apps say when a text field is selected; for the rest "
+ "(Chromium, Electron and X11 apps), map Open/close keyboard to a button on the Buttons or "
+ "Controllers page. Keyboards set to Ignore, and keyboards other programs make (like "
+ "frame-voice's), don't count as connected."
}
}
}
// ---------------------------------------------------------------- Pointer
Component {
id: pointerPage