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); } }