"Edit cell boxes" mode: the selected cell grows resize handles on the preview -- drag to move/resize with live outline tracking and a re-render on drop, or type exact X/Y/W/H in the new geometry fields. Plain clicks still select and cycle stacked cells (click-vs-drag resolved by a movement threshold on mouse-up). The move/resize math is pure and unit-tested (OverlayRegionEdit: corner/edge/move handle hit-testing with tolerance, min-size clamped resizing that pins the opposite edge). "Save template" persists the shared cell geometry back to the regions.json; "Reload template" re-reads it to discard unsaved box edits. Cursor feedback per handle; template save/reload disabled when no template path was supplied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
493 lines
21 KiB
C#
493 lines
21 KiB
C#
using RioJoy.Core.Overlay;
|
||
using RioJoy.Core.Profiles;
|
||
using SkiaSharp;
|
||
|
||
namespace RioJoy.Tray.Editor;
|
||
|
||
/// <summary>
|
||
/// Wallpaper maker: the interactive replacement for the legacy Google-Sheet →
|
||
/// GIMP/Script-Fu pipeline. Shows the profile's cockpit wallpaper rendered live
|
||
/// (via <see cref="RioJoy.Overlay.SkiaOverlayRenderer"/> on the overlay template),
|
||
/// lets the user click any of the template's cells — including the heading/banner
|
||
/// cells the button editor can't reach — and edit its text with immediate
|
||
/// re-render. Clicking a stacked cell repeatedly cycles through the regions at
|
||
/// that spot (the wallpaper feeds six chroma-split displays, so cells overlap).
|
||
/// Labels write back to <see cref="RioProfile.OverlayLabels"/>; <c>Save</c> raises
|
||
/// <see cref="Saved"/>. Also imports the legacy <c>.data</c> label sheet, exports
|
||
/// the PNG, and applies the wallpaper on demand.
|
||
/// </summary>
|
||
public sealed class WallpaperMakerForm : Form
|
||
{
|
||
private readonly RioProfile _profile;
|
||
private readonly OverlayTemplate _template;
|
||
private readonly string? _templatePath;
|
||
private readonly RioJoy.Overlay.SkiaOverlayRenderer _renderer = new();
|
||
private readonly SKBitmap _baseImage;
|
||
|
||
private readonly WallpaperCanvas _canvas = new() { Dock = DockStyle.Fill };
|
||
private readonly Label _cellInfo = new() { AutoSize = true, Location = new Point(12, 40), MaximumSize = new Size(310, 0), Text = "Click a cell to edit it." };
|
||
private readonly TextBox _labelBox = new() { Location = new Point(70, 74), Width = 230, Enabled = false };
|
||
private readonly CheckBox _outlines = new() { Text = "Show cell outlines", Location = new Point(12, 104), AutoSize = true, Checked = true };
|
||
private readonly CheckBox _editBoxes = new() { Text = "Edit cell boxes", Location = new Point(150, 104), AutoSize = true };
|
||
private readonly NumericUpDown _numX = new() { Location = new Point(34, 130), Width = 66, Enabled = false };
|
||
private readonly NumericUpDown _numY = new() { Location = new Point(132, 130), Width = 66, Enabled = false };
|
||
private readonly NumericUpDown _numW = new() { Location = new Point(34, 156), Width = 66, Minimum = 1, Enabled = false };
|
||
private readonly NumericUpDown _numH = new() { Location = new Point(132, 156), Width = 66, Minimum = 1, Enabled = false };
|
||
private readonly Button _saveTemplate = new() { Text = "Save template", Location = new Point(204, 130), Width = 114, Enabled = false };
|
||
private readonly Button _reloadTemplate = new() { Text = "Reload template", Location = new Point(204, 156), Width = 114, Enabled = false };
|
||
private readonly ListBox _list = new() { Location = new Point(12, 188), Size = new Size(306, 260), IntegralHeight = false, DrawMode = DrawMode.OwnerDrawFixed };
|
||
private readonly Label _hoverInfo = new() { AutoSize = true, Location = new Point(12, 456), ForeColor = SystemColors.GrayText };
|
||
private readonly Button _import = new() { Text = "Import labels (.data)…", Location = new Point(12, 484), Width = 306 };
|
||
private readonly Button _export = new() { Text = "Export PNG…", Location = new Point(12, 514), Width = 306 };
|
||
private readonly Button _applyWallpaper = new() { Text = "Apply as desktop wallpaper", Location = new Point(12, 544), Width = 306 };
|
||
private readonly Button _save = new() { Text = "Save profile", Location = new Point(12, 582), Width = 148 };
|
||
private readonly Button _close = new() { Text = "Close", Location = new Point(170, 582), Width = 148 };
|
||
|
||
// Re-render shortly after typing stops rather than per keystroke.
|
||
private readonly System.Windows.Forms.Timer _renderTimer = new() { Interval = 300 };
|
||
|
||
// Briefly flips a button to a confirmation, then reverts (one at a time).
|
||
private readonly System.Windows.Forms.Timer _saveFlash = new() { Interval = 1600 };
|
||
private Action? _saveFlashReset;
|
||
|
||
private byte[] _lastPng = Array.Empty<byte>();
|
||
private bool _loading;
|
||
|
||
/// <summary>Raised when the user saves; the argument is the edited profile.</summary>
|
||
public event Action<RioProfile>? Saved;
|
||
|
||
/// <param name="templateDirectory">
|
||
/// Directory the template's relative <see cref="OverlayTemplate.BaseImagePath"/>
|
||
/// is resolved against (normally the folder holding the regions.json).
|
||
/// </param>
|
||
/// <param name="templatePath">
|
||
/// The template file itself; enables the box editor's "Save template" /
|
||
/// "Reload template" (cell geometry is shared by every profile). Null = the
|
||
/// geometry can still be edited for this session but not persisted.
|
||
/// </param>
|
||
public WallpaperMakerForm(RioProfile profile, OverlayTemplate template, string templateDirectory, string? templatePath = null)
|
||
{
|
||
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
||
_template = template ?? throw new ArgumentNullException(nameof(template));
|
||
_templatePath = templatePath;
|
||
if (string.IsNullOrWhiteSpace(template.BaseImagePath))
|
||
throw new InvalidOperationException("OverlayTemplate.BaseImagePath is not set.");
|
||
|
||
string imagePath = template.BaseImagePath!;
|
||
string basePath = Path.IsPathRooted(imagePath)
|
||
? imagePath
|
||
: Path.Combine(templateDirectory, imagePath);
|
||
_baseImage = SKBitmap.Decode(basePath)
|
||
?? throw new InvalidOperationException($"Could not decode base image: {basePath}");
|
||
|
||
Text = $"RIOJoy — Wallpaper: {profile.Name}";
|
||
ClientSize = new Size(1360, 640);
|
||
MinimumSize = new Size(960, 560);
|
||
StartPosition = FormStartPosition.CenterScreen;
|
||
|
||
_canvas.Template = template;
|
||
_canvas.TemplateClicked += OnCanvasClicked;
|
||
_canvas.HoverChanged += r => _hoverInfo.Text = r is null ? "" : Describe(r);
|
||
_canvas.SelectedGeometryChanged += () => LoadGeometryFields(SelectedRegion());
|
||
_canvas.GeometryCommitted += () =>
|
||
{
|
||
LoadGeometryFields(SelectedRegion());
|
||
RenderPreview();
|
||
};
|
||
|
||
_numX.Maximum = _numW.Maximum = Math.Max(1, template.Width);
|
||
_numY.Maximum = _numH.Maximum = Math.Max(1, template.Height);
|
||
foreach (NumericUpDown n in new[] { _numX, _numY, _numW, _numH })
|
||
n.ValueChanged += OnGeometryFieldChanged;
|
||
|
||
_editBoxes.CheckedChanged += (_, _) =>
|
||
{
|
||
_canvas.EditMode = _editBoxes.Checked;
|
||
UpdateGeometryEnabled();
|
||
};
|
||
_saveTemplate.Click += (_, _) => SaveTemplate();
|
||
_reloadTemplate.Click += (_, _) => ReloadTemplate();
|
||
|
||
_list.Items.AddRange(template.Regions.Select(r => (object)r.Name).ToArray());
|
||
_list.DrawItem += DrawListItem;
|
||
_list.SelectedIndexChanged += (_, _) => { if (_list.SelectedItem is string name) SelectRegion(name); };
|
||
|
||
_labelBox.TextChanged += OnLabelChanged;
|
||
_outlines.CheckedChanged += (_, _) => _canvas.ShowOutlines = _outlines.Checked;
|
||
_import.Click += (_, _) => ImportData();
|
||
_export.Click += (_, _) => ExportPng();
|
||
_applyWallpaper.Click += (_, _) => ApplyAsWallpaper();
|
||
_save.Click += (_, _) => SaveProfile();
|
||
_close.Click += (_, _) => Close();
|
||
|
||
_renderTimer.Tick += (_, _) => { _renderTimer.Stop(); RenderPreview(); };
|
||
_saveFlash.Tick += (_, _) =>
|
||
{
|
||
_saveFlash.Stop();
|
||
_saveFlashReset?.Invoke();
|
||
_saveFlashReset = null;
|
||
};
|
||
|
||
var side = new Panel { Dock = DockStyle.Right, Width = 330, Padding = new Padding(8), BorderStyle = BorderStyle.FixedSingle, AutoScroll = true };
|
||
side.Controls.Add(new Label { Text = "Cells:", Location = new Point(12, 12), AutoSize = true });
|
||
side.Controls.Add(_cellInfo);
|
||
side.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 77), AutoSize = true });
|
||
side.Controls.Add(new Label { Text = "X:", Location = new Point(12, 133), AutoSize = true });
|
||
side.Controls.Add(new Label { Text = "Y:", Location = new Point(110, 133), AutoSize = true });
|
||
side.Controls.Add(new Label { Text = "W:", Location = new Point(12, 159), AutoSize = true });
|
||
side.Controls.Add(new Label { Text = "H:", Location = new Point(110, 159), AutoSize = true });
|
||
side.Controls.AddRange(new Control[]
|
||
{
|
||
_labelBox, _outlines, _editBoxes,
|
||
_numX, _numY, _numW, _numH, _saveTemplate, _reloadTemplate,
|
||
_list, _hoverInfo, _import, _export, _applyWallpaper, _save, _close,
|
||
});
|
||
|
||
Controls.Add(_canvas);
|
||
Controls.Add(side);
|
||
|
||
FormClosed += (_, _) =>
|
||
{
|
||
_renderTimer.Dispose();
|
||
_saveFlash.Dispose();
|
||
_baseImage.Dispose();
|
||
};
|
||
|
||
RenderPreview();
|
||
}
|
||
|
||
/// <summary>The selected region name (for tests / host inspection).</summary>
|
||
public string? SelectedRegionName => _list.SelectedItem as string;
|
||
|
||
/// <summary>Select a region programmatically (e.g. jump from the button editor).</summary>
|
||
public void SelectRegion(string name)
|
||
{
|
||
_loading = true;
|
||
int index = _list.Items.IndexOf(name);
|
||
if (index >= 0 && _list.SelectedIndex != index)
|
||
_list.SelectedIndex = index;
|
||
_labelBox.Text = _profile.OverlayLabels.TryGetValue(name, out string? text) ? text : string.Empty;
|
||
_labelBox.Enabled = true;
|
||
_canvas.SelectedRegionName = name;
|
||
OverlayRegion? region = _template.FindRegion(name);
|
||
_cellInfo.Text = region is { } r ? Describe(r) : name;
|
||
_loading = false;
|
||
|
||
LoadGeometryFields(region);
|
||
UpdateGeometryEnabled();
|
||
}
|
||
|
||
private OverlayRegion? SelectedRegion() =>
|
||
_list.SelectedItem is string name ? _template.FindRegion(name) : null;
|
||
|
||
// Reflect a region's box into the X/Y/W/H fields without triggering edits.
|
||
private void LoadGeometryFields(OverlayRegion? region)
|
||
{
|
||
if (region is not null)
|
||
_cellInfo.Text = Describe(region);
|
||
|
||
_loading = true;
|
||
_numX.Value = Clamp(region?.X ?? 0, _numX);
|
||
_numY.Value = Clamp(region?.Y ?? 0, _numY);
|
||
_numW.Value = Clamp(region?.Width ?? 1, _numW);
|
||
_numH.Value = Clamp(region?.Height ?? 1, _numH);
|
||
_loading = false;
|
||
|
||
static decimal Clamp(double value, NumericUpDown n) =>
|
||
Math.Min(n.Maximum, Math.Max(n.Minimum, (decimal)Math.Round(value)));
|
||
}
|
||
|
||
private void UpdateGeometryEnabled()
|
||
{
|
||
bool editing = _editBoxes.Checked;
|
||
bool hasRegion = SelectedRegion() is not null;
|
||
foreach (NumericUpDown n in new[] { _numX, _numY, _numW, _numH })
|
||
n.Enabled = editing && hasRegion;
|
||
_saveTemplate.Enabled = editing && _templatePath is not null;
|
||
_reloadTemplate.Enabled = editing && _templatePath is not null;
|
||
}
|
||
|
||
// Typed box geometry: write through to the region and re-render (debounced).
|
||
private void OnGeometryFieldChanged(object? sender, EventArgs e)
|
||
{
|
||
if (_loading || SelectedRegion() is not { } region)
|
||
return;
|
||
|
||
region.X = (double)_numX.Value;
|
||
region.Y = (double)_numY.Value;
|
||
region.Width = (double)_numW.Value;
|
||
region.Height = (double)_numH.Value;
|
||
|
||
_canvas.Invalidate();
|
||
_renderTimer.Stop();
|
||
_renderTimer.Start();
|
||
}
|
||
|
||
// Persist the (shared, all-profile) cell geometry back to the regions.json.
|
||
private void SaveTemplate()
|
||
{
|
||
if (_templatePath is null)
|
||
return;
|
||
|
||
try
|
||
{
|
||
OverlayTemplateStore.Save(_template, _templatePath);
|
||
Flash(_saveTemplate, "Saved ✓");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, $"Could not save the template:\n{ex.Message}",
|
||
"RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
}
|
||
}
|
||
|
||
// Discard unsaved box edits by re-reading the cell geometry from disk (labels
|
||
// are untouched — they live in the profile, not the template).
|
||
private void ReloadTemplate()
|
||
{
|
||
if (_templatePath is null)
|
||
return;
|
||
|
||
try
|
||
{
|
||
OverlayTemplate fresh = OverlayTemplateStore.Load(_templatePath);
|
||
foreach (OverlayRegion r in _template.Regions)
|
||
{
|
||
if (fresh.FindRegion(r.Name) is { } f)
|
||
(r.X, r.Y, r.Width, r.Height) = (f.X, f.Y, f.Width, f.Height);
|
||
}
|
||
|
||
LoadGeometryFields(SelectedRegion());
|
||
RenderPreview();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, $"Could not reload the template:\n{ex.Message}",
|
||
"RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
}
|
||
}
|
||
|
||
// Briefly show a confirmation on a button, then restore its caption.
|
||
private void Flash(Button button, string confirmation)
|
||
{
|
||
_saveFlash.Stop();
|
||
_saveFlashReset?.Invoke();
|
||
|
||
string original = button.Text;
|
||
Color originalColor = button.ForeColor;
|
||
button.Text = confirmation;
|
||
button.ForeColor = Color.ForestGreen;
|
||
_saveFlashReset = () => { button.Text = original; button.ForeColor = originalColor; };
|
||
_saveFlash.Start();
|
||
}
|
||
|
||
// A click on the preview: select the cell there, cycling through stacked cells.
|
||
private void OnCanvasClicked(double x, double y)
|
||
{
|
||
if (OverlayHitTester.NextAt(_template, x, y, _canvas.SelectedRegionName) is { } hit)
|
||
SelectRegion(hit.Name);
|
||
}
|
||
|
||
private void OnLabelChanged(object? sender, EventArgs e)
|
||
{
|
||
if (_loading || _list.SelectedItem is not string name)
|
||
return;
|
||
|
||
string text = _labelBox.Text.Trim();
|
||
if (text.Length == 0)
|
||
_profile.OverlayLabels.Remove(name);
|
||
else
|
||
_profile.OverlayLabels[name] = text;
|
||
|
||
_renderTimer.Stop();
|
||
_renderTimer.Start();
|
||
_list.Invalidate();
|
||
}
|
||
|
||
// Render the profile's labels onto the base image and show it (kept as PNG
|
||
// bytes so Export/Apply write exactly what is previewed).
|
||
private void RenderPreview()
|
||
{
|
||
_lastPng = _renderer.RenderToPng(_template, _profile.OverlayLabels, _baseImage);
|
||
|
||
// Copy so the Bitmap does not depend on the stream staying open (GDI+ rule).
|
||
using var ms = new MemoryStream(_lastPng);
|
||
using var streamed = new Bitmap(ms);
|
||
_canvas.SetImage(new Bitmap(streamed));
|
||
}
|
||
|
||
// "name · 197×54 @ (810,332)" with the label text when set.
|
||
private string Describe(OverlayRegion r)
|
||
{
|
||
string text = _profile.OverlayLabels.TryGetValue(r.Name, out string? t) ? $" “{t}”" : "";
|
||
return $"{r.Name} — {r.Width:0}×{r.Height:0} @ ({r.X:0},{r.Y:0}){text}";
|
||
}
|
||
|
||
// Owner-drawn so label edits show up in the list immediately on Invalidate.
|
||
private void DrawListItem(object? sender, DrawItemEventArgs e)
|
||
{
|
||
e.DrawBackground();
|
||
if (e.Index >= 0 && _list.Items[e.Index] is string name)
|
||
{
|
||
string text = _profile.OverlayLabels.TryGetValue(name, out string? t) ? t : "";
|
||
bool selected = (e.State & DrawItemState.Selected) != 0;
|
||
using var nameBrush = new SolidBrush(selected ? SystemColors.HighlightText : SystemColors.WindowText);
|
||
using var textBrush = new SolidBrush(selected ? SystemColors.HighlightText : SystemColors.GrayText);
|
||
e.Graphics.DrawString(name, e.Font!, nameBrush, e.Bounds.X + 2, e.Bounds.Y + 1);
|
||
e.Graphics.DrawString(text, e.Font!, textBrush, e.Bounds.X + 90, e.Bounds.Y + 1);
|
||
}
|
||
e.DrawFocusRectangle();
|
||
}
|
||
|
||
// Import a legacy Goobie .data label sheet (one row per game) into the profile.
|
||
private void ImportData()
|
||
{
|
||
using var dialog = new OpenFileDialog
|
||
{
|
||
Title = "Import Goobie label data",
|
||
Filter = "Goobie label data (*.data)|*.data|All files (*.*)|*.*",
|
||
};
|
||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||
return;
|
||
|
||
try
|
||
{
|
||
GoobieDataImporter.Sheet sheet = GoobieDataImporter.Load(dialog.FileName);
|
||
if (sheet.Rows.Count == 0)
|
||
{
|
||
MessageBox.Show(this, "The file contains no label rows.", "RIOJoy",
|
||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
return;
|
||
}
|
||
|
||
int row = sheet.Rows.Count == 1 ? 0 : PickRow(sheet);
|
||
if (row < 0)
|
||
return;
|
||
|
||
// Field 0 is the row's output-file tag, not a label.
|
||
string tagField = sheet.Fields.Count > 0 ? sheet.Fields[0] : "";
|
||
var labels = GoobieDataImporter.LabelsForRow(sheet, row)
|
||
.Where(kv => !string.Equals(kv.Key, tagField, StringComparison.OrdinalIgnoreCase))
|
||
.ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
|
||
|
||
if (_profile.OverlayLabels.Count > 0 &&
|
||
MessageBox.Show(this,
|
||
$"Replace the profile's {_profile.OverlayLabels.Count} existing label(s) with the imported row?",
|
||
"RIOJoy — Import labels", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
|
||
return;
|
||
|
||
_profile.OverlayLabels.Clear();
|
||
foreach (KeyValuePair<string, string> kv in labels)
|
||
_profile.OverlayLabels[kv.Key] = kv.Value;
|
||
|
||
if (_list.SelectedItem is string current)
|
||
SelectRegion(current); // refresh the label box from the new set
|
||
_list.Invalidate();
|
||
RenderPreview();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, $"Could not import the label data:\n{ex.Message}",
|
||
"RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
}
|
||
}
|
||
|
||
// Let the user pick which sheet row (game) to import; -1 = cancelled.
|
||
private int PickRow(GoobieDataImporter.Sheet sheet)
|
||
{
|
||
using var dlg = new Form
|
||
{
|
||
Text = "RIOJoy — Pick a label row",
|
||
ClientSize = new Size(320, 360),
|
||
FormBorderStyle = FormBorderStyle.FixedDialog,
|
||
MinimizeBox = false,
|
||
MaximizeBox = false,
|
||
StartPosition = FormStartPosition.CenterParent,
|
||
};
|
||
var list = new ListBox { Location = new Point(12, 12), Size = new Size(296, 296), IntegralHeight = false };
|
||
string tagField = sheet.Fields.Count > 0 ? sheet.Fields[0] : "";
|
||
for (int i = 0; i < sheet.Rows.Count; i++)
|
||
{
|
||
list.Items.Add(sheet.Rows[i].TryGetValue(tagField, out string? tag) && !string.IsNullOrEmpty(tag)
|
||
? tag
|
||
: $"Row {i + 1}");
|
||
}
|
||
list.SelectedIndex = 0;
|
||
list.DoubleClick += (_, _) => dlg.DialogResult = DialogResult.OK;
|
||
|
||
var ok = new Button { Text = "Import", Location = new Point(152, 320), Width = 75, DialogResult = DialogResult.OK };
|
||
var cancel = new Button { Text = "Cancel", Location = new Point(233, 320), Width = 75, DialogResult = DialogResult.Cancel };
|
||
dlg.Controls.AddRange(new Control[] { list, ok, cancel });
|
||
dlg.AcceptButton = ok;
|
||
dlg.CancelButton = cancel;
|
||
|
||
return dlg.ShowDialog(this) == DialogResult.OK ? list.SelectedIndex : -1;
|
||
}
|
||
|
||
private void ExportPng()
|
||
{
|
||
using var dialog = new SaveFileDialog
|
||
{
|
||
Title = "Export wallpaper PNG",
|
||
Filter = "PNG image (*.png)|*.png",
|
||
FileName = _profile.Name + ".png",
|
||
};
|
||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||
return;
|
||
|
||
try
|
||
{
|
||
File.WriteAllBytes(dialog.FileName, _lastPng);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, $"Could not write the PNG:\n{ex.Message}",
|
||
"RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
}
|
||
}
|
||
|
||
// Write the previewed PNG to the same per-profile path the runtime uses on
|
||
// profile activation, and set it as the desktop wallpaper now.
|
||
private void ApplyAsWallpaper()
|
||
{
|
||
try
|
||
{
|
||
string outDir = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
"RIOJoy", "wallpapers");
|
||
Directory.CreateDirectory(outDir);
|
||
string outPath = Path.Combine(outDir, SafeFileName(_profile.Name) + ".png");
|
||
|
||
File.WriteAllBytes(outPath, _lastPng);
|
||
_profile.WallpaperPath = outPath;
|
||
WallpaperApplier.Apply(outPath);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, $"Could not apply the wallpaper:\n{ex.Message}",
|
||
"RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
}
|
||
}
|
||
|
||
private void SaveProfile()
|
||
{
|
||
try
|
||
{
|
||
Saved?.Invoke(_profile);
|
||
Flash(_save, "Saved ✓");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(this, $"Could not save the profile:\n{ex.Message}",
|
||
"RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
}
|
||
}
|
||
|
||
private static string SafeFileName(string name)
|
||
{
|
||
foreach (char c in Path.GetInvalidFileNameChars())
|
||
name = name.Replace(c, '_');
|
||
return name;
|
||
}
|
||
}
|