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:
@@ -0,0 +1,50 @@
|
|||||||
|
using RioJoy.Core.Calibration;
|
||||||
|
|
||||||
|
namespace RioJoy.Core.Mapping;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IInputSink"/> that forwards to an inner sink only while
|
||||||
|
/// <see cref="Enabled"/> is set, and swallows output otherwise. Lets the editor put
|
||||||
|
/// real keyboard/mouse output behind a toggle (off by default, so editing the RIO
|
||||||
|
/// never injects input until the user opts in).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GatedInputSink : IInputSink
|
||||||
|
{
|
||||||
|
private readonly IInputSink _inner;
|
||||||
|
|
||||||
|
public GatedInputSink(IInputSink inner) =>
|
||||||
|
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||||
|
|
||||||
|
/// <summary>When false, all output is dropped.</summary>
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
|
public void KeyDown(byte virtualKey, bool extended) { if (Enabled) _inner.KeyDown(virtualKey, extended); }
|
||||||
|
|
||||||
|
public void KeyUp(byte virtualKey, bool extended) { if (Enabled) _inner.KeyUp(virtualKey, extended); }
|
||||||
|
|
||||||
|
public void MouseMove(int dx, int dy) { if (Enabled) _inner.MouseMove(dx, dy); }
|
||||||
|
|
||||||
|
public void MouseButton(MouseButton button, bool down) { if (Enabled) _inner.MouseButton(button, down); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IJoystickSink"/> that forwards to an inner sink only while
|
||||||
|
/// <see cref="Enabled"/> is set. The editor's output toggle gates the virtual
|
||||||
|
/// joystick the same way as the keyboard/mouse.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GatedJoystickSink : IJoystickSink
|
||||||
|
{
|
||||||
|
private readonly IJoystickSink _inner;
|
||||||
|
|
||||||
|
public GatedJoystickSink(IJoystickSink inner) =>
|
||||||
|
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||||
|
|
||||||
|
/// <summary>When false, all output is dropped.</summary>
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
|
public void SetButton(int button, bool pressed) { if (Enabled) _inner.SetButton(button, pressed); }
|
||||||
|
|
||||||
|
public void SetHat(RioHat position) { if (Enabled) _inner.SetHat(position); }
|
||||||
|
|
||||||
|
public void SetAxis(JoyAxis axis, int value) { if (Enabled) _inner.SetAxis(axis, value); }
|
||||||
|
}
|
||||||
@@ -110,19 +110,22 @@ public static class PanelView
|
|||||||
string? fn = function(b.Address);
|
string? fn = function(b.Address);
|
||||||
bool assigned = !string.IsNullOrEmpty(fn);
|
bool assigned = !string.IsNullOrEmpty(fn);
|
||||||
|
|
||||||
// A live (physically held) button lights bright in its own colour, as does
|
// Three lamp states in the cell's own colour: a physically held button is
|
||||||
// a lamp configured ON; otherwise the cell shows its idle/dim shade.
|
// BRIGHT; a lamp configured on (IsLit) is DIM at idle (so the press still
|
||||||
|
// reads brighter); everything else is OFF.
|
||||||
bool yellow = b.Group.Title is "Secondary" or "Screen";
|
bool yellow = b.Group.Title is "Secondary" or "Screen";
|
||||||
bool bright = lit(b.Address) || live(b.Address);
|
bool held = live(b.Address);
|
||||||
|
bool litCfg = lit(b.Address);
|
||||||
Color fill = !b.LampCapable
|
Color fill = !b.LampCapable
|
||||||
? (live(b.Address) ? Color.FromArgb(205, 228, 255) // keypad — bright blue when pressed
|
? (held ? Color.FromArgb(205, 228, 255) // keypad — bright blue when pressed
|
||||||
: Color.FromArgb(150, 182, 226)) // keypad — neutral blue
|
: Color.FromArgb(150, 182, 226)) // keypad — neutral blue
|
||||||
: yellow
|
: yellow
|
||||||
? (bright ? Color.FromArgb(240, 205, 50) // Secondary/Screen — yellow lamp
|
? (held ? Color.FromArgb(245, 210, 60) // Secondary/Screen — bright
|
||||||
: Color.FromArgb(120, 102, 30)) // Secondary/Screen — dim yellow
|
: litCfg ? Color.FromArgb(140, 118, 38) // dim (lamp on, idle)
|
||||||
: bright
|
: Color.FromArgb(70, 60, 24)) // off
|
||||||
? Color.FromArgb(215, 60, 60) // lamp ON (lit or held)
|
: (held ? Color.FromArgb(230, 70, 70) // lamp — bright (held)
|
||||||
: Color.FromArgb(86, 48, 48); // lamp OFF
|
: litCfg ? Color.FromArgb(120, 50, 50) // dim (lamp on, idle)
|
||||||
|
: Color.FromArgb(64, 40, 40)); // off
|
||||||
using (var fb = new SolidBrush(fill)) g.FillRectangle(fb, r);
|
using (var fb = new SolidBrush(fill)) g.FillRectangle(fb, r);
|
||||||
|
|
||||||
Color fg = (b.LampCapable && !yellow) ? Color.White : Color.Black;
|
Color fg = (b.LampCapable && !yellow) ? Color.White : Color.Black;
|
||||||
|
|||||||
@@ -29,25 +29,27 @@ public sealed class ProfileEditorForm : Form
|
|||||||
private readonly RioProfile _profile;
|
private readonly RioProfile _profile;
|
||||||
private readonly PanelCanvas _canvas = new();
|
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 _nameBox = new() { Location = new Point(66, 12), Width = 234 };
|
||||||
private readonly TextBox _labelBox = new() { Location = new Point(70, 44), Width = 230 };
|
private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 40), MaximumSize = new Size(310, 0) };
|
||||||
private readonly ComboBox _kindBox = new() { Location = new Point(70, 78), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
|
private readonly TextBox _labelBox = new() { Location = new Point(70, 72), Width = 230 };
|
||||||
private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 115), AutoSize = true };
|
private readonly ComboBox _kindBox = new() { Location = new Point(70, 106), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||||
private readonly ComboBox _valueCombo = new() { Location = new Point(70, 112), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList };
|
private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 143), AutoSize = true };
|
||||||
private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 144), AutoSize = true };
|
private readonly ComboBox _valueCombo = new() { Location = new Point(70, 140), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||||
private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 144), AutoSize = true };
|
private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 172), AutoSize = true };
|
||||||
private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 144), AutoSize = true };
|
private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 172), AutoSize = true };
|
||||||
private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 144), AutoSize = true };
|
private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 172), AutoSize = true };
|
||||||
private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 172), AutoSize = true };
|
private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 172), AutoSize = true };
|
||||||
private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 206), Width = 110 };
|
private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 200), AutoSize = true };
|
||||||
private readonly Button _unassign = new() { Text = "Unassign", Location = new Point(190, 206), Width = 110 };
|
private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 234), Width = 110 };
|
||||||
private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 240), Width = 110 };
|
private readonly Button _unassign = new() { Text = "Unassign", Location = new Point(190, 234), Width = 110 };
|
||||||
private readonly Button _close = new() { Text = "Close", Location = new Point(190, 240), Width = 80 };
|
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()
|
private readonly TextBox _statusBox = new()
|
||||||
{
|
{
|
||||||
Location = new Point(12, 566),
|
Location = new Point(12, 618),
|
||||||
Size = new Size(306, 190),
|
Size = new Size(306, 172),
|
||||||
Multiline = true,
|
Multiline = true,
|
||||||
ReadOnly = true,
|
ReadOnly = true,
|
||||||
ScrollBars = ScrollBars.Vertical,
|
ScrollBars = ScrollBars.Vertical,
|
||||||
@@ -57,6 +59,9 @@ public sealed class ProfileEditorForm : Form
|
|||||||
Text = "Press \"Request version & status\" to query the RIO.",
|
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 int? _address;
|
||||||
private bool _loading;
|
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>
|
/// <summary>Raised when the user saves; the argument is the edited profile.</summary>
|
||||||
public event Action<RioProfile>? Saved;
|
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>
|
/// <summary>Raised when the user clicks one of the live RIO-command buttons.</summary>
|
||||||
public event Action<RioCommandCode>? CommandRequested;
|
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)
|
public ProfileEditorForm(RioProfile profile)
|
||||||
{
|
{
|
||||||
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
||||||
|
|
||||||
Text = $"RIOJoy — Edit profile: {profile.Name}";
|
Text = $"RIOJoy — Edit profile: {profile.Name}";
|
||||||
|
_nameBox.Text = profile.Name;
|
||||||
ClientSize = new Size(1320, 820);
|
ClientSize = new Size(1320, 820);
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
MinimumSize = new Size(900, 500);
|
MinimumSize = new Size(900, 500);
|
||||||
@@ -119,8 +134,17 @@ public sealed class ProfileEditorForm : Form
|
|||||||
|
|
||||||
_apply.Click += (_, _) => ApplyToCell();
|
_apply.Click += (_, _) => ApplyToCell();
|
||||||
_unassign.Click += (_, _) => Unassign();
|
_unassign.Click += (_, _) => Unassign();
|
||||||
_save.Click += (_, _) => { ApplyToCell(); Saved?.Invoke(_profile); };
|
_save.Click += (_, _) => SaveProfile();
|
||||||
_close.Click += (_, _) => Close();
|
_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);
|
SetEditingEnabled(false);
|
||||||
_info.Text = "Click a button to edit it.";
|
_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 };
|
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(_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(_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(_kindBox);
|
||||||
panel.Controls.Add(_valueLabel);
|
panel.Controls.Add(_valueLabel);
|
||||||
panel.Controls.Add(_valueCombo);
|
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(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);
|
panel.Controls.Add(_statusBox);
|
||||||
|
|
||||||
return panel;
|
return panel;
|
||||||
@@ -151,7 +177,7 @@ public sealed class ProfileEditorForm : Form
|
|||||||
// A button per RIO device command, fired against the live RIO via CommandRequested.
|
// A button per RIO device command, fired against the live RIO via CommandRequested.
|
||||||
private GroupBox BuildCommandGroup()
|
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;
|
int y = 24;
|
||||||
foreach ((string label, RioCommandCode code) in RioCommands)
|
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
|
_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).
|
// Clear the selected button's action mapping (the label is left untouched).
|
||||||
private void Unassign()
|
private void Unassign()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ public sealed class RioCoordinator : IDisposable
|
|||||||
private bool _auto = true;
|
private bool _auto = true;
|
||||||
private bool _editorSession;
|
private bool _editorSession;
|
||||||
|
|
||||||
|
// While editing, real keyboard/mouse + joystick run through these gates (off until
|
||||||
|
// the editor's "send output" toggle opens them).
|
||||||
|
private GatedInputSink? _editorInput;
|
||||||
|
private GatedJoystickSink? _editorJoystick;
|
||||||
|
|
||||||
// The active connection (null when dormant).
|
// The active connection (null when dormant).
|
||||||
private IRioTransport? _transport;
|
private IRioTransport? _transport;
|
||||||
private RioSerialLink? _link;
|
private RioSerialLink? _link;
|
||||||
@@ -116,6 +121,23 @@ public sealed class RioCoordinator : IDisposable
|
|||||||
Activate(profile, routeInput: false); // hold the port but don't inject keystrokes/joystick
|
Activate(profile, routeInput: false); // hold the port but don't inject keystrokes/joystick
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Open or close the editor's output gates: when enabled, RIO button presses
|
||||||
|
/// drive real keyboard/mouse and the virtual joystick; when disabled they don't.
|
||||||
|
/// No-op outside an editor session.
|
||||||
|
/// </summary>
|
||||||
|
public void SetEditorOutputs(bool enabled)
|
||||||
|
{
|
||||||
|
if (_editorInput is null || _editorJoystick is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_editorInput.Enabled = enabled;
|
||||||
|
_editorJoystick.Enabled = enabled;
|
||||||
|
|
||||||
|
if (_activeProfileName is not null)
|
||||||
|
SetStatus($"Editing: {_activeProfileName} — outputs {(enabled ? "ON (sending to PC)" : "off")}");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Release the editor's port hold and hand control back to the watcher.</summary>
|
/// <summary>Release the editor's port hold and hand control back to the watcher.</summary>
|
||||||
public void EndEditorSession()
|
public void EndEditorSession()
|
||||||
{
|
{
|
||||||
@@ -138,17 +160,31 @@ public sealed class RioCoordinator : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Keyboard/mouse + joystick are suppressed while editing so the RIO can be
|
// Keyboard/mouse + joystick are gated off while editing so the RIO can be
|
||||||
// exercised without injecting input; lamp feedback over serial still applies.
|
// exercised without injecting input (the editor toggle opens the gates);
|
||||||
|
// lamp feedback over serial still applies.
|
||||||
IInputSink input;
|
IInputSink input;
|
||||||
IJoystickSink joystick;
|
IJoystickSink joystick;
|
||||||
string note;
|
string note;
|
||||||
if (!routeInput)
|
if (!routeInput)
|
||||||
{
|
{
|
||||||
input = new NullInputSink();
|
IJoystickSink realJoystick;
|
||||||
joystick = new NullJoystickSink();
|
if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? editorFeeder))
|
||||||
_joystick = null;
|
{
|
||||||
note = " [editing — input suppressed]";
|
realJoystick = editorFeeder!;
|
||||||
|
_joystick = editorFeeder;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
realJoystick = new NullJoystickSink();
|
||||||
|
_joystick = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_editorInput = new GatedInputSink(new SendInputSink());
|
||||||
|
_editorJoystick = new GatedJoystickSink(realJoystick);
|
||||||
|
input = _editorInput;
|
||||||
|
joystick = _editorJoystick;
|
||||||
|
note = " [editing — outputs off]";
|
||||||
}
|
}
|
||||||
else if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
|
else if (HidFeederJoystickSink.TryCreate(out HidFeederJoystickSink? feeder))
|
||||||
{
|
{
|
||||||
@@ -252,6 +288,8 @@ public sealed class RioCoordinator : IDisposable
|
|||||||
|
|
||||||
_joystick?.Dispose(); // closes the HID feeder handle
|
_joystick?.Dispose(); // closes the HID feeder handle
|
||||||
_joystick = null;
|
_joystick = null;
|
||||||
|
_editorInput = null;
|
||||||
|
_editorJoystick = null;
|
||||||
|
|
||||||
_transport?.Dispose(); // releases the COM port
|
_transport?.Dispose(); // releases the COM port
|
||||||
_transport = null;
|
_transport = null;
|
||||||
|
|||||||
@@ -69,7 +69,11 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
|||||||
RebuildProfileMenu(profileMenu);
|
RebuildProfileMenu(profileMenu);
|
||||||
menu.Items.Add(profileMenu);
|
menu.Items.Add(profileMenu);
|
||||||
|
|
||||||
menu.Items.Add("Edit profile…", null, (_, _) => OpenEditor());
|
var editMenu = new ToolStripMenuItem("Edit profile");
|
||||||
|
editMenu.DropDownOpening += (_, _) => RebuildEditMenu(editMenu);
|
||||||
|
RebuildEditMenu(editMenu);
|
||||||
|
menu.Items.Add(editMenu);
|
||||||
|
|
||||||
menu.Items.Add("Import .ini…", null, (_, _) => ImportIniProfiles());
|
menu.Items.Add("Import .ini…", null, (_, _) => ImportIniProfiles());
|
||||||
|
|
||||||
// RIO commands mirroring the legacy console menu.
|
// RIO commands mirroring the legacy console menu.
|
||||||
@@ -134,20 +138,50 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
|||||||
profileMenu.DropDownItems.Add(dormant);
|
profileMenu.DropDownItems.Add(dormant);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenEditor()
|
// Rebuild the "Edit profile" submenu: one entry per profile (opens it in the
|
||||||
|
// editor) plus an entry to create a new one.
|
||||||
|
private void RebuildEditMenu(ToolStripMenuItem editMenu)
|
||||||
{
|
{
|
||||||
// The cockpit panel layout is built in (CockpitPanel); the editor just needs
|
editMenu.DropDownItems.Clear();
|
||||||
// a profile to edit.
|
|
||||||
RioProfile profile = _config.Profiles.FirstOrDefault() ?? AddBlankProfile();
|
|
||||||
|
|
||||||
|
foreach (RioProfile profile in _config.Profiles)
|
||||||
|
{
|
||||||
|
RioProfile captured = profile;
|
||||||
|
editMenu.DropDownItems.Add(new ToolStripMenuItem(profile.Name, null, (_, _) => OpenEditor(captured)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_config.Profiles.Count > 0)
|
||||||
|
editMenu.DropDownItems.Add(new ToolStripSeparator());
|
||||||
|
|
||||||
|
editMenu.DropDownItems.Add(new ToolStripMenuItem("New profile…", null, (_, _) => OpenEditor(NewProfile())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create, register and persist a fresh empty profile with a unique name.
|
||||||
|
private RioProfile NewProfile()
|
||||||
|
{
|
||||||
|
string name = "New profile";
|
||||||
|
for (int i = 2; _config.FindProfile(name) is not null; i++)
|
||||||
|
name = $"New profile {i}";
|
||||||
|
|
||||||
|
var profile = new RioProfile { Name = name };
|
||||||
|
_config.Profiles.Add(profile);
|
||||||
|
ConfigStore.Save(_config, ConfigPath);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenEditor(RioProfile profile)
|
||||||
|
{
|
||||||
// Acquiring the RIO is one of the two reasons we take the serial port (the
|
// Acquiring the RIO is one of the two reasons we take the serial port (the
|
||||||
// other being a profiled game) — so the editor can check button function. Input
|
// other being a profiled game) — so the editor can check button function. Input
|
||||||
// is suppressed (no keystrokes); the editor only shows which button is pressed.
|
// is suppressed (no keystrokes); the editor only shows which button is pressed.
|
||||||
_coordinator.BeginEditorSession(profile);
|
_coordinator.BeginEditorSession(profile);
|
||||||
|
|
||||||
var editor = new ProfileEditorForm(profile);
|
var editor = new ProfileEditorForm(profile);
|
||||||
|
editor.IsNameAvailable = name => !_config.Profiles.Any(
|
||||||
|
p => !ReferenceEquals(p, profile) && string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||||
editor.Saved += _ => ConfigStore.Save(_config, ConfigPath);
|
editor.Saved += _ => ConfigStore.Save(_config, ConfigPath);
|
||||||
editor.CommandRequested += cmd => _coordinator.Runtime?.Trigger(cmd);
|
editor.CommandRequested += cmd => _coordinator.Runtime?.Trigger(cmd);
|
||||||
|
editor.OutputsEnabledChanged += enabled => _coordinator.SetEditorOutputs(enabled);
|
||||||
|
|
||||||
RioRuntime? runtime = _coordinator.Runtime;
|
RioRuntime? runtime = _coordinator.Runtime;
|
||||||
Action<int, bool>? activity = null;
|
Action<int, bool>? activity = null;
|
||||||
@@ -174,13 +208,6 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
|||||||
editor.Show();
|
editor.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RioProfile AddBlankProfile()
|
|
||||||
{
|
|
||||||
var profile = new RioProfile { Name = "New profile" };
|
|
||||||
_config.Profiles.Add(profile);
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import one or more legacy RIO.ini files (the gsheet "INI_V2" format) as
|
// Import one or more legacy RIO.ini files (the gsheet "INI_V2" format) as
|
||||||
// profiles, naming each after its filename and de-duping collisions.
|
// profiles, naming each after its filename and de-duping collisions.
|
||||||
private void ImportIniProfiles()
|
private void ImportIniProfiles()
|
||||||
|
|||||||
Reference in New Issue
Block a user