Editor: profile rename/new, PC-output toggle, save & lamp feedback

- Profile name field in the editor (rename, with empty/duplicate checks);
  "Edit profile" becomes a submenu listing each profile plus "New profile…".
- "Send button output to the PC" toggle: editor sessions route keyboard/mouse
  and joystick through gates (GatedInputSink/GatedJoystickSink) that stay closed
  until the user opts in, so editing never injects input by default.
- Save profile now confirms ("Saved ✓"/"Save failed") — the write was silent.
- A lit button renders dim at idle and bright only when physically pressed, so
  the press stays visible (three states: off / dim-lit / bright-held).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-28 22:39:50 -05:00
co-authored by Claude Opus 4.8
parent 905b9facc9
commit 195038080e
5 changed files with 244 additions and 50 deletions
+98 -22
View File
@@ -29,25 +29,27 @@ public sealed class ProfileEditorForm : Form
private readonly RioProfile _profile;
private readonly PanelCanvas _canvas = new();
private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 12), MaximumSize = new Size(310, 0) };
private readonly TextBox _labelBox = new() { Location = new Point(70, 44), Width = 230 };
private readonly ComboBox _kindBox = new() { Location = new Point(70, 78), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 115), AutoSize = true };
private readonly ComboBox _valueCombo = new() { Location = new Point(70, 112), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList };
private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 144), AutoSize = true };
private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 144), AutoSize = true };
private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 144), AutoSize = true };
private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 144), AutoSize = true };
private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 172), AutoSize = true };
private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 206), Width = 110 };
private readonly Button _unassign = new() { Text = "Unassign", Location = new Point(190, 206), Width = 110 };
private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 240), Width = 110 };
private readonly Button _close = new() { Text = "Close", Location = new Point(190, 240), Width = 80 };
private readonly TextBox _nameBox = new() { Location = new Point(66, 12), Width = 234 };
private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 40), MaximumSize = new Size(310, 0) };
private readonly TextBox _labelBox = new() { Location = new Point(70, 72), Width = 230 };
private readonly ComboBox _kindBox = new() { Location = new Point(70, 106), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 143), AutoSize = true };
private readonly ComboBox _valueCombo = new() { Location = new Point(70, 140), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList };
private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 172), AutoSize = true };
private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 172), AutoSize = true };
private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 172), AutoSize = true };
private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 172), AutoSize = true };
private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 200), AutoSize = true };
private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 234), Width = 110 };
private readonly Button _unassign = new() { Text = "Unassign", Location = new Point(190, 234), Width = 110 };
private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 268), Width = 110 };
private readonly Button _close = new() { Text = "Close", Location = new Point(190, 268), Width = 80 };
private readonly CheckBox _outputToggle = new() { Text = "Send button output to the PC", Location = new Point(12, 300), AutoSize = true };
private readonly TextBox _statusBox = new()
{
Location = new Point(12, 566),
Size = new Size(306, 190),
Location = new Point(12, 618),
Size = new Size(306, 172),
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical,
@@ -57,6 +59,9 @@ public sealed class ProfileEditorForm : Form
Text = "Press \"Request version & status\" to query the RIO.",
};
// Briefly flips the Save button to a confirmation, then reverts.
private readonly System.Windows.Forms.Timer _saveFlash = new() { Interval = 1600 };
private int? _address;
private bool _loading;
@@ -87,14 +92,24 @@ public sealed class ProfileEditorForm : Form
/// <summary>Raised when the user saves; the argument is the edited profile.</summary>
public event Action<RioProfile>? Saved;
/// <summary>
/// Returns whether a candidate profile name is free to use (i.e. not taken by a
/// <em>different</em> profile). Set by the host; null = no uniqueness check.
/// </summary>
public Func<string, bool>? IsNameAvailable { get; set; }
/// <summary>Raised when the user clicks one of the live RIO-command buttons.</summary>
public event Action<RioCommandCode>? CommandRequested;
/// <summary>Raised when the "send output to the PC" toggle changes (true = send).</summary>
public event Action<bool>? OutputsEnabledChanged;
public ProfileEditorForm(RioProfile profile)
{
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
Text = $"RIOJoy — Edit profile: {profile.Name}";
_nameBox.Text = profile.Name;
ClientSize = new Size(1320, 820);
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(900, 500);
@@ -119,8 +134,17 @@ public sealed class ProfileEditorForm : Form
_apply.Click += (_, _) => ApplyToCell();
_unassign.Click += (_, _) => Unassign();
_save.Click += (_, _) => { ApplyToCell(); Saved?.Invoke(_profile); };
_save.Click += (_, _) => SaveProfile();
_close.Click += (_, _) => Close();
_outputToggle.CheckedChanged += (_, _) => OutputsEnabledChanged?.Invoke(_outputToggle.Checked);
_saveFlash.Tick += (_, _) =>
{
_saveFlash.Stop();
_save.Text = "Save profile";
_save.ForeColor = SystemColors.ControlText;
};
FormClosed += (_, _) => _saveFlash.Dispose();
SetEditingEnabled(false);
_info.Text = "Click a button to edit it.";
@@ -133,16 +157,18 @@ public sealed class ProfileEditorForm : Form
{
var panel = new Panel { Dock = DockStyle.Right, Width = 330, Padding = new Padding(8), BorderStyle = BorderStyle.FixedSingle, AutoScroll = true };
panel.Controls.Add(new Label { Text = "Profile:", Location = new Point(12, 15), AutoSize = true });
panel.Controls.Add(_nameBox);
panel.Controls.Add(_info);
panel.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 47), AutoSize = true });
panel.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 75), AutoSize = true });
panel.Controls.Add(_labelBox);
panel.Controls.Add(new Label { Text = "Action:", Location = new Point(12, 81), AutoSize = true });
panel.Controls.Add(new Label { Text = "Action:", Location = new Point(12, 109), 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 });
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, 548), AutoSize = true });
panel.Controls.Add(new Label { Text = "RIO reply:", Location = new Point(12, 600), AutoSize = true });
panel.Controls.Add(_statusBox);
return panel;
@@ -151,7 +177,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, 278), Size = new Size(306, 262) };
var group = new GroupBox { Text = "RIO commands (live)", Location = new Point(12, 330), Size = new Size(306, 262) };
int y = 24;
foreach ((string label, RioCommandCode code) in RioCommands)
@@ -309,6 +335,56 @@ public sealed class ProfileEditorForm : Form
_canvas.Refresh(addr); // update lamp shade + label on the panel
}
// Apply the pending cell edit, persist via the Saved handler, and confirm on the
// button (the actual write happens in the host's Saved handler — config.json).
private void SaveProfile()
{
if (!ApplyName())
return;
ApplyToCell();
try
{
Saved?.Invoke(_profile);
_save.Text = "Saved ✓";
_save.ForeColor = Color.ForestGreen;
}
catch (Exception ex)
{
_save.Text = "Save failed";
_save.ForeColor = Color.Firebrick;
MessageBox.Show(this, $"Could not save the profile:\n{ex.Message}",
"RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
_saveFlash.Stop();
_saveFlash.Start();
}
// Validate and apply the profile name from the name box. Returns false (and warns)
// if the name is blank or already taken by another profile, so the save is aborted.
private bool ApplyName()
{
string name = _nameBox.Text.Trim();
if (string.IsNullOrEmpty(name))
{
MessageBox.Show(this, "Profile name cannot be empty.", "RIOJoy",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
bool changed = !string.Equals(name, _profile.Name, StringComparison.OrdinalIgnoreCase);
if (changed && IsNameAvailable?.Invoke(name) == false)
{
MessageBox.Show(this, $"A profile named \"{name}\" already exists.", "RIOJoy",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
_profile.Name = name;
Text = $"RIOJoy — Edit profile: {name}";
return true;
}
// Clear the selected button's action mapping (the label is left untouched).
private void Unassign()
{