using System.Runtime.Versioning;
using RioJoy.Core.Editing;
using RioJoy.Core.Mapping;
using RioJoy.Core.Profiles;
using RioJoy.Tray.Editor;
using RioJoy.Tray.Os;
namespace RioJoy.Tray;
///
/// Owns the tray icon, menu, and the RIOJoy runtime. The menu mirrors the legacy
/// console menu (axis resets, version/status, diagnostic toggles, quit) and adds
/// profile selection (auto vs. manual) and a "start with Windows" toggle. The
/// auto-switch watcher is polled on a UI timer so menu/status updates stay on the
/// UI thread.
///
[SupportedOSPlatform("windows")]
internal sealed class TrayApplicationContext : ApplicationContext
{
private static readonly string ConfigPath =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RIOJoy", "config.json");
private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(1);
private readonly AppConfig _config;
private readonly RioCoordinator _coordinator;
private readonly AutoSwitchWatcher _watcher;
private readonly System.Windows.Forms.Timer _pollTimer;
private readonly NotifyIcon _trayIcon;
private readonly ToolStripMenuItem _statusItem;
public TrayApplicationContext()
{
_config = ConfigStore.Load(ConfigPath);
_coordinator = new RioCoordinator(() => _config);
_coordinator.StatusChanged += _ => RefreshOnUiThread();
_watcher = new AutoSwitchWatcher(new ForegroundProcessProvider(), () => _config);
_watcher.DecisionChanged += d => _coordinator.ApplyDecision(d);
_statusItem = new ToolStripMenuItem("Status: starting…") { Enabled = false };
_trayIcon = new NotifyIcon
{
Icon = SystemIcons.Application,
Text = "RIOJoy",
Visible = true,
ContextMenuStrip = BuildMenu(),
};
// Poll the foreground app on the UI thread.
_pollTimer = new System.Windows.Forms.Timer { Interval = (int)PollInterval.TotalMilliseconds };
_pollTimer.Tick += (_, _) => _watcher.Poll();
_pollTimer.Start();
_watcher.Poll();
}
private ContextMenuStrip BuildMenu()
{
var menu = new ContextMenuStrip();
menu.Items.Add(_statusItem);
menu.Items.Add(new ToolStripSeparator());
// Profile selection: Auto + each profile + manual dormant.
var profileMenu = new ToolStripMenuItem("Profile");
profileMenu.DropDownOpening += (_, _) => RebuildProfileMenu(profileMenu);
RebuildProfileMenu(profileMenu);
menu.Items.Add(profileMenu);
menu.Items.Add("Edit profile…", null, (_, _) => OpenEditor());
// RIO commands mirroring the legacy console menu.
var commands = new ToolStripMenuItem("RIO commands");
AddCommand(commands, "Reset all analog axes", RioCommandCode.ResetAllAxes);
AddCommand(commands, "Reset throttle", RioCommandCode.ResetThrottle);
AddCommand(commands, "Reset left pedal", RioCommandCode.ResetLeftPedal);
AddCommand(commands, "Reset right pedal", RioCommandCode.ResetRightPedal);
AddCommand(commands, "Reset vertical joystick", RioCommandCode.ResetVerticalJoystick);
AddCommand(commands, "Reset horizontal joystick", RioCommandCode.ResetHorizontalJoystick);
commands.DropDownItems.Add(new ToolStripSeparator());
AddCommand(commands, "Reset digital (general)", RioCommandCode.GeneralReset);
AddCommand(commands, "Request version && status", RioCommandCode.RequestVersionAndCheck);
AddCommand(commands, "Toggle raw-axes readout", RioCommandCode.ToggleRawAxesReadout);
AddCommand(commands, "Toggle poll-rate readout", RioCommandCode.TogglePollRateReadout);
menu.Items.Add(commands);
menu.Items.Add(new ToolStripSeparator());
var autoStart = new ToolStripMenuItem("Start with Windows")
{
CheckOnClick = true,
Checked = AutoStartManager.IsEnabled(),
};
autoStart.Click += (_, _) => AutoStartManager.SetEnabled(autoStart.Checked);
menu.Items.Add(autoStart);
menu.Items.Add("Exit", null, (_, _) => Quit());
return menu;
}
private void RebuildProfileMenu(ToolStripMenuItem profileMenu)
{
profileMenu.DropDownItems.Clear();
var auto = new ToolStripMenuItem("Auto (foreground app)") { Checked = _coordinator.IsAuto };
auto.Click += (_, _) => _coordinator.SetAuto();
profileMenu.DropDownItems.Add(auto);
profileMenu.DropDownItems.Add(new ToolStripSeparator());
foreach (RioProfile profile in _config.Profiles)
{
RioProfile captured = profile;
var item = new ToolStripMenuItem(profile.Name)
{
Checked = !_coordinator.IsAuto && _coordinator.ActiveProfileName == profile.Name,
};
item.Click += (_, _) => _coordinator.SetManualProfile(captured);
profileMenu.DropDownItems.Add(item);
}
if (_config.Profiles.Count == 0)
profileMenu.DropDownItems.Add(new ToolStripMenuItem("(no profiles configured)") { Enabled = false });
profileMenu.DropDownItems.Add(new ToolStripSeparator());
var dormant = new ToolStripMenuItem("Dormant (release port)")
{
Checked = !_coordinator.IsAuto && _coordinator.ActiveProfileName is null,
};
dormant.Click += (_, _) => _coordinator.SetManualDormant();
profileMenu.DropDownItems.Add(dormant);
}
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 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;
}
RioProfile profile = _config.Profiles.FirstOrDefault() ?? AddBlankProfile();
var editor = new ProfileEditorForm(cells, profile);
editor.Saved += _ => ConfigStore.Save(_config, ConfigPath);
editor.Show();
}
private RioProfile AddBlankProfile()
{
var profile = new RioProfile { Name = "New profile" };
_config.Profiles.Add(profile);
return profile;
}
private void AddCommand(ToolStripMenuItem parent, string text, RioCommandCode code)
{
var item = new ToolStripMenuItem(text);
item.Click += (_, _) => _coordinator.Runtime?.Trigger(code);
parent.DropDownItems.Add(item);
}
private void RefreshOnUiThread()
{
if (_trayIcon.ContextMenuStrip?.InvokeRequired == true)
{
_trayIcon.ContextMenuStrip.BeginInvoke(RefreshStatus);
return;
}
RefreshStatus();
}
private void RefreshStatus()
{
_statusItem.Text = $"Status: {_coordinator.Status}";
_trayIcon.Text = $"RIOJoy — {_coordinator.Status}".Length <= 63
? $"RIOJoy — {_coordinator.Status}"
: "RIOJoy";
}
private void Quit()
{
_pollTimer.Stop();
_coordinator.Dispose();
_trayIcon.Visible = false;
ExitThread();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_pollTimer.Dispose();
_coordinator.Dispose();
_trayIcon.Dispose();
}
base.Dispose(disposing);
}
}