From 4a8f3e247be210142f076ce6d6ff44dbf824da0d Mon Sep 17 00:00:00 2001 From: Moheng-S <78278434+Moheng-S@users.noreply.github.com> Date: Sun, 8 Nov 2026 11:40:14 +0000 Subject: [PATCH 1/2] Add absolute Bluetooth mouse control in previews --- CHANGELOG.md | 4 + CHANGELOG.zh-TW.md | 4 + docs/USER_GUIDE.md | 8 +- .../BluetoothInputToggleTests.cs | 123 +++++++++ src/App/Controls/NativePreviewHost.cs | 37 ++- src/App/Controls/NativePreviewMouse.cs | 65 +++++ src/App/Localization/Strings.en-US.xaml | 1 + src/App/Localization/Strings.ja-JP.xaml | 1 + src/App/Localization/Strings.zh-CN.xaml | 1 + src/App/Localization/Strings.zh-HK.xaml | 1 + src/App/Localization/Strings.zh-TW.xaml | 1 + src/App/MainWindow.KeyboardFocus.cs | 2 + src/App/MainWindow.xaml | 4 + src/App/MainWindow.xaml.cs | 258 ++++++++++++++---- src/App/Services/BluetoothHidMouseService.cs | 131 +++++++-- src/App/Services/BluetoothHidProtocol.cs | 1 + src/App/Services/MultiDevicePreviewManager.cs | 7 + src/App/Updater/UpdateSettingsStore.cs | 2 + src/App/ViewModels/MainViewModel.cs | 61 ++++- src/App/Windows/NativePreviewWindow.cs | 48 +++- tools/BluetoothHidStress/Program.cs | 49 ++++ 21 files changed, 713 insertions(+), 96 deletions(-) create mode 100644 src/App/Controls/NativePreviewMouse.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b65d6c0..8837429b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to iPhoneMirror are documented here. The project follows ## [Unreleased] +### Added + +- Add opt-in absolute Bluetooth mouse input in the main and independent previews, with seamless cursor movement between Windows and the mirrored image. + ### Fixed - Improve Chinese IME key forwarding, shortcut press/release recovery and preview focus handling. See the [Sogou verification record](docs/SOGOU_IME_VERIFICATION_2026-10-31.zh-CN.md) for tested scope. diff --git a/CHANGELOG.zh-TW.md b/CHANGELOG.zh-TW.md index c2b4c368..28c1c7b9 100644 --- a/CHANGELOG.zh-TW.md +++ b/CHANGELOG.zh-TW.md @@ -4,6 +4,10 @@ iPhoneMirror 的重要變更均記錄於此。已發布版本遵循[語意化版 ## [Unreleased] +### 新增 + +- 新增可選的絕對座標藍牙滑鼠模式,可在主預覽、獨立投影視窗和 Windows 桌面之間直接移動游標。 + ### 修正 - 改善中文輸入法按鍵轉發、快捷鍵按下與放開恢復,以及預覽焦點處理。 diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 3c7780fe..8641b1b4 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -121,12 +121,18 @@ iPhoneMirror 的控制链路与投屏链路分开:电脑蓝牙会以标准 BLE 3. 选择设备后打开左侧“投屏”面板,点击“启用蓝牙反向控制”。可以在开始投屏前完成蓝牙 广播和配对;首次连接时,在蓝牙客户端绑定窗口中确认与当前手机对应的客户端。绑定完成后, 主预览中的鼠标移动、单击、拖拽、滚轮和常用键盘按键会发送到该设备。 -4. 反向控制开启后会隐藏 Windows 本机光标;默认按 `F9` 可释放鼠标并暂停输入,再按一次恢复控制。正常暂停期间保留蓝牙连接,恢复时不重复弹出连接提示;释放输入失败时会关闭连接,下次开启需重新连接。独立窗口可恢复拖动(手动固定的窗口除外)。也可以在 +4. 默认的相对鼠标模式会隐藏 Windows 本机光标;默认按 `F9` 可释放鼠标并暂停输入,再按一次恢复控制。正常暂停期间保留蓝牙连接,恢复时不重复弹出连接提示;释放输入失败时会关闭连接,下次开启需重新连接。独立窗口可恢复拖动(手动固定的窗口除外)。也可以在 “设置快捷键”窗口中改为其他全局快捷键。该窗口还可配置有线投屏、无线投屏、蓝牙控制、 控制中心、通知中心、App 切换器、主屏幕、Boss 键、音量、锁屏、Dock 和 Siri。F12 保留为 不可设置的系统键;F1-F11、右键和中键可以直接录入。滚轮方向和鼠标灵敏度可按竖屏/横屏 分别设置,打开独立预览窗口时,主窗口反向控制会自动关闭。 +如需直接点选投屏画面,请先停止蓝牙反向控制,在“设置 → 反向控制设置”勾选 +“在投屏画面内使用绝对坐标鼠标(无需 F9)”,再启动蓝牙反向控制。主预览和独立窗口 +会把鼠标位置映射到视频内容,移出画面或失去焦点时释放按键。Windows 光标保持可见。 +切换相对与绝对模式会更换 BLE HID 报告描述符。若设备缓存了旧描述符,请在系统蓝牙 +设置中忽略该设备,按连接提示重新配对。设备对绝对坐标鼠标的支持取决于系统版本。 + 控制能力受 iOS 系统鼠标模型限制:主要是单指指针操作,不能注入真正的多指触控或游戏 触摸事件。停止投屏时控制广播会自动关闭;也可以在同一面板手动关闭。客户端断开后不会 自动转移到另一台手机,原客户端重新连接后才会恢复;可在设置中解除单个绑定或清除全部绑定。 diff --git a/src/App.Runtime.Tests/BluetoothInputToggleTests.cs b/src/App.Runtime.Tests/BluetoothInputToggleTests.cs index 2ad0376c..288b86ce 100644 --- a/src/App.Runtime.Tests/BluetoothInputToggleTests.cs +++ b/src/App.Runtime.Tests/BluetoothInputToggleTests.cs @@ -18,6 +18,8 @@ private static int RunBluetoothInputTransportTests() { Task.Run(TestBluetoothPauseRouteChangeAsync).GetAwaiter().GetResult(); Task.Run(TestBluetoothMouseAdmissionAsync).GetAwaiter().GetResult(); + Task.Run(TestWindowBluetoothMouseClickAsync).GetAwaiter().GetResult(); + Task.Run(TestAbsoluteWindowMouseAsync).GetAwaiter().GetResult(); Task.Run(TestBluetoothHidSessionAdmissionAsync).GetAwaiter().GetResult(); TestBluetoothNotificationResults(); return 0; @@ -380,6 +382,16 @@ void Ready() KeyboardCall(shell, "ToggleFixedWindow"); Require(Hit() == 2, "unfixing a paused independent window did not restore dragging."); Console.WriteLine("PASS independent Bluetooth window: temporary input hit-testing, restored caption dragging/capture, explicit fixed preference retained."); + Toggle(); + SetKeyboardField(main, "_controlPointerFlushInFlight", 1); + try { TestWindowBluetoothPointer(main, device); } + finally + { + SetKeyboardField(main, "_bluetoothWindowMouseEnabled", false); + KeyboardCall(main, "ResetControlRouteState"); + SetKeyboardField(main, "_controlPointerFlushInFlight", 0); + } + Toggle(); AwaitMapping(vm.DisableBluetoothControlAsync()); Require(!vm.IsBluetoothControlEnabled && !vm.BluetoothControlIsInputPaused && vm.BluetoothControlTargetUdid is null, @@ -546,6 +558,117 @@ private static void TestBluetoothNotificationResults() Console.WriteLine("PASS notification result contract: only Success with exact bytes and no protocol error confirms delivery."); } + private static void TestWindowBluetoothPointer(MainWindow window, DeviceViewModel device) + { + KeyboardCall(window, "ResetControlRouteState"); + SetKeyboardField(window, "_bluetoothWindowMouseEnabled", true); + KeyboardCall(window, "ApplyBluetoothControlInputState", false); + InteractionAssert(!(bool)KeyboardField(window, "_rawMouseInputEnabled") && + !(bool)KeyboardField(window, "_windowsCursorHidden"), + "Window mouse must leave Windows mouse messages and cursor available."); + + void Send(PreviewPointerKind kind, short x = 150, byte button = 0, int wheel = 0, + int width = 300) => KeyboardCall(window, "OnIndependentPointerInput", device.Udid, + new PreviewPointerEventArgs(kind, x, 150, button, wheel, width, 300, 100, 300)); + byte Buttons() => (byte)KeyboardField(window, "_controlButtons"); + int Delta() => (int)KeyboardField(window, "_pendingControlDx"); + + Send(PreviewPointerKind.ButtonDown, x: 20, button: 1); + InteractionAssert(Buttons() == 0, "Clicks on black bars must stay local."); + Send(PreviewPointerKind.Move, x: 145); + InteractionAssert(Delta() == 0, "Entering the picture must anchor relative movement."); + Send(PreviewPointerKind.ButtonDown, button: 1); + Send(PreviewPointerKind.Move, x: 170); + InteractionAssert(Buttons() == 1 && Delta() != 0, "An in-picture drag must send motion and a held button."); + Send(PreviewPointerKind.Move, x: 210); + InteractionAssert(Buttons() == 0 && Delta() == 0, + "Crossing into a black bar must release the drag and discard queued movement."); + Send(PreviewPointerKind.Move, x: 165); + Send(PreviewPointerKind.ButtonUp, button: 1); + InteractionAssert(Buttons() == 0 && Delta() == 0, + "Re-entry must anchor again without restarting the old drag."); + Send(PreviewPointerKind.Wheel, wheel: 120); + InteractionAssert((int)KeyboardField(window, "_pendingControlWheel") != 0, + "The wheel must reach the device inside the picture."); + Send(PreviewPointerKind.Move, x: 175, width: 320); + InteractionAssert(Delta() == 0, "Resizing must re-anchor without a pointer jump."); + + Send(PreviewPointerKind.ButtonDown, button: 1); + Send(PreviewPointerKind.Reset); // WM_MOUSELEAVE or capture/focus loss + InteractionAssert(Buttons() == 0 && (int)KeyboardField(window, "_pendingControlWheel") == 0, + "Leaving the HWND must release the drag and clear pending wheel input."); + Console.WriteLine("Bluetooth window pointer: legacy Windows input, black bars, drag release, re-entry, wheel and resize passed."); + } + + private static async Task TestAbsoluteWindowMouseAsync() + { + var delivered = new ConcurrentQueue(); + await using var service = new BluetoothHidMouseService((id, payload, guard) => + { + if (id == 2 && guard?.Invoke() != false) delivered.Enqueue(payload.ToArray()); + return Task.FromResult(true); + }); + const string target = "absolute-window-mouse"; + SetKeyboardField(service, "_useAbsoluteMouse", true); + SetKeyboardField(service, "_targetDeviceUdid", target); + void HoldPump() => SetKeyboardField(service, "_mousePumpRunning", true); + Task Pump() => (Task)KeyboardCall(service, "PumpReportsAsync", + KeyboardField(service, "_mousePumpGeneration"))!; + Task Point(double x, double y, byte buttons, bool reset = false) => + service.SendAbsoluteMouseAsync(x, y, buttons, 0, target, () => true, reset); + static (byte Buttons, int X, int Y) Decode(byte[] report) => + (report[0], report[1] | report[2] << 8, report[3] | report[4] << 8); + + HoldPump(); + await Point(0, 1, 1); // The first sample is a click. + await Point(0, 1, 0); + await Pump(); + MappingAssert(delivered.Select(Decode).SequenceEqual(new[] { ((byte)1, 0, 32767), ((byte)0, 0, 32767) }), + "The first absolute click must preserve both button edges at its own coordinates."); + delivered.Clear(); + + HoldPump(); + await Point(0.5, 0.25, 1); + await Pump(); + delivered.Clear(); + + HoldPump(); + await Point(0.9, 0.9, 1); + await Point(0.75, 0.25, 0, reset: true); + await Point(1, 0, 0); + await Pump(); + MappingAssert(delivered.Select(Decode).SequenceEqual(new[] + { ((byte)0, 24575, 8192), ((byte)0, 32767, 0) }), + "Leaving the surface must drop pending drag motion and release at the last valid point before re-entry."); + delivered.Clear(); + + MappingAssert(await service.PauseInputAsync(), "Absolute mouse pause could not release the device."); + MappingAssert(delivered.Select(Decode).SequenceEqual(new[] { ((byte)0, 32767, 0) }), + "Pausing must release at the last absolute position."); + Console.WriteLine("PASS absolute window mouse: first click, drag cancellation, re-entry and pause coordinates."); + } + + private static async Task TestWindowBluetoothMouseClickAsync() + { + var delivered = new ConcurrentQueue(); + await using var service = new BluetoothHidMouseService((id, payload, guard) => + { + if (id == 2 && guard?.Invoke() != false) delivered.Enqueue(payload[0]); + return Task.FromResult(true); + }); + SetKeyboardField(service, "_targetDeviceUdid", "window-mouse"); + SetKeyboardField(service, "_mousePumpRunning", true); + Task Send(byte buttons, int wheel = 0) => service.SendMouseAsync(0, 0, buttons, wheel, + "window-mouse", preserveButtonTransitions: true); + await Send(1); + await Send(1, wheel: 1); + await Send(0); + await (Task)KeyboardCall(service, "PumpReportsAsync", KeyboardField(service, "_mousePumpGeneration"))!; + MappingAssert(delivered.SequenceEqual(new byte[] { 1, 0 }), + "A fast window click must retain both button transitions while BLE is busy."); + Console.WriteLine("PASS Bluetooth window mouse: queued click edges survive a wheel tick and a busy BLE pump."); + } + private static async Task TestBluetoothMouseAdmissionAsync() { var delivered = new ConcurrentQueue(); diff --git a/src/App/Controls/NativePreviewHost.cs b/src/App/Controls/NativePreviewHost.cs index 3404669c..91c11f90 100644 --- a/src/App/Controls/NativePreviewHost.cs +++ b/src/App/Controls/NativePreviewHost.cs @@ -32,6 +32,8 @@ internal sealed class NativePreviewHost : HwndHost private (int Width, int Height, int Radius, double Curve)? _appliedRegion; internal bool CapturePointerInput { get; set; } + private readonly NativePreviewMouse _windowMouse = new(); + internal bool BluetoothWindowMouseEnabled { get; set; } internal bool CaptureTouchInput { get; set; } internal bool SuppressMouseMove { get; set; } // Raw Input owns button and wheel transitions while Bluetooth control is @@ -146,6 +148,8 @@ internal void SetPresentationVisible(bool visible) { if (_presentationVisible == visible) return; _presentationVisible = visible; + if (!visible && BluetoothWindowMouseEnabled) + PointerInput?.Invoke(this, new PreviewPointerEventArgs(PreviewPointerKind.Reset, 0, 0, 0, 0)); if (_window != 0) _ = ShowWindow(_window, visible ? SwShowNoActivate : SwHide); } @@ -232,12 +236,19 @@ protected override void DestroyWindowCore(HandleRef hwnd) PreviewAttachmentCoordinator.Unregister(hwnd.Handle); if (hwnd.Handle != 0) DestroyWindow(hwnd.Handle); _window = 0; + _windowMouse.OnLeave(); _appliedRegion = null; } protected override nint WndProc(nint hwnd, int message, nint wParam, nint lParam, ref bool handled) { + if (message == NativePreviewMouse.WmMouseLeave) + { + _windowMouse.OnLeave(); + if (BluetoothWindowMouseEnabled) + PointerInput?.Invoke(this, new PreviewPointerEventArgs(PreviewPointerKind.Reset, 0, 0, 0, 0)); + } if (message == NativeTouchInput.WmTouch) { NativeTouchInput.Dispatch(hwnd, wParam, lParam, CapturePointerInput && CaptureTouchInput, @@ -300,7 +311,8 @@ protected override nint WndProc(nint hwnd, int message, nint wParam, nint lParam handled = true; return 0; } - lParam = DrainQueuedMouseMoves(_window, lParam); + if (BluetoothWindowMouseEnabled) _windowMouse.TrackLeave(hwnd); + else lParam = DrainQueuedMouseMoves(_window, lParam); PointerInput?.Invoke(this, new PreviewPointerEventArgs( PreviewPointerKind.Move, GetSignedLowWord(lParam), GetSignedHighWord(lParam), 0, 0, GetClientWidth(), @@ -310,6 +322,9 @@ protected override nint WndProc(nint hwnd, int message, nint wParam, nint lParam case 0x0201: // WM_LBUTTONDOWN case 0x0204: // WM_RBUTTONDOWN case 0x0207: // WM_MBUTTONDOWN + case 0x0203 when BluetoothWindowMouseEnabled: // WM_LBUTTONDBLCLK + case 0x0206 when BluetoothWindowMouseEnabled: // WM_RBUTTONDBLCLK + case 0x0209 when BluetoothWindowMouseEnabled: // WM_MBUTTONDBLCLK // STATIC children do not take keyboard focus on click. // Restore both WPF and native focus after a toolbar/editor // interaction before dispatching this preview input. @@ -325,8 +340,12 @@ protected override nint WndProc(nint hwnd, int message, nint wParam, nint lParam handled = true; return 0; } - _capturedMouseButtons |= MouseButtonFromMessage(message); - _ = SetCapture(_window); + if (BluetoothWindowMouseEnabled) _windowMouse.TrackLeave(hwnd); + else + { + _capturedMouseButtons |= MouseButtonFromMessage(message); + _ = SetCapture(_window); + } PointerInput?.Invoke(this, new PreviewPointerEventArgs( PreviewPointerKind.ButtonDown, GetSignedLowWord(lParam), GetSignedHighWord(lParam), MouseButtonFromMessage(message), 0, @@ -346,7 +365,7 @@ protected override nint WndProc(nint hwnd, int message, nint wParam, nint lParam PreviewPointerKind.ButtonUp, GetSignedLowWord(lParam), GetSignedHighWord(lParam), MouseButtonFromMessage(message), 0, GetClientWidth(), GetClientHeight())); - if (_capturedMouseButtons == 0) _ = ReleaseCapture(); + if (!BluetoothWindowMouseEnabled && _capturedMouseButtons == 0) _ = ReleaseCapture(); handled = true; return 0; case 0x020A: // WM_MOUSEWHEEL @@ -355,6 +374,12 @@ protected override nint WndProc(nint hwnd, int message, nint wParam, nint lParam handled = true; return 0; } + if (BluetoothWindowMouseEnabled) + { + if (!NativePreviewMouse.TryGetWheelClientPosition(hwnd, lParam, out lParam)) + break; + _windowMouse.TrackLeave(hwnd); + } PointerInput?.Invoke(this, new PreviewPointerEventArgs( PreviewPointerKind.Wheel, GetSignedLowWord(lParam), GetSignedHighWord(lParam), 0, (short)((long)wParam >> 16), @@ -402,8 +427,8 @@ protected override nint WndProc(nint hwnd, int message, nint wParam, nint lParam private static short GetSignedHighWord(nint value) => unchecked((short)(((long)value >> 16) & 0xFFFF)); private static byte MouseButtonFromMessage(int message) => message switch { - 0x0201 or 0x0202 => 1, - 0x0204 or 0x0205 => 2, + 0x0201 or 0x0202 or 0x0203 => 1, + 0x0204 or 0x0205 or 0x0206 => 2, _ => 4, }; diff --git a/src/App/Controls/NativePreviewMouse.cs b/src/App/Controls/NativePreviewMouse.cs new file mode 100644 index 00000000..be0b438a --- /dev/null +++ b/src/App/Controls/NativePreviewMouse.cs @@ -0,0 +1,65 @@ +using System.Runtime.InteropServices; + +namespace IPhoneMirror.App.Controls; + +internal sealed class NativePreviewMouse +{ + internal const int WmMouseLeave = 0x02A3; + private bool _trackingLeave; + + internal void TrackLeave(nint hwnd) + { + if (_trackingLeave) return; + var request = new MouseTracking + { + Size = (uint)Marshal.SizeOf(), + Flags = 2, // TME_LEAVE + Window = hwnd, + }; + _trackingLeave = TrackMouseEvent(ref request); + } + + internal void OnLeave() => _trackingLeave = false; + + internal static bool TryGetWheelClientPosition(nint hwnd, nint screenPosition, out nint clientPosition) + { + // A focused preview can receive wheel messages while the cursor is + // elsewhere. Require the actual hit window before forwarding them. + var point = new Point + { + X = unchecked((short)screenPosition.ToInt64()), + Y = unchecked((short)(screenPosition.ToInt64() >> 16)), + }; + clientPosition = 0; + if (WindowFromPoint(point) != hwnd || !ScreenToClient(hwnd, ref point)) return false; + clientPosition = (nint)((point.X & 0xFFFF) | (point.Y << 16)); + return true; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MouseTracking + { + internal uint Size; + internal uint Flags; + internal nint Window; + internal uint HoverTime; + } + + [StructLayout(LayoutKind.Sequential)] + private struct Point + { + internal int X; + internal int Y; + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TrackMouseEvent(ref MouseTracking request); + + [DllImport("user32.dll")] + private static extern nint WindowFromPoint(Point point); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ScreenToClient(nint hwnd, ref Point point); +} diff --git a/src/App/Localization/Strings.en-US.xaml b/src/App/Localization/Strings.en-US.xaml index b619fe7b..ff39b25e 100644 --- a/src/App/Localization/Strings.en-US.xaml +++ b/src/App/Localization/Strings.en-US.xaml @@ -233,6 +233,7 @@ Apply video settings Play audio on Windows Playback volume + Use absolute Bluetooth mouse in the preview (no F9) Bluetooth mouse sensitivity Reverse control settings Wheel sensitivity diff --git a/src/App/Localization/Strings.ja-JP.xaml b/src/App/Localization/Strings.ja-JP.xaml index 21ffc99c..9e9cd0a0 100644 --- a/src/App/Localization/Strings.ja-JP.xaml +++ b/src/App/Localization/Strings.ja-JP.xaml @@ -233,6 +233,7 @@ 映像設定を適用 Windowsで音声を再生 再生音量 + プレビュー内で絶対座標マウスを使用(F9 不要) Bluetoothマウスの感度 リモート操作の設定 ホイールの感度 diff --git a/src/App/Localization/Strings.zh-CN.xaml b/src/App/Localization/Strings.zh-CN.xaml index e99e71be..a6cc037a 100644 --- a/src/App/Localization/Strings.zh-CN.xaml +++ b/src/App/Localization/Strings.zh-CN.xaml @@ -233,6 +233,7 @@ 应用画面设置 在 Windows 播放声音 播放音量 + 在投屏画面内使用绝对坐标鼠标(无需 F9) 蓝牙鼠标灵敏度 反向控制设置 滚轮灵敏度 diff --git a/src/App/Localization/Strings.zh-HK.xaml b/src/App/Localization/Strings.zh-HK.xaml index bd5f7eb2..6f73ab03 100644 --- a/src/App/Localization/Strings.zh-HK.xaml +++ b/src/App/Localization/Strings.zh-HK.xaml @@ -233,6 +233,7 @@ 套用畫面設定 在 Windows 播放音訊 播放音量 + 在投影畫面內使用絕對座標滑鼠(無需 F9) 藍牙滑鼠靈敏度 反向控制設定 滾輪靈敏度 diff --git a/src/App/Localization/Strings.zh-TW.xaml b/src/App/Localization/Strings.zh-TW.xaml index 31fa1b77..eceff362 100644 --- a/src/App/Localization/Strings.zh-TW.xaml +++ b/src/App/Localization/Strings.zh-TW.xaml @@ -233,6 +233,7 @@ 套用畫面設定 在 Windows 播放音訊 播放音量 + 在投影畫面內使用絕對座標滑鼠(無需 F9) 藍牙滑鼠靈敏度 反向控制設定 滾輪靈敏度 diff --git a/src/App/MainWindow.KeyboardFocus.cs b/src/App/MainWindow.KeyboardFocus.cs index 7f43a5d2..b9432486 100644 --- a/src/App/MainWindow.KeyboardFocus.cs +++ b/src/App/MainWindow.KeyboardFocus.cs @@ -155,6 +155,7 @@ private void OnMainKeyboardDeactivated(object? sender, EventArgs e) if (_activeControlWindow == 0) { _keyboardFocusSuspended = true; + ResetWindowBluetoothPointer(_viewModel.SelectedDevice?.Udid); HandleControlKeyboardInput(new PreviewKeyboardEventArgs( PreviewKeyboardKind.Reset, 0), _viewModel.SelectedDevice?.Udid); _ = HandleUsbPointerInputAsync(new PreviewPointerEventArgs( @@ -172,6 +173,7 @@ private void OnIndependentKeyboardFocusChanged(string udid, nint hwnd, bool acti if (_activeControlWindow != hwnd || !DeviceViewModel.UdidEquals(_activeControlUdid, udid)) return; _keyboardFocusSuspended = true; + ResetWindowBluetoothPointer(udid); _mouseShortcutButtons.Clear(); _viewModel.SetControlInputDevice(null); UnregisterDeviceHotkeys(); diff --git a/src/App/MainWindow.xaml b/src/App/MainWindow.xaml index 161cb73c..c3d93663 100644 --- a/src/App/MainWindow.xaml +++ b/src/App/MainWindow.xaml @@ -1867,6 +1867,10 @@ +