Editor: rework to the original Win32 RIO control-panel layout

Reworked the profile/mapping editor to mirror the original unfinished Win32
RIO driver's control-panel design (docs/Win32RIO, by FASA / Michel Lowrance)
instead of the flat config-sheet grid. The wallpaper region positions are a
VGA chroma-split display artifact, not the cockpit's logical shape.

- RioJoy.Core.Editing.CockpitPanel: the functional layout — five MFD clusters,
  four board columns (Throttle/Secondary/Screen/Joystick-Hat), an encoder-gauge
  strip, and the two later-added 4x4 keypads (Internal/External). Places every
  address 0x00-0x47 / 0x50-0x6F exactly once (unit-tested).
- Tray PanelView/PanelCanvas render the panel; ProfileEditorForm drives it.
  Buttons show label + assigned function (ButtonBinding.Describe -> CTL-N, JOY1,
  POV-U, Mse LB, RIO command names; unit-tested). Lamp shade is driven by the
  IsLit flag (not by whether a function is assigned); keypads are neutral blue
  with no Lit checkbox. Added an Unassign button next to Apply.
- Removed the superseded sheet-grid UI (SheetView/SheetCanvas); SheetLayout +
  config-sheet.csv stay as reference data.

docs/Win32RIO: the original driver (tasgame.sys, a 32-bit kernel HID minidriver
that opened the serial port, set baud/8N1/DTR, and spoke the *identical* RIO
protocol -- AnalogRequest/Reply, Button Pressed/Released, Check/Lamp/Reset/
Version -- validating our protocol port), oemsetup.inf, RemoteDriver.doc, and the
extracted control-panel / game-controllers mockups.

