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>
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user