From 6104c3f192a42206db9e9b56ccf8d980377f0bbe Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:07:38 +1000 Subject: [PATCH] Refuse window input when Windows cannot focus its target --- docs/pc-in-headset.md | 4 +++- tests/test_pcview.py | 10 ++++++++++ ui/frame_pc_capture.py | 19 +++++++++++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/pc-in-headset.md b/docs/pc-in-headset.md index 66a5989..1d404ac 100644 --- a/docs/pc-in-headset.md +++ b/docs/pc-in-headset.md @@ -41,7 +41,9 @@ API choices are **documented**, not device verification: Windows needs a WGC-capable Windows 10/11 desktop and an available hardware Media Foundation H.264 encoder. Elevated windows and the secure desktop -cannot be driven by an ordinary Frame Control process. Minimized/closed +cannot be driven by an ordinary Frame Control process. If Windows refuses +to focus a selected window, input is refused rather than sent to the app +covering it; bring the selected window forward, then Stop and Show again. Minimized/closed windows may stop producing frames. Protected content is not supported. On Linux, press **Choose a window or screen…** and approve the desktop's diff --git a/tests/test_pcview.py b/tests/test_pcview.py index 826c3ab..442dbc7 100644 --- a/tests/test_pcview.py +++ b/tests/test_pcview.py @@ -93,6 +93,16 @@ class AdapterTests(unittest.TestCase): self.assertIn(mock.call(mouse=(0, 0, 0, 0x10)), host.send.call_args_list) self.assertIn(mock.call(key=(162, 0, 2)), host.send.call_args_list) + def test_refused_windows_focus_never_clicks_the_covering_app(self): + host = mock.Mock() + host.rect.return_value = (0, 0, 800, 600) + host.user.GetForegroundWindow.return_value = 99 + host.user.SetForegroundWindow.return_value = False + inp = capture.WindowsInput(host, dict(src='window:12', w=800, h=600)) + with self.assertRaisesRegex(RuntimeError, 'could not focus'): + inp.handle(dict(t='m', e='down', b=0, x=.5, y=.5)) + host.send.assert_not_called() + def test_pc_reuses_launcher_and_namespaces_panel_ids(self): view = frame_pcview.PCView(['ssh'], mock.Mock(return_value='panel created'), 'frame') view.call = mock.Mock(side_effect=lambda path, **kw: {'screen': True, 'ticket': 'one-use', 'streams': []}) diff --git a/ui/frame_pc_capture.py b/ui/frame_pc_capture.py index 671c1cd..6d29874 100644 --- a/ui/frame_pc_capture.py +++ b/ui/frame_pc_capture.py @@ -217,6 +217,8 @@ class Windows: self.user.GetWindowTextW.argtypes = [W.HWND, W.LPWSTR, C.c_int] self.user.GetWindowRect.argtypes = [W.HWND, C.POINTER(W.RECT)] self.user.SetForegroundWindow.argtypes = [W.HWND] + self.user.GetForegroundWindow.argtypes = [] + self.user.GetForegroundWindow.restype = W.HWND self.dwm.DwmGetWindowAttribute.argtypes = [W.HWND, W.DWORD, C.c_void_p, W.DWORD] self.callback = C.WINFUNCTYPE(W.BOOL, W.HWND, W.LPARAM) self.monitor_callback = C.WINFUNCTYPE(W.BOOL, W.HMONITOR, W.HDC, C.POINTER(W.RECT), W.LPARAM) @@ -305,6 +307,16 @@ class WindowsInput: if error: raise error + def focus(self): + if not self.source['src'].startswith('window:'): + return + hwnd = int(self.source['src'].split(':')[1]) + user = self.host.user + if user.GetForegroundWindow() != hwnd and not user.SetForegroundWindow(hwnd): + raise RuntimeError('Windows could not focus this window. Bring it to the front on your PC, then press Stop and Show again.') + if user.GetForegroundWindow() != hwnd: + raise RuntimeError('The selected window is not in front. Reopen the view after bringing it to the front on your PC.') + def handle(self, m): t, u = m['t'], self.host.user if t == 'release': @@ -314,8 +326,8 @@ class WindowsInput: if source['src'].startswith('window:'): hwnd = int(source['src'].split(':')[1]) x, y, w, h = self.host.rect(hwnd) - if m.get('e') == 'down': - u.SetForegroundWindow(hwnd) + if m.get('e') == 'down' or t == 'wheel': + self.focus() else: x, y, w, h = (source[k] for k in ('x', 'y', 'w', 'h')) x += max(0, min(1, float(m.get('x', 0)))) * (w - 1) @@ -335,6 +347,8 @@ class WindowsInput: self.host.send(mouse=(0, 0, delta & 0xffffffff, flag)) elif t == 'k': code, down = str(m.get('code', '')), m.get('e') == 'down' + if down: + self.focus() special = {'Enter': 13, 'Escape': 27, 'Tab': 9, 'Backspace': 8, 'Space': 32, 'ArrowLeft': 37, 'ArrowUp': 38, 'ArrowRight': 39, 'ArrowDown': 40, 'Delete': 46, 'Home': 36, 'End': 35, 'PageUp': 33, 'PageDown': 34, @@ -350,6 +364,7 @@ class WindowsInput: elif down and len(str(m.get('key', ''))) == 1: self.handle({'t': 'text', 's': m['key']}) elif t == 'text': + self.focus() raw = str(m.get('s', ''))[:4096].encode('utf-16-le') for i in range(0, len(raw), 2): unit = int.from_bytes(raw[i:i+2], 'little')