mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 01:00:06 +02:00
Merge branch 'experimental' into gaze-calibration
# Conflicts: # input-settings/ft_input_settings.py # input-settings/main.qml # input/input-relay.py
This commit is contained in:
commit
6ea70f32bc
9 files changed
+225
-83
No files matched your search
@@ -54,6 +54,7 @@ If you work in the desktop for long stretches, or leave the headset on a stand,
|
||||
| Set a screen to On your head (Frametop Display Settings, Visibility & pins) | Pins it to your head where it is, like a HUD. Grab its bar to move it; it stays on your head |
|
||||
| Save current arrangement… (Frametop Display Settings, Layout) | Saves where the screens are, with their sizes and pins, under a name. Pick a saved layout under Arrangement and press Arrange now to switch to it |
|
||||
| Meta+Shift+R in the desktop | Puts the screens back in their layout (also in the menu as Reset Screen Layout, and mappable to a mouse button) |
|
||||
| Meta+Shift+F over a desktop window | Floats that window in VR as a panel of its own, or puts it back on its screen if it floats. It acts on the window under the pointer, or the active one if the pointer is over the wallpaper. Rebind it, or map it to a mouse or controller button, in Frametop Input Settings (Keyboard page, or Buttons and Controllers as Float window in VR). Float in VR is also in every window's menu (Alt+F3) |
|
||||
| Meta+Shift+H in the desktop | Hides or shows all screens (also in the menu as Hide/Show Screens, and mappable). The Visibility & pins tab of Frametop Display Settings can instead show them only with the dashboard open, or while you look at your wrist |
|
||||
| Leave the headset on a stand | Its displays turn off once it has gone unused for the time set in Frametop Display Settings → Power, even if the stand covers its proximity sensor. Pick it up, or use any mouse, keyboard, or button, and they come back on |
|
||||
| Play a VR game | The screens hide and your controllers stay in the game. Open the SteamVR dashboard, or press Meta+Shift+H, to see and use them. To keep them visible over games, change During VR games on the Visibility & pins tab; the controllers still stay in the game, and you use the screens with the mouse or the dashboard |
|
||||
@@ -125,6 +126,7 @@ A Plasma session runs nested inside ft-screens (`screens/`), a small Wayland com
|
||||
| `screens/` | ft-screens, the compositor (wlroots and OpenVR). |
|
||||
| `session/` | The desktop session script and its config example. |
|
||||
| `layout/` | ft-layout: where the screens float, and their sizes. |
|
||||
| `float/` | Floating windows: ft-floatd and the KWin script that float a desktop window in VR. |
|
||||
| `input/` | The input relay (Bluetooth mice and keyboards, button maps). |
|
||||
| `pointer/` | The 3D mouse: SteamVR driver, helper service, and a probe tool. |
|
||||
| `power/` | ft-powerd: turns the displays off while the headset isn't used. |
|
||||
|
||||
+27
-10
@@ -10,7 +10,9 @@ Built so far (2026-09-29; the KWin side tested on the headless test desktop, `sc
|
||||
- The session adds `FLOAT_SLOTS` spares and starts ft-floatd from the desktop's autostart; ft-layout leaves the spares alone.
|
||||
- The 3D mouse's drag lock crosses onto other Frametop panels (not while carrying one).
|
||||
|
||||
Not built yet: phase 2 (the ghost, tear-off by dragging, push-flush docking), phase 3 (launching floating, the Frametop Apps entry and picker, remembered placement), and phase 4. Frametop Apps (decision 10) needs a per-screen hide, which ft-screens doesn't have yet: its hide and show are for all screens.
|
||||
Not built yet: phase 2 (the ghost, tear-off by dragging, push-flush docking), phase 3 (launching floating, Launch as Standalone, remembered placement), and phase 4.
|
||||
|
||||
Build order from 2026-09-30, each merged into `experimental` when done: (1) the float key and docking everything, (2) the title bar button, (3) phase 3, (4) hiding screens one at a time, (5) profiles (`docs/profiles.md`).
|
||||
|
||||
The goal is to let any desktop app float in VR in a panel of its own, like SteamVR's floating windows, while it stays part of the Frametop desktop. That means drag and drop, the clipboard, and focus keep working between floating windows and the screens.
|
||||
|
||||
@@ -20,7 +22,7 @@ The goal is to let any desktop app float in VR in a panel of its own, like Steam
|
||||
|
||||
## Decisions
|
||||
|
||||
Settled with the user on 2026-09-29. The sections below follow them.
|
||||
Settled with the user on 2026-09-29 (1 to 21) and 2026-09-30 (22 to 27). The sections below follow them.
|
||||
|
||||
| # | Question | Decision |
|
||||
|---|---|---|
|
||||
@@ -30,10 +32,10 @@ Settled with the user on 2026-09-29. The sections below follow them.
|
||||
| 4 | Docking by dragging | Push the window flush against a screen (within about 10 cm), with the landing spot highlighted, and let go |
|
||||
| 5 | Visibility | Floating windows follow the same rules as the screens: the hide hotkey, the visibility modes, and the games rule |
|
||||
| 6 | Windows a floating app opens | They float too |
|
||||
| 7 | Launching floating from the headset | One "Frametop Apps" launcher entry with a picker |
|
||||
| 7 | Launching floating from the headset | ~~One "Frametop Apps" launcher entry with a picker~~ Replaced by 26 and profiles (27) |
|
||||
| 8 | Build order | The catcher first, as a fix that stands on its own; `pointer-ignore` and `layouts-headpin` merged before phase 1; the hand cutouts stay out |
|
||||
| 9 | Show Desktop (Meta+D) | Floating windows stay |
|
||||
| 10 | Frametop Apps and visibility | The entry starts the desktop with each screen hidden on its own (the existing per-screen hide), so only floating windows show. No special mode |
|
||||
| 10 | Frametop Apps and visibility | ~~The entry starts the desktop with each screen hidden on its own, so only floating windows show~~ Replaced: a profile can hide screens (27) |
|
||||
| 11 | Window frame | KWin's title bar and border stay. Frametop's bar, close, and "back to desktop" are extras |
|
||||
| 12 | Resizing | The window's own edges and Frametop's corner tab both change the size in pixels at the same density; the output follows |
|
||||
| 13 | Margin | 300 px on each side, configurable |
|
||||
@@ -44,7 +46,13 @@ Settled with the user on 2026-09-29. The sections below follow them.
|
||||
| 18 | Bigger text | A scale for each window (KWin's output scale): Meta+scroll over the window, or +/- on its bar. Remembered for each app |
|
||||
| 19 | Switching to a window you can't see | It's focused, and a glow at the edge of your view points to it. Moving it in front of you is a setting |
|
||||
| 20 | Full screen | The window fills its own panel. The margin drops to zero while it's full screen, and the panel keeps its size and place |
|
||||
| 21 | Named layouts | They cover the screens only. Floating windows use the placement remembered for each app |
|
||||
| 21 | Named layouts | ~~They cover the screens only~~ Replaced by profiles (27). Outside a profile, floating windows use the placement remembered for each app |
|
||||
| 22 | The float key | One toggle: it floats a window, or docks it if it already floats. The input relay owns it (`float_toggle`), Meta+Shift+F by default, rebindable in Frametop Input Settings and mappable to mouse and controller buttons. KWin has no shortcut of its own for it, so one press can't fire twice |
|
||||
| 23 | Which window the key acts on | The window under the desktop's pointer; the active window if there's none there (the wallpaper, the taskbar) |
|
||||
| 24 | Docking everything | A `dock_all` action, with no default binding |
|
||||
| 25 | A button on every window | A float button left of Close in the title bar, from Frametop's own QML window decoration, made to look like Breeze. It shows a dock icon on floating windows. Apps that draw their own title bar (Chromium, Electron, GTK) use the key |
|
||||
| 26 | Launching one app floating | "Launch as Standalone" in the right-click menu of every app in the Application Launcher and the taskbar, from copies of the apps' desktop files that only the Frametop desktop reads. It replaces the Frametop Apps entry (7, 10) |
|
||||
| 27 | Profiles | Named layouts become profiles: the screens' places, which screens show, and the apps and their windows, floating or not. See `docs/profiles.md` |
|
||||
|
||||
Also assumed: floating windows get the wrist pin, the head pin, and pass-through (`pointer-ignore`) like screens. Every gesture works with the controllers as well as the 3D mouse. A window launched floating uses the primary screen's density. VNC shows only the primary screen, as now. Anything that restarts the live desktop waits for the user's OK.
|
||||
|
||||
@@ -121,13 +129,22 @@ Program names stay within 15 characters (`ft-floatd`). Overlay keys are `frameto
|
||||
- **Dragging.** Carry the floating window, by its title bar or its bar, until the spot you're pointing at is on a screen. Then push it flush with the screen, within about 10 cm of its surface: scroll away with the mouse, or move the controller forward. The screen shows where the window will land, and letting go docks it there at its current size in pixels, shrunk to fit if the screen is smaller. A carried panel keeps its distance, so moving a floating window in front of a screen never docks it by accident.
|
||||
- Docking disables the output and removes the panel.
|
||||
|
||||
## Getting at it: the float key and the title bar button
|
||||
|
||||
Settled on 2026-09-30 (decisions 22 to 25).
|
||||
|
||||
- **The float key.** The input relay owns it: the action `float_toggle`, bound to Meta+Shift+F unless the rules file says otherwise (a rules file with no `key_bindings` gets that default; one with its own list, even an empty one, doesn't). It can be rebound or removed in Frametop Input Settings, and mapped to a mouse button or a Frame controller button like any other action. The relay takes the combination before it reaches the desktop and sends `float pointer` to ft-floatd, which asks the script for the window under KWin's pointer (`workspace.cursorPos`, top of `workspace.stackingOrder`, popups and dialogs counting as their parent). With none there, the wallpaper or the taskbar, it's the active window. KWin's pointer is where the 3D mouse or a laser last was on a Frametop panel. A window that floats docks; any other floats. The KWin script no longer registers a shortcut of its own, so one press can't float a window and dock it again.
|
||||
- **Docking everything.** `dock_all` (no default binding) sends `dock all`, which docks every floating window where it came from.
|
||||
- **The title bar button.** Breeze can't take a button of its own, and a C++ fork of it would have to match SteamOS's exact KDecoration build (Plasma 6.3 replaces KDecoration2 with KDecoration3). So the Frametop desktop gets its own window decoration, written in QML for KWin's Aurorae engine, which loads it without compiling (`decoration/`, installed to `~/.local/share/kwin/decorations/kwin4_decoration_qml_frametop`, chosen in the session's `kwinrc` only, so Desktop Mode keeps Breeze). It's drawn to look like Breeze, with a float button left of Close. The button calls `requestToggleKeepBelow()`, the one window request a decoration can make that has no visible effect here, and the KWin script reads the change: keep-below set on a window on the screens floats it, cleared on a floating window docks it. The script keeps keep-below set on every floating window, however it was floated, so the button shows its dock icon there. A window alone on its own output loses nothing by being kept below (only the wallpaper is under it). If the window can't float (every spare is in use), the script clears the flag again. Keep Below Others in a window's menu does the same as the button.
|
||||
- **Apps that draw their own title bar** (Chromium and Electron apps, GTK apps) never show KWin's decoration, so they don't get the button. They use the key, or the window menu (Alt+F3).
|
||||
|
||||
## Launching an app floating
|
||||
|
||||
- **In the desktop.** Use "Float in VR" in any window's menu, or press Meta+Shift+F for the active window.
|
||||
- **In the desktop.** Use "Float in VR" in any window's menu, its title bar button, or the float key.
|
||||
- **From the menu.** Right-click an app in the Application Launcher, or in the taskbar (where it starts another window of that app), and pick "Launch as Standalone" (decision 26). The launcher has no way to add an entry to every app's menu, but its menu shows each app's own desktop actions. So the Frametop desktop reads copies of the apps' desktop files with one more action added. They're written to `~/.local/share/frametop/apps/applications` from every desktop file in `XDG_DATA_DIRS`: by the session script before Plasma starts, and by ft-floatd whenever an app is installed, changed, or removed. The session puts `~/.local/share/frametop/apps` first in `XDG_DATA_DIRS`. Plasma's app cache is keyed by those directories, so Desktop Mode never sees the copies. Desktop files in `~/.local/share/applications` come before every data dir, so an app you've customized there keeps your copy and has no Launch as Standalone. The action runs `ft-float launch <desktop file name>`.
|
||||
- **From a command.** `ft-float run <command>` and `ft-float launch <app.desktop>` start an app and float its first window. ft-floatd records the process it started, and the script matches new windows by PID, including child processes. Some single-instance apps (Firefox, D-Bus-activated apps) open the window from a process that was already running. Those are matched by desktop file name within a few seconds, or by `XDG_ACTIVATION_TOKEN` where the app honors it.
|
||||
- **From the headset without the desktop open.** One new launcher entry, "Frametop Apps". It starts the Frametop session with each screen hidden on its own (the per-screen hide that already exists), and opens an app picker as a floating window. Picking an app launches it floating. The hide hotkey and visibility modes still apply to everything, and a screen comes back with one click. Everything runs in one session, so dragging between a standalone app and a desktop app works. If the desktop is already running, the entry just opens the picker.
|
||||
- **The picker.** Either KRunner, floated, or a small Kirigami app like the settings apps, with a grid of apps and their icons.
|
||||
- **Remembered placement.** Each app's last floating pose, size, and scale, keyed by desktop file name. Named layouts don't include floating windows.
|
||||
- **From the headset with the desktop off.** A profile's launcher entry starts the desktop in that profile, and a profile can hide every screen and hold only floating apps (`docs/profiles.md`). There's no separate Frametop Apps entry or picker.
|
||||
- **Remembered placement.** Each app's last floating pose, size, and scale, keyed by desktop file name. A profile's own placement wins when the profile opens the app.
|
||||
|
||||
## Drag and drop between panels
|
||||
|
||||
@@ -196,7 +213,7 @@ The ghost and tear-off, and the dock highlight with push-flush docking.
|
||||
|
||||
### Phase 3: launch floating
|
||||
|
||||
`ft-float run` and `ft-float launch`, window matching, new windows of floating apps, the Frametop Apps launcher entry, the picker, remembered placement, and the notification when every spare is in use.
|
||||
`ft-float run` and `ft-float launch`, window matching, new windows of floating apps, Launch as Standalone, remembered placement, and the notification when every spare is in use.
|
||||
|
||||
### Phase 4: polish
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Profiles (plan)
|
||||
|
||||
Status: design settled with the user on 2026-09-30; not built yet. Profiles come last in the build order in `docs/floating-windows.md`, after launching apps floating (phase 3 there) and hiding screens one at a time.
|
||||
|
||||
A profile is a named layout that also opens apps. It holds:
|
||||
|
||||
- where each screen goes, with its size in metres, curve, roll, and pin (what a named layout holds today);
|
||||
- which screens show and which are hidden;
|
||||
- the apps, one entry per window: on a screen at a place and size, or floating at a pose, size, and scale.
|
||||
|
||||
So a "Work" profile can put three screens around you with a browser, two terminals, and an editor on them, and a "Couch" profile can hide every screen and float one video player in front of you.
|
||||
|
||||
## Decisions
|
||||
|
||||
| # | Question | Decision |
|
||||
|---|---|---|
|
||||
| 1 | Profiles and named layouts | One list. Named layouts grow into profiles; a layout saved before profiles existed is a profile with no apps and every screen shown |
|
||||
| 2 | Making one | Capture what's open: the screens and every app's windows. Display Settings lists a profile's apps, so one can be removed. No editor beyond that |
|
||||
| 3 | Apps with several windows | One entry per window. The app is launched once; when its first window shows up, it's launched again for each window still missing. A browser that restores its own windows gets launched once, a terminal twice |
|
||||
| 4 | What's recorded of an app | Its desktop file name, or its command line if it has none. Not what it had open: tabs, files, and folders are left to the app's own restore |
|
||||
| 5 | Switching while apps are open | Additive: launch what's missing, move the windows that match into place, leave the rest alone. Nothing is ever closed |
|
||||
| 6 | Saving changes | Only on an explicit save. Moving things after switching doesn't change the profile |
|
||||
| 7 | Starting one | Four ways: a default profile when the desktop starts, Display Settings, a launcher entry for each profile, and a mappable action |
|
||||
| 8 | Plasma's session restore | Off in the Frametop session, so a profile is the only thing that reopens apps |
|
||||
| 9 | Screen count and resolution | Not part of a profile. They're global, because changing them restarts the desktop, which closes every window |
|
||||
|
||||
## Where profiles live
|
||||
|
||||
`~/.config/frametop-layout.json` keeps its `layouts` as they are (`{"Work": [screen places]}`), so older copies of ft-layout (the `main` checkout) still read it. What a profile adds goes in a parallel `profiles` map under the same names:
|
||||
|
||||
```
|
||||
"layouts": {"Work": [{"pos": ..., "face": ..., "roll": ..., "metres": ..., "curve": ..., "pin": ...}, ...]},
|
||||
"profiles": {"Work": {"hidden": [3],
|
||||
"windows": [
|
||||
{"app": "org.kde.konsole", "screen": 2, "rect": [40, 60, 1200, 800], "maximized": false},
|
||||
{"app": "com.brave.Browser", "screen": 1, "maximized": true},
|
||||
{"cmd": ["/opt/tool/run"], "class": "tool", "screen": 1, "rect": [...]},
|
||||
{"app": "org.kde.dolphin", "float": {"pos": [...], "face": [...], "roll": 0,
|
||||
"pixels": [1400, 900], "scale": 1.2, "pin": ...}}]}},
|
||||
"default_profile": "Work"
|
||||
```
|
||||
|
||||
- `screen` is 1-based, as everywhere in Frametop. `rect` is the window's frame in KWin's logical units, relative to its screen's output, so it survives the screens being arranged differently.
|
||||
- A floating window's place is relative to your head when the profile is applied, the same way the screens' places are (position, and the direction you face, yaw only).
|
||||
- Renaming or deleting a layout renames or deletes its profile entry with it.
|
||||
|
||||
## How it works
|
||||
|
||||
- **Capture** (`ft-layout save NAME`, and Save current arrangement… in Display Settings). ft-layout captures the screens as today, then asks ft-floatd for the windows (`windows` on @frametop_float). ft-floatd answers from the KWin script's table: every normal window with its desktop file name, output, frame, and maximized state, and for floating windows their panel's pose from ft-screens, their size in pixels, and their scale. Windows with no desktop file name are kept by their process's command line (`/proc/<pid>/cmdline`) and window class. Windows of Plasma itself, the Frametop settings apps, and dialogs aren't recorded.
|
||||
- **Apply** (`ft-layout use NAME`). ft-layout arranges the screens as today, hides and shows them (the per-screen hide in ft-screens), then hands the profile's windows to ft-floatd (`profile NAME`). ft-floatd goes through the entries app by app. It claims windows of that app already open (oldest first, each claimed once), and moves each to its entry's place: onto its screen at its rect (or maximized), or floating at its pose. For the entries left over, it launches the app (`ft-float launch`, the same path as Launch as Standalone), places windows as they show up, and launches again for each one still missing once the first has appeared. It gives up on an app after 30 seconds and notifies.
|
||||
- **Default at start.** `default_profile` replaces today's "arrange when the desktop starts": the session's autostart runs `ft-layout start`, which applies the default profile (screens and apps), or just arranges the screens if there's none. Plasma's session restore is turned off in the session (`ksmserverrc`: `loginMode=emptySession`).
|
||||
- **Launcher entries.** Each profile gets `~/.local/share/applications/frametop-profile-<name>.desktop` ("Frametop: Work"), written when it's saved and removed when it's deleted. They show in SteamVR's Launch a program list, the Application Launcher, and KRunner. Running one (`ft-layout open NAME`) switches to that profile if the desktop runs, or starts the desktop with `FT_PROFILE` set, which overrides `default_profile` for that start.
|
||||
- **The action.** `profile:NAME` in the input relay, for key combinations, mouse buttons, and controller buttons. Input Settings lists one entry per profile.
|
||||
- **Display Settings.** The Layout page becomes Profiles: the list (switch, rename, delete, save the current state over one), each profile's apps with a remove button, which screens it hides, and which profile the desktop starts with.
|
||||
|
||||
## Open for when it's built
|
||||
|
||||
- How long to wait for an app's first window before launching it again (slow apps would open twice). Start at waiting for the first window, up to 30 seconds, then 3 seconds more for windows it restores itself.
|
||||
- Apps that are D-Bus activated or single-instance open their window from a process that was already running; matching them is phase 3's job (`docs/floating-windows.md`, Launching an app floating).
|
||||
+25
-4
@@ -2,7 +2,7 @@
|
||||
// loads it into the desktop's KWin over D-Bus (org.kde.kwin.Scripting) and talks to it:
|
||||
// - events go to ft-floatd as JSON strings (org.frametop.Float.Event), for the windows it
|
||||
// cares about: floating windows (the ones on a spare output, WL-<screens> and up), their
|
||||
// popups and dialogs, new windows, and "Float in VR" requests;
|
||||
// popups and dialogs, new windows, and requests to float or dock one;
|
||||
// - commands come back through a long poll: the script calls NextCommand, ft-floatd
|
||||
// answers when it has one (or after a while with nothing), and the script calls again.
|
||||
// KWin scripts can call D-Bus but can't serve it, hence the poll. Window ids are KWin's
|
||||
@@ -104,8 +104,26 @@ registerUserActionsMenu(w => {
|
||||
triggered: () => requestFloat(w)
|
||||
};
|
||||
});
|
||||
registerShortcut("Frametop Float Window", "Frametop: Float Window in VR (or put it back)", "Meta+Shift+F",
|
||||
() => requestFloat(workspace.activeWindow));
|
||||
// The float key is the input relay's (float_toggle, Meta+Shift+F by default): it reaches us as
|
||||
// "request-pointer". No shortcut of KWin's own, so one press can't float a window and dock it again.
|
||||
|
||||
// The window under KWin's pointer (where the 3D mouse or a laser last was on a panel): the top
|
||||
// one there, a popup or dialog standing for the window it belongs to. Null over the wallpaper
|
||||
// or the taskbar.
|
||||
function underPointer() {
|
||||
const p = workspace.cursorPos;
|
||||
const order = workspace.stackingOrder;
|
||||
for (let i = order.length - 1; i >= 0; --i) {
|
||||
const w = order[i];
|
||||
if (w.deleted || w.minimized || w.hidden || !w.managed) continue;
|
||||
const g = w.frameGeometry;
|
||||
if (p.x < g.x || p.y < g.y || p.x >= g.x + g.width || p.y >= g.y + g.height) continue;
|
||||
let top = w;
|
||||
for (let n = 0; top.transientFor && n < 10; ++n) top = top.transientFor;
|
||||
return top.normalWindow && !top.popupWindow ? top : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function run(c) {
|
||||
const w = c.id ? byId(c.id) : null;
|
||||
@@ -140,9 +158,12 @@ function run(c) {
|
||||
case "info":
|
||||
if (w) report("window", w);
|
||||
break;
|
||||
case "request-active": // ft-float float|dock active: like the shortcut
|
||||
case "request-active": // ft-float float|dock active
|
||||
requestFloat(workspace.activeWindow);
|
||||
break;
|
||||
case "request-pointer": // the float key: the window under the pointer, else the active one
|
||||
requestFloat(underPointer() || workspace.activeWindow);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"""ft-float: talk to ft-floatd (floating windows in the Frametop desktop).
|
||||
|
||||
ft-float float [ID|active] float a window (default: the active one; for it, a toggle)
|
||||
ft-float float pointer the float key: float the window under the pointer (else the
|
||||
active one), or put it back if it floats
|
||||
ft-float dock [ID|active] put a floating window back on the desktop
|
||||
ft-float dock all put every floating window back
|
||||
ft-float close ID close a window
|
||||
ft-float list the spare outputs and what floats on them
|
||||
FT_FLOAT_SOCKET names ft-floatd's socket (default frametop_float).
|
||||
|
||||
+18
-1
@@ -11,7 +11,9 @@ which window floats on which spare output and panel, and connects three parts:
|
||||
- kscreen-doctor, to turn spare outputs on and off and place them in KWin's layout.
|
||||
Commands and ft-screens' events arrive as datagrams on @frametop_float (ft-float is the
|
||||
command-line side). Replies go to the sender:
|
||||
float [ID|active] dock [ID|active] close ID list quit (ft-float)
|
||||
float [ID|active|pointer] dock [ID|active|all] close ID list quit (ft-float)
|
||||
("float pointer" is the float key: the window under the pointer, else the active one,
|
||||
floated or docked; the input relay sends it for float_toggle, and "dock all" for dock_all)
|
||||
dock N | close N | resize N W H | scale N STEPS (ft-screens, N = its screen)
|
||||
|
||||
Spare outputs are WL-<screens> .. WL-<screens + slots - 1>. A floating window's output is its
|
||||
@@ -197,6 +199,14 @@ class Daemon:
|
||||
raise RuntimeError("KWin didn't load the script")
|
||||
bus.get_object("org.kde.KWin", f"/Scripting/Script{sid}").run(dbus_interface="org.kde.kwin.Script")
|
||||
log(f"script loaded ({sid})")
|
||||
# Older scripts registered the float key with KWin; the input relay owns it now. Drop
|
||||
# that shortcut, so System Settings doesn't list one that does nothing.
|
||||
try:
|
||||
accel = dbus.Interface(bus.get_object("org.kde.kglobalaccel", "/kglobalaccel"), "org.kde.KGlobalAccel")
|
||||
if accel.unregister("kwin", "Frametop Float Window"):
|
||||
log("dropped KWin's old float shortcut")
|
||||
except dbus.DBusException:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------ events from the script
|
||||
|
||||
@@ -520,6 +530,13 @@ class Daemon:
|
||||
if cmd in ("float", "dock") and (not rest or rest[0] == "active"):
|
||||
self.command(cmd="request-active")
|
||||
return "ok"
|
||||
if cmd == "float" and rest == ["pointer"]:
|
||||
self.command(cmd="request-pointer")
|
||||
return "ok"
|
||||
if cmd == "dock" and rest == ["all"]:
|
||||
for f in list(self.floats.values()):
|
||||
self.dock(f)
|
||||
return "ok"
|
||||
if cmd in ("dock", "close", "resize", "scale") and rest and rest[0].isdigit():
|
||||
f = self.by_panel(int(rest[0]))
|
||||
if not f:
|
||||
|
||||
@@ -9,6 +9,8 @@ to the input relay over its control socket (@frametop_relay):
|
||||
- Controllers: the same for the Frame controllers' buttons, minus the gaze actions (gaze
|
||||
mode is a mouse feature). They're read by the pointer helper through SteamVR input
|
||||
(@ft_pointer_helper: vrstatus, vrglobal), and a mapped button is taken from games.
|
||||
- Keyboard: when Frametop's keyboard opens, and key combinations for any action (Meta+Shift+F
|
||||
floats a window unless the rules have their own list).
|
||||
- Pointer: speed, dot size, distance and the rest, applied live.
|
||||
- Ignored panels: SteamVR overlays the pointer passes through (POINTER_IGNORE), by app or
|
||||
one by one. The helper lists them (@ft_pointer_helper "overlays").
|
||||
@@ -68,6 +70,7 @@ ACTION_LABELS = {
|
||||
"sens_up": "Faster pointer",
|
||||
"sens_down": "Slower pointer", "layout_reset": "Reset desktop screen layout",
|
||||
"screens_toggle": "Hide/show desktop screens", "keyboard_toggle": "Open/close keyboard",
|
||||
"float_toggle": "Float window in VR / put it back", "dock_all": "Put all floating windows back",
|
||||
"key": "Pass through as key",
|
||||
"none": "Do nothing",
|
||||
}
|
||||
@@ -96,8 +99,6 @@ CONTROLLER_ACTIONS = [a for a in ACTION_LABELS if a not in ("key", "none") + GAZ
|
||||
# Key combinations take any action but key and none; the keyboard clicks only work there.
|
||||
SHORTCUT_ACTIONS = [a for a in ACTION_LABELS if a not in ("key", "none")]
|
||||
KEYBOARD_ONLY = ("gaze_left", "gaze_right")
|
||||
# The relay's DEFAULT_KEY_BINDINGS: used while the rules file has no "key_bindings" (Meta+J, Meta+K).
|
||||
DEFAULT_KEY_BINDINGS = {"125+36": "gaze_left", "125+37": "gaze_right"}
|
||||
# Gaze mode settings (pointer helper), like POINTER_SETTINGS.
|
||||
GAZE_SETTINGS = [
|
||||
("POINTER_GAZE_RETAKE", "Look away to hand back", 5, 1, 45, 0.5, "°"),
|
||||
@@ -113,6 +114,16 @@ POINTER_ROLES = {"right": "Right hand", "left": "Left hand", "stylus": "Stylus (
|
||||
# Key combinations ("key_bindings" in the rules): modifiers, either side folded into the left code.
|
||||
MODIFIER_CODES = {29: 29, 97: 29, 42: 42, 54: 42, 56: 56, 100: 56, 125: 125, 126: 125}
|
||||
MODIFIER_NAMES = {29: "Ctrl", 42: "Shift", 56: "Alt", 125: "Meta"}
|
||||
# What a rules file without "key_bindings" gets (the relay's DEFAULT_KEY_BINDINGS): Meta+J and
|
||||
# Meta+K click at the gaze, Meta+Shift+F floats a window.
|
||||
DEFAULT_KEY_BINDINGS = {"125+36": "gaze_left", "125+37": "gaze_right", "42+125+33": "float_toggle"}
|
||||
|
||||
|
||||
def key_bindings(rules):
|
||||
"""The rules' key combinations, or the defaults if it has none of its own (an empty list
|
||||
counts as its own)."""
|
||||
bound = rules.get("key_bindings")
|
||||
return dict(bound) if isinstance(bound, dict) else dict(DEFAULT_KEY_BINDINGS)
|
||||
# The gaze service's settings (gaze/ft-gazed): whose eye tracking, and the eye bias.
|
||||
GAZE_TRACKERS = {"steam": "SteamVR's eye tracker", "own": "our own eye tracker"}
|
||||
GAZE_EYES = {"auto": "auto", "left": "left eye", "right": "right eye"}
|
||||
@@ -851,7 +862,7 @@ class Backend(QObject):
|
||||
|
||||
@Property("QVariantList", notify=mappingsChanged)
|
||||
def keyShortcuts(self):
|
||||
bound = self._key_bindings(read_json(RULES_PATH))
|
||||
bound = key_bindings(read_json(RULES_PATH))
|
||||
return [{"combo": c, "label": self.comboName(c), "action": a, "actionLabel": ACTION_LABELS.get(a, a)}
|
||||
for c, a in sorted(bound.items())]
|
||||
|
||||
@@ -859,12 +870,6 @@ class Backend(QObject):
|
||||
def shortcutActions(self):
|
||||
return [{"value": a, "text": ACTION_LABELS[a]} for a in SHORTCUT_ACTIONS]
|
||||
|
||||
@staticmethod
|
||||
def _key_bindings(rules):
|
||||
"""The key combinations in effect: the defaults until the rules file has its own."""
|
||||
bound = rules.get("key_bindings")
|
||||
return dict(DEFAULT_KEY_BINDINGS) if bound is None else bound
|
||||
|
||||
@Property(bool, notify=shortcutCaptureChanged)
|
||||
def capturingShortcut(self):
|
||||
return bool(self._capture_combo)
|
||||
@@ -884,15 +889,14 @@ class Backend(QObject):
|
||||
|
||||
def _save_shortcut(self, combo, action):
|
||||
rules = read_json(RULES_PATH)
|
||||
rules["key_bindings"] = self._key_bindings(rules) # the defaults stay when the first one is added
|
||||
rules["key_bindings"][combo] = action
|
||||
rules["key_bindings"] = dict(key_bindings(rules), **{combo: action})
|
||||
self._save_rules(rules)
|
||||
self.message.emit(f"{self.comboName(combo)} → {ACTION_LABELS[action]}", False)
|
||||
|
||||
@Slot(str)
|
||||
def removeShortcut(self, combo):
|
||||
rules = read_json(RULES_PATH)
|
||||
rules["key_bindings"] = self._key_bindings(rules)
|
||||
rules["key_bindings"] = key_bindings(rules)
|
||||
rules["key_bindings"].pop(combo, None)
|
||||
self._save_rules(rules)
|
||||
self.message.emit(f"{self.comboName(combo)} removed", False)
|
||||
|
||||
+44
-43
@@ -538,6 +538,49 @@ Kirigami.ApplicationWindow {
|
||||
Kirigami.FormData.label: "Keyboards connected:"
|
||||
text: kpage.keyboards.length ? kpage.keyboards.map(d => d.name).join(", ") : "none"
|
||||
}
|
||||
Kirigami.Separator { Kirigami.FormData.isSection: true; Kirigami.FormData.label: "Key combinations" }
|
||||
|
||||
Repeater {
|
||||
model: backend.keyShortcuts
|
||||
delegate: RowLayout {
|
||||
required property var modelData
|
||||
Kirigami.FormData.label: modelData.label + ":"
|
||||
Controls.Label { text: modelData.actionLabel }
|
||||
Controls.ToolButton {
|
||||
icon.name: "edit-delete"
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
text: "Remove"
|
||||
Controls.ToolTip.text: text
|
||||
Controls.ToolTip.visible: hovered
|
||||
onClicked: backend.removeShortcut(modelData.combo)
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Kirigami.FormData.label: "New:"
|
||||
Controls.ComboBox {
|
||||
id: shortcutAction
|
||||
model: backend.shortcutActions
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
Component.onCompleted: currentIndex = indexOfValue("float_toggle")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 16
|
||||
}
|
||||
Controls.Button {
|
||||
text: backend.capturingShortcut ? "Press the keys… (Cancel)" : "Set keys…"
|
||||
onClicked: backend.capturingShortcut ? backend.cancelShortcutCapture()
|
||||
: backend.startShortcutCapture(shortcutAction.currentValue)
|
||||
}
|
||||
}
|
||||
Controls.Label {
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
|
||||
wrapMode: Text.WordWrap
|
||||
text: "Hold the modifiers (Ctrl, Alt, Shift, Meta), then press the key, on any keyboard. The "
|
||||
+ "combination's last key isn't typed; the modifiers still reach the app. Meta+Shift+F floats "
|
||||
+ "the desktop window under the pointer in VR, or puts it back, until you remove or change it."
|
||||
opacity: 0.7
|
||||
font: Kirigami.Theme.smallFont
|
||||
}
|
||||
}
|
||||
|
||||
footer: Controls.Label {
|
||||
@@ -846,49 +889,7 @@ Kirigami.ApplicationWindow {
|
||||
+ "default): tap to click where you look, or hold, turn your head until the dot sits on the "
|
||||
+ "target, and let go; hold still to drag. The Frame controllers don't take part, and moving "
|
||||
+ "one hands the pointer back to them. Gaze pointer on/off, Gaze precision, and Gaze drag can "
|
||||
+ "also go on a mouse button (Buttons page)."
|
||||
opacity: 0.7
|
||||
font: Kirigami.Theme.smallFont
|
||||
}
|
||||
Kirigami.Separator { Kirigami.FormData.isSection: true; Kirigami.FormData.label: "Key combinations" }
|
||||
|
||||
Repeater {
|
||||
model: backend.keyShortcuts
|
||||
delegate: RowLayout {
|
||||
required property var modelData
|
||||
Kirigami.FormData.label: modelData.label + ":"
|
||||
Controls.Label { text: modelData.actionLabel }
|
||||
Controls.ToolButton {
|
||||
icon.name: "edit-delete"
|
||||
display: Controls.AbstractButton.IconOnly
|
||||
text: "Remove"
|
||||
Controls.ToolTip.text: text
|
||||
Controls.ToolTip.visible: hovered
|
||||
onClicked: backend.removeShortcut(modelData.combo)
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Kirigami.FormData.label: "New:"
|
||||
Controls.ComboBox {
|
||||
id: shortcutAction
|
||||
model: backend.shortcutActions
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
Component.onCompleted: currentIndex = indexOfValue("gaze_toggle")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 16
|
||||
}
|
||||
Controls.Button {
|
||||
text: backend.capturingShortcut ? "Press the keys… (Cancel)" : "Set keys…"
|
||||
onClicked: backend.capturingShortcut ? backend.cancelShortcutCapture()
|
||||
: backend.startShortcutCapture(shortcutAction.currentValue)
|
||||
}
|
||||
}
|
||||
Controls.Label {
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 30
|
||||
wrapMode: Text.WordWrap
|
||||
text: "Hold the modifiers (Ctrl, Alt, Shift, Meta), then press the key, on any keyboard. The "
|
||||
+ "combination's last key isn't typed; the modifiers still reach the app."
|
||||
+ "also go on a mouse button (Buttons page) or another key combination (Keyboard page)."
|
||||
opacity: 0.7
|
||||
font: Kirigami.Theme.smallFont
|
||||
}
|
||||
|
||||
+31
-13
@@ -29,16 +29,21 @@ the release clicks; held still for half a second, it's a real press that your he
|
||||
("gazekey left|right 1|0" to the helper; by default Meta+J and Meta+K, DEFAULT_KEY_BINDINGS),
|
||||
gaze_quickcal = the gaze service's one-dot check ("quickcal" to @ft_gazed), sens_up, sens_down,
|
||||
layout_reset = put the desktop screens back in their saved layout, screens_toggle = hide or show the desktop screens,
|
||||
keyboard_toggle = open or close Frametop's keyboard, key = pass through as a key, none).
|
||||
keyboard_toggle = open or close Frametop's keyboard, float_toggle = float the desktop window under the
|
||||
pointer (else the active one) in VR, or put it back if it floats, dock_all = put every floating
|
||||
window back (both to ft-floatd, @frametop_float), key = pass through as a key, none).
|
||||
|
||||
Frame controller buttons can be mapped too ("controller_buttons": {"right/a": action} in the
|
||||
rules file; any action but key and the gaze ones, GAZE_ACTIONS: gaze mode is a mouse feature,
|
||||
docs/gaze-controllers.md). So can key combinations on any keyboard ("key_bindings":
|
||||
{"29+56+34": action}, evdev codes joined by "+", modifiers first and left-hand codes for
|
||||
either side, here Ctrl+Alt+G): the combination does the action, and its last key isn't typed.
|
||||
A combination with Meta also sends the desktop an F24 press and Meta's release right away: so
|
||||
letting go of Meta doesn't open Plasma's launcher, and a gaze click isn't Meta+click (KWin's
|
||||
window move and resize). Another key while Meta is still held gives the desktop Meta back. The controllers aren't input devices here, only SteamVR sees
|
||||
A rules file without "key_bindings" gets DEFAULT_KEY_BINDINGS (Meta+J: gaze_left, Meta+K:
|
||||
gaze_right, Meta+Shift+F: float_toggle); one with its own, even an empty one, doesn't. The float
|
||||
actions work without pointer mode too. A combination with Meta also sends the desktop an F24 press
|
||||
and Meta's release right away: so letting go of Meta doesn't open Plasma's launcher, and a gaze
|
||||
click isn't Meta+click (KWin's window move and resize). Another key while Meta is still held gives
|
||||
the desktop Meta back. The controllers aren't input devices here, only SteamVR sees
|
||||
them, so the pointer helper reads them with SteamVR input and sends "vrbtn <button> 1|0".
|
||||
It only takes the buttons the relay tells it to ("vrbind <button>..." to @ft_pointer_helper,
|
||||
sent on start, reload, and when the helper says "vrhello"), and only while no game runs,
|
||||
@@ -182,12 +187,13 @@ VIRTUAL_PREFIX = "frametop virtual"
|
||||
RULES_PATH = os.path.expanduser("~/.config/frametop-input.json")
|
||||
ACTIONS = ("left", "right", "middle", "back", "scroll_up", "scroll_down", "dashboard", "recenter",
|
||||
"pointer_toggle", "follow_toggle", "gaze_toggle", "gaze_precision", "gaze_drag", "gaze_left", "gaze_right",
|
||||
"gaze_quickcal", "sens_up", "sens_down", "layout_reset", "screens_toggle", "keyboard_toggle", "key", "none")
|
||||
"gaze_quickcal", "sens_up", "sens_down", "layout_reset", "screens_toggle", "keyboard_toggle", "float_toggle",
|
||||
"dock_all", "key", "none")
|
||||
# Gaze mode is a mouse feature: these never come from a controller button (docs/gaze-controllers.md).
|
||||
GAZE_ACTIONS = ("gaze_toggle", "gaze_precision", "gaze_drag", "gaze_left", "gaze_right", "gaze_quickcal")
|
||||
# Key combinations when the rules file has none: Meta+J and Meta+K click at the gaze (free on the
|
||||
# Frametop desktop, and apps don't use Meta).
|
||||
DEFAULT_KEY_BINDINGS = {"125+36": "gaze_left", "125+37": "gaze_right"}
|
||||
# Key combinations a rules file without "key_bindings" gets: Meta+J and Meta+K click at the gaze
|
||||
# (free on the Frametop desktop, and apps don't use Meta), Meta+Shift+F floats a window.
|
||||
DEFAULT_KEY_BINDINGS = {"125+36": "gaze_left", "125+37": "gaze_right", "42+125+33": "float_toggle"}
|
||||
KEY_F24 = 194 # sent to the desktop with a Meta combination (see the top)
|
||||
# Key combinations ("key_bindings"): modifiers, each side's code folded into the left one's.
|
||||
MODIFIERS = {29: 29, 97: 29, 42: 42, 54: 42, 56: 56, 100: 56, 125: 125, 126: 125}
|
||||
@@ -200,6 +206,9 @@ VR_BUTTONS = ("left/view", "left/dpad_up", "left/dpad_down", "left/dpad_left", "
|
||||
VR_DEVICE = "frame_controller" # the id controller buttons have in watch events
|
||||
SCREENS = "\0ft_screens"
|
||||
GAZED = "\0ft_gazed"
|
||||
FLOAT = "\0frametop_float" # ft-floatd, floating windows in the Frametop desktop
|
||||
# Actions for ft-floatd ("float_toggle", "dock_all"): they don't need pointer mode.
|
||||
FLOAT_ACTIONS = {"float_toggle": b"float pointer", "dock_all": b"dock all"}
|
||||
KEYS = "\0frametop_keys" # keys of keyboards grabbed for the desktop, for other readers
|
||||
FT_LAYOUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "layout", "ft-layout")
|
||||
DEFAULT_BUTTONS = {BTN_LEFT: "left", BTN_RIGHT: "right", BTN_MIDDLE: "middle",
|
||||
@@ -307,7 +316,8 @@ def read_rules(path=RULES_PATH):
|
||||
rules.setdefault("devices", {})
|
||||
rules.setdefault("buttons", {})
|
||||
rules.setdefault("controller_buttons", {})
|
||||
rules.setdefault("key_bindings", dict(DEFAULT_KEY_BINDINGS))
|
||||
if not isinstance(rules.get("key_bindings"), dict):
|
||||
rules["key_bindings"] = dict(DEFAULT_KEY_BINDINGS)
|
||||
return rules
|
||||
|
||||
|
||||
@@ -713,11 +723,19 @@ def main():
|
||||
vr_keyboard("show")
|
||||
|
||||
def do_action(action, value, now, source="mouse"):
|
||||
"""A mapped mouse or controller button, or key combination (pointer mode only)."""
|
||||
"""A mapped mouse or controller button, or key combination (pointer mode only, but
|
||||
for FLOAT_ACTIONS)."""
|
||||
if action == "keyboard_toggle":
|
||||
if value == 1 and vr_keyboard_mode() != "never":
|
||||
vr_keyboard("toggle")
|
||||
else:
|
||||
elif action in FLOAT_ACTIONS:
|
||||
if value == 1:
|
||||
try:
|
||||
screens_sock.sendto(FLOAT_ACTIONS[action], FLOAT)
|
||||
except OSError:
|
||||
pass # the Frametop desktop isn't running
|
||||
log(action)
|
||||
elif state["pointer"]:
|
||||
state["pointer"].action(action, value, now, source)
|
||||
|
||||
held_modifiers = set() # on any keyboard, folded (MODIFIERS)
|
||||
@@ -745,7 +763,7 @@ def main():
|
||||
meta_hidden.clear()
|
||||
if value == 0 and code in combos_down:
|
||||
action = combos_down.pop(code)
|
||||
if state["pointer"]:
|
||||
if state["pointer"] or action in FLOAT_ACTIONS:
|
||||
do_action(action, 0, now, "keyboard")
|
||||
return True
|
||||
if value != 1 or not state["rules"]["key_bindings"]:
|
||||
@@ -766,7 +784,7 @@ def main():
|
||||
for c in sorted(held_meta - meta_hidden):
|
||||
to_screens(c, 0)
|
||||
meta_hidden.update(held_meta)
|
||||
if state["pointer"]:
|
||||
if state["pointer"] or action in FLOAT_ACTIONS:
|
||||
do_action(action, 1, now, "keyboard")
|
||||
log(f"key combination {combo}: {action}")
|
||||
return True
|
||||
|
||||
Reference in new issue
Block a user