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