~236 xUnit tests green. PLAN.md updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-27 19:48:14 -05:00
co-authored by Claude Opus 4.8
parent 8d2d0b71aa
commit 094637b0a4
17 changed files with 625 additions and 212 deletions
+109
View File
@@ -0,0 +1,109 @@
namespace RioJoy.Core.Editing;
/// <summary>Shape of a panel group.</summary>
public enum PanelGroupKind
{
/// <summary>A 4×2 MFD button cluster (lamp buttons).</summary>
Mfd,
/// <summary>A vertical 1×8 board column (lamp buttons).</summary>
Column,
/// <summary>A 4×4 keypad (added later; no lamps).</summary>
Keypad,
}
/// <summary>
/// One functional group on the cockpit control panel: a titled block of address
/// buttons laid out in <see cref="Rows"/> × <see cref="Cols"/> (row-major, null =
/// empty slot). <see cref="OriginCol"/>/<see cref="OriginRow"/> place the block on
/// the panel in button-cell units (the renderer scales cells to pixels).
/// </summary>
public sealed record PanelGroup(
string Title,
PanelGroupKind Kind,
int Cols,
int Rows,
bool LampCapable,
int OriginCol,
int OriginRow,
IReadOnlyList<int?> Addresses);
/// <summary>One address button placed on the panel (absolute cell coordinates).</summary>
public sealed record PanelButton(int Address, PanelGroup Group, int Col, int Row, bool LampCapable);
/// <summary>
/// The cockpit control panel's logical layout — the functional grouping from the
/// original Win32 RIO design (docs/Win32RIO/control-panel-mockup.png): five MFD
/// clusters, four board columns, and the two later-added 4×4 keypads. Every RIO
/// address 0x000x47 / 0x500x6F is placed exactly once. Pure data so the mapping
/// is unit-tested; the WinForms editor renders these groups and binds each button.
/// </summary>
public static class CockpitPanel
{
// Helper: 4×2 MFD address block, top row then bottom row (addresses descend).
private static int?[] Mfd(int hi) => new int?[]
{
hi, hi - 1, hi - 2, hi - 3,
hi - 4, hi - 5, hi - 6, hi - 7,
};
// Helper: vertical 1×8 column from a base address upward.
private static int?[] Col8(int baseAddr) => new int?[]
{
baseAddr, baseAddr + 1, baseAddr + 2, baseAddr + 3,
baseAddr + 4, baseAddr + 5, baseAddr + 6, baseAddr + 7,
};
// Helper: 4×4 keypad. Physical keys 1 2 3 C / 4 5 6 D / 7 8 9 E / A 0 B F map to
// address = base + that hex digit.
private static int?[] Keypad(int baseAddr)
{
int[] digits = { 0x1, 0x2, 0x3, 0xC, 0x4, 0x5, 0x6, 0xD, 0x7, 0x8, 0x9, 0xE, 0xA, 0x0, 0xB, 0xF };
var a = new int?[16];
for (int i = 0; i < 16; i++) a[i] = baseAddr + digits[i];
return a;
}
/// <summary>All panel groups, positioned for rendering.</summary>
public static IReadOnlyList<PanelGroup> Groups { get; } = new[]
{
// Upper MFD row.
new PanelGroup("Upper Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 1, Mfd(0x2F)),
new PanelGroup("Upper Middle MFD", PanelGroupKind.Mfd, 4, 2, true, 5, 1, Mfd(0x27)),
new PanelGroup("Upper Right MFD", PanelGroupKind.Mfd, 4, 2, true, 10, 1, Mfd(0x37)),
// Lower MFD row.
new PanelGroup("Lower Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 5, Mfd(0x0F)),
new PanelGroup("Lower Right MFD", PanelGroupKind.Mfd, 4, 2, true, 10, 5, Mfd(0x07)),
// Board columns.
new PanelGroup("Throttle", PanelGroupKind.Column, 1, 8, true, 0, 9, Col8(0x38)),
new PanelGroup("Secondary", PanelGroupKind.Column, 1, 8, true, 5, 9, Col8(0x10)),
new PanelGroup("Screen", PanelGroupKind.Column, 1, 8, true, 7, 9, Col8(0x18)),
new PanelGroup("Joystick / Hat", PanelGroupKind.Column, 1, 8, true, 13, 9, Col8(0x40)),
// Keypads (no lamps): internal in the middle (below Upper Middle MFD, above
// the Secondary/Screen columns); external between those columns and Joystick.
new PanelGroup("Internal Keypad", PanelGroupKind.Keypad, 4, 4, false, 5, 4, Keypad(0x50)),
new PanelGroup("External Keypad", PanelGroupKind.Keypad, 4, 4, false, 9, 9, Keypad(0x60)),
};
/// <summary>Flatten the groups to positioned <see cref="PanelButton"/>s (absolute cells).</summary>
public static IReadOnlyList<PanelButton> Buttons()
{
var buttons = new List<PanelButton>();
foreach (PanelGroup g in Groups)
{
for (int i = 0; i < g.Addresses.Count; i++)
{
if (g.Addresses[i] is not { } addr) continue;
int localCol = i % g.Cols;
int localRow = i / g.Cols;
// +1 row leaves space under the title.
buttons.Add(new PanelButton(addr, g, g.OriginCol + localCol, g.OriginRow + 1 + localRow, g.LampCapable));
}
}
return buttons;
}
}
+33
View File
@@ -34,6 +34,39 @@ public sealed class ButtonBinding
/// <summary>True when this binding does nothing (an empty slot).</summary>
public bool IsUnmapped => ToWord() == 0;
/// <summary>
/// A short human-readable description of the assigned function — e.g.
/// <c>CTL-N</c>, <c>JOY1</c>, <c>POV-U</c>, <c>Mse LB</c>, or a RIO command name.
/// Empty when unmapped. Used as the editor cell caption.
/// </summary>
public string Describe() => Kind switch
{
_ when IsUnmapped => string.Empty,
RioRouteKind.Keyboard =>
(Ctrl ? "CTL-" : "") + (Shift ? "SFT-" : "") + (Alt ? "ALT-" : "") + KeyCatalog.NameFor(Value),
RioRouteKind.Joystick => $"JOY{Value}",
RioRouteKind.Hat => (RioHat)Value switch
{
RioHat.Up => "POV-U",
RioHat.Right => "POV-R",
RioHat.Down => "POV-D",
RioHat.Left => "POV-L",
_ => "POV",
},
RioRouteKind.Mouse => "Mse " + (MouseAction)Value switch
{
MouseAction.MoveUp => "Up",
MouseAction.MoveDown => "Dn",
MouseAction.MoveLeft => "Lt",
MouseAction.MoveRight => "Rt",
MouseAction.LeftClick => "LB",
MouseAction.RightClick => "RB",
_ => Value.ToString(),
},
RioRouteKind.RioCommand => ((RioCommandCode)Value).ToString(),
_ => string.Empty,
};
/// <summary>Pack to the 16-bit <c>iRIO</c> map word.</summary>
public ushort ToWord() =>
RioMapEntry.Create(Kind, Value, IsLit, Shift, Ctrl, Alt, Extended).Raw;
+67
View File
@@ -0,0 +1,67 @@
using System.Runtime.Versioning;
using RioJoy.Core.Editing;
namespace RioJoy.Tray.Editor;
/// <summary>
/// Interactive cockpit control-panel map: draws the functional groups (via
/// <see cref="PanelView"/>) and raises <see cref="AddressSelected"/> when a button
/// is clicked. Sized to the full panel so the host scrolls it.
/// </summary>
[SupportedOSPlatform("windows")]
internal sealed class PanelCanvas : Control
{
private static readonly IReadOnlyList<PanelButton> AllButtons = CockpitPanel.Buttons();
public PanelCanvas()
{
DoubleBuffered = true;
BackColor = Color.FromArgb(28, 28, 28);
Size = PanelView.GridSize(AllButtons);
}
/// <summary>Resolves the display label for an address (from the profile).</summary>
public Func<int, string?>? LabelProvider { get; set; }
/// <summary>Resolves the assigned-function caption for an address (empty = unmapped).</summary>
public Func<int, string?>? FunctionProvider { get; set; }
/// <summary>Whether an address's lamp is lit (the binding's IsLit flag) — drives the lamp shade.</summary>
public Func<int, bool>? LitProvider { get; set; }
public int? SelectedAddress { get; private set; }
public event Action<int>? AddressSelected;
public void Select(int? address)
{
SelectedAddress = address;
Invalidate();
}
public void Refresh(int? address)
{
SelectedAddress = address;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e) =>
PanelView.Paint(
e.Graphics, AllButtons, CockpitPanel.Groups,
a => LabelProvider?.Invoke(a),
a => FunctionProvider?.Invoke(a),
a => LitProvider?.Invoke(a) ?? false,
SelectedAddress);
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
PanelButton? hit = PanelView.HitTest(AllButtons, e.Location);
if (hit is not null)
{
SelectedAddress = hit.Address;
AddressSelected?.Invoke(hit.Address);
Invalidate();
}
}
}
+135
View File
@@ -0,0 +1,135 @@
using System.Runtime.Versioning;
using RioJoy.Core.Editing;
namespace RioJoy.Tray.Editor;
/// <summary>
/// Draws the cockpit control panel — the functional groups from
/// <see cref="CockpitPanel"/> (MFD clusters, board columns, keypads) plus the
/// encoder-gauge strip — mirroring the original Win32 RIO design. Lamp buttons are
/// shaded by whether the address is assigned (Off vs Dim); keypads are neutral
/// (no lamps). Shared by <see cref="PanelCanvas"/> and offline rendering.
/// </summary>
[SupportedOSPlatform("windows")]
public static class PanelView
{
public const int CellW = 66;
public const int CellH = 34;
public const int TopStrip = 92; // encoder-gauge band height
public static Size GridSize(IReadOnlyList<PanelButton> buttons)
{
int maxCol = 0, maxRow = 0;
foreach (PanelButton b in buttons)
{
if (b.Col > maxCol) maxCol = b.Col;
if (b.Row > maxRow) maxRow = b.Row;
}
return new Size((maxCol + 2) * CellW, TopStrip + (maxRow + 2) * CellH);
}
private static Rectangle Cell(int col, int row) =>
new(col * CellW + 2, TopStrip + row * CellH + 2, CellW - 4, CellH - 4);
public static void Paint(
Graphics g,
IReadOnlyList<PanelButton> buttons,
IReadOnlyList<PanelGroup> groups,
Func<int, string?> label,
Func<int, string?> function,
Func<int, bool> lit,
int? selected)
{
g.Clear(Color.FromArgb(28, 28, 28));
using var titleFont = new Font("Segoe UI", 8f, FontStyle.Bold);
using var btnFont = new Font("Segoe UI", 7.5f);
using var subFont = new Font("Segoe UI", 6.75f);
using var groupPen = new Pen(Color.FromArgb(80, 80, 80));
DrawEncoderStrip(g, titleFont);
// Group frames + titles.
foreach (PanelGroup grp in groups)
{
var frame = new Rectangle(
grp.OriginCol * CellW + 1,
TopStrip + grp.OriginRow * CellH + 1,
grp.Cols * CellW,
(grp.Rows + 1) * CellH);
g.DrawRectangle(groupPen, frame);
TextRenderer.DrawText(g, grp.Title, titleFont,
new Rectangle(frame.X + 2, frame.Y, frame.Width - 2, CellH), Color.Gainsboro,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
}
// Buttons: caption = label (top) + assigned function (bottom).
const TextFormatFlags oneLine = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter |
TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix;
foreach (PanelButton b in buttons)
{
Rectangle r = Cell(b.Col, b.Row);
string? lab = label(b.Address);
string? fn = function(b.Address);
bool assigned = !string.IsNullOrEmpty(fn);
Color fill = !b.LampCapable
? Color.FromArgb(150, 182, 226) // keypad — neutral blue
: lit(b.Address)
? Color.FromArgb(215, 60, 60) // lamp ON (IsLit checked)
: Color.FromArgb(86, 48, 48); // lamp OFF
using (var fb = new SolidBrush(fill)) g.FillRectangle(fb, r);
Color fg = b.LampCapable ? Color.White : Color.Black;
bool hasLab = !string.IsNullOrEmpty(lab);
if (hasLab && assigned)
{
TextRenderer.DrawText(g, lab, btnFont, new Rectangle(r.X, r.Y + 1, r.Width, r.Height / 2), fg,
TextFormatFlags.HorizontalCenter | TextFormatFlags.Top | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix);
TextRenderer.DrawText(g, fn, subFont, new Rectangle(r.X, r.Bottom - r.Height / 2 - 1, r.Width, r.Height / 2), fg,
TextFormatFlags.HorizontalCenter | TextFormatFlags.Bottom | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix);
}
else
{
string single = hasLab ? lab! : assigned ? fn! : $"{b.Address:X2}";
TextRenderer.DrawText(g, single, btnFont, r, fg, oneLine);
}
if (b.Address == selected)
{
using var hi = new Pen(Color.Yellow, 2f);
g.DrawRectangle(hi, r.X, r.Y, r.Width - 1, r.Height - 1);
}
}
}
private static void DrawEncoderStrip(Graphics g, Font font)
{
using var pen = new Pen(Color.FromArgb(120, 160, 120));
using var gauge = new SolidBrush(Color.FromArgb(20, 40, 20));
(string Label, int X, int W)[] boxes =
{
("X / Y", 4, 80),
("Z", 92, 26),
("L", 122, 26),
("R", 152, 26),
};
foreach (var (lbl, x, w) in boxes)
{
var r = new Rectangle(x, 6, w, TopStrip - 24);
g.FillRectangle(gauge, r);
g.DrawRectangle(pen, r);
TextRenderer.DrawText(g, lbl, font, new Rectangle(x, TopStrip - 18, w, 16), Color.Gainsboro,
TextFormatFlags.HorizontalCenter);
}
}
public static PanelButton? HitTest(IReadOnlyList<PanelButton> buttons, Point p)
{
foreach (PanelButton b in buttons)
if (Cell(b.Col, b.Row).Contains(p))
return b;
return null;
}
}
+48 -29
View File
@@ -7,9 +7,10 @@ using RioJoy.Core.Profiles;
namespace RioJoy.Tray.Editor;
/// <summary>
/// Profile editor: shows the cockpit authoring sheet (its logical layout, from
/// <see cref="SheetLayout"/>) as a scrollable, clickable grid and edits the
/// selected address cell's label and action/modifiers/lamp. Writes back to the
/// Profile editor: shows the cockpit control panel (the functional grouping from
/// the original Win32 RIO design — MFD clusters, board columns, keypads, encoder
/// gauges, via <see cref="CockpitPanel"/>) as a clickable map, and edits the
/// selected button's label and action/modifiers/lamp. Writes back to the
/// <see cref="RioProfile"/>'s <see cref="RioProfile.OverlayLabels"/> (keyed by the
/// wallpaper region <c>b-XX</c>) and <see cref="RioProfile.Buttons"/> (the iRIO
/// word); <c>Save</c> raises <see cref="Saved"/>.
@@ -17,9 +18,14 @@ namespace RioJoy.Tray.Editor;
[SupportedOSPlatform("windows")]
public sealed class ProfileEditorForm : Form
{
private static readonly Dictionary<int, string> GroupOf =
CockpitPanel.Buttons().ToDictionary(b => b.Address, b => b.Group.Title);
private static readonly Dictionary<int, bool> LampCapableOf =
CockpitPanel.Buttons().ToDictionary(b => b.Address, b => b.LampCapable);
private readonly RioProfile _profile;
private readonly IReadOnlyList<SheetCell> _cells;
private readonly SheetCanvas _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 _labelBox = new() { Location = new Point(70, 44), Width = 230 };
@@ -32,6 +38,7 @@ public sealed class ProfileEditorForm : Form
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 };
@@ -47,19 +54,21 @@ public sealed class ProfileEditorForm : Form
/// <summary>Raised when the user saves; the argument is the edited profile.</summary>
public event Action<RioProfile>? Saved;
public ProfileEditorForm(IReadOnlyList<SheetCell> cells, RioProfile profile)
public ProfileEditorForm(RioProfile profile)
{
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
Text = $"RIOJoy — Edit profile: {profile.Name}";
ClientSize = new Size(1500, 820);
ClientSize = new Size(1320, 820);
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(900, 500);
_kindBox.Items.AddRange(Enum.GetNames<RioRouteKind>());
_kindBox.SelectedIndexChanged += OnKindChanged;
_canvas.SetCells(cells);
_canvas.LabelProvider = a => _profile.OverlayLabels.TryGetValue(RegionFor(a), out string? t) ? t : null;
_canvas.FunctionProvider = a => _profile.Buttons.TryGetValue(a, out int w) ? ButtonBinding.FromWord((ushort)w).Describe() : null;
_canvas.LitProvider = a => _profile.Buttons.TryGetValue(a, out int w) && ButtonBinding.FromWord((ushort)w).IsLit;
_canvas.AddressSelected += OnAddressSelected;
var scroller = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.FromArgb(28, 28, 28) };
@@ -71,11 +80,12 @@ public sealed class ProfileEditorForm : Form
Controls.Add(editPanel);
_apply.Click += (_, _) => ApplyToCell();
_unassign.Click += (_, _) => Unassign();
_save.Click += (_, _) => { ApplyToCell(); Saved?.Invoke(_profile); };
_close.Click += (_, _) => Close();
SetEditingEnabled(false);
_info.Text = "Click a coloured address cell to edit it.";
_info.Text = "Click a button to edit it.";
}
/// <summary>The selected RIO address (for tests / offline rendering).</summary>
@@ -92,25 +102,23 @@ public sealed class ProfileEditorForm : Form
panel.Controls.Add(_kindBox);
panel.Controls.Add(_valueLabel);
panel.Controls.Add(_valueCombo);
panel.Controls.AddRange(new Control[] { _shift, _ctrl, _alt, _ext, _lit, _apply, _save, _close });
panel.Controls.AddRange(new Control[] { _shift, _ctrl, _alt, _ext, _lit, _apply, _unassign, _save, _close });
return panel;
}
private void OnAddressSelected(SheetCell cell)
private void OnAddressSelected(int addr)
{
if (cell.Address is not { } addr) { SetEditingEnabled(false); return; }
_address = addr;
string region = RegionFor(addr);
_info.Text = $"Cell {cell.Text} Address 0x{addr:X2} {cell.Section} (wallpaper {region})";
_labelBox.Text = _profile.OverlayLabels.TryGetValue(region, out string? label) ? label : string.Empty;
string group = GroupOf.TryGetValue(addr, out string? gn) ? gn : "—";
_info.Text = $"Address 0x{addr:X2} {group} (wallpaper {region})";
ButtonBinding b = ButtonBinding.FromWord(
_profile.Buttons.TryGetValue(addr, out int w) ? (ushort)w : (ushort)0);
_loading = true;
_labelBox.Text = _profile.OverlayLabels.TryGetValue(region, out string? label) ? label : string.Empty;
_kindBox.SelectedItem = b.Kind.ToString();
PopulateValues(b.Kind);
SelectValue(b.Value);
@@ -119,6 +127,7 @@ public sealed class ProfileEditorForm : Form
_alt.Checked = b.Alt;
_ext.Checked = b.Extended;
_lit.Checked = b.IsLit;
_lit.Visible = LampCapableOf.GetValueOrDefault(addr); // keypads have no lamp
_loading = false;
SetEditingEnabled(true);
@@ -147,7 +156,6 @@ public sealed class ProfileEditorForm : Form
_valueCombo.BeginUpdate();
_valueCombo.Items.Clear();
switch (kind)
{
case RioRouteKind.Keyboard:
@@ -171,7 +179,6 @@ public sealed class ProfileEditorForm : Form
_valueCombo.Items.Add(new ValueItem(c.ToString(), (byte)c));
break;
}
_valueCombo.EndUpdate();
UpdateModifierAvailability();
}
@@ -186,12 +193,10 @@ public sealed class ProfileEditorForm : Form
return;
}
}
// Not in the list (e.g. an uncatalogued VK): add a raw fallback entry.
int idx = _valueCombo.Items.Add(new ValueItem($"0x{value:X2}", value));
_valueCombo.SelectedIndex = idx;
}
// Modifiers/extended only make sense for keyboard actions.
private void UpdateModifierAvailability()
{
bool keyboard = SelectedKind() == RioRouteKind.Keyboard;
@@ -223,7 +228,7 @@ public sealed class ProfileEditorForm : Form
Ctrl = keyboard && _ctrl.Checked,
Alt = keyboard && _alt.Checked,
Extended = keyboard && _ext.Checked,
IsLit = _lit.Checked,
IsLit = LampCapableOf.GetValueOrDefault(addr) && _lit.Checked, // keypads never lit
};
ushort word = binding.ToWord();
@@ -231,11 +236,29 @@ public sealed class ProfileEditorForm : Form
_profile.Buttons.Remove(addr);
else
_profile.Buttons[addr] = word;
_canvas.Refresh(addr); // update lamp shade + label on the panel
}
// Clear the selected button's action mapping (the label is left untouched).
private void Unassign()
{
if (_address is not { } addr) return;
_profile.Buttons.Remove(addr);
_loading = true;
_kindBox.SelectedItem = RioRouteKind.Keyboard.ToString();
PopulateValues(RioRouteKind.Keyboard);
if (_valueCombo.Items.Count > 0) _valueCombo.SelectedIndex = 0;
_shift.Checked = _ctrl.Checked = _alt.Checked = _ext.Checked = _lit.Checked = false;
_loading = false;
_canvas.Refresh(addr);
}
private void SetEditingEnabled(bool enabled)
{
foreach (Control c in new Control[] { _labelBox, _kindBox, _valueCombo, _shift, _ctrl, _alt, _ext, _lit, _apply })
foreach (Control c in new Control[] { _labelBox, _kindBox, _valueCombo, _shift, _ctrl, _alt, _ext, _lit, _apply, _unassign })
c.Enabled = enabled;
if (enabled) UpdateModifierAvailability();
}
@@ -245,11 +268,7 @@ public sealed class ProfileEditorForm : Form
/// <summary>Select an address programmatically (for tests / offline preview).</summary>
public void SelectAddress(int address)
{
SheetCell? cell = _cells.FirstOrDefault(c => c.Address == address);
if (cell is not null)
{
_canvas.Select(address);
OnAddressSelected(cell);
}
_canvas.Select(address);
OnAddressSelected(address);
}
}
-57
View File
@@ -1,57 +0,0 @@
using System.Runtime.Versioning;
using RioJoy.Core.Editing;
namespace RioJoy.Tray.Editor;
/// <summary>
/// Scrollable cockpit-sheet grid: draws the cells (via <see cref="SheetView"/>) at a
/// fixed cell size and raises <see cref="AddressSelected"/> when an address cell is
/// clicked. Sized to the full grid so the host panel scrolls it.
/// </summary>
[SupportedOSPlatform("windows")]
internal sealed class SheetCanvas : Control
{
private const int CellW = 62;
private const int CellH = 22;
private IReadOnlyList<SheetCell> _cells = Array.Empty<SheetCell>();
public SheetCanvas()
{
DoubleBuffered = true;
BackColor = Color.FromArgb(28, 28, 28);
}
/// <summary>The RIO address currently highlighted, or null.</summary>
public int? SelectedAddress { get; private set; }
public event Action<SheetCell>? AddressSelected;
public void SetCells(IReadOnlyList<SheetCell> cells)
{
_cells = cells;
Size = SheetView.GridSize(cells, CellW, CellH);
Invalidate();
}
public void Select(int? address)
{
SelectedAddress = address;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e) =>
SheetView.Paint(e.Graphics, _cells, CellW, CellH, SelectedAddress);
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
SheetCell? hit = SheetView.HitTest(_cells, e.Location, CellW, CellH);
if (hit?.Address is { } addr)
{
SelectedAddress = addr;
AddressSelected?.Invoke(hit);
Invalidate();
}
}
}
-83
View File
@@ -1,83 +0,0 @@
using System.Runtime.Versioning;
using RioJoy.Core.Editing;
namespace RioJoy.Tray.Editor;
/// <summary>
/// Draws the cockpit authoring sheet as a grid of cells at their spreadsheet
/// (row, col) positions — the cockpit's logical layout. Address cells are filled by
/// section and clickable; scaffolding/label cells are drawn as context text. Shared
/// by <see cref="SheetCanvas"/> and offline bitmap rendering.
/// </summary>
[SupportedOSPlatform("windows")]
public static class SheetView
{
public static Color SectionColor(SheetSection section) => section switch
{
SheetSection.Contacts or SheetSection.Letters => Color.FromArgb(232, 150, 150), // red
SheetSection.Secondary or SheetSection.Screen => Color.FromArgb(245, 224, 150), // yellow
SheetSection.Throttle or SheetSection.Joystick => Color.FromArgb(150, 182, 226), // blue
SheetSection.Keypad or SheetSection.ExtKeypad => Color.FromArgb(165, 212, 165), // green
_ => Color.Gainsboro,
};
public static Size GridSize(IReadOnlyList<SheetCell> cells, int cellW, int cellH)
{
int maxCol = 0, maxRow = 0;
foreach (SheetCell c in cells)
{
if (c.Col > maxCol) maxCol = c.Col;
if (c.Row > maxRow) maxRow = c.Row;
}
return new Size((maxCol + 1) * cellW, (maxRow + 1) * cellH);
}
public static void Paint(
Graphics g,
IReadOnlyList<SheetCell> cells,
int cellW,
int cellH,
int? selectedAddress)
{
g.Clear(Color.FromArgb(28, 28, 28));
using var addrFont = new Font("Segoe UI", 7.5f, FontStyle.Bold);
using var textFont = new Font("Segoe UI", 7f);
using var border = new Pen(Color.FromArgb(70, 70, 70));
foreach (SheetCell cell in cells)
{
var r = new Rectangle(cell.Col * cellW, cell.Row * cellH, cellW, cellH);
if (cell.Address is { } addr)
{
using var fill = new SolidBrush(SectionColor(cell.Section));
g.FillRectangle(fill, r);
g.DrawRectangle(border, r);
TextRenderer.DrawText(g, cell.Text, addrFont, r, Color.Black,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix);
if (addr == selectedAddress)
{
using var hi = new Pen(Color.Yellow, 2f);
g.DrawRectangle(hi, r.X + 1, r.Y + 1, r.Width - 2, r.Height - 2);
}
}
else
{
// Scaffolding / KEY / LABEL / section headers — context only.
TextRenderer.DrawText(g, cell.Text, textFont, r, Color.Silver,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter |
TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix);
}
}
}
/// <summary>The address cell at a pixel point, or null.</summary>
public static SheetCell? HitTest(IReadOnlyList<SheetCell> cells, Point p, int cellW, int cellH)
{
int col = p.X / cellW;
int row = p.Y / cellH;
return cells.FirstOrDefault(c => c.Row == row && c.Col == col && c.Address is not null);
}
}
+3 -28
View File
@@ -1,5 +1,4 @@
using System.Runtime.Versioning;
using RioJoy.Core.Editing;
using RioJoy.Core.Mapping;
using RioJoy.Core.Profiles;
using RioJoy.Tray.Editor;
@@ -135,35 +134,11 @@ internal sealed class TrayApplicationContext : ApplicationContext
private void OpenEditor()
{
// The cockpit sheet (config-sheet.csv) sits beside the overlay template.
string? templatePath = _config.OverlayTemplatePath;
string? sheetPath = string.IsNullOrWhiteSpace(templatePath)
? null
: Path.Combine(Path.GetDirectoryName(Path.GetFullPath(templatePath)) ?? ".", "config-sheet.csv");
if (sheetPath is null || !File.Exists(sheetPath))
{
MessageBox.Show(
"Set 'OverlayTemplatePath' in the config and place 'config-sheet.csv' beside it to use the editor.",
"RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
IReadOnlyList<SheetCell> cells;
try
{
cells = SheetLayout.Load(sheetPath);
}
catch (Exception ex)
{
MessageBox.Show($"Could not load the cockpit sheet:\n{ex.Message}", "RIOJoy",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// The cockpit panel layout is built in (CockpitPanel); the editor just needs
// a profile to edit.
RioProfile profile = _config.Profiles.FirstOrDefault() ?? AddBlankProfile();
var editor = new ProfileEditorForm(cells, profile);
var editor = new ProfileEditorForm(profile);
editor.Saved += _ => ConfigStore.Save(_config, ConfigPath);
editor.Show();
}