mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 05:00:24 +02:00
Add frame-controls: move window controls between bar and ⋯ menu by long press
steamFrame.dashboard.frameControls: a long press (longPressMs, default
1.5 s; a ring shows the progress from half of it) on a control under a
dashboard window or on a More Options (three-dot) menu row opens a popup
with "Show in bar", which moves that control between the window's bottom
bar and the menu, for all windows. Placement is keyed by the control's
action icon, kept in the page and its localStorage (survives SteamVR
restarts); inBar / inMenu set defaults. floatInTheater gives theater
windows the Float control back.
The popup is a scene-graph panel of its own, attached to the pressed
button's (or the menu's) anchor like SteamVR's tooltips and menu, so the
bar and menu panels don't change; the menu's focus auto-close is held back
while its popup is open. Coordinates with window-curvature through its
sfui-curv-* contract. Lookups by signature ("frame-controls" in
lib/signatures.json, check-only anchors for the rest).
This commit is contained in:
1 parent
9993cbd4ab
commit
990ee78fe7
7 files changed
+918
-2
No files matched your search
@@ -144,6 +144,7 @@ the two files below).
|
|||||||
windowDistance.world.max = 10.0;
|
windowDistance.world.max = 10.0;
|
||||||
steamCloseButton.enable = true;
|
steamCloseButton.enable = true;
|
||||||
windowCurvature.enable = true;
|
windowCurvature.enable = true;
|
||||||
|
frameControls.enable = true;
|
||||||
};
|
};
|
||||||
firefox.enable = true;
|
firefox.enable = true;
|
||||||
# Only relevant with Steam Developer Mode on (see hidden-apps below).
|
# Only relevant with Steam Developer Mode on (see hidden-apps below).
|
||||||
@@ -168,7 +169,7 @@ home-manager switch --flake .#steamos
|
|||||||
```
|
```
|
||||||
|
|
||||||
Individual modules are available as
|
Individual modules are available as
|
||||||
`homeManagerModules.{session,portal,keyboard-layout,steam-keyboard-patch,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,dashboard-windows,steam-close-button,window-curvature,clipboard-sync,firefox}`;
|
`homeManagerModules.{session,portal,keyboard-layout,steam-keyboard-patch,hidden-apps,steam-ui-patches,launcher-menu,steamvr-debugger,dashboard-windows,steam-close-button,window-curvature,frame-controls,clipboard-sync,firefox}`;
|
||||||
`default` imports all of them.
|
`default` imports all of them.
|
||||||
|
|
||||||
**Steam Developer Mode** (a Steam setting, not managed here) makes the "+"
|
**Steam Developer Mode** (a Steam setting, not managed here) makes the "+"
|
||||||
@@ -214,6 +215,11 @@ menu list every desktop entry, including terminals such as Konsole.
|
|||||||
| `steamFrame.dashboard.windowCurvature.barDragPixelsPerUnit` | number (px) | `30` | Drag distance per 1.0 of curvature on the bottom-bar button. |
|
| `steamFrame.dashboard.windowCurvature.barDragPixelsPerUnit` | number (px) | `30` | Drag distance per 1.0 of curvature on the bottom-bar button. |
|
||||||
| `steamFrame.dashboard.windowCurvature.barDragRoom` | unsigned int (px) | `160` | Transparent room added above and below a window's bottom bar while its curvature button is dragged, so the laser stays on the bar. `0`: none. |
|
| `steamFrame.dashboard.windowCurvature.barDragRoom` | unsigned int (px) | `160` | Transparent room added above and below a window's bottom bar while its curvature button is dragged, so the laser stays on the bar. `0`: none. |
|
||||||
| `steamFrame.dashboard.windowCurvature.haptics` | bool | `true` | Controller haptics while dragging: snap at snap points, edge at 0 and `max`, a tick per other step. |
|
| `steamFrame.dashboard.windowCurvature.haptics` | bool | `true` | Controller haptics while dragging: snap at snap points, edge at 0 and `max`, a tick per other step. |
|
||||||
|
| `steamFrame.dashboard.frameControls.enable` | bool | `false` | Move the control icons under dashboard windows between the bottom bar and the More Options (three-dot) menu: long press an icon or menu row, then "Show in bar". Per control type, for all windows, kept across SteamVR restarts. See [Window control bar](#window-control-bar-dashboardframecontrols). |
|
||||||
|
| `steamFrame.dashboard.frameControls.longPressMs` | int, 300-10000 (ms) | `1500` | Hold time of the long press. A ring shows the progress from half of it (at most after 1 s). |
|
||||||
|
| `steamFrame.dashboard.frameControls.inBar` | list of control names | `[ ]` | Controls that start in the bar: `keyboard`, `float`, `dashboard`, `theater`, `dockLeft`, `dockRight`, `close`, `curvature`, or `"icon:<n>"`. A popup choice wins until the entry changes. |
|
||||||
|
| `steamFrame.dashboard.frameControls.inMenu` | list of control names | `[ ]` | Controls that start in the three-dot menu (same names). |
|
||||||
|
| `steamFrame.dashboard.frameControls.floatInTheater` | bool | `false` | Give theater windows the "Float" control (stock only shows it for dashboard-docked windows). |
|
||||||
| `steamFrame.steamvrDebugger.enable` | bool | `true` if a UI patch uses port 8087, else `false` | SteamVR dashboard DevTools on `127.0.0.1:8087` (`VRWebHelper/DebuggerEnabled`), needed by dashboard patches. See [SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable). |
|
| `steamFrame.steamvrDebugger.enable` | bool | `true` if a UI patch uses port 8087, else `false` | SteamVR dashboard DevTools on `127.0.0.1:8087` (`VRWebHelper/DebuggerEnabled`), needed by dashboard patches. See [SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable). |
|
||||||
| `steamFrame.hiddenApps` | list of str | `[ ]` | Desktop entry ids (without `.desktop`) to hide from the "+" and KDE menus. |
|
| `steamFrame.hiddenApps` | list of str | `[ ]` | Desktop entry ids (without `.desktop`) to hide from the "+" and KDE menus. |
|
||||||
| `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. |
|
| `steamFrame.clipboardSync.enable` | bool | `true` | Clipboard bridge between the Steam session and the nested desktop. |
|
||||||
@@ -835,6 +841,83 @@ offline (`window-curvature` in `modules/lib/signatures.json`,
|
|||||||
`scripts/check-signatures.mjs`); if a lookup fails, the patch reports it and
|
`scripts/check-signatures.mjs`); if a lookup fails, the patch reports it and
|
||||||
leaves the dashboard stock. Tested with SteamVR build 11008059.
|
leaves the dashboard stock. Tested with SteamVR build 11008059.
|
||||||
|
|
||||||
|
### Window control bar (`dashboard.frameControls.*`)
|
||||||
|
|
||||||
|
**Problem:** the controls under a SteamVR dashboard window are fixed: some
|
||||||
|
sit in the window's bottom bar (keyboard, float or back to the dashboard,
|
||||||
|
theater, close), others only in its More Options (three-dot) menu
|
||||||
|
(curvature, dock to a controller), and in the theater the "Float" button is
|
||||||
|
gone.
|
||||||
|
|
||||||
|
**Fix:** a UI patch (see above) of SteamVR's dashboard page (`vrwebhelper`,
|
||||||
|
`127.0.0.1:8087`, title `systemui`; it turns on the
|
||||||
|
[SteamVR debugger](#steamvr-debugger-steamvrdebuggerenable)):
|
||||||
|
|
||||||
|
- **long press** (`longPressMs`) a bar icon or a three-dot menu row: after
|
||||||
|
half the time (at most after 1 s) a ring around the icon shows the
|
||||||
|
progress, so ordinary clicks show nothing; moving the laser or leaving
|
||||||
|
the icon doesn't cancel it, releasing early does. Then a small popup
|
||||||
|
opens above the icon (or above the menu) with **Show in bar**; toggling
|
||||||
|
it moves that control between the bar and the menu, **for all windows**.
|
||||||
|
The release after the long press doesn't trigger the control. The
|
||||||
|
popup closes on a press elsewhere, Escape, 1 s after the laser left it,
|
||||||
|
and after toggling.
|
||||||
|
- a control moved into the bar joins the group left of the three-dot
|
||||||
|
button; the three-dot button itself can't be moved and, as in SteamVR,
|
||||||
|
shows only while its menu has entries.
|
||||||
|
- `inBar` / `inMenu` set where controls start; a choice made in the popup
|
||||||
|
wins until that control's entry changes. `floatInTheater` gives theater
|
||||||
|
windows the "Float" button back (same call as the stock one: the window
|
||||||
|
floats in the world).
|
||||||
|
|
||||||
|
```nix
|
||||||
|
steamFrame.dashboard.frameControls = {
|
||||||
|
enable = true;
|
||||||
|
# longPressMs = 1500;
|
||||||
|
# inBar = [ "curvature" ]; inMenu = [ "theater" ];
|
||||||
|
# floatInTheater = true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
How it works: each window hands its control lists to
|
||||||
|
`Frame.prototype.SetControlsItems(bottom, tabHover, additional)`; the patch
|
||||||
|
wraps it and stores a re-partitioned copy (stock order kept, gaps merged).
|
||||||
|
Controls are identified by their action icon (`icon:<n>`), the same in every
|
||||||
|
window and language. Placements are kept in the `systemui` page
|
||||||
|
(`window.__sfuiFrameControlsState`) and in its `localStorage`, so they
|
||||||
|
survive restarts of the patch service and of SteamVR. The popup is a
|
||||||
|
scene-graph panel of its own, attached to the pressed button's anchor like
|
||||||
|
SteamVR's tooltips and three-dot menu, so the bar and the menu don't change
|
||||||
|
size or move. SteamVR closes the three-dot menu as soon as the laser's focus
|
||||||
|
leaves the bar and the menu; while its popup is open that close is held
|
||||||
|
back and applied (unless the laser is on the bar or menu again) when the
|
||||||
|
popup closes. `window.__sfuiFrameControls.dump()` lists every window's
|
||||||
|
controls, `.placement()` the current choices, `.reset()` forgets them,
|
||||||
|
`.log` recent events.
|
||||||
|
|
||||||
|
With [Window curvature](#window-curvature-dashboardwindowcurvature), the
|
||||||
|
curvature control keeps its drag: a drag that starts before the ring shows
|
||||||
|
cancels the long press; once the ring shows, the long press takes the press
|
||||||
|
over (its `sfui-curv-*` contract), so a still hold on the curvature control
|
||||||
|
opens the popup.
|
||||||
|
|
||||||
|
**Limitations:**
|
||||||
|
|
||||||
|
- SteamVR delivers no right-click or thumbstick click to the dashboard, so
|
||||||
|
there is no context menu on a secondary button.
|
||||||
|
- Only the laser: with controller (gamepad) navigation the controls are
|
||||||
|
stock.
|
||||||
|
- Placements are per control type, not per window.
|
||||||
|
|
||||||
|
**Caveats:** depends on SteamVR UI internals: MobX, the `Frame` class, the
|
||||||
|
action store, the dock-location enum, the input-focus store and the
|
||||||
|
localization function are found by signature, and the rest (the control
|
||||||
|
list layout, the icon numbers, the frame-controls panel and anchor ids, the
|
||||||
|
scene-graph DOM walk and `SGApp`'s embedded-UV table, the menu's auto-close)
|
||||||
|
is checked offline (`frame-controls` in `modules/lib/signatures.json`,
|
||||||
|
`scripts/check-signatures.mjs`); if a lookup fails, the patch reports it and
|
||||||
|
leaves the dashboard stock. Tested with SteamVR build 11008059.
|
||||||
|
|
||||||
### SteamVR debugger (`steamvrDebugger.enable`)
|
### SteamVR debugger (`steamvrDebugger.enable`)
|
||||||
|
|
||||||
Patches of the SteamVR dashboard need its DevTools port, which SteamVR only
|
Patches of the SteamVR dashboard need its DevTools port, which SteamVR only
|
||||||
@@ -843,7 +926,8 @@ opens with the setting `VRWebHelper/DebuggerEnabled` (port
|
|||||||
(`mkDefault`) as soon as a patch in `steamFrame.uiPatches.patches` uses port
|
(`mkDefault`) as soon as a patch in `steamFrame.uiPatches.patches` uses port
|
||||||
8087 (e.g. [Dashboard windows](#dashboard-windows-dashboard),
|
8087 (e.g. [Dashboard windows](#dashboard-windows-dashboard),
|
||||||
[Steam close button](#steam-close-button-dashboardsteamclosebuttonenable),
|
[Steam close button](#steam-close-button-dashboardsteamclosebuttonenable),
|
||||||
[Window curvature](#window-curvature-dashboardwindowcurvature)).
|
[Window curvature](#window-curvature-dashboardwindowcurvature),
|
||||||
|
[Window control bar](#window-control-bar-dashboardframecontrols)).
|
||||||
|
|
||||||
SteamVR rewrites `~/.config/openvr/config/steamvr.vrsettings` while it runs
|
SteamVR rewrites `~/.config/openvr/config/steamvr.vrsettings` while it runs
|
||||||
and on exit, so the file can't be managed by Home Manager, and edits while it
|
and on exit, so the file can't be managed by Home Manager, and edits while it
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
dashboard-windows = ./modules/dashboard-windows.nix;
|
dashboard-windows = ./modules/dashboard-windows.nix;
|
||||||
steam-close-button = ./modules/steam-close-button.nix;
|
steam-close-button = ./modules/steam-close-button.nix;
|
||||||
window-curvature = ./modules/window-curvature.nix;
|
window-curvature = ./modules/window-curvature.nix;
|
||||||
|
frame-controls = ./modules/frame-controls.nix;
|
||||||
# Built with the consumer's pkgs; only the source comes from our input.
|
# Built with the consumer's pkgs; only the source comes from our input.
|
||||||
# The key lets the module system deduplicate it when it is imported both
|
# The key lets the module system deduplicate it when it is imported both
|
||||||
# directly and through `default`.
|
# directly and through `default`.
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Window control bar (steamFrame.dashboard.frameControls): move the control
|
||||||
|
# icons under SteamVR dashboard windows between the bottom bar and the More
|
||||||
|
# Options (three-dot) menu with a long press on the icon or menu row (a popup
|
||||||
|
# with "Show in bar"). Placement is per control type, for all windows, kept
|
||||||
|
# across SteamVR restarts; inBar / inMenu set defaults. Optionally gives
|
||||||
|
# theater windows the "Float" control back. Runtime patch of SteamVR's
|
||||||
|
# dashboard page (vrwebhelper "systemui", DevTools 127.0.0.1:8087; see
|
||||||
|
# frame-controls/patch.js for how it works), registered with
|
||||||
|
# steam-ui-patches.nix only when enabled; it turns on the SteamVR web helper
|
||||||
|
# debugger (steamvr-debugger.nix), which needs one SteamVR restart the first
|
||||||
|
# time. Depends on SteamVR UI internals, found by signature
|
||||||
|
# (lib/signatures.json, "frame-controls"; scripts/check-signatures.mjs checks
|
||||||
|
# them offline).
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.steamFrame.dashboard.frameControls;
|
||||||
|
inherit (lib) mkOption types;
|
||||||
|
inherit (import ./lib { inherit pkgs; }) mkPatch;
|
||||||
|
|
||||||
|
# Control names -> SteamVR action icon enums (the patch keys controls by icon).
|
||||||
|
names = [ "keyboard" "float" "dashboard" "theater" "dockLeft" "dockRight" "close" "curvature" ];
|
||||||
|
controlType = types.either (types.enum names) (types.strMatching "icon:[0-9]+");
|
||||||
|
controlList = description: mkOption {
|
||||||
|
type = types.listOf controlType;
|
||||||
|
default = [ ];
|
||||||
|
example = [ "curvature" ];
|
||||||
|
inherit description;
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
imports = [ ./steam-ui-patches.nix ./steamvr-debugger.nix ];
|
||||||
|
|
||||||
|
options.steamFrame.dashboard.frameControls = {
|
||||||
|
enable = lib.mkEnableOption ''
|
||||||
|
moving the control icons under SteamVR dashboard windows between the
|
||||||
|
window's bottom bar and its More Options (three-dot) menu: a long press
|
||||||
|
on a bar icon or a menu row opens a popup with "Show in bar"; the choice
|
||||||
|
applies to that control in all windows and is kept across SteamVR
|
||||||
|
restarts. Short presses work as usual. Runtime patch of the SteamVR
|
||||||
|
dashboard (steamFrame.uiPatches), applied immediately; turning it off
|
||||||
|
restores the stock controls (next switch)'';
|
||||||
|
longPressMs = mkOption {
|
||||||
|
type = types.ints.between 300 10000;
|
||||||
|
default = 1500;
|
||||||
|
description = ''
|
||||||
|
Hold time (ms) of the long press that opens the popup. A ring around
|
||||||
|
the icon shows the progress from half the time (at most after 1 s),
|
||||||
|
so ordinary clicks show nothing.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
inBar = controlList ''
|
||||||
|
Controls that start in the bar (by default: SteamVR's placement).
|
||||||
|
Names: ${lib.concatStringsSep ", " names}, or "icon:<n>" (the action's
|
||||||
|
icon number, see window.__sfuiFrameControls.dump() in the dashboard's
|
||||||
|
DevTools). A choice made in the popup wins until the control's entry
|
||||||
|
here changes.
|
||||||
|
'';
|
||||||
|
inMenu = controlList ''
|
||||||
|
Controls that start in the three-dot menu (same names as inBar).
|
||||||
|
'';
|
||||||
|
floatInTheater = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Give windows in the theater the "Float" control (SteamVR only shows it
|
||||||
|
for windows docked in the dashboard): floats the window in the world,
|
||||||
|
like the stock button. Follows Float's placement.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [ {
|
||||||
|
assertion = lib.intersectLists cfg.inBar cfg.inMenu == [ ];
|
||||||
|
message = "steamFrame.dashboard.frameControls: ${builtins.toJSON (lib.intersectLists cfg.inBar cfg.inMenu)} in both inBar and inMenu.";
|
||||||
|
} ];
|
||||||
|
|
||||||
|
steamFrame.uiPatches.patches = [ {
|
||||||
|
name = "frame-controls";
|
||||||
|
endpoint = "http://127.0.0.1:8087";
|
||||||
|
target.title = "systemui";
|
||||||
|
patch = mkPatch {
|
||||||
|
name = "frame-controls";
|
||||||
|
src = ./frame-controls/patch.js;
|
||||||
|
opts = removeAttrs cfg [ "enable" ];
|
||||||
|
};
|
||||||
|
unpatch = ./frame-controls/unpatch.js;
|
||||||
|
} ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,635 @@
|
|||||||
|
// frame-controls: move the control icons under SteamVR dashboard windows
|
||||||
|
// between the window's bottom bar and its More Options (three-dot) menu.
|
||||||
|
// A long press on a bar icon or a menu row (opts.longPressMs; a ring around
|
||||||
|
// the icon fills up once half a second .. a second has passed) opens a small
|
||||||
|
// popup with a "Show in bar" checkbox; toggling it moves that control for all
|
||||||
|
// windows. Short presses stay stock. The three-dot button itself can't be
|
||||||
|
// moved (stock shows it while its menu has entries). opts.floatInTheater
|
||||||
|
// gives theater windows the "Float" control back.
|
||||||
|
//
|
||||||
|
// Target: SteamVR's dashboard page (vrwebhelper, DevTools 127.0.0.1:8087,
|
||||||
|
// title "systemui"). This file is a function expression, called by the file
|
||||||
|
// lib/default.nix (mkPatch) generates: (<this file>)(find, sigs, opts), with
|
||||||
|
// find the finder library (lib/finders.js), sigs this patch's module
|
||||||
|
// signatures (lib/signatures.json, "frame-controls") and opts the options
|
||||||
|
// from frame-controls.nix.
|
||||||
|
//
|
||||||
|
// Stock frame controls (systemui): each window (Frame) renders its controls
|
||||||
|
// as <FrameControlsItem params={type, action_id}> (type 2 = action button,
|
||||||
|
// 1 = spacer); the items register while rendering and the frame's
|
||||||
|
// <FrameControls> hands the lists to Frame.prototype.SetControlsItems(bottom,
|
||||||
|
// tabHover, additional) (a MobX action). Items inside onlyVisibleIn=
|
||||||
|
// "additional-options" (curvature, dock to a controller) go to `additional`.
|
||||||
|
// systemui draws the controls of every window itself (also the Steam window):
|
||||||
|
// panel vsg-node#legacy-frame-controls-<frameID> from `bottom`, and, while
|
||||||
|
// the menu is open, #legacy-frame-controls-additional-options-<frameID> from
|
||||||
|
// `additional`. Buttons are div.ButtonControl whose React `control` prop is
|
||||||
|
// the item; stock actions run on click (primary button only).
|
||||||
|
//
|
||||||
|
// Placement: SetControlsItems is wrapped; the stock lists are remembered per
|
||||||
|
// frame and a re-partitioned copy is stored (stock order kept; a control
|
||||||
|
// moved into the bar joins the group left of the three-dot button, which
|
||||||
|
// keeps a group of its own; adjacent spacer runs are merged so gaps don't
|
||||||
|
// widen). Without a placement differing from stock the stock arrays pass
|
||||||
|
// through unchanged. A change re-applies the remembered lists of every frame
|
||||||
|
// (no React re-render needed). The tab-hover list is untouched.
|
||||||
|
// Placement is global per control type: a control's key is its action icon,
|
||||||
|
// "icon:<enum>" (the same in every window and language; action ids are per
|
||||||
|
// frame instance and app window keys change with every app start). Effective
|
||||||
|
// placement: the popup's choice, else opts.inBar / opts.inMenu, else stock; a
|
||||||
|
// popup choice is dropped when the option for that control changes. Kept in
|
||||||
|
// window.__sfuiFrameControlsState (survives patch restarts and upgrades;
|
||||||
|
// unpatch keeps it) and mirrored to the page's localStorage, so it also
|
||||||
|
// survives SteamVR restarts.
|
||||||
|
//
|
||||||
|
// Long press: SteamVR delivers only primary-button laser input to the
|
||||||
|
// dashboard (no right-click; the thumbstick click arrives as nothing), so a
|
||||||
|
// long press is the trigger. Nothing is stopped or restyled while holding;
|
||||||
|
// the timer keeps running when the laser moves or leaves the control and is
|
||||||
|
// cancelled only by an early release. When it completes, the popup opens and
|
||||||
|
// the one click that follows the release on that control is swallowed, so
|
||||||
|
// its stock action doesn't run.
|
||||||
|
// window-curvature (its controls own press-and-drag) contract: its elements
|
||||||
|
// carry the class sfui-curv-ctl; a drag there dispatches a bubbling
|
||||||
|
// CustomEvent 'sfui-curv-dragstart' (cancels the long press before the ring
|
||||||
|
// shows); once the ring shows, the long press takes the press over with
|
||||||
|
// window.__sfuiWindowCurvature.cancelPress(), so drifting doesn't start a
|
||||||
|
// drag. Neither side reads the other's thresholds.
|
||||||
|
//
|
||||||
|
// Popup: in a scene-graph panel of its own, attached like the stock menu, so
|
||||||
|
// neither the bar nor the menu panel changes. The dashboard builds its scene
|
||||||
|
// graph by walking the DOM (vsg-transform elements from their attributes,
|
||||||
|
// other nodes through an element's buildNode()); panel textures are regions
|
||||||
|
// of the page, published per panel through SGApp's embedded-UV table. So:
|
||||||
|
// <vsg-transform parent-id=<anchor> translation=...>
|
||||||
|
// <vsg-node> buildNode: a panel with the bar/menu panel's own properties
|
||||||
|
// (key, meters-per-pixel, curvature origin, laser visibility),
|
||||||
|
// origin bottom centre, own UVs and embedded-UV slot
|
||||||
|
// popup
|
||||||
|
// inserted after the bar's transform. Bar controls: the pressed button's
|
||||||
|
// anchor (the tooltips' anchor), 0.15 up (the tooltips' offset): above the
|
||||||
|
// icon. Menu rows: the three-dot button's popout anchor (from which the stock
|
||||||
|
// menu is placed), centred on the menu and GAP_PX above it. The stock menu
|
||||||
|
// closes when the compositor's input focus leaves the bar and menu panels,
|
||||||
|
// which pointing at the popup does, so while a menu popup is open a close of
|
||||||
|
// that menu is deferred (Frame.prototype.SetControlAdditionalOptionsOpen
|
||||||
|
// wrapped) and settled when the popup closes. The popup closes on a press
|
||||||
|
// outside it, Escape, 1 s after the laser left it and its bar/menu, and after
|
||||||
|
// toggling. Without SGApp/anchors the popup goes into the bar/menu panel.
|
||||||
|
//
|
||||||
|
// Debugging: window.__sfuiFrameControls: log, dump(), placement(),
|
||||||
|
// setPlacement(name or "icon:N", 'bar' | 'menu' | null), reset().
|
||||||
|
((find, sigs, opts) => {
|
||||||
|
const NAME = 'frame-controls';
|
||||||
|
const VERSION = 1;
|
||||||
|
const T_SPACER = 1, T_ACTION = 2;
|
||||||
|
const LS_KEY = 'sfui.frameControls.v1';
|
||||||
|
// Action icon enums (sigs.controls anchors them): names for the options.
|
||||||
|
const NAMES = { keyboard: 22, float: 26, dashboard: 27, theater: 28, dockLeft: 29, dockRight: 30, close: 31, curvature: 40 };
|
||||||
|
const ICON = { more: 38, float: 26, dashboard: 27 };
|
||||||
|
const NAME_OF = { ...Object.fromEntries(Object.entries(NAMES).map(([k, v]) => [`icon:${v}`, k])), [`icon:${ICON.more}`]: 'more' };
|
||||||
|
const GAP_PX = 10, MENU_LEFT_PX = 34; // stock menu: x -34 px from its anchor
|
||||||
|
const TOOLTIP_OFFSET = '0 0.15 0.06'; // stock tooltip / menu offset (y 0.15, z 0.05), in front
|
||||||
|
|
||||||
|
const o = { inBar: [], inMenu: [], longPressMs: 1500, floatInTheater: false, ...opts };
|
||||||
|
if (!(o.longPressMs >= 200)) return `invalid options ${JSON.stringify(opts)}`;
|
||||||
|
const toKey = (x) => (typeof x === 'number' ? `icon:${x}` : NAMES[x] != null ? `icon:${NAMES[x]}` : /^icon:\d+$/.test(x) ? x : null);
|
||||||
|
const optMap = {}; // key -> 'bar' | 'menu' from the options
|
||||||
|
for (const [list, where] of [[o.inBar, 'bar'], [o.inMenu, 'menu']]) {
|
||||||
|
for (const x of list ?? []) {
|
||||||
|
const k = toKey(x);
|
||||||
|
if (!k) return `invalid control ${JSON.stringify(x)} (use ${Object.keys(NAMES).join(', ')} or "icon:N")`;
|
||||||
|
if (optMap[k] && optMap[k] !== where) return `control ${JSON.stringify(x)} is in both inBar and inMenu`;
|
||||||
|
optMap[k] = where;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const id = JSON.stringify([VERSION, optMap, o.longPressMs, !!o.floatInTheater]);
|
||||||
|
|
||||||
|
const prev = window.__sfuiFrameControls;
|
||||||
|
if (prev?.version === VERSION && prev.id === id) return prev.check();
|
||||||
|
|
||||||
|
const FS = window.FrameStore;
|
||||||
|
if (!FS) return 'not patched: window.FrameStore missing';
|
||||||
|
let mods;
|
||||||
|
try {
|
||||||
|
mods = find.resolveAll(find.getWebpackRequire('webpackChunkvrwebui'), sigs);
|
||||||
|
} catch (e) {
|
||||||
|
return `signature not found, dashboard left unpatched: ${e.message}`;
|
||||||
|
}
|
||||||
|
const mx = mods.mobx.module;
|
||||||
|
const P = mods.frame.exports.Frame.prototype;
|
||||||
|
const actions = mods.actions.exports.store;
|
||||||
|
const DOCK = mods.dock.exports.EDockLocation;
|
||||||
|
const focus = mods.inputFocus.exports.store;
|
||||||
|
const localize = mods.localize.exports.localize;
|
||||||
|
if (typeof mx.runInAction !== 'function') return 'signature not found, dashboard left unpatched: mobx.runInAction';
|
||||||
|
|
||||||
|
prev?.teardown?.(); // other version or options: back to stock first
|
||||||
|
|
||||||
|
// ---- state + log -------------------------------------------------------------------
|
||||||
|
let S = window.__sfuiFrameControlsState;
|
||||||
|
if (S?.schema !== 1) {
|
||||||
|
let saved = null;
|
||||||
|
try { saved = JSON.parse(localStorage.getItem(LS_KEY) ?? 'null'); } catch { /* none */ }
|
||||||
|
const ok = saved?.schema === 1;
|
||||||
|
S = window.__sfuiFrameControlsState = { schema: 1, placement: ok ? saved.placement ?? {} : {}, nixSeen: ok ? saved.nixSeen ?? {} : {}, log: [] };
|
||||||
|
}
|
||||||
|
const logBuf = S.log ??= [];
|
||||||
|
const log = (msg, data) => {
|
||||||
|
logBuf.push({ t: new Date().toISOString().slice(11, 23), msg, ...(data !== undefined ? { data } : {}) });
|
||||||
|
if (logBuf.length > 200) logBuf.splice(0, logBuf.length - 200);
|
||||||
|
};
|
||||||
|
const save = () => {
|
||||||
|
try { localStorage.setItem(LS_KEY, JSON.stringify({ schema: 1, placement: S.placement, nixSeen: S.nixSeen })); } catch { /* in-page only */ }
|
||||||
|
};
|
||||||
|
for (const k of new Set([...Object.keys(optMap), ...Object.keys(S.nixSeen)])) {
|
||||||
|
if (S.nixSeen[k] !== optMap[k] && k in S.placement) {
|
||||||
|
log('option changed, popup choice dropped', { key: k, was: S.placement[k], option: optMap[k] ?? 'stock' });
|
||||||
|
delete S.placement[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
S.nixSeen = { ...optMap };
|
||||||
|
save();
|
||||||
|
|
||||||
|
// Popup text in the dashboard's language (its own rule: the first of
|
||||||
|
// navigator.languages it has a translation for, else English).
|
||||||
|
const LANGS = ['en', 'de', 'fr', 'it', 'ko', 'es-419', 'es', 'zh-CN', 'zh-TW', 'ru', 'th', 'ja', 'pt', 'pl', 'da', 'nl',
|
||||||
|
'fi', 'no', 'sv', 'hu', 'cs', 'ro', 'tr', 'pt-BR', 'bg', 'el', 'uk', 'vi'];
|
||||||
|
const lang = (() => {
|
||||||
|
for (const t of navigator.languages ?? []) {
|
||||||
|
if (LANGS.includes(t)) return t;
|
||||||
|
if (LANGS.includes(t.split('-')[0])) return t.split('-')[0];
|
||||||
|
}
|
||||||
|
return 'en';
|
||||||
|
})();
|
||||||
|
const TEXT_SHOW = { de: 'In der Leiste anzeigen' }[lang] ?? 'Show in bar';
|
||||||
|
|
||||||
|
// ---- controls ----------------------------------------------------------------------
|
||||||
|
const protoOf = (aid) => { try { return actions.GetAction(aid)?.protoForSteam; } catch { return undefined; } };
|
||||||
|
const iconOf = (aid) => { const p = protoOf(aid); return p?.icon?.enum ?? p?.icon_active?.enum; };
|
||||||
|
const keyOf = (aid) => { const e = iconOf(aid); return e ? `icon:${e}` : null; };
|
||||||
|
const isMore = (frame, it) => it?.action_id != null &&
|
||||||
|
(it.action_id === frame.m_unControlAdditionalOptionsActionID || iconOf(it.action_id) === ICON.more);
|
||||||
|
const movable = (frame, it) => it?.type === T_ACTION && it.action_id != null && !isMore(frame, it);
|
||||||
|
const wanted = (key) => (key ? S.placement[key] ?? optMap[key] : undefined); // 'bar' | 'menu' | undefined (stock)
|
||||||
|
|
||||||
|
// Float in theater: stock shows "Float" (SetDockLocation(World)) only for
|
||||||
|
// windows docked in the dashboard; a theater window gets an action of its
|
||||||
|
// own (frame.CreateAction, destroyed with the frame) with the stock label,
|
||||||
|
// icon and call, after "back to dashboard". Same key, so it follows Float's
|
||||||
|
// placement.
|
||||||
|
const floatActions = new Map(); // frame -> action
|
||||||
|
const withFloat = (frame, bottom) => {
|
||||||
|
if (!o.floatInTheater || !Array.isArray(bottom) || frame.docking?.dockLocation !== DOCK.Theater) return bottom;
|
||||||
|
if (bottom.some((it) => iconOf(it?.action_id) === ICON.float)) return bottom;
|
||||||
|
let a = floatActions.get(frame);
|
||||||
|
if (!a?.isAlive) {
|
||||||
|
a = frame.CreateAction({ display_name: localize('#FloatInWorld'), icon: { enum: ICON.float }, invocation: 1 },
|
||||||
|
() => frame.docking.SetDockLocation(DOCK.World));
|
||||||
|
floatActions.set(frame, a);
|
||||||
|
}
|
||||||
|
const out = [...bottom];
|
||||||
|
let at = out.findIndex((it) => iconOf(it?.action_id) === ICON.dashboard) + 1;
|
||||||
|
if (at === 0) {
|
||||||
|
at = Math.max(0, out.findIndex((it) => isMore(frame, it)));
|
||||||
|
while (at > 0 && out[at - 1]?.type === T_SPACER) at--;
|
||||||
|
}
|
||||||
|
out.splice(at, 0, { type: T_ACTION, action_id: a.actionID });
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stock lists -> [bottom, additional] with the placements applied, or null
|
||||||
|
// when nothing moves.
|
||||||
|
const partition = (frame, bottom, add) => {
|
||||||
|
if (!Array.isArray(bottom) || !Array.isArray(add)) return null;
|
||||||
|
const toMenu = bottom.filter((it) => movable(frame, it) && wanted(keyOf(it.action_id)) === 'menu');
|
||||||
|
const toBar = add.filter((it) => movable(frame, it) && wanted(keyOf(it.action_id)) === 'bar');
|
||||||
|
if (!toMenu.length && !toBar.length) return null;
|
||||||
|
const tokens = [], runLen = []; // bar items; spacers tagged with their run
|
||||||
|
let run = -1, inRun = false;
|
||||||
|
for (const it of bottom) {
|
||||||
|
if (it?.type === T_SPACER) {
|
||||||
|
if (!inRun) { run++; runLen[run] = 0; inRun = true; }
|
||||||
|
runLen[run]++;
|
||||||
|
tokens.push({ it, run });
|
||||||
|
} else {
|
||||||
|
inRun = false;
|
||||||
|
if (!toMenu.includes(it)) tokens.push({ it });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toBar.length) {
|
||||||
|
const ins = toBar.map((it) => ({ it }));
|
||||||
|
const more = tokens.findIndex((t) => t.run == null && isMore(frame, t.it));
|
||||||
|
let at = more;
|
||||||
|
if (at >= 0) { // end of the group left of the three-dot button
|
||||||
|
while (at > 0 && tokens[at - 1].run != null) at--;
|
||||||
|
if (at === more) { runLen.push(1); ins.push({ it: { type: T_SPACER }, run: runLen.length - 1 }); }
|
||||||
|
} else { // no three-dot button: before the first wide gap
|
||||||
|
at = tokens.findIndex((t) => t.run != null && runLen[t.run] >= 2);
|
||||||
|
if (at < 0) at = tokens.length;
|
||||||
|
}
|
||||||
|
tokens.splice(at, 0, ...ins);
|
||||||
|
}
|
||||||
|
const outBar = [];
|
||||||
|
for (let i = 0; i < tokens.length;) {
|
||||||
|
if (tokens[i].run == null) { outBar.push(tokens[i++].it); continue; }
|
||||||
|
const group = [];
|
||||||
|
while (i < tokens.length && tokens[i].run != null) group.push(tokens[i++]);
|
||||||
|
const runs = [...new Set(group.map((t) => t.run))];
|
||||||
|
const keep = runs.reduce((a, b) => (runLen[b] > runLen[a] ? b : a));
|
||||||
|
for (const t of group) if (runs.length === 1 || t.run === keep) outBar.push(t.it);
|
||||||
|
}
|
||||||
|
return [outBar, [...toMenu, ...add.filter((it) => !toBar.includes(it))]];
|
||||||
|
};
|
||||||
|
|
||||||
|
// SetControlsItems wrapper: one per page, version independent (looks the
|
||||||
|
// current state up per call, inert once it is gone; found anywhere in the
|
||||||
|
// chain, so a wrapper on top of it doesn't make us re-wrap).
|
||||||
|
const stock = new WeakMap(); // frame -> [bottom, tab, additional] as passed by React
|
||||||
|
const onSet = (frame, args) => {
|
||||||
|
stock.set(frame, [args[0], args[1], args[2]]);
|
||||||
|
const bottom = withFloat(frame, args[0]);
|
||||||
|
const r = partition(frame, bottom, args[2]);
|
||||||
|
if (r) [args[0], args[2]] = r;
|
||||||
|
else args[0] = bottom;
|
||||||
|
};
|
||||||
|
const findOurs = () => {
|
||||||
|
for (let f = P.SetControlsItems, i = 0; f && i < 20; f = f.__sfuiOrig, i++) if (f.__sfuiPatch === NAME) return f;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const install = () => {
|
||||||
|
if (findOurs()) return;
|
||||||
|
const orig = P.SetControlsItems;
|
||||||
|
P.SetControlsItems = Object.assign(function SetControlsItems(...args) {
|
||||||
|
const st = window.__sfuiFrameControls;
|
||||||
|
if (st?.onSet) {
|
||||||
|
try { st.onSet(this, args); } catch (e) { st.addLog?.('partition error', String(e)); }
|
||||||
|
}
|
||||||
|
return orig.apply(this, args);
|
||||||
|
}, { __sfuiPatch: NAME, __sfuiOrig: orig, isMobxAction: orig.isMobxAction });
|
||||||
|
};
|
||||||
|
// Frames that existed before the patch: their current lists are stock.
|
||||||
|
const captureExisting = () => {
|
||||||
|
for (const f of FS.frames ?? []) {
|
||||||
|
if (stock.has(f)) continue;
|
||||||
|
const pl = (a) => (a ? Array.from(a) : []);
|
||||||
|
stock.set(f, [pl(f.m_rgControlsItems_BottomFrameControls), pl(f.m_rgControlsItems_TabHoverControls), pl(f.m_rgControlsItems_AdditionalOptions)]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Re-applies every frame's stock lists through the wrapper (or, useStock,
|
||||||
|
// straight to the stock setter).
|
||||||
|
const applyAll = (useStock = false) => {
|
||||||
|
const set = useStock ? findOurs()?.__sfuiOrig ?? P.SetControlsItems : null;
|
||||||
|
mx.runInAction(() => {
|
||||||
|
for (const f of FS.frames ?? []) {
|
||||||
|
const s = stock.get(f);
|
||||||
|
if (!s) continue;
|
||||||
|
try {
|
||||||
|
if (useStock) set.call(f, s[0], s[1], s[2]);
|
||||||
|
else f.SetControlsItems(s[0], s[1], s[2]);
|
||||||
|
// A menu emptied by a move would pop open again once it gets an entry back.
|
||||||
|
if (!f.m_rgControlsItems_AdditionalOptions?.length && f.m_bControlAdditionalOptionsOpen) f.SetControlAdditionalOptionsOpen(false);
|
||||||
|
} catch (e) { log('apply error', { frame: f.frameID, e: String(e) }); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const placeIn = (lists, aid) => (lists?.[0]?.some((it) => it?.action_id === aid) ? 'bar'
|
||||||
|
: lists?.[1]?.some((it) => it?.action_id === aid) ? 'menu' : undefined);
|
||||||
|
const stockPlace = (f, aid) => { const s = stock.get(f); return placeIn(s && [s[0], s[2]], aid); };
|
||||||
|
const currentPlace = (f, aid) => placeIn([f.m_rgControlsItems_BottomFrameControls, f.m_rgControlsItems_AdditionalOptions], aid);
|
||||||
|
const setPlacement = (key, where, why = 'api') => { // where: 'bar' | 'menu' | null (option / stock)
|
||||||
|
if (!key) return 'unknown control';
|
||||||
|
if (where == null) delete S.placement[key];
|
||||||
|
else S.placement[key] = where;
|
||||||
|
save();
|
||||||
|
log('placement', { key, name: NAME_OF[key], where: where ?? 'default', why });
|
||||||
|
applyAll();
|
||||||
|
return S.placement[key] ?? optMap[key] ?? 'stock';
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- popup -------------------------------------------------------------------------
|
||||||
|
const STYLE_ID = 'sfui-frame-controls-style';
|
||||||
|
const CSS = `
|
||||||
|
.sfui-fc-pop { display: block; width: max-content; box-sizing: border-box; background: var(--gamepadui-darkish-grey, #2d3239);
|
||||||
|
border-radius: 10px; overflow: hidden; color: #fff; font-size: 24px; line-height: 1.2; text-align: left;
|
||||||
|
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.55); }
|
||||||
|
.sfui-fc-own { display: inline-block; vertical-align: top; }
|
||||||
|
.sfui-fc-in-bar { margin: 10px 0 0 0; }
|
||||||
|
.sfui-fc-in-menu { margin: 0 0 10px 0; }
|
||||||
|
.sfui-fc-row { display: flex; flex-direction: row; align-items: center; gap: 16px; padding: 14px 24px 14px 20px;
|
||||||
|
white-space: nowrap; cursor: pointer; }
|
||||||
|
.sfui-fc-row:hover { background: var(--gamepadui-dark-grey, #3d4450); }
|
||||||
|
.sfui-fc-row:active { background: var(--gamepadui-grey, #67707b); }
|
||||||
|
.sfui-fc-box { flex: none; position: relative; width: 1em; height: 1em; box-sizing: border-box;
|
||||||
|
border: 2px solid currentColor; border-radius: 4px; }
|
||||||
|
.sfui-fc-box.sfui-on { background: #1a9fff; border-color: #1a9fff; }
|
||||||
|
.sfui-fc-box.sfui-on::after { content: ''; position: absolute; left: 30%; top: 6%; width: 28%; height: 56%;
|
||||||
|
border: solid #fff; border-width: 0 3px 3px 0; transform: rotate(45deg); }
|
||||||
|
.sfui-fc-pressing { position: relative; }
|
||||||
|
`;
|
||||||
|
const ensureStyle = () => {
|
||||||
|
if (document.getElementById(STYLE_ID)) return;
|
||||||
|
const s = document.createElement('style');
|
||||||
|
s.id = STYLE_ID; s.textContent = CSS;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PANEL_RE = /^legacy-frame-controls-(additional-options-)?(\d+)$/;
|
||||||
|
// The control under an event target: { frame, actionId, where, button, panel } or null.
|
||||||
|
const controlAt = (target) => {
|
||||||
|
const button = target?.closest?.('.ButtonControl');
|
||||||
|
const panel = button?.closest('vsg-node[id^="legacy-frame-controls-"]');
|
||||||
|
const m = panel && PANEL_RE.exec(panel.id);
|
||||||
|
if (!m) return null;
|
||||||
|
const actionId = find.findFiberUp(button, (f) => f.memoizedProps?.control != null, 12)?.memoizedProps.control.action_id;
|
||||||
|
const frame = FS.GetFrame(+m[2]);
|
||||||
|
return actionId != null && frame ? { frame, actionId, where: m[1] ? 'menu' : 'bar', button, panel } : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let pop = null; // { el, frame, actionId, panel, button, where, own, leaveTimer, off, menuCloseDeferred }
|
||||||
|
const closePopup = (why) => {
|
||||||
|
const p = pop;
|
||||||
|
if (!p) return;
|
||||||
|
pop = null;
|
||||||
|
clearTimeout(p.leaveTimer);
|
||||||
|
p.off();
|
||||||
|
p.el.remove();
|
||||||
|
try { p.own?.dispose(); } catch (e) { log('popup panel dispose failed', String(e)); }
|
||||||
|
if (p.menuCloseDeferred) setTimeout(() => settleMenu(p.frame), 250);
|
||||||
|
log('popup closed', { why });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep the three-dot menu open while its popup is pointed at (see header).
|
||||||
|
const GUARD = 'frame-controls-menu-guard';
|
||||||
|
const origOpen = P.SetControlAdditionalOptionsOpen;
|
||||||
|
const guardOpen = Object.assign(function SetControlAdditionalOptionsOpen(v, ...rest) {
|
||||||
|
if (window.__sfuiFrameControls?.deferMenuClose?.(this, v)) return undefined;
|
||||||
|
return guardOpen.__sfuiOrig.call(this, v, ...rest);
|
||||||
|
}, { __sfuiPatch: GUARD, __sfuiOrig: origOpen, isMobxAction: origOpen.isMobxAction });
|
||||||
|
const deferMenuClose = (frame, v) => {
|
||||||
|
if (v !== false || !pop?.own || pop.where !== 'menu' || pop.frame !== frame) return false;
|
||||||
|
if (!pop.menuCloseDeferred) log('menu close deferred while its popup is open', { frame: frame.frameID });
|
||||||
|
pop.menuCloseDeferred = true;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
// After the popup closed: close the menu unless the laser is on the bar or menu.
|
||||||
|
const settleMenu = (frame) => {
|
||||||
|
if (pop?.frame === frame && pop.where === 'menu') return;
|
||||||
|
let keep = false;
|
||||||
|
try { keep = !!frame.inputFocus?.frameControlsHaveFocus || focus.BPanelHasFocus(frame.panels?.additionalOptionsPanelSGID); } catch { /* close */ }
|
||||||
|
if (!keep) { try { origOpen.call(frame, false); } catch (e) { log('menu close failed', String(e)); } }
|
||||||
|
};
|
||||||
|
|
||||||
|
// The popup's own scene-graph panel (see header); null if SGApp or the
|
||||||
|
// anchor isn't there.
|
||||||
|
const mountOwnPanel = (hit, content) => {
|
||||||
|
const { panel: base, button, frame } = hit; // base: the bar or menu panel, whose look is copied
|
||||||
|
const app = window.SGApp;
|
||||||
|
const menu = hit.where === 'menu';
|
||||||
|
const host = document.getElementById(`legacy-frame-controls-${frame.frameID}`)?.parentElement;
|
||||||
|
const anchor = menu ? document.getElementById(`legacy-frame-controls-additional-options-anchor-${frame.frameID}`)
|
||||||
|
: button.querySelector('vsg-node[vsg-type="panel-anchor"] vsg-transform[id]');
|
||||||
|
if (typeof app?.addEmbeddedPanelUVs !== 'function' || typeof base.buildNode !== 'function' ||
|
||||||
|
typeof VRHTML?.NextSGID !== 'function' || !anchor || !host) return null;
|
||||||
|
const baseProps = (ctx) => base.buildNode(ctx, base)?.[1]?.properties ?? {};
|
||||||
|
let translation = TOOLTIP_OFFSET;
|
||||||
|
if (menu) { // centred on the menu, GAP_PX above it
|
||||||
|
const mpp = baseProps({ currentPanel: null, bInsideReparentedPanel: false, bShouldAbort: false })['meters-per-pixel'];
|
||||||
|
const r = base.getBoundingClientRect();
|
||||||
|
if (!(mpp > 0 && r.width > 0 && r.height > 0)) return null;
|
||||||
|
const x = -MENU_LEFT_PX * mpp * (app.m_fCurrentScale ?? 1) + (r.width / 2) * mpp, y = 0.15 + (r.height + GAP_PX) * mpp;
|
||||||
|
translation = `${x.toFixed(5)} ${y.toFixed(5)} 0.06`;
|
||||||
|
}
|
||||||
|
const outer = document.createElement('vsg-transform');
|
||||||
|
for (const [k, v] of Object.entries({ 'parent-id': anchor.id, translation, rotation: '1 0 0 0', scale: '1 1 1', sgid: VRHTML.NextSGID() }))
|
||||||
|
outer.setAttribute(k, String(v));
|
||||||
|
const node = document.createElement('vsg-node');
|
||||||
|
node.id = `sfui-fc-popup-${frame.frameID}`;
|
||||||
|
node.className = 'sfui-fc-own';
|
||||||
|
node.setAttribute('vsg-type', 'panel');
|
||||||
|
const sgid = VRHTML.NextSGID();
|
||||||
|
node.setAttribute('sgid', String(sgid));
|
||||||
|
node.appendChild(content);
|
||||||
|
outer.appendChild(node);
|
||||||
|
const fp = { // what SGApp's embedded-UV table reads of a panel
|
||||||
|
props: { debug_name: node.id }, isExternal: false, m_Rect: { x: 0, y: 0, width: 0, height: 0 }, idx: undefined,
|
||||||
|
getSGID: () => sgid, getEmbeddedIndex: () => fp.idx, getCurrentRootElement: () => node,
|
||||||
|
updateLayoutValues() { const r = node.getBoundingClientRect(); fp.m_Rect = { x: r.x, y: r.y, width: r.width, height: r.height }; },
|
||||||
|
};
|
||||||
|
fp.idx = app.addEmbeddedPanelUVs(fp);
|
||||||
|
if (fp.idx == null) return null;
|
||||||
|
const KEEP = ['key', 'overlay_handle', 'meters-per-pixel', 'curvature-origin-id', 'reflect', 'lasermouse-filtering',
|
||||||
|
'focus-outline', 'only-visible-with-laser', 'steam-input-appid', 'scale-index'];
|
||||||
|
node.buildNode = (ctx) => {
|
||||||
|
if (!base.isConnected || !anchor.isConnected) { // bar / menu gone: drop the popup
|
||||||
|
queueMicrotask(() => { if (pop?.own?.node === node) closePopup('panel gone'); });
|
||||||
|
return [ctx, null];
|
||||||
|
}
|
||||||
|
const bp = baseProps(ctx), r = node.getBoundingClientRect(), W = window.innerWidth, H = window.innerHeight;
|
||||||
|
const props = {};
|
||||||
|
for (const k of KEEP) if (bp[k] !== undefined) props[k] = bp[k];
|
||||||
|
Object.assign(props, {
|
||||||
|
id: typeof bp.id === 'string' ? bp.id.replace(/[^:]*$/, node.id) : node.id, sgid, debug_name: node.id,
|
||||||
|
origin: [0, -1], interactive: true, visibility: 0, 'sort-depth-bias': -1, 'can-take-keyboard-focus': false,
|
||||||
|
'embedded-uv-index': fp.idx, uv_min: [r.x / W, r.y / H], uv_max: [(r.x + r.width) / W, (r.y + r.height) / H],
|
||||||
|
});
|
||||||
|
return [{ ...ctx, currentPanel: fp, bInsideReparentedPanel: false }, { type: 'panel', properties: props }];
|
||||||
|
};
|
||||||
|
host.after(outer);
|
||||||
|
window.forceLayoutUpdate?.();
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
dispose: () => { outer.remove(); app.removeEmbeddedPanelUVs(fp); window.forceLayoutUpdate?.(); },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPopup = (hit) => {
|
||||||
|
closePopup('reopen');
|
||||||
|
const { frame, actionId, panel, button, where } = hit;
|
||||||
|
const key = keyOf(actionId);
|
||||||
|
if (!key) return;
|
||||||
|
ensureStyle();
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'sfui-fc-pop';
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'sfui-fc-row';
|
||||||
|
const box = document.createElement('span');
|
||||||
|
box.className = `sfui-fc-box${currentPlace(frame, actionId) === 'bar' ? ' sfui-on' : ''}`;
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.textContent = TEXT_SHOW;
|
||||||
|
row.append(box, label);
|
||||||
|
el.append(row);
|
||||||
|
for (const t of ['mousedown', 'mouseup', 'click', 'dblclick', 'pointerdown', 'pointerup', 'contextmenu'])
|
||||||
|
el.addEventListener(t, (e) => e.stopPropagation()); // nothing reaches the page's handlers
|
||||||
|
row.addEventListener('click', () => {
|
||||||
|
const now = currentPlace(frame, actionId), to = now === 'bar' ? 'menu' : 'bar';
|
||||||
|
const dflt = optMap[key] ?? stockPlace(frame, actionId);
|
||||||
|
closePopup('toggled');
|
||||||
|
setPlacement(key, to === dflt ? null : to, `popup (${now} -> ${to})`);
|
||||||
|
});
|
||||||
|
const own = mountOwnPanel(hit, el);
|
||||||
|
if (!own) { // fallback: inside the bar (below) / menu (on top)
|
||||||
|
log('no own popup panel', { where });
|
||||||
|
el.classList.add(where === 'bar' ? 'sfui-fc-in-bar' : 'sfui-fc-in-menu');
|
||||||
|
if (where === 'bar') {
|
||||||
|
panel.appendChild(el);
|
||||||
|
const pr = panel.getBoundingClientRect(), br = button.getBoundingClientRect(), w = el.getBoundingClientRect().width;
|
||||||
|
el.style.marginLeft = `${Math.round(Math.max(0, Math.min(pr.width - w, br.left + br.width / 2 - pr.left - w / 2)))}px`;
|
||||||
|
} else panel.insertBefore(el, panel.firstChild);
|
||||||
|
}
|
||||||
|
const p = pop = { el, frame, actionId, panel, button, where, own, leaveTimer: null, off: null, menuCloseDeferred: false };
|
||||||
|
// Leaving the popup and its bar/menu for 1 s closes it.
|
||||||
|
const onLeave = () => { clearTimeout(p.leaveTimer); p.leaveTimer = setTimeout(() => { if (pop === p) closePopup('laser left'); }, 1000); };
|
||||||
|
const onEnter = () => clearTimeout(p.leaveTimer);
|
||||||
|
const watched = own ? [panel, own.node] : [panel];
|
||||||
|
for (const w of watched) { w.addEventListener('mouseleave', onLeave); w.addEventListener('mouseenter', onEnter); }
|
||||||
|
p.off = () => { for (const w of watched) { w.removeEventListener('mouseleave', onLeave); w.removeEventListener('mouseenter', onEnter); } };
|
||||||
|
log('popup opened', { frame: frame.frameID, key, name: NAME_OF[key], where, ownPanel: !!own });
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- long press --------------------------------------------------------------------
|
||||||
|
const SVGNS = 'http://www.w3.org/2000/svg';
|
||||||
|
const RING_DELAY = Math.min(1000, o.longPressMs / 2);
|
||||||
|
const curvApi = () => window.__sfuiWindowCurvature;
|
||||||
|
let press = null; // { hit, t0, timer, ringTimer, ring, done, curv }
|
||||||
|
let swallow = null; // { until, button }: the click after a completed long press
|
||||||
|
|
||||||
|
// Ring around the pressed control's icon (svg.Icon, or img.Icon for the
|
||||||
|
// dock-to-controller rows), ~icon size, starting at the elapsed fraction;
|
||||||
|
// essential styling inline, so it can't show unstyled.
|
||||||
|
const showRing = (p) => {
|
||||||
|
const btn = p.hit.button;
|
||||||
|
if (!btn.isConnected) return null;
|
||||||
|
const icon = btn.querySelector('.Icon');
|
||||||
|
const br = btn.getBoundingClientRect(), ir = icon?.getBoundingClientRect();
|
||||||
|
const ok = ir?.width > 0 && ir.height > 0;
|
||||||
|
const size = Math.round(Math.min(48, Math.max(20, ok ? Math.max(ir.width, ir.height) : Math.min(br.height, 38))) + 20);
|
||||||
|
const cx = ok ? ir.left - br.left + ir.width / 2 : br.height / 2 + 12;
|
||||||
|
const cy = ok ? ir.top - br.top + ir.height / 2 : br.height / 2;
|
||||||
|
const ring = document.createElementNS(SVGNS, 'svg');
|
||||||
|
for (const [k, v] of Object.entries({ class: 'sfui-fc-ring', viewBox: '0 0 40 40', width: size, height: size })) ring.setAttribute(k, String(v));
|
||||||
|
const circle = (stroke, extra = {}) => {
|
||||||
|
const c = document.createElementNS(SVGNS, 'circle');
|
||||||
|
for (const [k, v] of Object.entries({ cx: 20, cy: 20, r: 17, fill: 'none', stroke, 'stroke-width': 3.5, ...extra })) c.setAttribute(k, String(v));
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
const fg = circle('#1a9fff', { pathLength: 100, 'stroke-dasharray': 100, 'stroke-linecap': 'round' });
|
||||||
|
ring.append(circle('rgba(255,255,255,0.18)'), fg);
|
||||||
|
const elapsed = performance.now() - p.t0;
|
||||||
|
Object.assign(ring.style, {
|
||||||
|
position: 'absolute', pointerEvents: 'none', overflow: 'visible', margin: '0', zIndex: '1', transform: 'rotate(-90deg)',
|
||||||
|
width: `${size}px`, height: `${size}px`, left: `${Math.round(cx - size / 2)}px`, top: `${Math.round(cy - size / 2)}px`,
|
||||||
|
});
|
||||||
|
fg.style.strokeDashoffset = String(100 * (1 - Math.min(1, elapsed / o.longPressMs)));
|
||||||
|
btn.appendChild(ring);
|
||||||
|
ring.getBoundingClientRect(); // start value applied, then animate the rest
|
||||||
|
fg.style.transition = `stroke-dashoffset ${Math.round(Math.max(0, o.longPressMs - elapsed))}ms linear`;
|
||||||
|
fg.style.strokeDashoffset = '0';
|
||||||
|
return ring;
|
||||||
|
};
|
||||||
|
const endPress = (why) => {
|
||||||
|
const p = press;
|
||||||
|
if (!p) return;
|
||||||
|
press = null;
|
||||||
|
clearTimeout(p.timer); clearTimeout(p.ringTimer);
|
||||||
|
p.ring?.remove();
|
||||||
|
p.hit.button.classList.remove('sfui-fc-pressing');
|
||||||
|
if (!p.done) log('long press cancelled', { why, ms: Math.round(performance.now() - p.t0) });
|
||||||
|
};
|
||||||
|
const completePress = () => {
|
||||||
|
const p = press;
|
||||||
|
if (!p?.hit.button.isConnected) { endPress('button gone'); return; }
|
||||||
|
p.done = true;
|
||||||
|
p.ring?.remove(); p.ring = null;
|
||||||
|
if (p.curv) curvApi()?.cancelPress?.();
|
||||||
|
openPopup(p.hit);
|
||||||
|
};
|
||||||
|
const onCurvDrag = () => { if (press && !press.done) endPress('curvature drag'); };
|
||||||
|
|
||||||
|
const onEvent = (e) => {
|
||||||
|
const t = e.type;
|
||||||
|
if (t === 'click') {
|
||||||
|
const w = swallow;
|
||||||
|
if (w && e.button === 0 && performance.now() < w.until && w.button.contains(e.target)) {
|
||||||
|
swallow = null;
|
||||||
|
e.stopImmediatePropagation(); e.preventDefault();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t === 'mousedown' || t === 'pointerdown') {
|
||||||
|
if (pop && (!pop.el.contains(e.target) || !pop.button.isConnected)) closePopup('press outside');
|
||||||
|
if (t !== 'mousedown' || e.button !== 0) return;
|
||||||
|
swallow = null;
|
||||||
|
endPress('new press');
|
||||||
|
const hit = controlAt(e.target);
|
||||||
|
if (!hit || !movable(hit.frame, { type: T_ACTION, action_id: hit.actionId })) return; // three-dot: stock
|
||||||
|
ensureStyle();
|
||||||
|
const p = press = {
|
||||||
|
hit, t0: performance.now(), done: false, ring: null, curv: hit.button.classList.contains('sfui-curv-ctl'),
|
||||||
|
timer: setTimeout(completePress, o.longPressMs), ringTimer: null,
|
||||||
|
};
|
||||||
|
hit.button.classList.add('sfui-fc-pressing'); // positioning context for the ring
|
||||||
|
p.ringTimer = setTimeout(() => {
|
||||||
|
if (press !== p || p.done) return;
|
||||||
|
if (p.curv) curvApi()?.cancelPress?.(); // the long press takes the press over
|
||||||
|
p.ring = showRing(p);
|
||||||
|
}, RING_DELAY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t === 'mouseup' && e.button === 0 && press) {
|
||||||
|
const p = press;
|
||||||
|
endPress(p.done ? 'released after long press' : 'released');
|
||||||
|
if (p.done) swallow = { until: performance.now() + 1000, button: p.hit.button };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKey = (e) => { if (pop && e.key === 'Escape') { e.stopPropagation(); closePopup('escape'); } };
|
||||||
|
const EVENTS = ['mousedown', 'pointerdown', 'mouseup', 'click'];
|
||||||
|
|
||||||
|
// ---- setup / teardown --------------------------------------------------------------
|
||||||
|
const teardown = () => {
|
||||||
|
endPress('teardown');
|
||||||
|
closePopup('teardown');
|
||||||
|
for (const t of EVENTS) window.removeEventListener(t, onEvent, true);
|
||||||
|
window.removeEventListener('sfui-curv-dragstart', onCurvDrag);
|
||||||
|
window.removeEventListener('keydown', onKey, true);
|
||||||
|
if (P.SetControlAdditionalOptionsOpen === guardOpen) P.SetControlAdditionalOptionsOpen = origOpen;
|
||||||
|
if (window.__sfuiFrameControls === state) delete window.__sfuiFrameControls; // wrappers now inert
|
||||||
|
applyAll(true); // stock lists back
|
||||||
|
for (const a of floatActions.values()) { try { a.Destroy(); } catch { /* gone */ } }
|
||||||
|
floatActions.clear();
|
||||||
|
if (P.SetControlsItems?.__sfuiPatch === NAME) P.SetControlsItems = P.SetControlsItems.__sfuiOrig;
|
||||||
|
document.getElementById(STYLE_ID)?.remove();
|
||||||
|
};
|
||||||
|
const check = () => {
|
||||||
|
if (findOurs()) return 'unchanged';
|
||||||
|
install(); captureExisting(); applyAll();
|
||||||
|
return 'patched (wrapper reinstalled)';
|
||||||
|
};
|
||||||
|
const effective = () => Object.fromEntries([...new Set([...Object.keys(optMap), ...Object.keys(S.placement)])]
|
||||||
|
.map((k) => [NAME_OF[k] ?? k, S.placement[k] ? `${S.placement[k]} (popup)` : `${optMap[k]} (option)`]));
|
||||||
|
const names = (list) => (list ?? []).map((it) => (it?.type === T_SPACER ? '|' : NAME_OF[keyOf(it?.action_id)] ?? keyOf(it?.action_id) ?? '?')).join(' ');
|
||||||
|
const dump = () => (FS.frames ?? []).map((f) => ({
|
||||||
|
frame: f.frameID, title: f.title, bar: names(f.m_rgControlsItems_BottomFrameControls), menu: names(f.m_rgControlsItems_AdditionalOptions),
|
||||||
|
stockBar: names(stock.get(f)?.[0]), stockMenu: names(stock.get(f)?.[2]), menuOpen: f.isControlAdditionalOptionsOpen,
|
||||||
|
}));
|
||||||
|
const state = {
|
||||||
|
version: VERSION, id, opts: o, log: logBuf, addLog: log, onSet, deferMenuClose, teardown, check, dump,
|
||||||
|
placement: effective,
|
||||||
|
setPlacement: (name, where) => setPlacement(toKey(name), where),
|
||||||
|
reset: () => { S.placement = {}; save(); log('reset'); applyAll(); return 'reset'; },
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__sfuiFrameControls = state;
|
||||||
|
if (P.SetControlAdditionalOptionsOpen?.__sfuiPatch !== GUARD) P.SetControlAdditionalOptionsOpen = guardOpen;
|
||||||
|
for (const t of EVENTS) window.addEventListener(t, onEvent, true);
|
||||||
|
window.addEventListener('sfui-curv-dragstart', onCurvDrag);
|
||||||
|
window.addEventListener('keydown', onKey, true);
|
||||||
|
ensureStyle();
|
||||||
|
install();
|
||||||
|
captureExisting();
|
||||||
|
applyAll();
|
||||||
|
log('patched', { version: VERSION, options: optMap, popup: S.placement });
|
||||||
|
const eff = Object.entries(effective());
|
||||||
|
return `patched (long press ${o.longPressMs} ms${o.floatInTheater ? ', float in theater' : ''}` +
|
||||||
|
`${eff.length ? `; ${eff.map(([k, v]) => `${k}: ${v}`).join(', ')}` : ''})`;
|
||||||
|
})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Reverts frame-controls/patch.js: ends a running long press, closes the
|
||||||
|
// popup (and removes its scene-graph panel), gives every window its stock
|
||||||
|
// control lists back and removes its wrappers (Frame.prototype
|
||||||
|
// SetControlsItems / SetControlAdditionalOptionsOpen; a wrapper something
|
||||||
|
// else wrapped since stays in the chain but is inert), listeners, style and
|
||||||
|
// the Float actions it added to theater windows.
|
||||||
|
// window.__sfuiFrameControlsState (placements, log) and its localStorage
|
||||||
|
// mirror are kept for a re-injection. Safe when not patched.
|
||||||
|
(() => {
|
||||||
|
const s = window.__sfuiFrameControls;
|
||||||
|
if (!s) return 'not patched';
|
||||||
|
s.teardown();
|
||||||
|
return 'unpatched';
|
||||||
|
})()
|
||||||
@@ -349,6 +349,96 @@
|
|||||||
"module": { "includes": ["case\"VSG-TRANSFORM\":", ".properties.translation="] }
|
"module": { "includes": ["case\"VSG-TRANSFORM\":", ".properties.translation="] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"frame-controls": {
|
||||||
|
"bundle": "vrwebui-systemui",
|
||||||
|
"modules": {
|
||||||
|
"mobx": {
|
||||||
|
"module": { "includes": ["runInAction:()=>", "makeObservable:()=>", "reaction:()=>"] }
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"module": { "includes": ["m_rgControlsItems_BottomFrameControls", "get protoForSteam()", "SetControlsItems("] },
|
||||||
|
"exports": {
|
||||||
|
"Frame": { "type": "class", "protoMethods": ["SetControlsItems", "SetControlAdditionalOptionsOpen", "CreateAction"] }
|
||||||
|
},
|
||||||
|
"expects": ["SetControlsItems(e,t,o){this.m_rgControlsItems_BottomFrameControls=e,this.m_rgControlsItems_TabHoverControls=t,this.m_rgControlsItems_AdditionalOptions=o}",
|
||||||
|
"get isControlAdditionalOptionsOpen(){return this.m_bControlAdditionalOptionsOpen&&this.m_rgControlsItems_AdditionalOptions.length>0}",
|
||||||
|
"BPanelHasFocus(this.panels.additionalOptionsPanelSGID)", "this.SetControlAdditionalOptionsOpen(!1)", "m_unControlAdditionalOptionsActionID"]
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"module": { "includes": ["window.dashboardActionStore="] },
|
||||||
|
"exports": {
|
||||||
|
"store": { "type": "object", "methods": ["GetAction", "BIsVRLinkRemoteAction"] }
|
||||||
|
},
|
||||||
|
"expects": ["get protoForSteam()", "get isAlive()"]
|
||||||
|
},
|
||||||
|
"dock": {
|
||||||
|
"module": { "includes": ["e[e.Dashboard=1]=\"Dashboard\",e[e.LeftHand=2]=\"LeftHand\""] },
|
||||||
|
"exports": {
|
||||||
|
"EDockLocation": { "type": "object", "props": { "Dashboard": 1, "World": 4, "Theater": 5 } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inputFocus": {
|
||||||
|
"module": { "includes": ["BPanelHasFocus(e,t=!0)", "\"defocus_panel\""] },
|
||||||
|
"exports": {
|
||||||
|
"store": { "type": "object", "methods": ["BPanelHasFocus", "BPanelSubviewHasFocus", "RemoveInputFocus"] }
|
||||||
|
},
|
||||||
|
"expects": ["m_entryAtTopOfStack.m_unPanelSGID"]
|
||||||
|
},
|
||||||
|
"localize": {
|
||||||
|
"module": { "includes": ["LocalizeStringFromFallback(", "m_mapFallbackTokens"] },
|
||||||
|
"exports": {
|
||||||
|
"localize": { "type": "function", "includes": [".LocalizeString(e)", "$s/g"] }
|
||||||
|
},
|
||||||
|
"expects": ["[\"en\",\"english\"],[\"de\",\"german\"],[\"fr\",\"french\"]", "for(let t of navigator.languages)"]
|
||||||
|
},
|
||||||
|
"controls": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["\"#AdditionalOptionsTooltip\"", "\"#FloatInWorld\""] },
|
||||||
|
"expects": ["icon:{enum:38},invocation:2", "\"#FloatInWorld\"),icon:{enum:26},invocation:1", "\"#ReturnToDashboard\"),icon:{enum:27}",
|
||||||
|
"\"#ViewInTheater\"),icon:{enum:28}", "icon:{enum:\"left\"==t?29:30}", "icon:{enum:22},icon_active:{enum:24}",
|
||||||
|
"icon:{enum:40},icon_active:{enum:39}", "onlyVisibleIn:\"additional-options\"", "controlAdditionalOptionsItems.length>0",
|
||||||
|
"SetDockLocation(l.yWq.World)"]
|
||||||
|
},
|
||||||
|
"closeIcon": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["icon_close_black.png"] },
|
||||||
|
"expects": ["case 31:"]
|
||||||
|
},
|
||||||
|
"frameControls": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["legacy-frame-controls-additional-options-anchor-", "controlAdditionalOptionsItems.filter("] },
|
||||||
|
"expects": ["id:`legacy-frame-controls-${", "id:`legacy-frame-controls-additional-options-${", "parent_id:p,translation:{x:-34*",
|
||||||
|
"\"ButtonControl\"", "onClick:()=>{", "control:e,key:e.action_id", "const v={x:0,y:.15,z:.05}"]
|
||||||
|
},
|
||||||
|
"panel": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["getNodeType(){return\"panel\"}"] },
|
||||||
|
"expects": ["addEmbeddedPanelUVs(this)", "\"embedded-uv-index\"", "uv_min", "\"curvature-origin-id\"", "\"only-visible-with-laser\"",
|
||||||
|
"\"meters-per-pixel\"", "\"sort-depth-bias\""]
|
||||||
|
},
|
||||||
|
"panelAnchor": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["getNodeType(){return\"panel-anchor\"}"] },
|
||||||
|
"expects": ["\"anchor-u\"", "\"anchor-v\""]
|
||||||
|
},
|
||||||
|
"sgApp": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["window.SGApp=this", "addEmbeddedPanelUVs("] },
|
||||||
|
"expects": ["removeEmbeddedPanelUVs(", "updateAllPanelBounds(){", "m_fCurrentScale", "t.forceLayoutUpdate=this.forceLayoutUpdate",
|
||||||
|
"e.updateLayoutValues()", "getEmbeddedIndex()"]
|
||||||
|
},
|
||||||
|
"sceneGraphDom": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["\"update_scene_graph\"", "case\"VSG-TRANSFORM\""] },
|
||||||
|
"expects": [".buildNode;if(", "\"parent-id\"", "\"translation\""]
|
||||||
|
},
|
||||||
|
"docking": {
|
||||||
|
"checkOnly": true,
|
||||||
|
"module": { "includes": ["SetInTheater(e=!0){", "GetTrackedDeviceIndexForControllerRole"] },
|
||||||
|
"expects": ["SetDockLocation(e){", "SetJustFloatedFromDashboard("]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,6 +46,9 @@
|
|||||||
# # window's More Options menu (or its bar button) to toggle, drag
|
# # window's More Options menu (or its bar button) to toggle, drag
|
||||||
# # up/down to adjust (with controller haptics):
|
# # up/down to adjust (with controller haptics):
|
||||||
# windowCurvature.enable = true;
|
# windowCurvature.enable = true;
|
||||||
|
# # Long press a control under a window (or a three-dot menu row) to
|
||||||
|
# # move it between the bar and the three-dot menu:
|
||||||
|
# frameControls.enable = true;
|
||||||
# };
|
# };
|
||||||
# firefox.enable = true; # launcher for the Firefox Flatpak
|
# firefox.enable = true; # launcher for the Firefox Flatpak
|
||||||
# # Hidden from the "+" menu (with Steam Developer Mode on):
|
# # Hidden from the "+" menu (with Steam Developer Mode on):
|
||||||
|
|||||||
Reference in new issue
Block a user