From 575f26fa5bf7c8eb78076464b74c6489bc3d4618 Mon Sep 17 00:00:00 2001 From: Jafz2001 Date: Tue, 21 Jul 2026 15:36:16 -0400 Subject: [PATCH] fix(windows): recover orphaned keyboard focus after native VST editor closes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users reported that while in Rig Builder, suddenly every text box in the app stops accepting input, app-wide, until they hit the Windows key. Cause: on Windows, VST editors open as top-level windows owned by the out-of-process vst-host sandbox (the WndProc crash fix). When that child-process window closes while holding keyboard focus — the user clicks its X, or a chain rebuild tears editors down — Windows leaves the Electron window looking active while the OS keyboard focus points at nothing: Chromium still gets pointer events but no keys. The Windows key "fix" works because it forces an OS foreground cycle. Fix: preload.ts installs a win32-only capture-phase pointerdown watchdog. A click landing while document.hasFocus() is false means the OS won't type into the window being clicked — it asks main via window:recoverFocus to run win.blur() + win.focus() (blur first: in the stale state Electron may believe the window is already focused, so a bare focus() can no-op). The user's first click inside the dead window self-heals it, covering every trigger path without having to enumerate them. Co-Authored-By: Claude Fable 5 --- src/main/main.ts | 18 ++++++++++++++++++ src/main/preload.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/main/main.ts b/src/main/main.ts index cb8f1e6..2ebbd8e 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -568,6 +568,24 @@ function createWindow(port: number): void { return mainWindow.webContents.isAudioMuted(); }); + // Keyboard-focus recovery (Windows). A native VST editor window — owned by + // the out-of-process vst-host sandbox since the WndProc crash fix — can + // close while it holds keyboard focus (user clicks its X, or a chain + // rebuild tears it down). Windows then leaves this window LOOKING active + // while the OS keyboard focus points at nothing: every text box in the app + // goes dead until an OS-level foreground cycle (users found the Windows + // key un-sticks it). The renderer detects the state — a click landing + // while document.hasFocus() is false (see preload.ts watchdog) — and asks + // us to run that same cycle programmatically. blur() first: in the stale + // state Electron may still believe the window is focused, so a bare + // focus() can no-op. + ipcMain.on('window:recoverFocus', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (!win || win.isDestroyed() || !win.isVisible() || win.isMinimized()) return; + win.blur(); + win.focus(); + }); + const serverUrl = `http://127.0.0.1:${port}`; // Clear the Chromium HTTP cache before the first load. The server diff --git a/src/main/preload.ts b/src/main/preload.ts index e96bd7a..520c622 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -637,4 +637,32 @@ if (isMainFrame) { contextBridge.exposeInMainWorld('slopsmithDesktop', feedBackDesktopApi); } +// Keyboard-focus recovery watchdog (Windows). When the sandbox vst-host's +// native editor window closes while focused, the OS keyboard focus can be +// left orphaned: this window still looks active but document.hasFocus() is +// false and NO text box in the app receives keys until a foreground cycle +// (the "hit the Windows key and typing works again" reports, always starting +// in Rig Builder — the one place native editor windows open). Pointer events +// still arrive without keyboard focus, so the user's first click inside the +// dead window is the recovery signal: a pointerdown while document.hasFocus() +// is false means the OS won't type into the window being interacted with — +// ask main to run the blur()/focus() cycle. Capture phase so no +// stopPropagation hides the click; throttled in case focus is contended. +if (isMainFrame && process.platform === 'win32') { + // preload.ts compiles without the DOM lib (see isMainFrame): reach the + // frame's window/document through globalThis with minimal local shapes. + const g = globalThis as unknown as { + addEventListener?: (type: string, cb: () => void, capture?: boolean) => void; + document?: { hasFocus?: () => boolean }; + }; + let lastFocusRecovery = 0; + g.addEventListener?.('pointerdown', () => { + if (g.document?.hasFocus?.()) return; + const now = Date.now(); + if (now - lastFocusRecovery < 1000) return; + lastFocusRecovery = now; + ipcRenderer.send('window:recoverFocus'); + }, true); +} + export {};