Author SHA1 Message Date
CydandClaude Fable 5 d13d434e88 editor: RIO port/pipe picker on the profile edit panel
The right-hand panel gains a "RIO port" row under Triggers: an editable
combo offering the app default (shown with its value), the machine's COM
ports, and pipe:vrio, with free text for anything else. Save stores the
endpoint in RioProfile.RioComPort (blank or the default entry = null =
follow DefaultRioComPort).

Because the editor session holds the endpoint it opened with, a saved
port change now re-arms the session live: the tray host unhooks the old
runtime's editor wiring, re-activates on the new endpoint, re-hooks, and
restores the output-gate state - so the live RIO commands and button
echo follow the new port/pipe without closing the editor.

Verified with the offline DrawToBitmap harness (layout, both default and
pipe:vrio states) plus a scripted save round-trip (default->null,
pipe:vrio->stored, blank->null, reopen shows stored COM7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 11:39:52 -05:00
2 changed files with 108 additions and 37 deletions
+61 -22
View File
@@ -5,6 +5,7 @@ using RioJoy.Core.Output;
using RioJoy.Core.Mapping;
using RioJoy.Core.Profiles;
using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;
namespace RioJoy.Tray.Editor;
@@ -30,26 +31,32 @@ public sealed class ProfileEditorForm : Form
private readonly TextBox _nameBox = new() { Location = new Point(66, 12), Width = 234 };
private readonly TextBox _matchBox = new() { Location = new Point(66, 40), Width = 234 };
private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 68), MaximumSize = new Size(310, 0) };
private readonly TextBox _labelBox = new() { Location = new Point(70, 100), Width = 230 };
private readonly ComboBox _kindBox = new() { Location = new Point(70, 134), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 171), AutoSize = true };
private readonly ComboBox _valueCombo = new() { Location = new Point(70, 168), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList };
private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 200), AutoSize = true };
private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 200), AutoSize = true };
private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 200), AutoSize = true };
private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 200), AutoSize = true };
private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 228), AutoSize = true };
private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 262), Width = 110 };
private readonly Button _unassign = new() { Text = "Unassign", Location = new Point(190, 262), Width = 110 };
private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 296), Width = 110 };
private readonly Button _close = new() { Text = "Close", Location = new Point(190, 296), Width = 80 };
private readonly CheckBox _outputToggle = new() { Text = "Send button output to the PC", Location = new Point(12, 328), AutoSize = true };
// RIO endpoint: editable so any COM name or pipe:name goes; the drop-down
// offers the app default, the machine's COM ports, and the vRIO pipe.
private readonly ComboBox _portBox = new() { Location = new Point(66, 68), Width = 234, DropDownStyle = ComboBoxStyle.DropDown };
private readonly string _defaultPortItem;
private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 96), MaximumSize = new Size(310, 0) };
private readonly TextBox _labelBox = new() { Location = new Point(70, 128), Width = 230 };
private readonly ComboBox _kindBox = new() { Location = new Point(70, 162), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 199), AutoSize = true };
private readonly ComboBox _valueCombo = new() { Location = new Point(70, 196), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList };
private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 228), AutoSize = true };
private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 228), AutoSize = true };
private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 228), AutoSize = true };
private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 228), AutoSize = true };
private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 256), AutoSize = true };
private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 290), Width = 110 };
private readonly Button _unassign = new() { Text = "Unassign", Location = new Point(190, 290), Width = 110 };
private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 324), Width = 110 };
private readonly Button _close = new() { Text = "Close", Location = new Point(190, 324), Width = 80 };
private readonly CheckBox _outputToggle = new() { Text = "Send button output to the PC", Location = new Point(12, 356), AutoSize = true };
private readonly TextBox _statusBox = new()
{
Location = new Point(12, 646),
Size = new Size(306, 145),
Location = new Point(12, 674),
Size = new Size(306, 140),
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical,
@@ -104,13 +111,38 @@ public sealed class ProfileEditorForm : Form
/// <summary>Raised when the "send output to the PC" toggle changes (true = send).</summary>
public event Action<bool>? OutputsEnabledChanged;
public ProfileEditorForm(RioProfile profile)
/// <param name="profile">The profile to edit (mutated in place; Save persists).</param>
/// <param name="defaultEndpoint">
/// The app-wide RIO endpoint (<see cref="AppConfig.DefaultRioComPort"/>), shown
/// on the port picker's "(app default)" entry. Null just hides the value.
/// </param>
public ProfileEditorForm(RioProfile profile, string? defaultEndpoint = null)
{
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
Text = $"RIOJoy — Edit profile: {profile.Name}";
_nameBox.Text = profile.Name;
_matchBox.Text = string.Join(", ", profile.MatchExecutables);
// Endpoint suggestions: app default, the machine's COM ports, the vRIO
// pipe. Free text stays allowed — any COM name or pipe:name works.
_defaultPortItem = defaultEndpoint is null ? "(app default)" : $"(app default: {defaultEndpoint})";
_portBox.Items.Add(_defaultPortItem);
try
{
foreach (string port in System.IO.Ports.SerialPort.GetPortNames()
.Distinct().OrderBy(p => p, StringComparer.OrdinalIgnoreCase))
_portBox.Items.Add(port);
}
catch (Exception)
{
// Enumerating ports is best-effort (registry read) — typing still works.
}
_portBox.Items.Add(RioTransportFactory.PipeScheme + "vrio");
if (string.IsNullOrWhiteSpace(profile.RioComPort))
_portBox.SelectedIndex = 0;
else
_portBox.Text = profile.RioComPort;
ClientSize = new Size(1320, 820);
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(900, 500);
@@ -162,16 +194,18 @@ public sealed class ProfileEditorForm : Form
panel.Controls.Add(_nameBox);
panel.Controls.Add(new Label { Text = "Triggers:", Location = new Point(12, 43), AutoSize = true });
panel.Controls.Add(_matchBox);
panel.Controls.Add(new Label { Text = "RIO port:", Location = new Point(12, 71), AutoSize = true });
panel.Controls.Add(_portBox);
panel.Controls.Add(_info);
panel.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 103), AutoSize = true });
panel.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 131), AutoSize = true });
panel.Controls.Add(_labelBox);
panel.Controls.Add(new Label { Text = "Action:", Location = new Point(12, 137), AutoSize = true });
panel.Controls.Add(new Label { Text = "Action:", Location = new Point(12, 165), AutoSize = true });
panel.Controls.Add(_kindBox);
panel.Controls.Add(_valueLabel);
panel.Controls.Add(_valueCombo);
panel.Controls.AddRange(new Control[] { _shift, _ctrl, _alt, _ext, _lit, _apply, _unassign, _save, _close, _outputToggle });
panel.Controls.Add(BuildCommandGroup());
panel.Controls.Add(new Label { Text = "RIO reply:", Location = new Point(12, 628), AutoSize = true });
panel.Controls.Add(new Label { Text = "RIO reply:", Location = new Point(12, 656), AutoSize = true });
panel.Controls.Add(_statusBox);
return panel;
@@ -180,7 +214,7 @@ public sealed class ProfileEditorForm : Form
// A button per RIO device command, fired against the live RIO via CommandRequested.
private GroupBox BuildCommandGroup()
{
var group = new GroupBox { Text = "RIO commands (live)", Location = new Point(12, 358), Size = new Size(306, 262) };
var group = new GroupBox { Text = "RIO commands (live)", Location = new Point(12, 386), Size = new Size(306, 262) };
int y = 24;
foreach ((string label, RioCommandCode code) in RioCommands)
@@ -359,6 +393,11 @@ public sealed class ProfileEditorForm : Form
.Where(s => s.Length > 0)
.ToList();
// RIO endpoint: a COM name or pipe:name (e.g. pipe:vrio); blank or the
// "(app default)" entry stores null = follow DefaultRioComPort.
string port = _portBox.Text.Trim();
_profile.RioComPort = port.Length == 0 || port == _defaultPortItem ? null : port;
ApplyToCell();
try
{
+47 -15
View File
@@ -231,33 +231,65 @@ internal sealed class TrayApplicationContext : ApplicationContext
// is suppressed (no keystrokes); the editor only shows which button is pressed.
_coordinator.BeginEditorSession(profile);
var editor = new ProfileEditorForm(profile);
var editor = new ProfileEditorForm(profile, _config.DefaultRioComPort);
editor.IsNameAvailable = name => !_config.Profiles.Any(
p => !ReferenceEquals(p, profile) && string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
editor.Saved += _ => ConfigStore.Save(_config, ConfigPath);
editor.CommandRequested += cmd => _coordinator.Runtime?.Trigger(cmd);
editor.OutputsEnabledChanged += enabled => _coordinator.SetEditorOutputs(enabled);
RioRuntime? runtime = _coordinator.Runtime;
Action<int, bool>? activity = null;
if (runtime is not null)
// The editor's live wiring targets the CURRENT runtime, which is replaced
// when the session re-arms (endpoint change below) — so hook/unhook by pair.
Action? unhook = null;
void HookRuntime()
{
activity = editor.ShowLiveActivity;
runtime.ButtonActivity += activity;
RioRuntime? runtime = _coordinator.Runtime;
if (runtime is null)
{
unhook = null;
return;
}
runtime.ButtonActivity += editor.ShowLiveActivity;
runtime.AxesUpdated += editor.ShowAxes;
runtime.VersionReceived += editor.ShowVersion;
runtime.CheckReceived += editor.AddCheckStatus;
}
editor.FormClosed += (_, _) =>
{
if (runtime is not null && activity is not null)
unhook = () =>
{
runtime.ButtonActivity -= activity;
runtime.ButtonActivity -= editor.ShowLiveActivity;
runtime.AxesUpdated -= editor.ShowAxes;
runtime.VersionReceived -= editor.ShowVersion;
runtime.CheckReceived -= editor.AddCheckStatus;
};
}
bool outputsOn = false;
string armedEndpoint = profile.RioComPort ?? _config.DefaultRioComPort;
editor.Saved += _ =>
{
ConfigStore.Save(_config, ConfigPath);
// A saved port/pipe change takes effect immediately: re-arm the held
// session on the new endpoint so the live RIO buttons follow it.
string endpoint = profile.RioComPort ?? _config.DefaultRioComPort;
if (!string.Equals(endpoint, armedEndpoint, StringComparison.OrdinalIgnoreCase))
{
unhook?.Invoke();
_coordinator.BeginEditorSession(profile);
HookRuntime();
_coordinator.SetEditorOutputs(outputsOn); // new gates start closed
armedEndpoint = endpoint;
}
};
editor.CommandRequested += cmd => _coordinator.Runtime?.Trigger(cmd);
editor.OutputsEnabledChanged += enabled =>
{
outputsOn = enabled;
_coordinator.SetEditorOutputs(enabled);
};
HookRuntime();
editor.FormClosed += (_, _) =>
{
unhook?.Invoke();
_coordinator.EndEditorSession();
_watcher.Reset(); // re-sync with the foreground app (may re-activate a game)
_watcher.Poll();