Mirror lamps onto RGB keyboards via Windows Dynamic Lighting
Opt-in checkbox in the Input group: keys bound to lamp-capable button addresses glow with the panel's palette (red family, yellow for the Secondary/Screen columns) and blink at the panel's own flash rates, so keyboard and on-screen panel stay in step; unbound keys are blacked out so the keyboard reads as the button field. Zone-lit keyboards without per-key addressing (e.g. this laptop's 24-zone ITE board) fall back to mirroring the strongest current lamp board-wide. A picker under the checkbox narrows the mirror to one keyboard when several are attached (hot-plug aware; releases the others back to Windows). KeyboardLampMirror claims keyboard-kind LampArrays via a DeviceWatcher, repaints at 100 ms only when colors actually change, survives paint faults (a leaked Timer exception would kill the process), and logs attach/detach plus IsAvailable transitions with a pointer to the Dynamic Lighting background-control setting. Disabling releases every claim so the LEDs revert to the ambient scene. Also: the wire log gets a horizontal scrollbar (long lines don't wrap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ internal sealed class MainForm : Form
|
||||
private readonly PanelCanvas _canvas = new();
|
||||
private readonly InputRouter _router;
|
||||
private readonly XInputGamepad _gamepad = new();
|
||||
private readonly KeyboardLampMirror _lampMirror;
|
||||
private readonly string _bindingsPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "vRIO", "bindings.txt");
|
||||
|
||||
@@ -57,17 +58,36 @@ internal sealed class MainForm : Form
|
||||
};
|
||||
private readonly Button _reloadBindings = new() { Text = "Reload bindings", Location = new Point(10, 68), Width = 140, Height = 26 };
|
||||
private readonly Button _editBindings = new() { Text = "Edit bindings…", Location = new Point(156, 68), Width = 140, Height = 26 };
|
||||
private readonly CheckBox _kbLights = new()
|
||||
{
|
||||
Text = "Mirror lamps on RGB keyboard (Dynamic Lighting)",
|
||||
Location = new Point(10, 100),
|
||||
AutoSize = true,
|
||||
};
|
||||
private readonly ComboBox _kbLightsTarget = new()
|
||||
{
|
||||
Location = new Point(10, 122),
|
||||
Width = 286,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
Enabled = false, // populated while the mirror is on
|
||||
};
|
||||
|
||||
/// <summary>A keyboard choice in the lamp-mirror picker (null id = all).</summary>
|
||||
private sealed record KbChoice(string? Id, string Name)
|
||||
{
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
private readonly Label _counters = new()
|
||||
{
|
||||
Location = new Point(12, 336),
|
||||
Location = new Point(12, 390),
|
||||
AutoSize = true,
|
||||
Font = new Font("Consolas", 8f),
|
||||
};
|
||||
|
||||
private readonly Label _help = new()
|
||||
{
|
||||
Location = new Point(12, 392),
|
||||
Location = new Point(12, 446),
|
||||
MaximumSize = new Size(306, 0),
|
||||
AutoSize = true,
|
||||
ForeColor = Color.Gray,
|
||||
@@ -78,10 +98,10 @@ internal sealed class MainForm : Form
|
||||
|
||||
private readonly TextBox _logBox = new()
|
||||
{
|
||||
Location = new Point(12, 476),
|
||||
Location = new Point(12, 530),
|
||||
Multiline = true,
|
||||
ReadOnly = true,
|
||||
ScrollBars = ScrollBars.Vertical,
|
||||
ScrollBars = ScrollBars.Both, // long wire lines don't wrap — scroll to read
|
||||
BackColor = Color.FromArgb(24, 24, 24),
|
||||
ForeColor = Color.Gainsboro,
|
||||
Font = new Font("Consolas", 8f),
|
||||
@@ -112,6 +132,7 @@ internal sealed class MainForm : Form
|
||||
|
||||
_service = new VRioSerialService(_device);
|
||||
_router = new InputRouter(_device);
|
||||
_lampMirror = new KeyboardLampMirror(_device);
|
||||
|
||||
// Panel canvas, scrolled if the window is smaller than the grid.
|
||||
var scroller = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.FromArgb(28, 28, 28) };
|
||||
@@ -137,6 +158,7 @@ internal sealed class MainForm : Form
|
||||
_device.ResetReceived += _ => RunOnUi(_router.ResetAxisState);
|
||||
_device.Logged += line => RunOnUi(() => PrependLog(line));
|
||||
_service.Logged += line => RunOnUi(() => PrependLog(line));
|
||||
_lampMirror.Logged += line => RunOnUi(() => PrependLog(line));
|
||||
_service.ConnectionChanged += open => RunOnUi(() => OnConnectionChanged(open));
|
||||
_service.HostHandshake += high => RunOnUi(() =>
|
||||
PrependLog(high ? "Host raised DTR (board-reset handshake)" : "Host dropped DTR"));
|
||||
@@ -166,6 +188,16 @@ internal sealed class MainForm : Form
|
||||
if (!_kbInput.Checked)
|
||||
_router.ReleaseAllKeys();
|
||||
};
|
||||
_kbLights.CheckedChanged += (_, _) =>
|
||||
{
|
||||
_lampMirror.Enabled = _kbLights.Checked;
|
||||
_kbLightsTarget.Enabled = _kbLights.Checked;
|
||||
};
|
||||
_kbLightsTarget.SelectedIndexChanged += (_, _) =>
|
||||
_lampMirror.SetTarget((_kbLightsTarget.SelectedItem as KbChoice)?.Id);
|
||||
_lampMirror.KeyboardsChanged += list => RunOnUi(() => RebuildKeyboardPicker(list));
|
||||
_kbLightsTarget.Items.Add(new KbChoice(null, "All keyboards"));
|
||||
_kbLightsTarget.SelectedIndex = 0;
|
||||
_reloadBindings.Click += (_, _) => LoadBindings();
|
||||
_editBindings.Click += (_, _) => OpenBindingsFile();
|
||||
|
||||
@@ -179,6 +211,7 @@ internal sealed class MainForm : Form
|
||||
{
|
||||
_uiTimer.Dispose();
|
||||
_inputTimer.Dispose();
|
||||
_lampMirror.Dispose();
|
||||
_service.Dispose();
|
||||
};
|
||||
|
||||
@@ -217,13 +250,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, 104) };
|
||||
input.Controls.AddRange(new Control[] { _kbInput, _padInput, _padStatus, _reloadBindings, _editBindings });
|
||||
var input = new GroupBox { Text = "Input", Location = new Point(12, 224), Size = new Size(306, 158) };
|
||||
input.Controls.AddRange(new Control[] { _kbInput, _padInput, _padStatus, _reloadBindings, _editBindings, _kbLights, _kbLightsTarget });
|
||||
panel.Controls.Add(input);
|
||||
|
||||
panel.Controls.Add(_counters);
|
||||
panel.Controls.Add(_help);
|
||||
panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 458), AutoSize = true });
|
||||
panel.Controls.Add(new Label { Text = "Wire log:", Location = new Point(12, 512), AutoSize = true });
|
||||
|
||||
_logBox.Size = new Size(306, ClientSize.Height - _logBox.Top - 44);
|
||||
panel.Controls.Add(_logBox);
|
||||
@@ -370,6 +403,7 @@ internal sealed class MainForm : Form
|
||||
|
||||
var profile = BindingProfileFormat.Parse(text, out var errors);
|
||||
_router.Profile = profile;
|
||||
_lampMirror.SetProfile(profile);
|
||||
foreach (string error in errors)
|
||||
PrependLog($"Bindings: {error}");
|
||||
PrependLog($"Bindings loaded: {profile.Count} ({_bindingsPath})");
|
||||
@@ -395,6 +429,26 @@ internal sealed class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Refresh the lamp-mirror keyboard picker, keeping the pick if it survived.</summary>
|
||||
private void RebuildKeyboardPicker(IReadOnlyList<(string Id, string Name)> keyboards)
|
||||
{
|
||||
string? selected = (_kbLightsTarget.SelectedItem as KbChoice)?.Id;
|
||||
_kbLightsTarget.BeginUpdate();
|
||||
_kbLightsTarget.Items.Clear();
|
||||
_kbLightsTarget.Items.Add(new KbChoice(null, "All keyboards"));
|
||||
int select = 0;
|
||||
foreach ((string id, string name) in keyboards)
|
||||
{
|
||||
int idx = _kbLightsTarget.Items.Add(new KbChoice(id, name));
|
||||
if (id == selected)
|
||||
select = idx;
|
||||
}
|
||||
// Falls back to "All keyboards" if the picked device vanished (the
|
||||
// selection-changed handler re-targets the mirror accordingly).
|
||||
_kbLightsTarget.SelectedIndex = select;
|
||||
_kbLightsTarget.EndUpdate();
|
||||
}
|
||||
|
||||
// ---- Status / log ------------------------------------------------------
|
||||
|
||||
private void UpdateStatus()
|
||||
|
||||
Reference in New Issue
Block a user