mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 04:04:16 +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.
|
or AltGr.
|
||||||
- AltGr + arrows: Home, End, Page Up, Page Down (hinted on the keys);
|
- AltGr + arrows: Home, End, Page Up, Page Down (hinted on the keys);
|
||||||
Shift + arrows select text.
|
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
|
- 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
|
the VR-selected window); a toggled Ctrl/Alt is held down while the
|
||||||
keyboard is open (e.g. Ctrl+scroll).
|
keyboard is open (e.g. Ctrl+scroll).
|
||||||
|
|||||||
@@ -60,7 +60,9 @@
|
|||||||
"keyboardComponent": {
|
"keyboardComponent": {
|
||||||
"checkOnly": true,
|
"checkOnly": true,
|
||||||
"module": { "includes": ["TypeKeyInternal(", "toggleStates", "standardLayout"] },
|
"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": {
|
"popupManager": {
|
||||||
"checkOnly": true,
|
"checkOnly": true,
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
# - patch.js (injected over CEF DevTools, 127.0.0.1:8080) adds a bottom row
|
# - 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
|
# Esc Ctrl Alt [space] AltGr ← ↑ ↓ → (AltGr+arrows = Pos1/PgUp/PgDn/End) and
|
||||||
# hands chords, Shift+arrows and characters Steam would turn into "1"
|
# 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
|
# - helper.mjs keeps it injected, sends those keys with xdotool on :0 (an
|
||||||
# allowlist: no ASCII text, no Enter) and unpatches on stop.
|
# allowlist: no ASCII text, no Enter) and unpatches on stop.
|
||||||
# Tested with Steam client 1790377368 (UI build 11041156).
|
# Tested with Steam client 1790377368 (UI build 11041156).
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// Extends Steam's VR keyboard for gamescope app windows:
|
// Extends Steam's VR keyboard for gamescope app windows:
|
||||||
// - bottom row: Esc Ctrl Alt [space] AltGr ← ↑ ↓ → Close, stable with Shift
|
// - bottom row: Esc Ctrl Alt [space] AltGr ← ↑ ↓ → Close, stable with Shift
|
||||||
// and AltGr; AltGr + arrows = Pos1/PgUp/PgDn/End
|
// 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
|
// - Ctrl/Alt chords, Esc, the AltGr arrow keys and Shift+arrows are pressed
|
||||||
// for real (Steam can't: in VR SteamClient.Input.ControllerKeyboardSetKeyState
|
// for real (Steam can't: in VR SteamClient.Input.ControllerKeyboardSetKeyState
|
||||||
// throws "Unknown method")
|
// throws "Unknown method")
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
// mkPatch patch (see lib/default.nix); no options. On a signature mismatch
|
// mkPatch patch (see lib/default.nix); no options. On a signature mismatch
|
||||||
// it returns an error and changes nothing. Idempotent.
|
// it returns an error and changes nothing. Idempotent.
|
||||||
((find, sigs) => {
|
((find, sigs) => {
|
||||||
const VERSION = 10;
|
const VERSION = 11;
|
||||||
const send = (msg) => window.__vrkbdKey && window.__vrkbdKey(msg);
|
const send = (msg) => window.__vrkbdKey && window.__vrkbdKey(msg);
|
||||||
let mods;
|
let mods;
|
||||||
try {
|
try {
|
||||||
@@ -33,6 +35,7 @@
|
|||||||
// For unpatch.js, which runs without the finder library.
|
// For unpatch.js, which runs without the finder library.
|
||||||
window.__vrkbdRefs = { Manager, currentLayout: Layouts.currentLayout };
|
window.__vrkbdRefs = { Manager, currentLayout: Layouts.currentLayout };
|
||||||
let changed = false;
|
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.
|
// Replace obj[name] with make(original), once per VERSION.
|
||||||
const wrap = (obj, name, make) => {
|
const wrap = (obj, name, make) => {
|
||||||
@@ -70,12 +73,39 @@
|
|||||||
out.splice(out.length - 1, 0, ...ARROWS); // before Close/Done (last entry)
|
out.splice(out.length - 1, 0, ...ARROWS); // before Close/Done (last entry)
|
||||||
return out;
|
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
|
window.__vrkbdLayouts ??= new Set(); // for unpatch.js, incl. layouts disabled since
|
||||||
for (const l of [...(Layouts.enabledLayouts() || []), Layouts.currentLayout()]) {
|
for (const l of [...(Layouts.enabledLayouts() || []), Layouts.currentLayout()]) {
|
||||||
if (typeof l?.rgLayout !== 'function') continue;
|
if (typeof l?.rgLayout !== 'function') continue;
|
||||||
window.__vrkbdLayouts.add(l);
|
window.__vrkbdLayouts.add(l);
|
||||||
wrap(l, 'rgLayout', (orig) => (opts) => {
|
wrap(l, 'rgLayout', (orig) => (opts) => {
|
||||||
const rows = orig(opts);
|
const rows = withDelete(orig(opts));
|
||||||
return [...rows.slice(0, -1), bottomRow(rows[rows.length - 1])];
|
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);
|
while (!Object.prototype.hasOwnProperty.call(proto, 'TypeKeyInternal')) proto = Object.getPrototypeOf(proto);
|
||||||
window.__vrkbdInst = inst; // for the hold timer and unpatch.js
|
window.__vrkbdInst = inst; // for the hold timer and unpatch.js
|
||||||
window.__vrkbdProto = proto;
|
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
|
// 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.
|
// 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.
|
// 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 ~ 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; }',
|
'[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');
|
].join('\n');
|
||||||
|
|
||||||
// ---- key handling ----------------------------------------------------------
|
// ---- 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 release = (v) => { const z = v & 3; return (z === 1 ? 0 : z) | (v & 4); };
|
||||||
const TOGGLES = ['Shift', 'CapsLock', 'Control', 'Alt', 'AltGr'];
|
const TOGGLES = ['Shift', 'CapsLock', 'Control', 'Alt', 'AltGr'];
|
||||||
const SYMS = {
|
const SYMS = {
|
||||||
@@ -187,6 +224,70 @@
|
|||||||
Alt: release(s.toggleStates.Alt), AltGr: release(s.toggleStates.AltGr) } }));
|
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 ---------------------------------------------------------
|
// ---- held modifiers ---------------------------------------------------------
|
||||||
// Keep the real Ctrl/Alt pressed while the toggle is active and the keyboard is
|
// 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.
|
// 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).
|
// Reverts patch.js in Steam's SharedJSContext (run by helper.mjs on stop).
|
||||||
// Patched functions keep their original as __vrkbdOrig; objects Steam doesn't
|
// 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.
|
// Safe when not patched.
|
||||||
(() => {
|
(() => {
|
||||||
const refs = window.__vrkbdRefs;
|
const refs = window.__vrkbdRefs;
|
||||||
@@ -12,17 +13,20 @@
|
|||||||
for (const l of window.__vrkbdLayouts || []) unwrap(l, 'rgLayout');
|
for (const l of window.__vrkbdLayouts || []) unwrap(l, 'rgLayout');
|
||||||
unwrap(SteamClient.Input, 'ControllerKeyboardSendText');
|
unwrap(SteamClient.Input, 'ControllerKeyboardSendText');
|
||||||
unwrap(window.__vrkbdProto, 'TypeKeyInternal');
|
unwrap(window.__vrkbdProto, 'TypeKeyInternal');
|
||||||
|
unwrap(window.__vrkbdProto, 'RenderStandardKeyboard');
|
||||||
unwrap(refs?.Manager, 'GetEnterKeyLabel');
|
unwrap(refs?.Manager, 'GetEnterKeyLabel');
|
||||||
unwrap(refs?.Manager, 'HandleVirtualKeyDown');
|
unwrap(refs?.Manager, 'HandleVirtualKeyDown');
|
||||||
clearInterval(window.__vrkbdHoldTimer);
|
clearInterval(window.__vrkbdHoldTimer);
|
||||||
|
|
||||||
|
for (const p of g_PopupManager.GetPopups?.() || []) p.window?.__vrkbdDelDetach?.();
|
||||||
const inst = window.__vrkbdInst;
|
const inst = window.__vrkbdInst;
|
||||||
if (inst) {
|
if (inst) {
|
||||||
delete inst.__vrkbd;
|
delete inst.__vrkbd;
|
||||||
|
delete inst.__vrkbdDelHold;
|
||||||
if (refs?.currentLayout) inst.setState({ standardLayout: refs.currentLayout() });
|
if (refs?.currentLayout) inst.setState({ standardLayout: refs.currentLayout() });
|
||||||
inst.forceUpdate();
|
inst.forceUpdate();
|
||||||
}
|
}
|
||||||
for (const p of g_PopupManager.GetPopups?.() || []) p.window?.document.getElementById('vrkbd-style')?.remove();
|
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';
|
return 'unpatched';
|
||||||
})()
|
})()
|
||||||
Reference in new issue
Block a user