mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 03:00:13 +02:00
VR keyboard: AltGr + the key left of Backspace = Delete
While AltGr is active, the key left of Backspace (German ´/`, empty on AltGr) becomes Delete, labelled like Steam's Delete key (Entf; Del when longer), sent by the helper; repeats while held like Backspace.
This commit is contained in:
1 parent
ba4354de9e
commit
a3b05b7474
5 files changed
+120
-8
No files matched your search
@@ -459,6 +459,10 @@ reboot or Steam restart needed.
|
||||
or AltGr.
|
||||
- AltGr + arrows: Home, End, Page Up, Page Down (hinted on the keys);
|
||||
Shift + arrows select text.
|
||||
- AltGr + the key left of Backspace (`´` on German, `=` on US): Delete,
|
||||
labelled like Steam's Delete key in its language (`Entf`; `Del` if that is
|
||||
longer); repeats while held. Layouts with an AltGr character on that key
|
||||
get none.
|
||||
- Ctrl/Alt chords and Esc are sent with `xdotool key` on `:0` (focus follows
|
||||
the VR-selected window); a toggled Ctrl/Alt is held down while the
|
||||
keyboard is open (e.g. Ctrl+scroll).
|
||||
|
||||
@@ -60,7 +60,9 @@
|
||||
"keyboardComponent": {
|
||||
"checkOnly": true,
|
||||
"module": { "includes": ["TypeKeyInternal(", "toggleStates", "standardLayout"] },
|
||||
"expects": ["\"data-key\"", "\"data-key-row\"", "HandleVirtualKeyDown"]
|
||||
"expects": ["\"data-key\"", "\"data-key-row\"", "HandleVirtualKeyDown", "RenderStandardKeyboard(",
|
||||
"rgLayout:", "s_longPressThreshold", "s_longPressRepeatThreshold", "m_mapTouched",
|
||||
"CancelLongPressTimer(){", "DismissLongPress(){", "ClearHoldTarget(){", "\"Backspace\""]
|
||||
},
|
||||
"popupManager": {
|
||||
"checkOnly": true,
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
# - patch.js (injected over CEF DevTools, 127.0.0.1:8080) adds a bottom row
|
||||
# Esc Ctrl Alt [space] AltGr ← ↑ ↓ → (AltGr+arrows = Pos1/PgUp/PgDn/End) and
|
||||
# hands chords, Shift+arrows and characters Steam would turn into "1"
|
||||
# (non-ASCII, AltGr/dead keys) to the helper.
|
||||
# (non-ASCII, AltGr/dead keys) to the helper. AltGr + the key left of
|
||||
# Backspace = Delete.
|
||||
# - helper.mjs keeps it injected, sends those keys with xdotool on :0 (an
|
||||
# allowlist: no ASCII text, no Enter) and unpatches on stop.
|
||||
# Tested with Steam client 1790377368 (UI build 11041156).
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Extends Steam's VR keyboard for gamescope app windows:
|
||||
// - bottom row: Esc Ctrl Alt [space] AltGr ← ↑ ↓ → Close, stable with Shift
|
||||
// and AltGr; AltGr + arrows = Pos1/PgUp/PgDn/End
|
||||
// - AltGr + the key left of Backspace (German ´/`, empty on AltGr) = Delete
|
||||
// (Steam's "#Key_Delete" label, e.g. Entf), repeating while held
|
||||
// - Ctrl/Alt chords, Esc, the AltGr arrow keys and Shift+arrows are pressed
|
||||
// for real (Steam can't: in VR SteamClient.Input.ControllerKeyboardSetKeyState
|
||||
// throws "Unknown method")
|
||||
@@ -19,7 +21,7 @@
|
||||
// mkPatch patch (see lib/default.nix); no options. On a signature mismatch
|
||||
// it returns an error and changes nothing. Idempotent.
|
||||
((find, sigs) => {
|
||||
const VERSION = 10;
|
||||
const VERSION = 11;
|
||||
const send = (msg) => window.__vrkbdKey && window.__vrkbdKey(msg);
|
||||
let mods;
|
||||
try {
|
||||
@@ -33,6 +35,7 @@
|
||||
// For unpatch.js, which runs without the finder library.
|
||||
window.__vrkbdRefs = { Manager, currentLayout: Layouts.currentLayout };
|
||||
let changed = false;
|
||||
const active = (v) => (v & 7) !== 0; // toggle state bits: 1 one-shot, 2 locked, 4 held
|
||||
|
||||
// Replace obj[name] with make(original), once per VERSION.
|
||||
const wrap = (obj, name, make) => {
|
||||
@@ -70,12 +73,39 @@
|
||||
out.splice(out.length - 1, 0, ...ARROWS); // before Close/Done (last entry)
|
||||
return out;
|
||||
};
|
||||
// ---- AltGr: Delete on the key left of Backspace --------------------------------
|
||||
// rgLayout is called while the keyboard component renders
|
||||
// (RenderStandardKeyboard, wrapped below to publish the instance as
|
||||
// window.__vrkbdRendering), so the rows follow its AltGr state. While AltGr
|
||||
// is active, the key left of Backspace becomes Delete if it has no AltGr
|
||||
// character (German ´/`, US =/+; Steam would draw it empty). Also while a
|
||||
// Delete hold repeats: its first Delete releases a one-shot AltGr, like any
|
||||
// key, but the held key stays until release.
|
||||
const DEL = 'VKX_Delete';
|
||||
const delLabel = () => { // Steam's own: Entf, Suppr, Canc, Del; English "Delete" -> Del
|
||||
let l = null;
|
||||
try { l = window.LocalizationManager?.LocalizeString?.('#Key_Delete'); } catch { /* not loaded */ }
|
||||
return typeof l === 'string' && l && l.length <= 5 ? l : 'Del';
|
||||
};
|
||||
const freeOnAltGr = (x) => (Array.isArray(x) ? !x[2] : (typeof x === 'string' ? x : x?.key)?.length === 1);
|
||||
const withDelete = (rows) => {
|
||||
const kb = window.__vrkbdRendering;
|
||||
if (!kb || !(active(kb.state?.toggleStates?.AltGr) || kb.__vrkbdDelHold)) return rows;
|
||||
let done = false;
|
||||
return rows.map((row) => {
|
||||
const i = done ? -1 : row.findIndex((x) => !Array.isArray(x) && x?.key === 'Backspace');
|
||||
if (i < 0) return row;
|
||||
done = true;
|
||||
if (i < 1 || !freeOnAltGr(row[i - 1])) return row;
|
||||
return [...row.slice(0, i - 1), { key: DEL, label: delLabel(), type: first(row[i - 1])?.type }, ...row.slice(i)];
|
||||
});
|
||||
};
|
||||
window.__vrkbdLayouts ??= new Set(); // for unpatch.js, incl. layouts disabled since
|
||||
for (const l of [...(Layouts.enabledLayouts() || []), Layouts.currentLayout()]) {
|
||||
if (typeof l?.rgLayout !== 'function') continue;
|
||||
window.__vrkbdLayouts.add(l);
|
||||
wrap(l, 'rgLayout', (orig) => (opts) => {
|
||||
const rows = orig(opts);
|
||||
const rows = withDelete(orig(opts));
|
||||
return [...rows.slice(0, -1), bottomRow(rows[rows.length - 1])];
|
||||
});
|
||||
}
|
||||
@@ -137,6 +167,14 @@
|
||||
while (!Object.prototype.hasOwnProperty.call(proto, 'TypeKeyInternal')) proto = Object.getPrototypeOf(proto);
|
||||
window.__vrkbdInst = inst; // for the hold timer and unpatch.js
|
||||
window.__vrkbdProto = proto;
|
||||
const canDelete = typeof proto.RenderStandardKeyboard === 'function';
|
||||
if (canDelete) {
|
||||
wrap(proto, 'RenderStandardKeyboard', (orig) => function (...a) {
|
||||
const prev = window.__vrkbdRendering;
|
||||
window.__vrkbdRendering = this;
|
||||
try { return orig.apply(this, a); } finally { window.__vrkbdRendering = prev; }
|
||||
});
|
||||
}
|
||||
|
||||
// Stylesheet, re-applied on every inject (the popup can be recreated). The
|
||||
// space bar is the only flexible key, so it absorbs the width changes.
|
||||
@@ -154,11 +192,10 @@
|
||||
// Arrow keys: smaller (still centered) icon and a small hint at the bottom edge.
|
||||
'[data-key^="Arrow"] span ~ span { font-size: 7px !important; top: auto !important; bottom: 3px !important; line-height: 1 !important; }',
|
||||
'[data-key^="Arrow"] span:first-child svg { height: 18px !important; }',
|
||||
`${sel(ALTGR_ARROWS.map((x) => x.key), ' span')} { font-size: 13px !important; }`,
|
||||
`${sel([...ALTGR_ARROWS.map((x) => x.key), DEL], ' span')} { font-size: 13px !important; }`,
|
||||
].join('\n');
|
||||
|
||||
// ---- key handling ----------------------------------------------------------
|
||||
const active = (v) => (v & 7) !== 0; // toggle state bits: 1 one-shot, 2 locked
|
||||
const release = (v) => { const z = v & 3; return (z === 1 ? 0 : z) | (v & 4); };
|
||||
const TOGGLES = ['Shift', 'CapsLock', 'Control', 'Alt', 'AltGr'];
|
||||
const SYMS = {
|
||||
@@ -187,6 +224,70 @@
|
||||
Alt: release(s.toggleStates.Alt), AltGr: release(s.toggleStates.AltGr) } }));
|
||||
});
|
||||
|
||||
// ---- Delete repeat -----------------------------------------------------------
|
||||
// Steam repeats only data-key "Backspace" on a long press (and types other
|
||||
// keys once at release). Like it: after s_longPressThreshold of holding
|
||||
// Delete, take the press over (Steam's own touch-end cleanup: no key at
|
||||
// release, no long press) and type Delete every s_longPressRepeatThreshold
|
||||
// until release. Passive capture listeners, one set per keyboard window.
|
||||
const win = kbPopup.window;
|
||||
if (canDelete && win.__vrkbdDelRepeat !== VERSION) {
|
||||
win.__vrkbdDelDetach?.();
|
||||
let hold = null; // { inst, el, target, timer, taken }
|
||||
const stop = () => {
|
||||
if (!hold) return;
|
||||
const h = hold;
|
||||
hold = null;
|
||||
clearTimeout(h.timer);
|
||||
if (h.inst.__vrkbdDelHold) { h.inst.__vrkbdDelHold = false; h.inst.forceUpdate(); }
|
||||
};
|
||||
const start = (target, el) => {
|
||||
const kb = find.findFiberUp(el, (f) => typeof f.stateNode?.TypeKeyInternal === 'function', 200)?.stateNode;
|
||||
if (!kb) return;
|
||||
const C = kb.constructor;
|
||||
const delay = C.s_longPressThreshold ?? 450, repeat = C.s_longPressRepeatThreshold ?? 200;
|
||||
const h = hold = { inst: kb, el, target, timer: 0, taken: false };
|
||||
const tick = (ms) => {
|
||||
h.timer = setTimeout(() => {
|
||||
if (hold !== h) return;
|
||||
if (!el.isConnected || el.getAttribute('data-key') !== DEL || !Status.VRKeyboardStatus?.bIsOpen) { stop(); return; }
|
||||
if (!h.taken) {
|
||||
h.taken = true;
|
||||
kb.__vrkbdDelHold = true; // keeps the key after a one-shot AltGr is released
|
||||
try {
|
||||
for (const t of [...kb.m_mapTouched]) if (el.contains(t)) kb.m_mapTouched.delete(t);
|
||||
kb.CancelLongPressTimer(); kb.DismissLongPress(); kb.ClearHoldTarget();
|
||||
} catch { /* Steam changed: at worst one more Delete at release */ }
|
||||
}
|
||||
kb.TypeKeyInternal({ strKey: DEL });
|
||||
tick(repeat);
|
||||
}, ms);
|
||||
};
|
||||
tick(delay - 10); // just before Steam's own long-press timer
|
||||
};
|
||||
const keyEl = (t) => t?.closest?.('[data-key]');
|
||||
const onDown = (e) => {
|
||||
stop();
|
||||
const t = e.type === 'touchstart' ? (e.touches.length === 1 ? e.changedTouches[0]?.target : null) : (e.button === 0 ? e.target : null);
|
||||
const el = keyEl(t);
|
||||
if (el?.getAttribute('data-key') === DEL) start(t, el);
|
||||
};
|
||||
const onUp = (e) => {
|
||||
if (!hold) return;
|
||||
if (e.type.startsWith('touch') && ![...e.changedTouches].some((t) => t.target === hold.target)) return;
|
||||
stop();
|
||||
};
|
||||
const L = { capture: true, passive: true };
|
||||
const types = [['touchstart', onDown], ['mousedown', onDown], ['touchend', onUp], ['touchcancel', onUp], ['mouseup', onUp]];
|
||||
for (const [type, f] of types) win.addEventListener(type, f, L);
|
||||
win.__vrkbdDelDetach = () => {
|
||||
stop();
|
||||
for (const [type, f] of types) win.removeEventListener(type, f, L);
|
||||
delete win.__vrkbdDelDetach; delete win.__vrkbdDelRepeat;
|
||||
};
|
||||
win.__vrkbdDelRepeat = VERSION; // __vrkbdDelDetach: for unpatch.js
|
||||
}
|
||||
|
||||
// ---- held modifiers ---------------------------------------------------------
|
||||
// Keep the real Ctrl/Alt pressed while the toggle is active and the keyboard is
|
||||
// open (for Ctrl+scroll etc.). One timer per Steam UI instance.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Reverts patch.js in Steam's SharedJSContext (run by helper.mjs on stop).
|
||||
// Patched functions keep their original as __vrkbdOrig; objects Steam doesn't
|
||||
// expose are in window.__vrkbdRefs/__vrkbdLayouts/__vrkbdProto/__vrkbdInst.
|
||||
// expose are in window.__vrkbdRefs/__vrkbdLayouts/__vrkbdProto/__vrkbdInst;
|
||||
// the Delete-repeat listeners of a keyboard window in its __vrkbdDelDetach.
|
||||
// Safe when not patched.
|
||||
(() => {
|
||||
const refs = window.__vrkbdRefs;
|
||||
@@ -12,17 +13,20 @@
|
||||
for (const l of window.__vrkbdLayouts || []) unwrap(l, 'rgLayout');
|
||||
unwrap(SteamClient.Input, 'ControllerKeyboardSendText');
|
||||
unwrap(window.__vrkbdProto, 'TypeKeyInternal');
|
||||
unwrap(window.__vrkbdProto, 'RenderStandardKeyboard');
|
||||
unwrap(refs?.Manager, 'GetEnterKeyLabel');
|
||||
unwrap(refs?.Manager, 'HandleVirtualKeyDown');
|
||||
clearInterval(window.__vrkbdHoldTimer);
|
||||
|
||||
for (const p of g_PopupManager.GetPopups?.() || []) p.window?.__vrkbdDelDetach?.();
|
||||
const inst = window.__vrkbdInst;
|
||||
if (inst) {
|
||||
delete inst.__vrkbd;
|
||||
delete inst.__vrkbdDelHold;
|
||||
if (refs?.currentLayout) inst.setState({ standardLayout: refs.currentLayout() });
|
||||
inst.forceUpdate();
|
||||
}
|
||||
for (const p of g_PopupManager.GetPopups?.() || []) p.window?.document.getElementById('vrkbd-style')?.remove();
|
||||
for (const k of ['__vrkbdHoldTimer', '__vrkbdHeld', '__vrkbdInst', '__vrkbdProto', '__vrkbdLayouts', '__vrkbdRefs']) delete window[k];
|
||||
for (const k of ['__vrkbdHoldTimer', '__vrkbdHeld', '__vrkbdInst', '__vrkbdProto', '__vrkbdLayouts', '__vrkbdRefs', '__vrkbdRendering']) delete window[k];
|
||||
return 'unpatched';
|
||||
})()
|
||||
Reference in new issue
Block a user