mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-17 15:00:36 +02:00
SteamVR's dashboard starts a floating window's move only for a press from the head or a hand: UndockedOverlay.startFloatingWindowMove reads the pressing device from the mouse event's CapsLock/NumLock bits and logs "mouse event carried no input path" for anything else. So the 3D mouse as a treadmill clicked and resized SteamVR's windows but couldn't move them, the regression since 2026-10-09. pointer/dashpatch wraps the grab bar's handler in the dashboard page: a press with no device runs it as the right hand, with that pose answered by /user/treadmill, and the move then follows /user/treadmill. frametop-dashpatch.service puts it in over the dashboard's debugger, again whenever SteamVR restarts. The installer turns the debugger on at 127.0.0.1:8089 (not 8087, which SteamOS forwards to the network on 8088); the uninstaller turns it off if Frametop turned it on. Headset test 2026-10-13: window moves with the treadmill role work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
3.8 KiB
JavaScript
77 lines
3.8 KiB
JavaScript
// Frametop: SteamVR's floating windows move for the 3D mouse's treadmill device (pointer/dashpatch).
|
|
// The dashboard's UndockedOverlay.startFloatingWindowMove reads the pressing device from the mouse
|
|
// event's CapsLock/NumLock modifier bits (head, left hand, right hand) and gives up when there are
|
|
// none, as for a treadmill. This wraps the grab bar's onMouseDown: a press with no source runs the
|
|
// original as the right hand, with that one pose query answered by /user/treadmill, and the move
|
|
// then follows /user/treadmill.
|
|
(() => {
|
|
const VERSION = 1;
|
|
if (window.__ftTreadmillMove && window.__ftTreadmillMove.version >= VERSION) return 'already ' + window.__ftTreadmillMove.version;
|
|
let req = null;
|
|
self.webpackChunkvrwebui.push([[Symbol('frametop-treadmill-move')], {}, r => { req = r; }]);
|
|
if (!req) return 'no webpack';
|
|
// React: module 6540 in SteamVR 2.18.2's dashboard; else the module that defines React's element
|
|
// symbol (loaded already, so requiring it runs nothing new)
|
|
const isReact = m => m && typeof m.createElement === 'function' && typeof m.Component === 'function' && /^18\./.test(m.version || '');
|
|
let React = req.m['6540'] ? req('6540') : null;
|
|
if (!isReact(React)) {
|
|
React = null;
|
|
for (const id of Object.keys(req.m)) {
|
|
const src = req.m[id].toString();
|
|
if (src.length < 40000 && src.includes('Symbol.for("react.element")') && src.includes('createElement')) {
|
|
const m = req(id);
|
|
if (isReact(m)) { React = m; break; }
|
|
}
|
|
}
|
|
}
|
|
if (!React) return 'no React';
|
|
const internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
|
const TREADMILL = '/user/treadmill', RIGHT = '/user/hand/right';
|
|
const state = window.__ftTreadmillMove = {version: VERSION, moves: 0, last: ''};
|
|
const wrapped = new WeakMap(); // the original bound handler -> ours
|
|
|
|
const wrap = (orig, inst) => function (e) {
|
|
let src = 0;
|
|
try { src = (e.getModifierState('CapsLock') ? 1 : 0) | (e.getModifierState('NumLock') ? 2 : 0); } catch (err) { src = -1; }
|
|
if (src !== 0 || !inst) return orig(e);
|
|
const [pose] = VRHTML.GetPose(TREADMILL, 0);
|
|
if (!pose || !pose.bPoseIsValid) { state.last = 'treadmill pose invalid'; return orig(e); }
|
|
const fake = new Proxy(e, {get(t, k) {
|
|
if (k === 'getModifierState') return m => m === 'CapsLock' || m === 'NumLock';
|
|
const v = t[k];
|
|
return typeof v === 'function' ? v.bind(t) : v;
|
|
}});
|
|
const getPose = VRHTML.GetPose;
|
|
let first = true; // only the moving device's query (the first one); a hand-docked window's own stays
|
|
VRHTML.GetPose = function (path, origin) {
|
|
if (first && path === RIGHT) { first = false; path = TREADMILL; }
|
|
return getPose.call(this, path, origin);
|
|
};
|
|
let r;
|
|
try { r = orig(fake); } finally { VRHTML.GetPose = getPose; }
|
|
if (inst.m_sMoveDevicePath === RIGHT) {
|
|
inst.m_sMoveDevicePath = TREADMILL;
|
|
inst.setState({sParentDevice: TREADMILL});
|
|
state.moves++;
|
|
state.last = 'moved by the treadmill';
|
|
}
|
|
return r;
|
|
};
|
|
|
|
const createElement = React.createElement;
|
|
React.createElement = function (type, props) {
|
|
if (props && typeof props.onMouseDown === 'function' && props.explicitSGID !== undefined &&
|
|
props.onMouseDown.name === 'bound startFloatingWindowMove') {
|
|
const owner = internals && internals.ReactCurrentOwner && internals.ReactCurrentOwner.current;
|
|
const inst = owner && owner.stateNode;
|
|
let ours = wrapped.get(props.onMouseDown);
|
|
if (!ours) { ours = wrap(props.onMouseDown, inst); wrapped.set(props.onMouseDown, ours); }
|
|
const args = Array.prototype.slice.call(arguments);
|
|
args[1] = Object.assign({}, props, {onMouseDown: ours});
|
|
return createElement.apply(this, args);
|
|
}
|
|
return createElement.apply(this, arguments);
|
|
};
|
|
return 'installed ' + VERSION;
|
|
})()
|