Capture a chosen keyboard's keys without window focus (Raw Input)
The WinForms focus path only sees keys while vRIO is foreground and can't tell one keyboard from another, so vRIO went deaf the moment the sim took focus. The new "Capture keyboard" picker in the Input panel taps one physical keyboard through the Win32 Raw Input API (RIDEV_INPUTSINK) and routes its keys to the panel even in the background — the input-side twin of the lamp mirror writing that keyboard's LEDs under the same condition. Raw Input observes without intercepting, so keys still reach the focused app; the picker defaults to "All keyboards (focus only)", preserving the prior focus-bound behavior, and only registers Raw Input while a specific device is selected. While capturing, the focus path stands down but still swallows bound keys so they can't click panel buttons. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+104
-11
@@ -20,6 +20,8 @@ internal sealed class MainForm : Form
|
||||
private readonly InputRouter _router;
|
||||
private readonly XInputGamepad _gamepad = new();
|
||||
private readonly KeyboardLampMirror _lampMirror;
|
||||
private readonly RawKeyboardSource _rawKeyboard = new();
|
||||
private string? _activeInputId; // interface path of the keyboard being captured; null = WinForms focus path
|
||||
private readonly string _bindingsPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "vRIO", "bindings.txt");
|
||||
|
||||
@@ -72,6 +74,18 @@ internal sealed class MainForm : Form
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
Enabled = false, // populated while the mirror is on
|
||||
};
|
||||
private readonly Label _kbInputLabel = new()
|
||||
{
|
||||
Text = "Capture keyboard (no window focus needed):",
|
||||
Location = new Point(10, 168),
|
||||
AutoSize = true,
|
||||
};
|
||||
private readonly ComboBox _kbInputTarget = new()
|
||||
{
|
||||
Location = new Point(10, 186),
|
||||
Width = 286,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
};
|
||||
|
||||
/// <summary>A keyboard choice in the lamp-mirror picker (null id = all).</summary>
|
||||
private sealed record KbChoice(string? Id, string Name)
|
||||
@@ -81,14 +95,14 @@ internal sealed class MainForm : Form
|
||||
|
||||
private readonly Label _counters = new()
|
||||
{
|
||||
Location = new Point(12, 414),
|
||||
Location = new Point(12, 458),
|
||||
AutoSize = true,
|
||||
Font = new Font("Consolas", 8f),
|
||||
};
|
||||
|
||||
private readonly Label _help = new()
|
||||
{
|
||||
Location = new Point(12, 470),
|
||||
Location = new Point(12, 514),
|
||||
MaximumSize = new Size(306, 0),
|
||||
AutoSize = true,
|
||||
ForeColor = Color.Gray,
|
||||
@@ -99,7 +113,7 @@ internal sealed class MainForm : Form
|
||||
|
||||
private readonly TextBox _logBox = new()
|
||||
{
|
||||
Location = new Point(12, 554),
|
||||
Location = new Point(12, 598),
|
||||
Multiline = true,
|
||||
ReadOnly = true,
|
||||
ScrollBars = ScrollBars.Both, // long wire lines don't wrap — scroll to read
|
||||
@@ -209,6 +223,18 @@ internal sealed class MainForm : Form
|
||||
_lampMirror.KeyboardsChanged += list => RunOnUi(() => RebuildKeyboardPicker(list));
|
||||
_kbLightsTarget.Items.Add(new KbChoice(null, "All keyboards"));
|
||||
_kbLightsTarget.SelectedIndex = 0;
|
||||
|
||||
// Raw-input capture: a chosen keyboard drives the panel even while the
|
||||
// sim has focus. Down-edges only fire when keyboard input is enabled;
|
||||
// up-edges always land so a release is never stranded by a mid-hold flip.
|
||||
_rawKeyboard.KeyDown += name => RunOnUi(() => { if (_kbInput.Checked) _router.KeyDown(name); });
|
||||
_rawKeyboard.KeyUp += name => RunOnUi(() => _router.KeyUp(name));
|
||||
_rawKeyboard.Logged += line => RunOnUi(() => PrependLog(line));
|
||||
_rawKeyboard.KeyboardsChanged += list => RunOnUi(() => RebuildInputKeyboardPicker(list));
|
||||
_kbInputTarget.SelectedIndexChanged += (_, _) => ApplyInputSource();
|
||||
_kbInputTarget.DropDown += (_, _) => _rawKeyboard.RefreshKeyboards();
|
||||
_kbInputTarget.Items.Add(new KbChoice(null, "All keyboards (focus only)"));
|
||||
_kbInputTarget.SelectedIndex = 0;
|
||||
_reloadBindings.Click += (_, _) => LoadBindings();
|
||||
_editBindings.Click += (_, _) => OpenBindingsFile();
|
||||
|
||||
@@ -223,6 +249,7 @@ internal sealed class MainForm : Form
|
||||
_uiTimer.Dispose();
|
||||
_inputTimer.Dispose();
|
||||
_lampMirror.Dispose();
|
||||
_rawKeyboard.Dispose();
|
||||
_service.Dispose();
|
||||
};
|
||||
|
||||
@@ -261,13 +288,13 @@ internal sealed class MainForm : Form
|
||||
device.Controls.AddRange(new Control[] { _spring, _centerAxes, _lampsOff, _testEnter, _testExit });
|
||||
panel.Controls.Add(device);
|
||||
|
||||
var input = new GroupBox { Text = "Input", Location = new Point(12, 224), Size = new Size(306, 182) };
|
||||
input.Controls.AddRange(new Control[] { _kbInput, _padInput, _invertY, _padStatus, _reloadBindings, _editBindings, _kbLights, _kbLightsTarget });
|
||||
var input = new GroupBox { Text = "Input", Location = new Point(12, 224), Size = new Size(306, 226) };
|
||||
input.Controls.AddRange(new Control[] { _kbInput, _padInput, _invertY, _padStatus, _reloadBindings, _editBindings, _kbLights, _kbLightsTarget, _kbInputLabel, _kbInputTarget });
|
||||
panel.Controls.Add(input);
|
||||
|
||||
panel.Controls.Add(_counters);
|
||||
panel.Controls.Add(_help);
|
||||
panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 536), AutoSize = true });
|
||||
panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 580), AutoSize = true });
|
||||
|
||||
_logBox.Size = new Size(306, ClientSize.Height - _logBox.Top - 44);
|
||||
panel.Controls.Add(_logBox);
|
||||
@@ -338,6 +365,30 @@ internal sealed class MainForm : Form
|
||||
_kbInput.Checked &&
|
||||
!_portBox.ContainsFocus && !_verMajor.ContainsFocus && !_verMinor.ContainsFocus && !_logBox.ContainsFocus;
|
||||
|
||||
protected override void OnHandleCreated(EventArgs e)
|
||||
{
|
||||
base.OnHandleCreated(e);
|
||||
_rawKeyboard.Attach(Handle);
|
||||
_rawKeyboard.RefreshKeyboards(); // seed the capture picker
|
||||
}
|
||||
|
||||
protected override void WndProc(ref Message m)
|
||||
{
|
||||
// Raw Input arrives outside the normal key-message path; feed it to the
|
||||
// capture source, then let DefWindowProc run its WM_INPUT cleanup.
|
||||
const int WM_INPUT = 0x00FF, WM_INPUT_DEVICE_CHANGE = 0x00FE;
|
||||
switch (m.Msg)
|
||||
{
|
||||
case WM_INPUT:
|
||||
_rawKeyboard.ProcessInput(m.LParam);
|
||||
break;
|
||||
case WM_INPUT_DEVICE_CHANGE:
|
||||
_rawKeyboard.HandleDeviceChange();
|
||||
break;
|
||||
}
|
||||
base.WndProc(ref m);
|
||||
}
|
||||
|
||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||
{
|
||||
// Intercept WM_KEYDOWN before dialog-key processing, or arrows/space
|
||||
@@ -350,7 +401,10 @@ internal sealed class MainForm : Form
|
||||
string name = (keyData & Keys.KeyCode).ToString();
|
||||
if (_router.HasKeyBinding(name))
|
||||
{
|
||||
_router.KeyDown(name);
|
||||
// While a specific keyboard is captured, that source owns
|
||||
// routing; still swallow the key so it can't click a button.
|
||||
if (!_rawKeyboard.IsCapturing)
|
||||
_router.KeyDown(name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -359,18 +413,57 @@ internal sealed class MainForm : Form
|
||||
|
||||
protected override void OnKeyUp(KeyEventArgs e)
|
||||
{
|
||||
// Unconditional: the router ignores keys it never saw go down, and a
|
||||
// release must land even if the checkbox flipped mid-hold.
|
||||
_router.KeyUp(e.KeyCode.ToString());
|
||||
// Unconditional in focus mode: the router ignores keys it never saw go
|
||||
// down, and a release must land even if the checkbox flipped mid-hold.
|
||||
// In capture mode the raw source owns edges, so don't double-release.
|
||||
if (!_rawKeyboard.IsCapturing)
|
||||
_router.KeyUp(e.KeyCode.ToString());
|
||||
base.OnKeyUp(e);
|
||||
}
|
||||
|
||||
protected override void OnDeactivate(EventArgs e)
|
||||
{
|
||||
_router.ReleaseAllKeys(); // key-up events are lost once unfocused
|
||||
// Focus mode loses key-up events once unfocused, so release held keys.
|
||||
// Capture mode keeps receiving them in the background — leave holds be.
|
||||
if (!_rawKeyboard.IsCapturing)
|
||||
_router.ReleaseAllKeys();
|
||||
base.OnDeactivate(e);
|
||||
}
|
||||
|
||||
/// <summary>Apply the capture-picker selection: swap the keyboard input source.</summary>
|
||||
private void ApplyInputSource()
|
||||
{
|
||||
string? id = (_kbInputTarget.SelectedItem as KbChoice)?.Id;
|
||||
if (id == _activeInputId)
|
||||
return;
|
||||
_activeInputId = id;
|
||||
_router.ReleaseAllKeys(); // don't strand holds across a source swap
|
||||
_rawKeyboard.SetTarget(id);
|
||||
PrependLog(id is null
|
||||
? "Keyboard input: all keyboards, only while vRIO has focus"
|
||||
: $"Keyboard input: capturing \"{(_kbInputTarget.SelectedItem as KbChoice)?.Name}\" in the background");
|
||||
}
|
||||
|
||||
/// <summary>Refresh the capture picker, keeping the pick if the device survived.</summary>
|
||||
private void RebuildInputKeyboardPicker(IReadOnlyList<(string Id, string Name)> keyboards)
|
||||
{
|
||||
string? selected = (_kbInputTarget.SelectedItem as KbChoice)?.Id;
|
||||
_kbInputTarget.BeginUpdate();
|
||||
_kbInputTarget.Items.Clear();
|
||||
_kbInputTarget.Items.Add(new KbChoice(null, "All keyboards (focus only)"));
|
||||
int select = 0;
|
||||
foreach ((string id, string name) in keyboards)
|
||||
{
|
||||
int idx = _kbInputTarget.Items.Add(new KbChoice(id, name));
|
||||
if (id == selected)
|
||||
select = idx;
|
||||
}
|
||||
// A vanished capture device falls back to "All keyboards"; the
|
||||
// selection-changed handler (ApplyInputSource) unregisters accordingly.
|
||||
_kbInputTarget.SelectedIndex = select;
|
||||
_kbInputTarget.EndUpdate();
|
||||
}
|
||||
|
||||
private void InputTick()
|
||||
{
|
||||
double now = _clock.Elapsed.TotalSeconds;
|
||||
|
||||
Reference in New Issue
Block a user