using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Windows.Forms; using Microsoft.Win32; namespace VRio.App; /// /// A background keyboard input source built on the Win32 Raw Input API. /// Where the WinForms focus path (ProcessCmdKey/OnKeyUp) only sees /// keys while vRIO is the foreground window and can't tell one keyboard from /// another, Raw Input carries a per-device handle with every keystroke and — /// with the RIDEV_INPUTSINK flag — keeps delivering WM_INPUT even /// when vRIO is in the background. That lets the user nominate one physical /// keyboard as a dedicated cockpit panel: its keys drive the RIO controls while /// the sim (or RIOJoy) holds focus, and other keyboards are ignored. /// /// This is the input-side twin of : that /// class writes a chosen keyboard's LEDs while the game has focus, this /// one reads a chosen keyboard's keys under the same condition. Raw /// Input observes without intercepting — the keystroke still reaches whatever /// app has focus — so the selected keyboard should be one dedicated to the /// panel, not the one you also type on. /// /// Registration is scoped to when a device is actually selected /// (): picking "all keyboards" unregisters and hands /// input back to the focus path, so vRIO isn't pumping WM_INPUT for every /// keystroke system-wide when the feature is idle. Not thread-safe; /// and the events fire on the UI thread that owns the /// window handle, which is exactly where wants /// to be called. /// public sealed class RawKeyboardSource : IDisposable { // --- Win32 Raw Input interop --------------------------------------------- [StructLayout(LayoutKind.Sequential)] private struct RAWINPUTDEVICE { public ushort usUsagePage; public ushort usUsage; public uint dwFlags; public IntPtr hwndTarget; } [StructLayout(LayoutKind.Sequential)] private struct RAWINPUTDEVICELIST { public IntPtr hDevice; public uint dwType; } [StructLayout(LayoutKind.Sequential)] private struct RAWINPUTHEADER { public uint dwType; public uint dwSize; public IntPtr hDevice; public IntPtr wParam; } [StructLayout(LayoutKind.Sequential)] private struct RAWKEYBOARD { public ushort MakeCode; public ushort Flags; public ushort Reserved; public ushort VKey; public uint Message; public uint ExtraInformation; } [DllImport("user32.dll", SetLastError = true)] private static extern bool RegisterRawInputDevices( [In] RAWINPUTDEVICE[] pRawInputDevices, uint uiNumDevices, uint cbSize); [DllImport("user32.dll", SetLastError = true)] private static extern uint GetRawInputData( IntPtr hRawInput, uint uiCommand, IntPtr pData, ref uint pcbSize, uint cbSizeHeader); [DllImport("user32.dll", SetLastError = true)] private static extern uint GetRawInputDeviceList( [In, Out] RAWINPUTDEVICELIST[]? pRawInputDeviceList, ref uint puiNumDevices, uint cbSize); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern uint GetRawInputDeviceInfoW( IntPtr hDevice, uint uiCommand, IntPtr pData, ref uint pcbSize); private const uint Err = 0xFFFFFFFF; // sentinel: all three APIs return (UINT)-1 on failure private const uint RIDEV_REMOVE = 0x00000001; private const uint RIDEV_INPUTSINK = 0x00000100; private const uint RIDEV_DEVNOTIFY = 0x00002000; // WM_INPUT_DEVICE_CHANGE on hot-plug private const uint RID_INPUT = 0x10000003; private const uint RIDI_DEVICENAME = 0x20000007; private const uint RIM_TYPEKEYBOARD = 1; private const ushort RI_KEY_BREAK = 0x01; // set = key up (make/break) private const ushort HidUsagePageGeneric = 0x01; private const ushort HidUsageKeyboard = 0x06; private static readonly int HeaderSize = Marshal.SizeOf(); // --- State --------------------------------------------------------------- // hDevice → interface path, so the hot per-keystroke path skips the // two-call GetRawInputDeviceInfo dance once a device is known. private readonly Dictionary _pathByHandle = new(); private IntPtr _hwnd; private string? _target; // interface path of the keyboard to capture; null = disabled private bool _registered; private bool _disposed; /// A key on the captured keyboard went down (.NET name). public event Action? KeyDown; /// A key on the captured keyboard came up (.NET name). public event Action? KeyUp; /// The set of attached keyboards changed (interface path, display name) — for a picker. public event Action>? KeyboardsChanged; /// Status/diagnostic lines. public event Action? Logged; /// True while a specific keyboard is being captured (focus path should stand down). public bool IsCapturing => _target is not null; /// Bind to the window whose message loop delivers WM_INPUT. public void Attach(IntPtr hwnd) => _hwnd = hwnd; /// /// Capture one keyboard by interface path, or none ( /// null) to release input back to the focus path. Registering/unregistering /// Raw Input is scoped here so vRIO only taps the global key stream while the /// feature is actually in use. /// public void SetTarget(string? deviceId) { if (_target == deviceId) return; _target = deviceId; if (deviceId is null) Unregister(); else Register(); } /// Re-enumerate keyboards and raise . public void RefreshKeyboards() { _pathByHandle.Clear(); // handles are only stable per connection KeyboardsChanged?.Invoke(EnumerateKeyboards()); } /// Handle a WM_INPUT_DEVICE_CHANGE: a keyboard arrived or left. public void HandleDeviceChange() => RefreshKeyboards(); /// /// Handle a WM_INPUT message (pass its LParam). Silently ignores /// input from any keyboard other than the captured one. /// public void ProcessInput(IntPtr hRawInput) { string? target = _target; if (target is null) return; uint size = 0; if (GetRawInputData(hRawInput, RID_INPUT, IntPtr.Zero, ref size, (uint)HeaderSize) == Err || size == 0) return; IntPtr buffer = Marshal.AllocHGlobal((int)size); try { if (GetRawInputData(hRawInput, RID_INPUT, buffer, ref size, (uint)HeaderSize) != size) return; var header = Marshal.PtrToStructure(buffer); if (header.dwType != RIM_TYPEKEYBOARD) return; string? path = PathFor(header.hDevice); if (!string.Equals(path, target, StringComparison.OrdinalIgnoreCase)) return; var kb = Marshal.PtrToStructure(IntPtr.Add(buffer, HeaderSize)); ushort vkey = kb.VKey; // 0 = no VK, 0xFF = fake key emitted as part of an escaped sequence. if (vkey is 0 or 0xFF) return; string key = ((Keys)vkey).ToString(); if ((kb.Flags & RI_KEY_BREAK) != 0) KeyUp?.Invoke(key); else KeyDown?.Invoke(key); // repeats while held are dropped by the router } finally { Marshal.FreeHGlobal(buffer); } } public void Dispose() { if (_disposed) return; _disposed = true; _target = null; Unregister(); KeyDown = null; KeyUp = null; KeyboardsChanged = null; Logged = null; } // --- Registration -------------------------------------------------------- private void Register() { if (_registered || _hwnd == IntPtr.Zero) return; var rid = new[] { new RAWINPUTDEVICE { usUsagePage = HidUsagePageGeneric, usUsage = HidUsageKeyboard, dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY, // background delivery + hot-plug notices hwndTarget = _hwnd, }, }; if (RegisterRawInputDevices(rid, 1, (uint)Marshal.SizeOf())) _registered = true; else Logged?.Invoke($"Raw keyboard capture unavailable (error {Marshal.GetLastWin32Error()})"); } private void Unregister() { if (!_registered) return; // RIDEV_REMOVE must pass a null target window, or the call fails. var rid = new[] { new RAWINPUTDEVICE { usUsagePage = HidUsagePageGeneric, usUsage = HidUsageKeyboard, dwFlags = RIDEV_REMOVE, hwndTarget = IntPtr.Zero, }, }; RegisterRawInputDevices(rid, 1, (uint)Marshal.SizeOf()); _registered = false; } // --- Enumeration / naming ------------------------------------------------ private List<(string Id, string Name)> EnumerateKeyboards() { var result = new List<(string, string)>(); uint listSize = (uint)Marshal.SizeOf(); uint count = 0; if (GetRawInputDeviceList(null, ref count, listSize) == Err || count == 0) return result; var devices = new RAWINPUTDEVICELIST[count]; uint got = GetRawInputDeviceList(devices, ref count, listSize); if (got == Err) return result; var labelCounts = new Dictionary(); for (int i = 0; i < got; i++) { if (devices[i].dwType != RIM_TYPEKEYBOARD) continue; string? path = PathFor(devices[i].hDevice); if (string.IsNullOrEmpty(path)) continue; string label = FriendlyName(path!); // Physical keyboards can surface as more than one collection; a bare // count suffix keeps otherwise-identical labels distinguishable. if (labelCounts.TryGetValue(label, out int n)) { labelCounts[label] = n + 1; label = $"{label} ({n + 1})"; } else { labelCounts[label] = 1; } result.Add((path!, label)); } return result; } private string? PathFor(IntPtr hDevice) { if (_pathByHandle.TryGetValue(hDevice, out string? cached)) return cached; string? path = DeviceName(hDevice); if (path is not null) _pathByHandle[hDevice] = path; return path; } private static string? DeviceName(IntPtr hDevice) { uint chars = 0; if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, IntPtr.Zero, ref chars) == Err || chars == 0) return null; IntPtr buffer = Marshal.AllocHGlobal((int)chars * sizeof(char)); try { if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, buffer, ref chars) == Err) return null; return Marshal.PtrToStringUni(buffer); } finally { Marshal.FreeHGlobal(buffer); } } /// /// Turn a raw device interface path into something a human recognizes: the /// device's registry description if we can resolve it, else its VID:PID. /// private static string FriendlyName(string interfacePath) { string? desc = RegistryName(interfacePath); if (!string.IsNullOrWhiteSpace(desc)) return desc!; Match m = Regex.Match(interfacePath, @"VID_([0-9A-Fa-f]{4}).*?PID_([0-9A-Fa-f]{4})"); return m.Success ? $"Keyboard {m.Groups[1].Value.ToUpperInvariant()}:{m.Groups[2].Value.ToUpperInvariant()}" : "Keyboard"; } private static string? RegistryName(string interfacePath) { try { // \\?\HID#VID_x&PID_y#instance#{class-guid} → Enum key HID\VID_x&PID_y\instance string s = interfacePath; if (s.StartsWith(@"\\?\", StringComparison.Ordinal) || s.StartsWith(@"\\.\", StringComparison.Ordinal)) s = s.Substring(4); int guid = s.LastIndexOf("#{", StringComparison.Ordinal); if (guid >= 0) s = s.Substring(0, guid); string instance = s.Replace('#', '\\'); using RegistryKey? key = Registry.LocalMachine.OpenSubKey( @"SYSTEM\CurrentControlSet\Enum\" + instance); if (key is null) return null; string? name = key.GetValue("FriendlyName") as string; if (string.IsNullOrWhiteSpace(name)) name = key.GetValue("DeviceDesc") as string; if (string.IsNullOrWhiteSpace(name)) return null; // DeviceDesc is "@driver.inf,%token%;Actual Name" — keep the tail. int semi = name!.LastIndexOf(';'); return semi >= 0 ? name.Substring(semi + 1) : name; } catch (Exception ex) when (ex is System.Security.SecurityException or UnauthorizedAccessException or IOException) { return null; } } }