From 75d6cc4946f8832713f6409b6ac8b80863a9f6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Fri, 20 Jun 2025 17:44:51 +0300 Subject: [PATCH 01/10] Controller overlay showing which player is bound to which controller --- .../UI/ViewModels/MainWindowViewModel.cs | 27 +++ .../UI/Windows/ControllerOverlayWindow.axaml | 50 ++++++ .../Windows/ControllerOverlayWindow.axaml.cs | 166 ++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml create mode 100644 src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 5e7df4d62..049ada4b5 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -32,6 +32,7 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Utilities; using Ryujinx.Common; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.Common.UI; @@ -1686,11 +1687,37 @@ namespace Ryujinx.Ava.UI.ViewModels SetMainContent(RendererHostControl); RendererHostControl.Focus(); + + // Show controller overlay + ShowControllerOverlay(); }); public static void UpdateGameMetadata(string titleId, TimeSpan playTime) => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); + private void ShowControllerOverlay() + { + try + { + var inputConfigs = ConfigurationState.Instance.System.UseInputGlobalConfig.Value && Program.UseExtraConfig + ? ConfigurationState.InstanceExtra.Hid.InputConfig.Value + : ConfigurationState.Instance.Hid.InputConfig.Value; + + // Only show overlay if there are actual controller configurations for players 1-4 + if (inputConfigs?.Any(c => c.PlayerIndex <= PlayerIndex.Player4) == true) + { + var overlay = new Windows.ControllerOverlayWindow(Window); + overlay.ShowControllerBindings(inputConfigs); + overlay.Show(); + } + } + catch (Exception ex) + { + // Log the error but don't let it crash the game launch + Logger.Error?.Print(LogClass.UI, $"Failed to show controller overlay: {ex.Message}"); + } + } + public void RefreshFirmwareStatus() { SystemVersion version = null; diff --git a/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml b/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml new file mode 100644 index 000000000..d5c205667 --- /dev/null +++ b/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs b/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs new file mode 100644 index 000000000..e70b8239e --- /dev/null +++ b/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs @@ -0,0 +1,166 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Threading; +using Ryujinx.Common.Configuration.Hid; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ControllerOverlayWindow : StyleableWindow + { + private const int AutoHideDelayMs = 4000; // 4 seconds + + public ControllerOverlayWindow() + { + InitializeComponent(); + + TransparencyLevelHint = [WindowTransparencyLevel.Transparent]; + SystemDecorations = SystemDecorations.None; + ExtendClientAreaTitleBarHeightHint = 0; + Background = Brushes.Transparent; + CanResize = false; + ShowInTaskbar = false; + } + + public ControllerOverlayWindow(Window owner) : this() + { + if (owner != null) + { + // Position the overlay in the top-right corner of the owner window + WindowStartupLocation = WindowStartupLocation.Manual; + + // Set position after the window is loaded + Loaded += (s, e) => + { + if (owner.WindowState != WindowState.Minimized) + { + Position = new Avalonia.PixelPoint( + (int)(owner.Position.X + owner.Width - Width - 20), + (int)(owner.Position.Y + 50) + ); + } + }; + } + } + + public void ShowControllerBindings(List inputConfigs) + { + // Clear existing bindings + PlayerBindings.Children.Clear(); + + // Group controllers by player index + var playerBindings = new Dictionary>(); + + foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= PlayerIndex.Player4)) + { + if (!playerBindings.ContainsKey(config.PlayerIndex)) + { + playerBindings[config.PlayerIndex] = new List(); + } + playerBindings[config.PlayerIndex].Add(config); + } + + // Add player bindings to UI + for (int i = 0; i < 4; i++) + { + var playerIndex = (PlayerIndex)i; + var playerPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 12 }; + + // Player number with colored background + var playerNumberBorder = new Border + { + Background = GetPlayerColor(i), + CornerRadius = new Avalonia.CornerRadius(12), + Padding = new Avalonia.Thickness(8, 4), + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + + var playerLabel = new TextBlock + { + Text = $"P{i + 1}", + FontWeight = FontWeight.Bold, + Foreground = Brushes.White, + FontSize = 12, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center + }; + playerNumberBorder.Child = playerLabel; + playerPanel.Children.Add(playerNumberBorder); + + if (playerBindings.ContainsKey(playerIndex)) + { + var controllers = playerBindings[playerIndex]; + var controllerNames = controllers.Select(c => GetControllerDisplayName(c)).ToList(); + + var controllerText = new TextBlock + { + Text = string.Join(", ", controllerNames), + Foreground = Brushes.LightGreen, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + FontWeight = FontWeight.SemiBold + }; + playerPanel.Children.Add(controllerText); + } + else + { + var noControllerText = new TextBlock + { + Text = "No controller assigned", + Foreground = Brushes.Gray, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + FontStyle = FontStyle.Italic + }; + playerPanel.Children.Add(noControllerText); + } + + PlayerBindings.Children.Add(playerPanel); + } + + // Auto-hide after delay + _ = Task.Run(async () => + { + await Task.Delay(AutoHideDelayMs); + await Dispatcher.UIThread.InvokeAsync(() => + { + Close(); + }); + }); + } + + private static IBrush GetPlayerColor(int playerIndex) + { + return playerIndex switch + { + 0 => new SolidColorBrush(Color.FromRgb(255, 92, 92)), // Red for Player 1 + 1 => new SolidColorBrush(Color.FromRgb(54, 162, 235)), // Blue for Player 2 + 2 => new SolidColorBrush(Color.FromRgb(255, 206, 84)), // Yellow for Player 3 + 3 => new SolidColorBrush(Color.FromRgb(75, 192, 192)), // Green for Player 4 + _ => new SolidColorBrush(Color.FromRgb(128, 128, 128)) // Gray fallback + }; + } + + private static string GetControllerDisplayName(InputConfig config) + { + if (string.IsNullOrEmpty(config.Name)) + { + return config.Backend switch + { + InputBackendType.WindowKeyboard => "Keyboard", + InputBackendType.GamepadSDL2 => "Controller", + _ => "Unknown" + }; + } + + // Truncate long controller names + string name = config.Name; + if (name.Length > 25) + { + name = name.Substring(0, 22) + "..."; + } + + return name; + } + } +} From e4a39ffa81ba3425dd7781804d72167cd094c46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Fri, 20 Jun 2025 18:06:05 +0300 Subject: [PATCH 02/10] Input cycling hotkeys --- assets/locales.json | 100 +++++++++ .../Hid/DefaultInputConfigurationProvider.cs | 200 ++++++++++++++++++ .../Configuration/Hid/KeyboardHotkeys.cs | 4 + src/Ryujinx/Common/KeyboardHotkeyState.cs | 4 + src/Ryujinx/Systems/AppHost.cs | 145 +++++++++++++ src/Ryujinx/UI/Models/Input/HotkeyConfig.cs | 18 +- .../UI/ViewModels/Input/InputViewModel.cs | 146 +------------ .../Views/Settings/SettingsHotkeysView.axaml | 24 +++ .../Settings/SettingsHotkeysView.axaml.cs | 18 +- 9 files changed, 520 insertions(+), 139 deletions(-) create mode 100644 src/Ryujinx.Common/Configuration/Hid/DefaultInputConfigurationProvider.cs diff --git a/assets/locales.json b/assets/locales.json index 2f52ee71b..498bcb81e 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -24372,6 +24372,106 @@ "zh_TW": "只在按下時" } }, + { + "ID": "SettingsTabHotkeysCycleInputDevicePlayer1", + "Translations": { + "ar_SA": "تبديل جهاز الإدخال للاعب 1:", + "de_DE": "Eingabegerät für Spieler 1 wechseln:", + "el_GR": "Εναλλαγή Συσκευής Εισόδου Παίκτη 1:", + "en_US": "Cycle Input Device Player 1:", + "es_ES": "Cambiar dispositivo de entrada jugador 1:", + "fr_FR": "Changer périphérique d'entrée joueur 1 :", + "he_IL": "החלף התקן קלט שחקן 1:", + "it_IT": "Cambia dispositivo di input giocatore 1:", + "ja_JP": "プレイヤー1の入力デバイス切り替え:", + "ko_KR": "플레이어 1 입력 장치 순환:", + "no_NO": "Bytt inndataenhet spiller 1:", + "pl_PL": "Przełącz urządzenie wejściowe gracza 1:", + "pt_BR": "Alternar dispositivo de entrada jogador 1:", + "ru_RU": "Переключить устройство ввода игрока 1:", + "sv_SE": "Växla indataenhet spelare 1:", + "th_TH": "สลับอุปกรณ์ป้อนข้อมูลผู้เล่น 1:", + "tr_TR": "Oyuncu 1 Giriş Cihazını Değiştir:", + "uk_UA": "Перемкнути пристрій введення гравця 1:", + "zh_CN": "切换玩家1输入设备:", + "zh_TW": "切換玩家1輸入裝置:" + } + }, + { + "ID": "SettingsTabHotkeysCycleInputDevicePlayer2", + "Translations": { + "ar_SA": "تبديل جهاز الإدخال للاعب 2:", + "de_DE": "Eingabegerät für Spieler 2 wechseln:", + "el_GR": "Εναλλαγή Συσκευής Εισόδου Παίκτη 2:", + "en_US": "Cycle Input Device Player 2:", + "es_ES": "Cambiar dispositivo de entrada jugador 2:", + "fr_FR": "Changer périphérique d'entrée joueur 2 :", + "he_IL": "החלף התקן קלט שחקן 2:", + "it_IT": "Cambia dispositivo di input giocatore 2:", + "ja_JP": "プレイヤー2の入力デバイス切り替え:", + "ko_KR": "플레이어 2 입력 장치 순환:", + "no_NO": "Bytt inndataenhet spiller 2:", + "pl_PL": "Przełącz urządzenie wejściowe gracza 2:", + "pt_BR": "Alternar dispositivo de entrada jogador 2:", + "ru_RU": "Переключить устройство ввода игрока 2:", + "sv_SE": "Växla indataenhet spelare 2:", + "th_TH": "สลับอุปกรณ์ป้อนข้อมูลผู้เล่น 2:", + "tr_TR": "Oyuncu 2 Giriş Cihazını Değiştir:", + "uk_UA": "Перемкнути пристрій введення гравця 2:", + "zh_CN": "切换玩家2输入设备:", + "zh_TW": "切換玩家2輸入裝置:" + } + }, + { + "ID": "SettingsTabHotkeysCycleInputDevicePlayer3", + "Translations": { + "ar_SA": "تبديل جهاز الإدخال للاعب 3:", + "de_DE": "Eingabegerät für Spieler 3 wechseln:", + "el_GR": "Εναλλαγή Συσκευής Εισόδου Παίκτη 3:", + "en_US": "Cycle Input Device Player 3:", + "es_ES": "Cambiar dispositivo de entrada jugador 3:", + "fr_FR": "Changer périphérique d'entrée joueur 3 :", + "he_IL": "החלף התקן קלט שחקן 3:", + "it_IT": "Cambia dispositivo di input giocatore 3:", + "ja_JP": "プレイヤー3の入力デバイス切り替え:", + "ko_KR": "플레이어 3 입력 장치 순환:", + "no_NO": "Bytt inndataenhet spiller 3:", + "pl_PL": "Przełącz urządzenie wejściowe gracza 3:", + "pt_BR": "Alternar dispositivo de entrada jogador 3:", + "ru_RU": "Переключить устройство ввода игрока 3:", + "sv_SE": "Växla indataenhet spelare 3:", + "th_TH": "สลับอุปกรณ์ป้อนข้อมูลผู้เล่น 3:", + "tr_TR": "Oyuncu 3 Giriş Cihazını Değiştir:", + "uk_UA": "Перемкнути пристрій введення гравця 3:", + "zh_CN": "切换玩家3输入设备:", + "zh_TW": "切換玩家3輸入裝置:" + } + }, + { + "ID": "SettingsTabHotkeysCycleInputDevicePlayer4", + "Translations": { + "ar_SA": "تبديل جهاز الإدخال للاعب 4:", + "de_DE": "Eingabegerät für Spieler 4 wechseln:", + "el_GR": "Εναλλαγή Συσκευής Εισόδου Παίκτη 4:", + "en_US": "Cycle Input Device Player 4:", + "es_ES": "Cambiar dispositivo de entrada jugador 4:", + "fr_FR": "Changer périphérique d'entrée joueur 4 :", + "he_IL": "החלף התקן קלט שחקן 4:", + "it_IT": "Cambia dispositivo di input giocatore 4:", + "ja_JP": "プレイヤー4の入力デバイス切り替え:", + "ko_KR": "플레이어 4 입력 장치 순환:", + "no_NO": "Bytt inndataenhet spiller 4:", + "pl_PL": "Przełącz urządzenie wejściowe gracza 4:", + "pt_BR": "Alternar dispositivo de entrada jogador 4:", + "ru_RU": "Переключить устройство ввода игрока 4:", + "sv_SE": "Växla indataenhet spelare 4:", + "th_TH": "สลับอุปกรณ์ป้อนข้อมูลผู้เล่น 4:", + "tr_TR": "Oyuncu 4 Giriş Cihazını Değiştir:", + "uk_UA": "Перемкнути пристрій введення гравця 4:", + "zh_CN": "切换玩家4输入设备:", + "zh_TW": "切換玩家4輸入裝置:" + } + }, { "ID": "CompatibilityListLastUpdated", "Translations": { diff --git a/src/Ryujinx.Common/Configuration/Hid/DefaultInputConfigurationProvider.cs b/src/Ryujinx.Common/Configuration/Hid/DefaultInputConfigurationProvider.cs new file mode 100644 index 000000000..be145029f --- /dev/null +++ b/src/Ryujinx.Common/Configuration/Hid/DefaultInputConfigurationProvider.cs @@ -0,0 +1,200 @@ +using System; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; +using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; + +namespace Ryujinx.Common.Configuration.Hid +{ + /// + /// Provides default input configurations for keyboard and controller devices + /// + public static class DefaultInputConfigurationProvider + { + /// + /// Creates a default keyboard input configuration + /// + /// Device ID + /// Device name + /// Player index + /// Controller type (defaults to ProController) + /// Default keyboard input configuration + public static StandardKeyboardInputConfig CreateDefaultKeyboardConfig(string id, string name, PlayerIndex playerIndex, ControllerType controllerType = ControllerType.ProController) + { + return new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = id, + Name = name, + ControllerType = ControllerType.ProController, + PlayerIndex = playerIndex, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + }, + }; + } + + /// + /// Creates a default controller input configuration + /// + /// Device ID + /// Device name + /// Player index + /// Whether to use Nintendo-style button mapping + /// Default controller input configuration + public static StandardControllerInputConfig CreateDefaultControllerConfig(string id, string name, PlayerIndex playerIndex, bool isNintendoStyle = false) + { + // Split the ID for controller configs + string cleanId = id.Split(" ")[0]; + + return new StandardControllerInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.GamepadSDL2, + Id = cleanId, + Name = name, + ControllerType = ControllerType.ProController, + PlayerIndex = playerIndex, + DeadzoneLeft = 0.1f, + DeadzoneRight = 0.1f, + RangeLeft = 1.0f, + RangeRight = 1.0f, + TriggerThreshold = 0.5f, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = ConfigGamepadInputId.DpadUp, + DpadDown = ConfigGamepadInputId.DpadDown, + DpadLeft = ConfigGamepadInputId.DpadLeft, + DpadRight = ConfigGamepadInputId.DpadRight, + ButtonMinus = ConfigGamepadInputId.Minus, + ButtonL = ConfigGamepadInputId.LeftShoulder, + ButtonZl = ConfigGamepadInputId.LeftTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Left, + StickButton = ConfigGamepadInputId.LeftStick, + InvertStickX = false, + InvertStickY = false, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B, + ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A, + ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y, + ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X, + ButtonPlus = ConfigGamepadInputId.Plus, + ButtonR = ConfigGamepadInputId.RightShoulder, + ButtonZr = ConfigGamepadInputId.RightTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Right, + StickButton = ConfigGamepadInputId.RightStick, + InvertStickX = false, + InvertStickY = false, + }, + Motion = new StandardMotionConfigController + { + EnableMotion = true, + MotionBackend = MotionInputBackendType.GamepadDriver, + GyroDeadzone = 1, + Sensitivity = 100, + }, + Rumble = new RumbleConfigController + { + EnableRumble = false, + WeakRumble = 1f, + StrongRumble = 1f, + }, + Led = new LedConfigController + { + EnableLed = false, + TurnOffLed = false, + UseRainbow = false, + LedColor = 0xFFFFFFFF, + } + }; + } + + /// + /// Gets the short name of a gamepad by removing SDL prefix and truncating if too long + /// + /// Full gamepad name + /// Maximum length before truncation (default: 50) + /// Short gamepad name + public static string GetShortGamepadName(string name, int maxLength = 50) + { + const string SdlGamepadNamePrefix = "SDL2 Gamepad "; + const string Ellipsis = "..."; + + // First remove SDL prefix if present + string shortName = name; + if (name.StartsWith(SdlGamepadNamePrefix)) + { + shortName = name[SdlGamepadNamePrefix.Length..]; + } + + // Then truncate if too long + if (shortName.Length > maxLength) + { + return $"{shortName.AsSpan(0, maxLength - Ellipsis.Length)}{Ellipsis}"; + } + + return shortName; + } + + /// + /// Determines if a controller uses Nintendo-style button mapping + /// + /// Controller name + /// True if Nintendo-style mapping should be used + public static bool IsNintendoStyleController(string name) + { + return name.Contains("Nintendo"); + } + } +} diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index efdb422e7..f84434ba1 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -15,5 +15,9 @@ namespace Ryujinx.Common.Configuration.Hid public Key CustomVSyncIntervalDecrement { get; set; } public Key TurboMode { get; set; } public bool TurboModeWhileHeld { get; set; } + public Key CycleInputDevicePlayer1 { get; set; } + public Key CycleInputDevicePlayer2 { get; set; } + public Key CycleInputDevicePlayer3 { get; set; } + public Key CycleInputDevicePlayer4 { get; set; } } } diff --git a/src/Ryujinx/Common/KeyboardHotkeyState.cs b/src/Ryujinx/Common/KeyboardHotkeyState.cs index b6fb02f04..f7b7406d2 100644 --- a/src/Ryujinx/Common/KeyboardHotkeyState.cs +++ b/src/Ryujinx/Common/KeyboardHotkeyState.cs @@ -15,5 +15,9 @@ namespace Ryujinx.Ava.Common CustomVSyncIntervalIncrement, CustomVSyncIntervalDecrement, TurboMode, + CycleInputDevicePlayer1, + CycleInputDevicePlayer2, + CycleInputDevicePlayer3, + CycleInputDevicePlayer4, } } diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 1c5f64309..801677cb2 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -40,6 +40,11 @@ using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.Input; using Ryujinx.Input.HLE; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using System.Linq; using SkiaSharp; using SPB.Graphics.Vulkan; using System; @@ -1333,6 +1338,18 @@ namespace Ryujinx.Ava.Systems _viewModel.Volume = Device.GetVolume(); break; + case KeyboardHotkeyState.CycleInputDevicePlayer1: + CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player1); + break; + case KeyboardHotkeyState.CycleInputDevicePlayer2: + CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player2); + break; + case KeyboardHotkeyState.CycleInputDevicePlayer3: + CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player3); + break; + case KeyboardHotkeyState.CycleInputDevicePlayer4: + CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player4); + break; case KeyboardHotkeyState.None: (_keyboardInterface as AvaloniaKeyboard).Clear(); break; @@ -1366,6 +1383,118 @@ namespace Ryujinx.Ava.Systems return true; } + private void CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex playerIndex) + { + // Get current input configuration + List currentConfig = new(ConfigurationState.Instance.Hid.InputConfig.Value); + + // Find current configuration for this player + InputConfig playerConfig = currentConfig.FirstOrDefault(x => x.PlayerIndex == (PlayerIndex)playerIndex); + + // Get available devices from InputManager + List<(DeviceType Type, string Id, string Name)> availableDevices = []; + + // Add disabled option + availableDevices.Add((DeviceType.None, "disabled", "Disabled")); + + // Add keyboard devices + foreach (string id in _inputManager.KeyboardDriver.GamepadsIds) + { + using var gamepad = _inputManager.KeyboardDriver.GetGamepad(id); + if (gamepad != null) + { + availableDevices.Add((DeviceType.Keyboard, id, gamepad.Name)); + } + } + + // Add controller devices + int controllerNumber = 0; + foreach (string id in _inputManager.GamepadDriver.GamepadsIds) + { + using var gamepad = _inputManager.GamepadDriver.GetGamepad(id); + if (gamepad != null) + { + string name = $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)} ({controllerNumber++})"; + availableDevices.Add((DeviceType.Controller, id, name)); + } + } + + // Find current device index + int currentIndex = 0; + if (playerConfig != null) + { + DeviceType currentType = DeviceType.None; + if (playerConfig is StandardKeyboardInputConfig) + currentType = DeviceType.Keyboard; + else if (playerConfig is StandardControllerInputConfig) + currentType = DeviceType.Controller; + + currentIndex = availableDevices.FindIndex(x => x.Type == currentType && x.Id == playerConfig.Id); + if (currentIndex == -1) currentIndex = 0; + } + + // Cycle to next device + int nextIndex = (currentIndex + 1) % availableDevices.Count; + var nextDevice = availableDevices[nextIndex]; + + // Remove existing configuration for this player + currentConfig.RemoveAll(x => x.PlayerIndex == (PlayerIndex)playerIndex); + + // Add new configuration if not disabled + if (nextDevice.Type != DeviceType.None) + { + InputConfig newConfig = CreateDefaultInputConfig(nextDevice, (PlayerIndex)playerIndex); + if (newConfig != null) + { + currentConfig.Add(newConfig); + } + } + + // Apply the new configuration + ConfigurationState.Instance.Hid.InputConfig.Value = currentConfig; + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + // Reload the input system + NpadManager.ReloadConfiguration(currentConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); + + // Show controller overlay + ShowControllerOverlay(currentConfig); + } + + private InputConfig CreateDefaultInputConfig((DeviceType Type, string Id, string Name) device, PlayerIndex playerIndex) + { + if (device.Type == DeviceType.Keyboard) + { + return DefaultInputConfigurationProvider.CreateDefaultKeyboardConfig(device.Id, device.Name, playerIndex); + } + else if (device.Type == DeviceType.Controller) + { + bool isNintendoStyle = DefaultInputConfigurationProvider.IsNintendoStyleController(device.Name); + return DefaultInputConfigurationProvider.CreateDefaultControllerConfig(device.Id, device.Name, playerIndex, isNintendoStyle); + } + + return null; + } + + + + private void ShowControllerOverlay(List inputConfigs) + { + Dispatcher.UIThread.InvokeAsync(() => + { + try + { + var overlayWindow = new UI.Windows.ControllerOverlayWindow(_topLevel as Avalonia.Controls.Window); + overlayWindow.ShowControllerBindings(inputConfigs); + overlayWindow.Show(); + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.Application, $"Failed to show controller overlay: {ex.Message}"); + } + }); + } + private KeyboardHotkeyState GetHotkeyState() { KeyboardHotkeyState state = KeyboardHotkeyState.None; @@ -1418,6 +1547,22 @@ namespace Ryujinx.Ava.Systems { state = KeyboardHotkeyState.TurboMode; } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer1)) + { + state = KeyboardHotkeyState.CycleInputDevicePlayer1; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer2)) + { + state = KeyboardHotkeyState.CycleInputDevicePlayer2; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer3)) + { + state = KeyboardHotkeyState.CycleInputDevicePlayer3; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer4)) + { + state = KeyboardHotkeyState.CycleInputDevicePlayer4; + } return state; } diff --git a/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs b/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs index 9e557d7b1..78ab40678 100644 --- a/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs +++ b/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs @@ -32,6 +32,14 @@ namespace Ryujinx.Ava.UI.Models.Input [ObservableProperty] private bool _turboModeWhileHeld; + [ObservableProperty] private Key _cycleInputDevicePlayer1; + + [ObservableProperty] private Key _cycleInputDevicePlayer2; + + [ObservableProperty] private Key _cycleInputDevicePlayer3; + + [ObservableProperty] private Key _cycleInputDevicePlayer4; + public HotkeyConfig(KeyboardHotkeys config) { if (config == null) @@ -50,6 +58,10 @@ namespace Ryujinx.Ava.UI.Models.Input CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement; TurboMode = config.TurboMode; TurboModeWhileHeld = config.TurboModeWhileHeld; + CycleInputDevicePlayer1 = config.CycleInputDevicePlayer1; + CycleInputDevicePlayer2 = config.CycleInputDevicePlayer2; + CycleInputDevicePlayer3 = config.CycleInputDevicePlayer3; + CycleInputDevicePlayer4 = config.CycleInputDevicePlayer4; } public KeyboardHotkeys GetConfig() => @@ -67,7 +79,11 @@ namespace Ryujinx.Ava.UI.Models.Input CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement, CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement, TurboMode = TurboMode, - TurboModeWhileHeld = TurboModeWhileHeld + TurboModeWhileHeld = TurboModeWhileHeld, + CycleInputDevicePlayer1 = CycleInputDevicePlayer1, + CycleInputDevicePlayer2 = CycleInputDevicePlayer2, + CycleInputDevicePlayer3 = CycleInputDevicePlayer3, + CycleInputDevicePlayer4 = CycleInputDevicePlayer4 }; } } diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 81aae6b74..7a775aa7c 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -526,18 +526,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } - private static string GetShortGamepadName(string str) - { - const string Ellipsis = "..."; - const int MaxSize = 50; - if (str.Length > MaxSize) - { - return $"{str.AsSpan(0, MaxSize - Ellipsis.Length)}{Ellipsis}"; - } - - return str; - } private static string GetShortGamepadId(string str) { @@ -551,7 +540,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input { string GetGamepadName(IGamepad gamepad, int controllerNumber) { - return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})"; + return $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)} ({controllerNumber})"; } string GetUniqueGamepadName(IGamepad gamepad, ref int controllerNumber) @@ -579,7 +568,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (gamepad != null) { - Devices.Add((DeviceType.Keyboard, id, $"{GetShortGamepadName(gamepad.Name)}")); + Devices.Add((DeviceType.Keyboard, id, $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)}")); } } @@ -652,138 +641,21 @@ namespace Ryujinx.Ava.UI.ViewModels.Input InputConfig config; if (activeDevice.Type == DeviceType.Keyboard) { - string id = activeDevice.Id; - string name = activeDevice.Name; - - config = new StandardKeyboardInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.WindowKeyboard, - Id = id, - Name = name, - ControllerType = ControllerType.ProController, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = Key.Up, - DpadDown = Key.Down, - DpadLeft = Key.Left, - DpadRight = Key.Right, - ButtonMinus = Key.Minus, - ButtonL = Key.E, - ButtonZl = Key.Q, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - LeftJoyconStick = - new JoyconConfigKeyboardStick - { - StickUp = Key.W, - StickDown = Key.S, - StickLeft = Key.A, - StickRight = Key.D, - StickButton = Key.F, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = Key.Z, - ButtonB = Key.X, - ButtonX = Key.C, - ButtonY = Key.V, - ButtonPlus = Key.Plus, - ButtonR = Key.U, - ButtonZr = Key.O, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = Key.I, - StickDown = Key.K, - StickLeft = Key.J, - StickRight = Key.L, - StickButton = Key.H, - }, - }; + config = DefaultInputConfigurationProvider.CreateDefaultKeyboardConfig(activeDevice.Id, activeDevice.Name, _playerId); } else if (activeDevice.Type == DeviceType.Controller) { - bool isNintendoStyle = Devices.ToList().FirstOrDefault(x => x.Id == activeDevice.Id).Name.Contains("Nintendo"); - - string id = activeDevice.Id.Split(" ")[0]; - string name = activeDevice.Name; - - config = new StandardControllerInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.GamepadSDL2, - Id = id, - Name = name, - ControllerType = ControllerType.ProController, - DeadzoneLeft = 0.1f, - DeadzoneRight = 0.1f, - RangeLeft = 1.0f, - RangeRight = 1.0f, - TriggerThreshold = 0.5f, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = ConfigGamepadInputId.DpadUp, - DpadDown = ConfigGamepadInputId.DpadDown, - DpadLeft = ConfigGamepadInputId.DpadLeft, - DpadRight = ConfigGamepadInputId.DpadRight, - ButtonMinus = ConfigGamepadInputId.Minus, - ButtonL = ConfigGamepadInputId.LeftShoulder, - ButtonZl = ConfigGamepadInputId.LeftTrigger, - ButtonSl = ConfigGamepadInputId.Unbound, - ButtonSr = ConfigGamepadInputId.Unbound, - }, - LeftJoyconStick = new JoyconConfigControllerStick - { - Joystick = ConfigStickInputId.Left, - StickButton = ConfigGamepadInputId.LeftStick, - InvertStickX = false, - InvertStickY = false, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B, - ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A, - ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y, - ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X, - ButtonPlus = ConfigGamepadInputId.Plus, - ButtonR = ConfigGamepadInputId.RightShoulder, - ButtonZr = ConfigGamepadInputId.RightTrigger, - ButtonSl = ConfigGamepadInputId.Unbound, - ButtonSr = ConfigGamepadInputId.Unbound, - }, - RightJoyconStick = new JoyconConfigControllerStick - { - Joystick = ConfigStickInputId.Right, - StickButton = ConfigGamepadInputId.RightStick, - InvertStickX = false, - InvertStickY = false, - }, - Motion = new StandardMotionConfigController - { - MotionBackend = MotionInputBackendType.GamepadDriver, - EnableMotion = true, - Sensitivity = 100, - GyroDeadzone = 1, - }, - Rumble = new RumbleConfigController - { - StrongRumble = 1f, - WeakRumble = 1f, - EnableRumble = false, - }, - }; + bool isNintendoStyle = DefaultInputConfigurationProvider.IsNintendoStyleController(activeDevice.Name); + config = DefaultInputConfigurationProvider.CreateDefaultControllerConfig(activeDevice.Id, activeDevice.Name, _playerId, isNintendoStyle); } else { - config = new InputConfig(); + config = new InputConfig + { + PlayerIndex = _playerId + }; } - config.PlayerIndex = _playerId; - return config; } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml index 917177fb5..ba82812b9 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml @@ -120,6 +120,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs index b9a5462b2..7f481f82e 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs @@ -82,7 +82,11 @@ namespace Ryujinx.Ava.UI.Views.Settings { "VolumeDown", () => viewModel.KeyboardHotkey.VolumeDown = Key.Unbound }, { "CustomVSyncIntervalIncrement", () => viewModel.KeyboardHotkey.CustomVSyncIntervalIncrement = Key.Unbound }, { "CustomVSyncIntervalDecrement", () => viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = Key.Unbound }, - { "TurboMode", () => viewModel.KeyboardHotkey.TurboMode = Key.Unbound } + { "TurboMode", () => viewModel.KeyboardHotkey.TurboMode = Key.Unbound }, + { "CycleInputDevicePlayer1", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer1 = Key.Unbound }, + { "CycleInputDevicePlayer2", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer2 = Key.Unbound }, + { "CycleInputDevicePlayer3", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer3 = Key.Unbound }, + { "CycleInputDevicePlayer4", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer4 = Key.Unbound } }; if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action)) @@ -162,6 +166,18 @@ namespace Ryujinx.Ava.UI.Views.Settings case "TurboMode": ViewModel.KeyboardHotkey.TurboMode = buttonValue.AsHidType(); break; + case "CycleInputDevicePlayer1": + ViewModel.KeyboardHotkey.CycleInputDevicePlayer1 = buttonValue.AsHidType(); + break; + case "CycleInputDevicePlayer2": + ViewModel.KeyboardHotkey.CycleInputDevicePlayer2 = buttonValue.AsHidType(); + break; + case "CycleInputDevicePlayer3": + ViewModel.KeyboardHotkey.CycleInputDevicePlayer3 = buttonValue.AsHidType(); + break; + case "CycleInputDevicePlayer4": + ViewModel.KeyboardHotkey.CycleInputDevicePlayer4 = buttonValue.AsHidType(); + break; } }); } From 7e40ca3d929a35ef0f799e0814197add8efda212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Fri, 20 Jun 2025 18:13:55 +0300 Subject: [PATCH 03/10] Controller overlay duration config --- .../Configuration/ConfigurationFileFormat.cs | 12 ++++++++- .../ConfigurationState.Migration.cs | 9 ++++++- .../Configuration/ConfigurationState.Model.cs | 12 +++++++++ .../Configuration/ConfigurationState.cs | 4 +++ .../UI/ViewModels/SettingsViewModel.cs | 6 +++++ .../UI/Views/Settings/SettingsUIView.axaml | 26 +++++++++++++++++++ 6 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index c21383349..0f45d04d1 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 70; + public const int CurrentVersion = 71; /// /// Version of the configuration file format @@ -217,6 +217,16 @@ namespace Ryujinx.Ava.Systems.Configuration /// public HideCursorMode HideCursor { get; set; } + /// + /// Duration to show controller overlay when game starts (seconds, 0 = disabled) + /// + public int ControllerOverlayGameStartDuration { get; set; } + + /// + /// Duration to show controller overlay when input is cycled (seconds, 0 = disabled) + /// + public int ControllerOverlayInputCycleDuration { get; set; } + /// /// Enables or disables Vertical Sync /// diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index afabdb4e3..6ad687688 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -53,6 +53,8 @@ namespace Ryujinx.Ava.Systems.Configuration ShowOldUI.Value = shouldLoadFromFile ? cff.ShowTitleBar : ShowOldUI.Value; // Get from global config only EnableHardwareAcceleration.Value = shouldLoadFromFile ? cff.EnableHardwareAcceleration : EnableHardwareAcceleration.Value; // Get from global config only HideCursor.Value = cff.HideCursor; + ControllerOverlayGameStartDuration.Value = cff.ControllerOverlayGameStartDuration; + ControllerOverlayInputCycleDuration.Value = cff.ControllerOverlayInputCycleDuration; Logger.EnableFileLog.Value = cff.EnableFileLog; Logger.EnableDebug.Value = cff.LoggingEnableDebug; @@ -479,7 +481,12 @@ namespace Ryujinx.Ava.Systems.Configuration }; } ), - (69, static cff => cff.SkipUserProfiles = false) + (69, static cff => cff.SkipUserProfiles = false), + (71, static cff => + { + cff.ControllerOverlayGameStartDuration = 3; + cff.ControllerOverlayInputCycleDuration = 2; + }) ); } } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs index 29a390b26..2f9a0787c 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs @@ -846,6 +846,16 @@ namespace Ryujinx.Ava.Systems.Configuration /// public ReactiveObject HideCursor { get; private set; } + /// + /// Duration to show controller overlay when game starts (seconds, 0 = disabled) + /// + public ReactiveObject ControllerOverlayGameStartDuration { get; private set; } + + /// + /// Duration to show controller overlay when input is cycled (seconds, 0 = disabled) + /// + public ReactiveObject ControllerOverlayInputCycleDuration { get; private set; } + private ConfigurationState() { UI = new UISection(); @@ -863,6 +873,8 @@ namespace Ryujinx.Ava.Systems.Configuration RememberWindowState = new ReactiveObject(); ShowOldUI = new ReactiveObject(); EnableHardwareAcceleration = new ReactiveObject(); + ControllerOverlayGameStartDuration = new ReactiveObject(); + ControllerOverlayInputCycleDuration = new ReactiveObject(); } public HleConfiguration CreateHleConfiguration() => diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs index 4a565d5d3..71f73bc65 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs @@ -65,6 +65,8 @@ namespace Ryujinx.Ava.Systems.Configuration ShowTitleBar = ShowOldUI, EnableHardwareAcceleration = EnableHardwareAcceleration, HideCursor = HideCursor, + ControllerOverlayGameStartDuration = ControllerOverlayGameStartDuration, + ControllerOverlayInputCycleDuration = ControllerOverlayInputCycleDuration, VSyncMode = Graphics.VSyncMode, EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval, CustomVSyncInterval = Graphics.CustomVSyncInterval, @@ -190,6 +192,8 @@ namespace Ryujinx.Ava.Systems.Configuration ShowOldUI.Value = !OperatingSystem.IsWindows(); EnableHardwareAcceleration.Value = true; HideCursor.Value = HideCursorMode.OnIdle; + ControllerOverlayGameStartDuration.Value = 3; + ControllerOverlayInputCycleDuration.Value = 2; Graphics.VSyncMode.Value = VSyncMode.Switch; Graphics.CustomVSyncInterval.Value = 120; Graphics.EnableCustomVSyncInterval.Value = false; diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 654eb0c43..b24b74dcb 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -146,6 +146,8 @@ namespace Ryujinx.Ava.UI.ViewModels public bool EnableMouse { get; set; } public bool DisableInputWhenOutOfFocus { get; set; } public int FocusLostActionType { get; set; } + public int ControllerOverlayGameStartDuration { get; set; } + public int ControllerOverlayInputCycleDuration { get; set; } public bool UseGlobalInputConfig { @@ -587,6 +589,8 @@ namespace Ryujinx.Ava.UI.ViewModels HideCursor = (int)config.HideCursor.Value; UpdateCheckerType = (int)config.UpdateCheckerType.Value; FocusLostActionType = (int)config.FocusLostActionType.Value; + ControllerOverlayGameStartDuration = config.ControllerOverlayGameStartDuration.Value; + ControllerOverlayInputCycleDuration = config.ControllerOverlayInputCycleDuration.Value; GameDirectories.Clear(); GameDirectories.AddRange(config.UI.GameDirs.Value); @@ -698,6 +702,8 @@ namespace Ryujinx.Ava.UI.ViewModels config.HideCursor.Value = (HideCursorMode)HideCursor; config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType; config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType; + config.ControllerOverlayGameStartDuration.Value = ControllerOverlayGameStartDuration; + config.ControllerOverlayInputCycleDuration.Value = ControllerOverlayInputCycleDuration; config.UI.GameDirs.Value = [.. GameDirectories]; config.UI.AutoloadDirs.Value = [.. AutoloadDirectories]; diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml index ad05efd06..611e8d196 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -157,6 +157,32 @@ + + + + + + + + + + + + Date: Fri, 20 Jun 2025 18:32:17 +0300 Subject: [PATCH 04/10] Controller overlay changed from Window to UserControl --- src/Ryujinx/Systems/AppHost.cs | 9 ++-- .../ControllerOverlay.axaml} | 32 +++++------- .../ControllerOverlay.axaml.cs} | 51 +++++++------------ .../UI/ViewModels/MainWindowViewModel.cs | 5 +- src/Ryujinx/UI/Windows/MainWindow.axaml | 6 +++ 5 files changed, 45 insertions(+), 58 deletions(-) rename src/Ryujinx/UI/{Windows/ControllerOverlayWindow.axaml => Controls/ControllerOverlay.axaml} (65%) rename src/Ryujinx/UI/{Windows/ControllerOverlayWindow.axaml.cs => Controls/ControllerOverlay.axaml.cs} (78%) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 801677cb2..fef287f93 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -1484,9 +1484,12 @@ namespace Ryujinx.Ava.Systems { try { - var overlayWindow = new UI.Windows.ControllerOverlayWindow(_topLevel as Avalonia.Controls.Window); - overlayWindow.ShowControllerBindings(inputConfigs); - overlayWindow.Show(); + // Access the overlay through the MainWindow via the ViewModel + if (_viewModel?.Window?.ControllerOverlay != null) + { + int duration = ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value; + _viewModel.Window.ControllerOverlay.ShowControllerBindings(inputConfigs, duration); + } } catch (Exception ex) { diff --git a/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml b/src/Ryujinx/UI/Controls/ControllerOverlay.axaml similarity index 65% rename from src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml rename to src/Ryujinx/UI/Controls/ControllerOverlay.axaml index d5c205667..0208e8853 100644 --- a/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml +++ b/src/Ryujinx/UI/Controls/ControllerOverlay.axaml @@ -1,20 +1,15 @@ - + IsVisible="False" + Name="ControllerOverlayControl"> - - - - \ No newline at end of file + + + + diff --git a/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs b/src/Ryujinx/UI/Controls/ControllerOverlay.axaml.cs similarity index 78% rename from src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs rename to src/Ryujinx/UI/Controls/ControllerOverlay.axaml.cs index e70b8239e..8799050ef 100644 --- a/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs +++ b/src/Ryujinx/UI/Controls/ControllerOverlay.axaml.cs @@ -7,46 +7,21 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -namespace Ryujinx.Ava.UI.Windows +namespace Ryujinx.Ava.UI.Controls { - public partial class ControllerOverlayWindow : StyleableWindow + public partial class ControllerOverlay : UserControl { - private const int AutoHideDelayMs = 4000; // 4 seconds - public ControllerOverlayWindow() + public ControllerOverlay() { InitializeComponent(); - - TransparencyLevelHint = [WindowTransparencyLevel.Transparent]; - SystemDecorations = SystemDecorations.None; - ExtendClientAreaTitleBarHeightHint = 0; Background = Brushes.Transparent; - CanResize = false; - ShowInTaskbar = false; + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right; + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top; + Margin = new Avalonia.Thickness(0, 50, 20, 0); } - public ControllerOverlayWindow(Window owner) : this() - { - if (owner != null) - { - // Position the overlay in the top-right corner of the owner window - WindowStartupLocation = WindowStartupLocation.Manual; - - // Set position after the window is loaded - Loaded += (s, e) => - { - if (owner.WindowState != WindowState.Minimized) - { - Position = new Avalonia.PixelPoint( - (int)(owner.Position.X + owner.Width - Width - 20), - (int)(owner.Position.Y + 50) - ); - } - }; - } - } - - public void ShowControllerBindings(List inputConfigs) + public void ShowControllerBindings(List inputConfigs, int durationSeconds = 3) { // Clear existing bindings PlayerBindings.Children.Clear(); @@ -118,13 +93,21 @@ namespace Ryujinx.Ava.UI.Windows PlayerBindings.Children.Add(playerPanel); } + // Update duration text + DurationText.Text = durationSeconds == 1 + ? "This overlay will disappear in 1 second" + : $"This overlay will disappear in {durationSeconds} seconds"; + + // Show the overlay + IsVisible = true; + // Auto-hide after delay _ = Task.Run(async () => { - await Task.Delay(AutoHideDelayMs); + await Task.Delay(durationSeconds * 1000); await Dispatcher.UIThread.InvokeAsync(() => { - Close(); + IsVisible = false; }); }); } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 049ada4b5..b2ead0796 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1706,9 +1706,8 @@ namespace Ryujinx.Ava.UI.ViewModels // Only show overlay if there are actual controller configurations for players 1-4 if (inputConfigs?.Any(c => c.PlayerIndex <= PlayerIndex.Player4) == true) { - var overlay = new Windows.ControllerOverlayWindow(Window); - overlay.ShowControllerBindings(inputConfigs); - overlay.Show(); + int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value; + Window.ControllerOverlay.ShowControllerBindings(inputConfigs, duration); } } catch (Exception ex) diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml b/src/Ryujinx/UI/Windows/MainWindow.axaml index 498b77100..8085ffe93 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml @@ -10,6 +10,7 @@ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:main="clr-namespace:Ryujinx.Ava.UI.Views.Main" xmlns:viewsMisc="clr-namespace:Ryujinx.Ava.UI.Views.Misc" + xmlns:overlayControls="clr-namespace:Ryujinx.Ava.UI.Controls" Cursor="{Binding Cursor}" Title="{Binding Title}" WindowState="{Binding WindowState}" @@ -178,5 +179,10 @@ Name="StatusBarView" Grid.Row="2" /> + + + From 3be20385ed6e1a5e1e7f4a5fb54f54e0cd34664f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Fri, 20 Jun 2025 18:45:53 +0300 Subject: [PATCH 05/10] SettingsUIView: localize --- assets/locales.json | 125 ++++++++++++++++++ .../UI/Views/Settings/SettingsUIView.axaml | 10 +- 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index 498bcb81e..6dd48e767 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -25121,6 +25121,131 @@ "zh_CN": "动态 Rich Presence", "zh_TW": "動態 Rich Presence" } + }, + { + "ID": "SettingsTabUIControllerOverlay", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Controller Overlay", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabUIControllerOverlayGameStartDuration", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show on game start (seconds):", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabUIControllerOverlayGameStartDurationTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Duration to show controller overlay when game starts (0 = disabled)", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabUIControllerOverlayInputCycleDuration", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Show on input cycle (seconds):", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "SettingsTabUIControllerOverlayInputCycleDurationTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Duration to show controller overlay when cycling inputs (0 = disabled)", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } } ] } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml index 611e8d196..0b9aa1514 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -158,29 +158,29 @@ - + + ToolTip.Tip="{ext:Locale SettingsTabUIControllerOverlayGameStartDurationTooltip}" /> + ToolTip.Tip="{ext:Locale SettingsTabUIControllerOverlayInputCycleDurationTooltip}" /> From 069a703f22ffecb6b51d50987bddee68b77fe1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Sat, 21 Jun 2025 00:17:10 +0300 Subject: [PATCH 06/10] Overlay system --- .../Multithreading/ThreadedWindow.cs | 5 + .../Overlay/ControllerOverlay.cs | 244 ++++++++++++++++++ .../Overlay/ImageElement.cs | 145 +++++++++++ src/Ryujinx.Graphics.Gpu/Overlay/Overlay.cs | 115 +++++++++ .../Overlay/OverlayElement.cs | 66 +++++ .../Overlay/OverlayManager.cs | 160 ++++++++++++ .../Overlay/RectangleElement.cs | 78 ++++++ .../Overlay/TextElement.cs | 121 +++++++++ .../Ryujinx.Graphics.Gpu.csproj | 4 + src/Ryujinx.Graphics.Gpu/Window.cs | 122 +++++++++ src/Ryujinx/Systems/AppHost.cs | 23 +- .../UI/ViewModels/MainWindowViewModel.cs | 19 +- src/Ryujinx/UI/Windows/MainWindow.axaml | 5 +- 13 files changed, 1088 insertions(+), 19 deletions(-) create mode 100644 src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs create mode 100644 src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs create mode 100644 src/Ryujinx.Graphics.Gpu/Overlay/Overlay.cs create mode 100644 src/Ryujinx.Graphics.Gpu/Overlay/OverlayElement.cs create mode 100644 src/Ryujinx.Graphics.Gpu/Overlay/OverlayManager.cs create mode 100644 src/Ryujinx.Graphics.Gpu/Overlay/RectangleElement.cs create mode 100644 src/Ryujinx.Graphics.Gpu/Overlay/TextElement.cs diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs index 7a4836982..340a47321 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -41,5 +41,10 @@ namespace Ryujinx.Graphics.GAL.Multithreading public void SetScalingFilterLevel(float level) { } public void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled) { } + + /// + /// Gets the underlying implementation window for direct access + /// + public IWindow BaseWindow => _impl.Window; } } diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs b/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs new file mode 100644 index 000000000..2d5904628 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs @@ -0,0 +1,244 @@ +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using OriginalInputConfig = Ryujinx.Common.Configuration.Hid.InputConfig; +using OriginalPlayerIndex = Ryujinx.Common.Configuration.Hid.PlayerIndex; +using OriginalInputBackendType = Ryujinx.Common.Configuration.Hid.InputBackendType; + +namespace Ryujinx.Graphics.Gpu.Overlay +{ + /// + /// Controller overlay that shows controller bindings matching the original AXAML design + /// + public class ControllerOverlay : Overlay + { + private const float OverlayWidth = 400; + private const float Padding = 24; + private const float Spacing = 16; + private const float PlayerSpacing = 12; + private const float PlayerRowHeight = 32; + + private const float TitleTextSize = 25; + private const float PlayerTextSize = 22; + + private float _lifespan = 0f; + + public ControllerOverlay() : base("ControllerOverlay") + { + CreateBaseElements(); + } + + private void CreateBaseElements() + { + // Main background container + var background = new RectangleElement(0, 0, OverlayWidth, 200, // Dynamic height will be set later + new SKColor(0, 0, 0, 224)) // #E0000000 + { + Name = "Background", + CornerRadius = 12, + BorderColor = new SKColor(255, 255, 255, 64), // #40FFFFFF + BorderWidth = 1 + }; + AddElement(background); + + // Title text + var titleText = new TextElement(Padding + 30, Padding, "Controller Bindings", TitleTextSize, SKColors.White) + { + Name = "TitleText", + FontStyle = SKFontStyle.Bold + }; + AddElement(titleText); + } + + /// + /// Show controller bindings matching the original AXAML implementation + /// + public void ShowControllerBindings(List inputConfigs, int durationSeconds = 3) + { + // Reset lifespan and opacity + _lifespan = durationSeconds; + + // Clear existing player bindings + ClearPlayerBindings(); + + // Debug: Log input data + // Group controllers by player index + var playerBindings = new Dictionary>(); + + foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= OriginalPlayerIndex.Player4)) + { + Console.WriteLine($"ControllerOverlay: Config for Player {config.PlayerIndex}: {config.Name} ({config.Backend})"); + if (!playerBindings.ContainsKey(config.PlayerIndex)) + { + playerBindings[config.PlayerIndex] = new List(); + } + playerBindings[config.PlayerIndex].Add(config); + } + + float currentY = Padding + 40; // After title + + // Add player bindings to UI + for (int i = 0; i < 4; i++) + { + var playerIndex = (OriginalPlayerIndex)i; + float rowY = currentY + (i * (PlayerRowHeight + PlayerSpacing)); + + // Player number with colored background (circular badge) + var playerColor = GetPlayerColor(i); + var playerBadge = new RectangleElement(Padding, rowY, 24, 20, playerColor) + { + Name = $"PlayerBadge_{i}", + CornerRadius = 12 + }; + AddElement(playerBadge); + + // Player number text + var playerLabel = new TextElement(Padding + 12, rowY + 2, $"P{i + 1}", PlayerTextSize, SKColors.White) + { + Name = $"PlayerLabel_{i}", + FontStyle = SKFontStyle.Bold, + TextAlign = SKTextAlign.Center + }; + AddElement(playerLabel); + + // Controller info + if (playerBindings.ContainsKey(playerIndex)) + { + var controllers = playerBindings[playerIndex]; + var controllerNames = controllers.Select(c => GetControllerDisplayName(c)).ToList(); + + var controllerText = new TextElement(Padding + 56, rowY + 2, string.Join(", ", controllerNames), PlayerTextSize, new SKColor(144, 238, 144)) // LightGreen + { + Name = $"ControllerText_{i}", + FontStyle = SKFontStyle.Bold + }; + AddElement(controllerText); + } + else + { + var noControllerText = new TextElement(Padding + 56, rowY + 2, "No controller assigned", PlayerTextSize, new SKColor(128, 128, 128)) // Gray + { + Name = $"NoControllerText_{i}", + FontStyle = SKFontStyle.Italic + }; + AddElement(noControllerText); + } + } + + // Calculate total height and update background + float totalHeight = Padding + 40 + (4 * (PlayerRowHeight + PlayerSpacing)) + Padding + 40; // Extra space for duration text + var background = FindElement("Background"); + if (background != null) + { + background.Height = totalHeight; + } + + // Duration text at bottom + string durationText = durationSeconds == 1 + ? "This overlay will disappear in 1 second" + : $"This overlay will disappear in {durationSeconds} seconds"; + + // Show the overlay (position will be set by Window class with actual dimensions) + IsVisible = true; + } + + private static SKColor GetPlayerColor(int playerIndex) + { + return playerIndex switch + { + 0 => new SKColor(255, 92, 92), // Red for Player 1 + 1 => new SKColor(54, 162, 235), // Blue for Player 2 + 2 => new SKColor(255, 206, 84), // Yellow for Player 3 + 3 => new SKColor(75, 192, 192), // Green for Player 4 + _ => new SKColor(128, 128, 128) // Gray fallback + }; + } + + private static string GetControllerDisplayName(OriginalInputConfig config) + { + if (string.IsNullOrEmpty(config.Name)) + { + return config.Backend switch + { + OriginalInputBackendType.WindowKeyboard => "Keyboard", + OriginalInputBackendType.GamepadSDL2 => "Controller", + _ => "Unknown" + }; + } + + // Truncate long controller names + string name = config.Name; + if (name.Length > 25) + { + name = name.Substring(0, 22) + "..."; + } + + return name; + } + + /// + /// Clear all player bindings + /// + private void ClearPlayerBindings() + { + var elementsToRemove = new List(); + + foreach (var element in GetElements()) + { + if (element.Name.StartsWith("PlayerBadge_") || + element.Name.StartsWith("PlayerLabel_") || + element.Name.StartsWith("ControllerText_") || + element.Name.StartsWith("NoControllerText_")) + { + elementsToRemove.Add(element); + } + } + + foreach (var element in elementsToRemove) + { + RemoveElement(element); + element.Dispose(); + } + } + + /// + /// Update overlay + /// + public override void Update(float deltaTime, SKSize screenSize) + { + _lifespan -= deltaTime; + + if (_lifespan <= 0) + { + IsVisible = false; + return; + } + + if (_lifespan <= 0.5f) + { + // Fade out during the last 0.5 seconds + Opacity = _lifespan / 0.5f; + } + else + { + Opacity = 1; + } + + // Update position if screen size is provided + if (screenSize.Width > 0 && screenSize.Height > 0) + { + SetPositionToTopRight(screenSize.Width, screenSize.Height); + } + } + + /// + /// Position overlay to top-right matching original AXAML positioning + /// + public void SetPositionToTopRight(float screenWidth, float screenHeight) + { + X = screenWidth - OverlayWidth - 20; // 20px margin from right + Y = 50; // 50px margin from top + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs b/src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs new file mode 100644 index 000000000..57122d73d --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs @@ -0,0 +1,145 @@ +using SkiaSharp; +using System; + +namespace Ryujinx.Graphics.Gpu.Overlay +{ + /// + /// Image overlay element + /// + public class ImageElement : OverlayElement + { + private SKBitmap? _bitmap; + private byte[]? _imageData; + private string? _imagePath; + + public SKFilterQuality FilterQuality { get; set; } = SKFilterQuality.Medium; + public bool MaintainAspectRatio { get; set; } = true; + + public ImageElement() + { + } + + public ImageElement(float x, float y, float width, float height, byte[] imageData) + { + X = x; + Y = y; + Width = width; + Height = height; + SetImageData(imageData); + } + + public ImageElement(float x, float y, float width, float height, string imagePath) + { + X = x; + Y = y; + Width = width; + Height = height; + SetImagePath(imagePath); + } + + /// + /// Set image from byte array + /// + public void SetImageData(byte[] imageData) + { + _imageData = imageData; + _imagePath = null; + LoadBitmap(); + } + + /// + /// Set image from file path + /// + public void SetImagePath(string imagePath) + { + _imagePath = imagePath; + _imageData = null; + LoadBitmap(); + } + + /// + /// Set image from existing SKBitmap + /// + public void SetBitmap(SKBitmap bitmap) + { + _bitmap?.Dispose(); + _bitmap = bitmap; + _imageData = null; + _imagePath = null; + } + + private void LoadBitmap() + { + try + { + _bitmap?.Dispose(); + _bitmap = null; + + if (_imageData != null) + { + _bitmap = SKBitmap.Decode(_imageData); + } + else if (!string.IsNullOrEmpty(_imagePath)) + { + _bitmap = SKBitmap.Decode(_imagePath); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to load image: {ex.Message}"); + _bitmap = null; + } + } + + public override void Render(SKCanvas canvas, float globalOpacity = 1.0f) + { + if (!IsVisible || _bitmap == null || Width <= 0 || Height <= 0) + return; + + float effectiveOpacity = Opacity * globalOpacity; + + using var paint = new SKPaint + { + FilterQuality = FilterQuality, + Color = SKColors.White.WithAlpha((byte)(255 * effectiveOpacity)) + }; + + var sourceRect = new SKRect(0, 0, _bitmap.Width, _bitmap.Height); + var destRect = new SKRect(X, Y, X + Width, Y + Height); + + if (MaintainAspectRatio) + { + // Calculate aspect ratio preserving destination rectangle + float sourceAspect = (float)_bitmap.Width / _bitmap.Height; + float destAspect = Width / Height; + + if (sourceAspect > destAspect) + { + // Source is wider, fit to width + float newHeight = Width / sourceAspect; + float yOffset = (Height - newHeight) / 2; + destRect = new SKRect(X, Y + yOffset, X + Width, Y + yOffset + newHeight); + } + else + { + // Source is taller, fit to height + float newWidth = Height * sourceAspect; + float xOffset = (Width - newWidth) / 2; + destRect = new SKRect(X + xOffset, Y, X + xOffset + newWidth, Y + Height); + } + } + + canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _bitmap?.Dispose(); + _bitmap = null; + } + base.Dispose(disposing); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/Overlay.cs b/src/Ryujinx.Graphics.Gpu/Overlay/Overlay.cs new file mode 100644 index 000000000..a414888e1 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Overlay/Overlay.cs @@ -0,0 +1,115 @@ +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Graphics.Gpu.Overlay +{ + /// + /// Base overlay class containing multiple elements + /// + public abstract class Overlay : IDisposable + { + private readonly List _elements = new(); + + public string Name { get; set; } = string.Empty; + public bool IsVisible { get; set; } = true; + public float Opacity { get; set; } = 1.0f; + public float X { get; set; } + public float Y { get; set; } + public int ZIndex { get; set; } = 0; + + public Overlay() + { + } + + public Overlay(string name) + { + Name = name; + } + + /// + /// Add an element to this overlay + /// + public void AddElement(OverlayElement element) + { + _elements.Add(element); + } + + /// + /// Remove an element from this overlay + /// + public void RemoveElement(OverlayElement element) + { + _elements.Remove(element); + } + + /// + /// Get all elements + /// + public IReadOnlyList GetElements() + { + return _elements.AsReadOnly(); + } + + /// + /// Find element by name + /// + public T FindElement(string name) where T : OverlayElement + { + return _elements.OfType().FirstOrDefault(e => e.Name == name); + } + + /// + /// Clear all elements + /// + public void Clear() + { + foreach (var element in _elements) + { + element.Dispose(); + } + _elements.Clear(); + } + + /// + /// Update overlay + /// + public abstract void Update(float deltaTime, SKSize screenSize = default); + + /// + /// Render this overlay + /// + public void Render(SKCanvas canvas) + { + if (!IsVisible || Opacity <= 0.0f) + return; + + // Save canvas state + canvas.Save(); + + // Apply overlay position offset + if (X != 0 || Y != 0) + { + canvas.Translate(X, Y); + } + + // Render all elements + foreach (var element in _elements) + { + if (element.IsVisible) + { + element.Render(canvas, Opacity); + } + } + + // Restore canvas state + canvas.Restore(); + } + + public void Dispose() + { + Clear(); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/OverlayElement.cs b/src/Ryujinx.Graphics.Gpu/Overlay/OverlayElement.cs new file mode 100644 index 000000000..c4c528d65 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Overlay/OverlayElement.cs @@ -0,0 +1,66 @@ +using SkiaSharp; +using System; + +namespace Ryujinx.Graphics.Gpu.Overlay +{ + /// + /// Base class for all overlay elements + /// + public abstract class OverlayElement : IDisposable + { + public float X { get; set; } + public float Y { get; set; } + public float Width { get; set; } + public float Height { get; set; } + public bool IsVisible { get; set; } = true; + public float Opacity { get; set; } = 1.0f; + public string Name { get; set; } = string.Empty; + + /// + /// Render this element to the canvas + /// + /// The canvas to draw on + /// Global opacity multiplier + public abstract void Render(SKCanvas canvas, float globalOpacity = 1.0f); + + /// + /// Check if a point is within this element's bounds + /// + public virtual bool Contains(float x, float y) + { + return x >= X && x <= X + Width && y >= Y && y <= Y + Height; + } + + /// + /// Get the bounds of this element + /// + public SKRect GetBounds() + { + return new SKRect(X, Y, X + Width, Y + Height); + } + + /// + /// Apply opacity to a color + /// + protected SKColor ApplyOpacity(SKColor color, float opacity) + { + return color.WithAlpha((byte)(color.Alpha * opacity)); + } + + /// + /// Dispose of resources + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose of resources + /// + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/OverlayManager.cs b/src/Ryujinx.Graphics.Gpu/Overlay/OverlayManager.cs new file mode 100644 index 000000000..fb0a4906f --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Overlay/OverlayManager.cs @@ -0,0 +1,160 @@ +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using Ryujinx.Graphics.Gpu.Image; + +namespace Ryujinx.Graphics.Gpu.Overlay +{ + /// + /// Manages multiple overlays and handles rendering + /// + public class OverlayManager : IDisposable + { + private readonly List _overlays = new(); + private readonly object _lock = new(); + + /// + /// Add an overlay to the manager + /// + public void AddOverlay(Overlay overlay) + { + lock (_lock) + { + _overlays.Add(overlay); + SortOverlays(); + } + } + + /// + /// Remove an overlay from the manager + /// + public void RemoveOverlay(Overlay overlay) + { + lock (_lock) + { + _overlays.Remove(overlay); + } + } + + /// + /// Remove overlay by name + /// + public void RemoveOverlay(string name) + { + lock (_lock) + { + var overlay = _overlays.FirstOrDefault(o => o.Name == name); + if (overlay != null) + { + _overlays.Remove(overlay); + overlay.Dispose(); + } + } + } + + /// + /// Find overlay by name + /// + public Overlay FindOverlay(string name) + { + lock (_lock) + { + return _overlays.FirstOrDefault(o => o.Name == name); + } + } + + /// + /// Get all overlays + /// + public IReadOnlyList GetOverlays() + { + lock (_lock) + { + return _overlays.AsReadOnly(); + } + } + + /// + /// Clear all overlays + /// + public void Clear() + { + lock (_lock) + { + foreach (var overlay in _overlays) + { + overlay.Dispose(); + } + _overlays.Clear(); + } + } + + /// + /// Update all overlays (for animations) + /// + public void Update(float deltaTime, SKSize screenSize = default) + { + lock (_lock) + { + foreach (var overlay in _overlays.Where(o => o.IsVisible)) + { + overlay.Update(deltaTime, screenSize); + } + } + } + + /// + /// Render all visible overlays + /// + public void Render(SKCanvas canvas) + { + lock (_lock) + { + foreach (var overlay in _overlays.Where(o => o.IsVisible && o.Opacity > 0.0f)) + { + overlay.Render(canvas); + } + } + } + + /// + /// Sort overlays by Z-index + /// + private void SortOverlays() + { + _overlays.Sort((a, b) => a.ZIndex.CompareTo(b.ZIndex)); + } + + /// + /// Show overlay + /// + public void ShowOverlay(string name) + { + var overlay = FindOverlay(name); + if (overlay != null) + { + overlay.IsVisible = true; + overlay.Opacity = 1.0f; + } + } + + /// + /// Hide overlay + /// + public void HideOverlay(string name) + { + var overlay = FindOverlay(name); + if (overlay != null) + { + overlay.IsVisible = false; + overlay.Opacity = 0.0f; + } + } + + public void Dispose() + { + Clear(); + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/RectangleElement.cs b/src/Ryujinx.Graphics.Gpu/Overlay/RectangleElement.cs new file mode 100644 index 000000000..b310ef2fc --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Overlay/RectangleElement.cs @@ -0,0 +1,78 @@ +using SkiaSharp; + +namespace Ryujinx.Graphics.Gpu.Overlay +{ + /// + /// Rectangle overlay element + /// + public class RectangleElement : OverlayElement + { + public SKColor BackgroundColor { get; set; } = SKColors.Transparent; + public SKColor BorderColor { get; set; } = SKColors.Transparent; + public float BorderWidth { get; set; } = 0; + public float CornerRadius { get; set; } = 0; + + public RectangleElement() + { + } + + public RectangleElement(float x, float y, float width, float height, SKColor backgroundColor) + { + X = x; + Y = y; + Width = width; + Height = height; + BackgroundColor = backgroundColor; + } + + public override void Render(SKCanvas canvas, float globalOpacity = 1.0f) + { + if (!IsVisible || Width <= 0 || Height <= 0) + return; + + float effectiveOpacity = Opacity * globalOpacity; + var bounds = new SKRect(X, Y, X + Width, Y + Height); + + // Draw background + if (BackgroundColor.Alpha > 0) + { + using var backgroundPaint = new SKPaint + { + Color = ApplyOpacity(BackgroundColor, effectiveOpacity), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + if (CornerRadius > 0) + { + canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, backgroundPaint); + } + else + { + canvas.DrawRect(bounds, backgroundPaint); + } + } + + // Draw border + if (BorderWidth > 0 && BorderColor.Alpha > 0) + { + using var borderPaint = new SKPaint + { + Color = ApplyOpacity(BorderColor, effectiveOpacity), + Style = SKPaintStyle.Stroke, + StrokeWidth = BorderWidth, + IsAntialias = true + }; + + if (CornerRadius > 0) + { + canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, borderPaint); + } + else + { + canvas.DrawRect(bounds, borderPaint); + } + } + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/TextElement.cs b/src/Ryujinx.Graphics.Gpu/Overlay/TextElement.cs new file mode 100644 index 000000000..a1788fde6 --- /dev/null +++ b/src/Ryujinx.Graphics.Gpu/Overlay/TextElement.cs @@ -0,0 +1,121 @@ +using SkiaSharp; + +namespace Ryujinx.Graphics.Gpu.Overlay +{ + /// + /// Text overlay element + /// + public class TextElement : OverlayElement + { + public string Text { get; set; } = string.Empty; + public SKColor TextColor { get; set; } = SKColors.White; + public float FontSize { get; set; } = 16; + public string FontFamily { get; set; } = "Arial"; + public SKFontStyle FontStyle { get; set; } = SKFontStyle.Normal; + public SKTextAlign TextAlign { get; set; } = SKTextAlign.Left; + public bool IsAntialias { get; set; } = true; + + // Shadow properties + public bool HasShadow { get; set; } = false; + public SKColor ShadowColor { get; set; } = SKColors.Black; + public float ShadowOffsetX { get; set; } = 1; + public float ShadowOffsetY { get; set; } = 1; + public float ShadowBlur { get; set; } = 0; + + public TextElement() + { + } + + public TextElement(float x, float y, string text, float fontSize = 16, SKColor? color = null) + { + X = x; + Y = y; + Text = text; + FontSize = fontSize; + TextColor = color ?? SKColors.White; + + // Auto-calculate width and height based on text + UpdateDimensions(); + } + + public override void Render(SKCanvas canvas, float globalOpacity = 1.0f) + { + if (!IsVisible || string.IsNullOrEmpty(Text)) + return; + + float effectiveOpacity = Opacity * globalOpacity; + + using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle); + using var paint = new SKPaint + { + Color = ApplyOpacity(TextColor, effectiveOpacity), + TextSize = FontSize, + Typeface = typeface, + TextAlign = TextAlign, + IsAntialias = IsAntialias + }; + + float textX = X; + float textY = Y + FontSize; // Baseline adjustment + + // Adjust X position based on alignment + if (TextAlign == SKTextAlign.Center) + { + textX += Width / 2; + } + else if (TextAlign == SKTextAlign.Right) + { + textX += Width; + } + + // Draw shadow if enabled + if (HasShadow) + { + using var shadowPaint = new SKPaint + { + Color = ApplyOpacity(ShadowColor, effectiveOpacity), + TextSize = FontSize, + Typeface = typeface, + TextAlign = TextAlign, + IsAntialias = IsAntialias + }; + + if (ShadowBlur > 0) + { + shadowPaint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, ShadowBlur); + } + + canvas.DrawText(Text, textX + ShadowOffsetX, textY + ShadowOffsetY, shadowPaint); + } + + // Draw main text + canvas.DrawText(Text, textX, textY, paint); + } + + /// + /// Update width and height based on current text and font settings + /// + public void UpdateDimensions() + { + if (string.IsNullOrEmpty(Text)) + { + Width = 0; + Height = 0; + return; + } + + using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle); + using var paint = new SKPaint + { + TextSize = FontSize, + Typeface = typeface + }; + + var bounds = new SKRect(); + paint.MeasureText(Text, ref bounds); + + Width = bounds.Width; + Height = FontSize; // Use font size as height for consistency + } + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj index 4cd9c1d5c..9228fceba 100644 --- a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj +++ b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/src/Ryujinx.Graphics.Gpu/Window.cs b/src/Ryujinx.Graphics.Gpu/Window.cs index 5c3463f2a..0c59967a0 100644 --- a/src/Ryujinx.Graphics.Gpu/Window.cs +++ b/src/Ryujinx.Graphics.Gpu/Window.cs @@ -1,10 +1,14 @@ +using Ryujinx.Common.Memory; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Image; using Ryujinx.Graphics.Gpu.Memory; +using Ryujinx.Graphics.Gpu.Overlay; using Ryujinx.Graphics.Texture; using Ryujinx.Memory.Range; +using SkiaSharp; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading; namespace Ryujinx.Graphics.Gpu @@ -15,6 +19,8 @@ namespace Ryujinx.Graphics.Gpu public class Window { private readonly GpuContext _context; + private readonly OverlayManager _overlayManager; + private DateTime _lastUpdateTime = DateTime.UtcNow; /// /// Texture presented on the window. @@ -98,8 +104,12 @@ namespace Ryujinx.Graphics.Gpu public Window(GpuContext context) { _context = context; + _overlayManager = new OverlayManager(); _frameQueue = new ConcurrentQueue(); + + // Initialize controller overlay + InitializeControllerOverlay(); } /// @@ -207,6 +217,9 @@ namespace Ryujinx.Graphics.Gpu texture.SynchronizeMemory(); + // Add overlays by modifying texture data directly + AddOverlaysToTexture(texture); + ImageCrop crop = new( (int)(pt.Crop.Left * texture.ScaleFactor), (int)MathF.Ceiling(pt.Crop.Right * texture.ScaleFactor), @@ -244,6 +257,88 @@ namespace Ryujinx.Graphics.Gpu } } + /// + /// Initialize controller overlay + /// + private void InitializeControllerOverlay() + { + var controllerOverlay = new ControllerOverlay(); + _overlayManager.AddOverlay(controllerOverlay); + } + + /// + /// Add overlays to the texture using SkiaSharp + /// + /// The texture to modify + private void AddOverlaysToTexture(Image.Texture texture) + { + try + { + // Calculate delta time for lifespan updates + DateTime currentTime = DateTime.UtcNow; + float deltaTime = (float)(currentTime - _lastUpdateTime).TotalSeconds; + _lastUpdateTime = currentTime; + + // Update overlay animations + _overlayManager.Update(deltaTime, new SKSize(texture.Info.Width, texture.Info.Height)); + + // Get texture data from host texture + using var pinnedData = texture.HostTexture.GetData(); + var data = pinnedData.Get().ToArray(); + if (data == null || data.Length == 0) + return; + + int width = texture.Info.Width; + int height = texture.Info.Height; + int bytesPerPixel = texture.Info.FormatInfo.BytesPerPixel; + + // Determine the SKColorType based on bytes per pixel + SKColorType colorType = bytesPerPixel switch + { + 4 => SKColorType.Rgba8888, + 3 => SKColorType.Rgb888x, + 2 => SKColorType.Rgb565, + _ => SKColorType.Rgba8888 + }; + + // Create SKBitmap from texture data + var imageInfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + using var bitmap = new SKBitmap(imageInfo); + + // Copy texture data to bitmap + unsafe + { + fixed (byte* dataPtr = data) + { + bitmap.SetPixels((IntPtr)dataPtr); + } + } + + // Create canvas for drawing overlays + using var canvas = new SKCanvas(bitmap); + + // Render all overlays + _overlayManager.Render(canvas); + + // Copy modified bitmap data back to texture data array + var pixels = bitmap.Bytes; + if (pixels.Length <= data.Length) + { + Array.Copy(pixels, data, pixels.Length); + } + + // Upload modified data back to texture + var memoryOwner = MemoryOwner.Rent(data.Length); + data.CopyTo(memoryOwner.Span); + texture.HostTexture.SetData(memoryOwner); // SetData will dispose the MemoryOwner + } + catch (Exception ex) + { + // Silently fail if overlay rendering doesn't work + System.Diagnostics.Debug.WriteLine($"Overlay rendering failed: {ex.Message}"); + } + } + /// /// Indicate that a frame on the queue is ready to be acquired. /// @@ -267,5 +362,32 @@ namespace Ryujinx.Graphics.Gpu return false; } + + /// + /// Show controller overlay with the provided input configurations + /// + /// List of input configurations to display + /// Duration to show the overlay in seconds + public void ShowControllerBindings(List inputConfigs, int durationSeconds = 3) + { + var controllerOverlay = _overlayManager.FindOverlay("ControllerOverlay") as ControllerOverlay; + controllerOverlay?.ShowControllerBindings(inputConfigs, durationSeconds); + } + + /// + /// Get the overlay manager for external access + /// + public OverlayManager GetOverlayManager() + { + return _overlayManager; + } + + /// + /// Dispose resources + /// + public void Dispose() + { + _overlayManager?.Dispose(); + } } } diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index fef287f93..6c9cec18a 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -1480,22 +1480,23 @@ namespace Ryujinx.Ava.Systems private void ShowControllerOverlay(List inputConfigs) { - Dispatcher.UIThread.InvokeAsync(() => + try { - try + // Show overlay through the GPU context window directly + if (Device?.Gpu?.Window != null) { - // Access the overlay through the MainWindow via the ViewModel - if (_viewModel?.Window?.ControllerOverlay != null) - { - int duration = ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value; - _viewModel.Window.ControllerOverlay.ShowControllerBindings(inputConfigs, duration); - } + int duration = ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value; + Device.Gpu.Window.ShowControllerBindings(inputConfigs, duration); } - catch (Exception ex) + else { - Logger.Error?.Print(LogClass.Application, $"Failed to show controller overlay: {ex.Message}"); + Logger.Warning?.Print(LogClass.Application, "AppHost: Cannot show overlay - Device.Gpu.Window is null"); } - }); + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.Application, $"Failed to show controller overlay: {ex.Message}"); + } } private KeyboardHotkeyState GetHotkeyState() diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index b2ead0796..f1e4a0ae6 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1703,11 +1703,22 @@ namespace Ryujinx.Ava.UI.ViewModels ? ConfigurationState.InstanceExtra.Hid.InputConfig.Value : ConfigurationState.Instance.Hid.InputConfig.Value; - // Only show overlay if there are actual controller configurations for players 1-4 - if (inputConfigs?.Any(c => c.PlayerIndex <= PlayerIndex.Player4) == true) + // Debug: Log what we're getting + Logger.Info?.Print(LogClass.UI, $"MainWindowViewModel: Found {inputConfigs?.Count ?? 0} input configs"); + if (inputConfigs != null) { - int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value; - Window.ControllerOverlay.ShowControllerBindings(inputConfigs, duration); + foreach (var config in inputConfigs) + { + Logger.Info?.Print(LogClass.UI, $" Config: Player {config.PlayerIndex}, Backend {config.Backend}, Name '{config.Name}', Type {config.ControllerType}"); + } + } + + // Always show overlay - if no configs, it will show test data + int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value; + // Show overlay through the GPU context window directly + if (AppHost?.Device?.Gpu?.Window != null) + { + AppHost.Device.Gpu.Window.ShowControllerBindings(inputConfigs, duration); } } catch (Exception ex) diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml b/src/Ryujinx/UI/Windows/MainWindow.axaml index 8085ffe93..db0385696 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml @@ -180,9 +180,6 @@ Grid.Row="2" /> - - + From 525bd1019c1a5d1126931d2d691fd2e0e875e322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Sat, 21 Jun 2025 00:43:41 +0300 Subject: [PATCH 07/10] ControllerOverlay: forgot to remove unused axaml one --- .../UI/Controls/ControllerOverlay.axaml | 46 ------ .../UI/Controls/ControllerOverlay.axaml.cs | 149 ------------------ 2 files changed, 195 deletions(-) delete mode 100644 src/Ryujinx/UI/Controls/ControllerOverlay.axaml delete mode 100644 src/Ryujinx/UI/Controls/ControllerOverlay.axaml.cs diff --git a/src/Ryujinx/UI/Controls/ControllerOverlay.axaml b/src/Ryujinx/UI/Controls/ControllerOverlay.axaml deleted file mode 100644 index 0208e8853..000000000 --- a/src/Ryujinx/UI/Controls/ControllerOverlay.axaml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/Ryujinx/UI/Controls/ControllerOverlay.axaml.cs b/src/Ryujinx/UI/Controls/ControllerOverlay.axaml.cs deleted file mode 100644 index 8799050ef..000000000 --- a/src/Ryujinx/UI/Controls/ControllerOverlay.axaml.cs +++ /dev/null @@ -1,149 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Media; -using Avalonia.Threading; -using Ryujinx.Common.Configuration.Hid; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Ryujinx.Ava.UI.Controls -{ - public partial class ControllerOverlay : UserControl - { - - public ControllerOverlay() - { - InitializeComponent(); - Background = Brushes.Transparent; - HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right; - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top; - Margin = new Avalonia.Thickness(0, 50, 20, 0); - } - - public void ShowControllerBindings(List inputConfigs, int durationSeconds = 3) - { - // Clear existing bindings - PlayerBindings.Children.Clear(); - - // Group controllers by player index - var playerBindings = new Dictionary>(); - - foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= PlayerIndex.Player4)) - { - if (!playerBindings.ContainsKey(config.PlayerIndex)) - { - playerBindings[config.PlayerIndex] = new List(); - } - playerBindings[config.PlayerIndex].Add(config); - } - - // Add player bindings to UI - for (int i = 0; i < 4; i++) - { - var playerIndex = (PlayerIndex)i; - var playerPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 12 }; - - // Player number with colored background - var playerNumberBorder = new Border - { - Background = GetPlayerColor(i), - CornerRadius = new Avalonia.CornerRadius(12), - Padding = new Avalonia.Thickness(8, 4), - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center - }; - - var playerLabel = new TextBlock - { - Text = $"P{i + 1}", - FontWeight = FontWeight.Bold, - Foreground = Brushes.White, - FontSize = 12, - HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center - }; - playerNumberBorder.Child = playerLabel; - playerPanel.Children.Add(playerNumberBorder); - - if (playerBindings.ContainsKey(playerIndex)) - { - var controllers = playerBindings[playerIndex]; - var controllerNames = controllers.Select(c => GetControllerDisplayName(c)).ToList(); - - var controllerText = new TextBlock - { - Text = string.Join(", ", controllerNames), - Foreground = Brushes.LightGreen, - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, - FontWeight = FontWeight.SemiBold - }; - playerPanel.Children.Add(controllerText); - } - else - { - var noControllerText = new TextBlock - { - Text = "No controller assigned", - Foreground = Brushes.Gray, - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, - FontStyle = FontStyle.Italic - }; - playerPanel.Children.Add(noControllerText); - } - - PlayerBindings.Children.Add(playerPanel); - } - - // Update duration text - DurationText.Text = durationSeconds == 1 - ? "This overlay will disappear in 1 second" - : $"This overlay will disappear in {durationSeconds} seconds"; - - // Show the overlay - IsVisible = true; - - // Auto-hide after delay - _ = Task.Run(async () => - { - await Task.Delay(durationSeconds * 1000); - await Dispatcher.UIThread.InvokeAsync(() => - { - IsVisible = false; - }); - }); - } - - private static IBrush GetPlayerColor(int playerIndex) - { - return playerIndex switch - { - 0 => new SolidColorBrush(Color.FromRgb(255, 92, 92)), // Red for Player 1 - 1 => new SolidColorBrush(Color.FromRgb(54, 162, 235)), // Blue for Player 2 - 2 => new SolidColorBrush(Color.FromRgb(255, 206, 84)), // Yellow for Player 3 - 3 => new SolidColorBrush(Color.FromRgb(75, 192, 192)), // Green for Player 4 - _ => new SolidColorBrush(Color.FromRgb(128, 128, 128)) // Gray fallback - }; - } - - private static string GetControllerDisplayName(InputConfig config) - { - if (string.IsNullOrEmpty(config.Name)) - { - return config.Backend switch - { - InputBackendType.WindowKeyboard => "Keyboard", - InputBackendType.GamepadSDL2 => "Controller", - _ => "Unknown" - }; - } - - // Truncate long controller names - string name = config.Name; - if (name.Length > 25) - { - name = name.Substring(0, 22) + "..."; - } - - return name; - } - } -} From 9c47011e73ae0f9f435ff370f7694c521e5c03af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Sat, 21 Jun 2025 00:50:59 +0300 Subject: [PATCH 08/10] ControllerOverlay: remove auto generated translations --- assets/locales.json | 152 ++++++++++++++++++++++---------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index 6dd48e767..27c92ccad 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -24375,101 +24375,101 @@ { "ID": "SettingsTabHotkeysCycleInputDevicePlayer1", "Translations": { - "ar_SA": "تبديل جهاز الإدخال للاعب 1:", - "de_DE": "Eingabegerät für Spieler 1 wechseln:", - "el_GR": "Εναλλαγή Συσκευής Εισόδου Παίκτη 1:", + "ar_SA": "", + "de_DE": "", + "el_GR": "", "en_US": "Cycle Input Device Player 1:", - "es_ES": "Cambiar dispositivo de entrada jugador 1:", - "fr_FR": "Changer périphérique d'entrée joueur 1 :", - "he_IL": "החלף התקן קלט שחקן 1:", - "it_IT": "Cambia dispositivo di input giocatore 1:", - "ja_JP": "プレイヤー1の入力デバイス切り替え:", - "ko_KR": "플레이어 1 입력 장치 순환:", - "no_NO": "Bytt inndataenhet spiller 1:", - "pl_PL": "Przełącz urządzenie wejściowe gracza 1:", - "pt_BR": "Alternar dispositivo de entrada jogador 1:", - "ru_RU": "Переключить устройство ввода игрока 1:", - "sv_SE": "Växla indataenhet spelare 1:", - "th_TH": "สลับอุปกรณ์ป้อนข้อมูลผู้เล่น 1:", - "tr_TR": "Oyuncu 1 Giriş Cihazını Değiştir:", - "uk_UA": "Перемкнути пристрій введення гравця 1:", - "zh_CN": "切换玩家1输入设备:", - "zh_TW": "切換玩家1輸入裝置:" + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { "ID": "SettingsTabHotkeysCycleInputDevicePlayer2", "Translations": { - "ar_SA": "تبديل جهاز الإدخال للاعب 2:", - "de_DE": "Eingabegerät für Spieler 2 wechseln:", - "el_GR": "Εναλλαγή Συσκευής Εισόδου Παίκτη 2:", + "ar_SA": "", + "de_DE": "", + "el_GR": "", "en_US": "Cycle Input Device Player 2:", - "es_ES": "Cambiar dispositivo de entrada jugador 2:", - "fr_FR": "Changer périphérique d'entrée joueur 2 :", - "he_IL": "החלף התקן קלט שחקן 2:", - "it_IT": "Cambia dispositivo di input giocatore 2:", - "ja_JP": "プレイヤー2の入力デバイス切り替え:", - "ko_KR": "플레이어 2 입력 장치 순환:", - "no_NO": "Bytt inndataenhet spiller 2:", - "pl_PL": "Przełącz urządzenie wejściowe gracza 2:", - "pt_BR": "Alternar dispositivo de entrada jogador 2:", - "ru_RU": "Переключить устройство ввода игрока 2:", - "sv_SE": "Växla indataenhet spelare 2:", - "th_TH": "สลับอุปกรณ์ป้อนข้อมูลผู้เล่น 2:", - "tr_TR": "Oyuncu 2 Giriş Cihazını Değiştir:", - "uk_UA": "Перемкнути пристрій введення гравця 2:", - "zh_CN": "切换玩家2输入设备:", - "zh_TW": "切換玩家2輸入裝置:" + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { "ID": "SettingsTabHotkeysCycleInputDevicePlayer3", "Translations": { - "ar_SA": "تبديل جهاز الإدخال للاعب 3:", - "de_DE": "Eingabegerät für Spieler 3 wechseln:", - "el_GR": "Εναλλαγή Συσκευής Εισόδου Παίκτη 3:", + "ar_SA": "", + "de_DE": "", + "el_GR": "", "en_US": "Cycle Input Device Player 3:", - "es_ES": "Cambiar dispositivo de entrada jugador 3:", - "fr_FR": "Changer périphérique d'entrée joueur 3 :", - "he_IL": "החלף התקן קלט שחקן 3:", - "it_IT": "Cambia dispositivo di input giocatore 3:", - "ja_JP": "プレイヤー3の入力デバイス切り替え:", - "ko_KR": "플레이어 3 입력 장치 순환:", - "no_NO": "Bytt inndataenhet spiller 3:", - "pl_PL": "Przełącz urządzenie wejściowe gracza 3:", - "pt_BR": "Alternar dispositivo de entrada jogador 3:", - "ru_RU": "Переключить устройство ввода игрока 3:", - "sv_SE": "Växla indataenhet spelare 3:", - "th_TH": "สลับอุปกรณ์ป้อนข้อมูลผู้เล่น 3:", - "tr_TR": "Oyuncu 3 Giriş Cihazını Değiştir:", - "uk_UA": "Перемкнути пристрій введення гравця 3:", - "zh_CN": "切换玩家3输入设备:", - "zh_TW": "切換玩家3輸入裝置:" + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { "ID": "SettingsTabHotkeysCycleInputDevicePlayer4", "Translations": { - "ar_SA": "تبديل جهاز الإدخال للاعب 4:", - "de_DE": "Eingabegerät für Spieler 4 wechseln:", - "el_GR": "Εναλλαγή Συσκευής Εισόδου Παίκτη 4:", + "ar_SA": "", + "de_DE": "", + "el_GR": "", "en_US": "Cycle Input Device Player 4:", - "es_ES": "Cambiar dispositivo de entrada jugador 4:", - "fr_FR": "Changer périphérique d'entrée joueur 4 :", - "he_IL": "החלף התקן קלט שחקן 4:", - "it_IT": "Cambia dispositivo di input giocatore 4:", - "ja_JP": "プレイヤー4の入力デバイス切り替え:", - "ko_KR": "플레이어 4 입력 장치 순환:", - "no_NO": "Bytt inndataenhet spiller 4:", - "pl_PL": "Przełącz urządzenie wejściowe gracza 4:", - "pt_BR": "Alternar dispositivo de entrada jogador 4:", - "ru_RU": "Переключить устройство ввода игрока 4:", - "sv_SE": "Växla indataenhet spelare 4:", - "th_TH": "สลับอุปกรณ์ป้อนข้อมูลผู้เล่น 4:", - "tr_TR": "Oyuncu 4 Giriş Cihazını Değiştir:", - "uk_UA": "Перемкнути пристрій введення гравця 4:", - "zh_CN": "切换玩家4输入设备:", - "zh_TW": "切換玩家4輸入裝置:" + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { From 0afc910f3998fbd6de07013e2e0c06b348dd0316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Sat, 21 Jun 2025 00:59:17 +0300 Subject: [PATCH 09/10] Removed bogus logs --- src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs | 1 - src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs b/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs index 2d5904628..69795d48e 100644 --- a/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs +++ b/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs @@ -68,7 +68,6 @@ namespace Ryujinx.Graphics.Gpu.Overlay foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= OriginalPlayerIndex.Player4)) { - Console.WriteLine($"ControllerOverlay: Config for Player {config.PlayerIndex}: {config.Name} ({config.Backend})"); if (!playerBindings.ContainsKey(config.PlayerIndex)) { playerBindings[config.PlayerIndex] = new List(); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index f1e4a0ae6..cfd8a6835 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1703,16 +1703,6 @@ namespace Ryujinx.Ava.UI.ViewModels ? ConfigurationState.InstanceExtra.Hid.InputConfig.Value : ConfigurationState.Instance.Hid.InputConfig.Value; - // Debug: Log what we're getting - Logger.Info?.Print(LogClass.UI, $"MainWindowViewModel: Found {inputConfigs?.Count ?? 0} input configs"); - if (inputConfigs != null) - { - foreach (var config in inputConfigs) - { - Logger.Info?.Print(LogClass.UI, $" Config: Player {config.PlayerIndex}, Backend {config.Backend}, Name '{config.Name}', Type {config.ControllerType}"); - } - } - // Always show overlay - if no configs, it will show test data int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value; // Show overlay through the GPU context window directly From 72752d2c4b652f2c93c76ba37c494983082cc998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Sat, 21 Jun 2025 01:34:50 +0300 Subject: [PATCH 10/10] Extended hotkeys to player1-8 + h, localized the overlay --- assets/locales.json | 66 +++++++++++++-- .../Configuration/Hid/KeyboardHotkeys.cs | 5 ++ .../Overlay/ControllerOverlay.cs | 80 +++++++++++++------ .../Overlay/ImageElement.cs | 6 +- src/Ryujinx.Graphics.Gpu/Window.cs | 21 +---- src/Ryujinx/Common/KeyboardHotkeyState.cs | 5 ++ src/Ryujinx/Common/Markup/MarkupExtensions.cs | 11 ++- src/Ryujinx/Systems/AppHost.cs | 63 ++++++++++++--- .../Configuration/ConfigurationFileFormat.cs | 2 +- .../ConfigurationState.Migration.cs | 28 +++++++ .../Configuration/ConfigurationState.cs | 11 ++- .../Converters/PlayerHotkeyLabelConverter.cs | 28 +++++++ src/Ryujinx/UI/Models/Input/HotkeyConfig.cs | 22 ++++- .../UI/ViewModels/MainWindowViewModel.cs | 9 +-- .../Views/Settings/SettingsHotkeysView.axaml | 38 ++++++++- .../Settings/SettingsHotkeysView.axaml.cs | 22 ++++- 16 files changed, 338 insertions(+), 79 deletions(-) create mode 100644 src/Ryujinx/UI/Helpers/Converters/PlayerHotkeyLabelConverter.cs diff --git a/assets/locales.json b/assets/locales.json index 27c92ccad..533bd5cc3 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -24373,12 +24373,12 @@ } }, { - "ID": "SettingsTabHotkeysCycleInputDevicePlayer1", + "ID": "SettingsTabHotkeysCycleInputDevicePlayerX", "Translations": { "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Cycle Input Device Player 1:", + "en_US": "Cycle Input Device {0}:", "es_ES": "", "fr_FR": "", "he_IL": "", @@ -24398,12 +24398,12 @@ } }, { - "ID": "SettingsTabHotkeysCycleInputDevicePlayer2", + "ID": "ControllerOverlayTitle", "Translations": { "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Cycle Input Device Player 2:", + "en_US": "Controller Bindings", "es_ES": "", "fr_FR": "", "he_IL": "", @@ -24423,12 +24423,12 @@ } }, { - "ID": "SettingsTabHotkeysCycleInputDevicePlayer3", + "ID": "ControllerOverlayNoController", "Translations": { "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Cycle Input Device Player 3:", + "en_US": "No controller assigned", "es_ES": "", "fr_FR": "", "he_IL": "", @@ -24448,12 +24448,62 @@ } }, { - "ID": "SettingsTabHotkeysCycleInputDevicePlayer4", + "ID": "ControllerOverlayKeyboard", "Translations": { "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Cycle Input Device Player 4:", + "en_US": "Keyboard", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "ControllerOverlayController", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Controller", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "ControllerOverlayUnknown", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Unknown", "es_ES": "", "fr_FR": "", "he_IL": "", diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index f84434ba1..cc6ec55d5 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -19,5 +19,10 @@ namespace Ryujinx.Common.Configuration.Hid public Key CycleInputDevicePlayer2 { get; set; } public Key CycleInputDevicePlayer3 { get; set; } public Key CycleInputDevicePlayer4 { get; set; } + public Key CycleInputDevicePlayer5 { get; set; } + public Key CycleInputDevicePlayer6 { get; set; } + public Key CycleInputDevicePlayer7 { get; set; } + public Key CycleInputDevicePlayer8 { get; set; } + public Key CycleInputDeviceHandheld { get; set; } } } diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs b/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs index 69795d48e..d22389ed5 100644 --- a/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs +++ b/src/Ryujinx.Graphics.Gpu/Overlay/ControllerOverlay.cs @@ -8,6 +8,18 @@ using OriginalInputBackendType = Ryujinx.Common.Configuration.Hid.InputBackendTy namespace Ryujinx.Graphics.Gpu.Overlay { + /// + /// Localization strings for the controller overlay + /// + public class ControllerOverlayLocalization + { + public string TitleText { get; set; } = "Controller Bindings"; + public string NoControllerText { get; set; } = "No controller assigned"; + public string KeyboardText { get; set; } = "Keyboard"; + public string ControllerText { get; set; } = "Controller"; + public string UnknownText { get; set; } = "Unknown"; + } + /// /// Controller overlay that shows controller bindings matching the original AXAML design /// @@ -23,9 +35,11 @@ namespace Ryujinx.Graphics.Gpu.Overlay private const float PlayerTextSize = 22; private float _lifespan = 0f; + private ControllerOverlayLocalization _localization; - public ControllerOverlay() : base("ControllerOverlay") + public ControllerOverlay(ControllerOverlayLocalization localization) : base("ControllerOverlay") { + _localization = localization; CreateBaseElements(); } @@ -42,8 +56,8 @@ namespace Ryujinx.Graphics.Gpu.Overlay }; AddElement(background); - // Title text - var titleText = new TextElement(Padding + 30, Padding, "Controller Bindings", TitleTextSize, SKColors.White) + // Title text (will be updated with localized text) + var titleText = new TextElement(Padding + 30, Padding, _localization.TitleText, TitleTextSize, SKColors.White) { Name = "TitleText", FontStyle = SKFontStyle.Bold @@ -52,21 +66,27 @@ namespace Ryujinx.Graphics.Gpu.Overlay } /// - /// Show controller bindings matching the original AXAML implementation + /// Show controller bindings with localized strings /// public void ShowControllerBindings(List inputConfigs, int durationSeconds = 3) { + // Update title text + var titleElement = FindElement("TitleText"); + if (titleElement != null) + { + titleElement.Text = _localization.TitleText; + } + // Reset lifespan and opacity _lifespan = durationSeconds; // Clear existing player bindings ClearPlayerBindings(); - // Debug: Log input data - // Group controllers by player index + // Group controllers by player index (support all players + handheld) var playerBindings = new Dictionary>(); - foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= OriginalPlayerIndex.Player4)) + foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= OriginalPlayerIndex.Handheld)) { if (!playerBindings.ContainsKey(config.PlayerIndex)) { @@ -77,10 +97,17 @@ namespace Ryujinx.Graphics.Gpu.Overlay float currentY = Padding + 40; // After title - // Add player bindings to UI - for (int i = 0; i < 4; i++) + // Add player bindings to UI (support 8 players + handheld) + var playerIndices = new[] + { + OriginalPlayerIndex.Player1, OriginalPlayerIndex.Player2, OriginalPlayerIndex.Player3, OriginalPlayerIndex.Player4, + OriginalPlayerIndex.Player5, OriginalPlayerIndex.Player6, OriginalPlayerIndex.Player7, OriginalPlayerIndex.Player8, + OriginalPlayerIndex.Handheld + }; + + for (int i = 0; i < playerIndices.Length; i++) { - var playerIndex = (OriginalPlayerIndex)i; + var playerIndex = playerIndices[i]; float rowY = currentY + (i * (PlayerRowHeight + PlayerSpacing)); // Player number with colored background (circular badge) @@ -93,13 +120,14 @@ namespace Ryujinx.Graphics.Gpu.Overlay AddElement(playerBadge); // Player number text - var playerLabel = new TextElement(Padding + 12, rowY + 2, $"P{i + 1}", PlayerTextSize, SKColors.White) + string playerLabel = playerIndex == OriginalPlayerIndex.Handheld ? "H" : $"P{(int)playerIndex + 1}"; + var playerLabelElement = new TextElement(Padding + 12, rowY + 2, playerLabel, PlayerTextSize, SKColors.White) { Name = $"PlayerLabel_{i}", FontStyle = SKFontStyle.Bold, TextAlign = SKTextAlign.Center }; - AddElement(playerLabel); + AddElement(playerLabelElement); // Controller info if (playerBindings.ContainsKey(playerIndex)) @@ -107,37 +135,32 @@ namespace Ryujinx.Graphics.Gpu.Overlay var controllers = playerBindings[playerIndex]; var controllerNames = controllers.Select(c => GetControllerDisplayName(c)).ToList(); - var controllerText = new TextElement(Padding + 56, rowY + 2, string.Join(", ", controllerNames), PlayerTextSize, new SKColor(144, 238, 144)) // LightGreen + var controllerTextElement = new TextElement(Padding + 56, rowY + 2, string.Join(", ", controllerNames), PlayerTextSize, new SKColor(144, 238, 144)) // LightGreen { Name = $"ControllerText_{i}", FontStyle = SKFontStyle.Bold }; - AddElement(controllerText); + AddElement(controllerTextElement); } else { - var noControllerText = new TextElement(Padding + 56, rowY + 2, "No controller assigned", PlayerTextSize, new SKColor(128, 128, 128)) // Gray + var noControllerTextElement = new TextElement(Padding + 56, rowY + 2, _localization.NoControllerText, PlayerTextSize, new SKColor(128, 128, 128)) // Gray { Name = $"NoControllerText_{i}", FontStyle = SKFontStyle.Italic }; - AddElement(noControllerText); + AddElement(noControllerTextElement); } } // Calculate total height and update background - float totalHeight = Padding + 40 + (4 * (PlayerRowHeight + PlayerSpacing)) + Padding + 40; // Extra space for duration text + float totalHeight = Padding + 40 + (playerIndices.Length * (PlayerRowHeight + PlayerSpacing)) + Padding + 20; var background = FindElement("Background"); if (background != null) { background.Height = totalHeight; } - // Duration text at bottom - string durationText = durationSeconds == 1 - ? "This overlay will disappear in 1 second" - : $"This overlay will disappear in {durationSeconds} seconds"; - // Show the overlay (position will be set by Window class with actual dimensions) IsVisible = true; } @@ -150,19 +173,24 @@ namespace Ryujinx.Graphics.Gpu.Overlay 1 => new SKColor(54, 162, 235), // Blue for Player 2 2 => new SKColor(255, 206, 84), // Yellow for Player 3 3 => new SKColor(75, 192, 192), // Green for Player 4 + 4 => new SKColor(153, 102, 255), // Purple for Player 5 + 5 => new SKColor(255, 159, 64), // Orange for Player 6 + 6 => new SKColor(199, 199, 199), // Light Gray for Player 7 + 7 => new SKColor(83, 102, 255), // Indigo for Player 8 + 8 => new SKColor(255, 99, 132), // Pink for Handheld _ => new SKColor(128, 128, 128) // Gray fallback }; } - private static string GetControllerDisplayName(OriginalInputConfig config) + private string GetControllerDisplayName(OriginalInputConfig config) { if (string.IsNullOrEmpty(config.Name)) { return config.Backend switch { - OriginalInputBackendType.WindowKeyboard => "Keyboard", - OriginalInputBackendType.GamepadSDL2 => "Controller", - _ => "Unknown" + OriginalInputBackendType.WindowKeyboard => _localization.KeyboardText, + OriginalInputBackendType.GamepadSDL2 => _localization.ControllerText, + _ => _localization.UnknownText }; } diff --git a/src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs b/src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs index 57122d73d..849a1e5af 100644 --- a/src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs +++ b/src/Ryujinx.Graphics.Gpu/Overlay/ImageElement.cs @@ -8,9 +8,9 @@ namespace Ryujinx.Graphics.Gpu.Overlay /// public class ImageElement : OverlayElement { - private SKBitmap? _bitmap; - private byte[]? _imageData; - private string? _imagePath; + private SKBitmap _bitmap; + private byte[] _imageData; + private string _imagePath; public SKFilterQuality FilterQuality { get; set; } = SKFilterQuality.Medium; public bool MaintainAspectRatio { get; set; } = true; diff --git a/src/Ryujinx.Graphics.Gpu/Window.cs b/src/Ryujinx.Graphics.Gpu/Window.cs index 0c59967a0..e07aa29e2 100644 --- a/src/Ryujinx.Graphics.Gpu/Window.cs +++ b/src/Ryujinx.Graphics.Gpu/Window.cs @@ -107,9 +107,6 @@ namespace Ryujinx.Graphics.Gpu _overlayManager = new OverlayManager(); _frameQueue = new ConcurrentQueue(); - - // Initialize controller overlay - InitializeControllerOverlay(); } /// @@ -258,12 +255,11 @@ namespace Ryujinx.Graphics.Gpu } /// - /// Initialize controller overlay + /// Add overlay to the overlay manager /// - private void InitializeControllerOverlay() + public void AddOverlay(Overlay.Overlay overlay) { - var controllerOverlay = new ControllerOverlay(); - _overlayManager.AddOverlay(controllerOverlay); + _overlayManager.AddOverlay(overlay); } /// @@ -363,17 +359,6 @@ namespace Ryujinx.Graphics.Gpu return false; } - /// - /// Show controller overlay with the provided input configurations - /// - /// List of input configurations to display - /// Duration to show the overlay in seconds - public void ShowControllerBindings(List inputConfigs, int durationSeconds = 3) - { - var controllerOverlay = _overlayManager.FindOverlay("ControllerOverlay") as ControllerOverlay; - controllerOverlay?.ShowControllerBindings(inputConfigs, durationSeconds); - } - /// /// Get the overlay manager for external access /// diff --git a/src/Ryujinx/Common/KeyboardHotkeyState.cs b/src/Ryujinx/Common/KeyboardHotkeyState.cs index f7b7406d2..2df1d5970 100644 --- a/src/Ryujinx/Common/KeyboardHotkeyState.cs +++ b/src/Ryujinx/Common/KeyboardHotkeyState.cs @@ -19,5 +19,10 @@ namespace Ryujinx.Ava.Common CycleInputDevicePlayer2, CycleInputDevicePlayer3, CycleInputDevicePlayer4, + CycleInputDevicePlayer5, + CycleInputDevicePlayer6, + CycleInputDevicePlayer7, + CycleInputDevicePlayer8, + CycleInputDeviceHandheld, } } diff --git a/src/Ryujinx/Common/Markup/MarkupExtensions.cs b/src/Ryujinx/Common/Markup/MarkupExtensions.cs index b2ed01517..07d7b3613 100644 --- a/src/Ryujinx/Common/Markup/MarkupExtensions.cs +++ b/src/Ryujinx/Common/Markup/MarkupExtensions.cs @@ -1,3 +1,4 @@ +using Avalonia.Data.Converters; using Avalonia.Markup.Xaml.MarkupExtensions; using Projektanker.Icons.Avalonia; using Ryujinx.Ava.Common.Locale; @@ -18,11 +19,19 @@ namespace Ryujinx.Ava.Common.Markup internal class LocaleExtension(LocaleKeys key) : BasicMarkupExtension { + public IValueConverter Converter { get; set; } + public override string Name => "Translation"; protected override string Value => LocaleManager.Instance[key]; protected override void ConfigureBindingExtension(CompiledBindingExtension bindingExtension) - => bindingExtension.Source = LocaleManager.Instance; + { + bindingExtension.Source = LocaleManager.Instance; + if (Converter != null) + { + bindingExtension.Converter = Converter; + } + } } internal class WindowTitleExtension(LocaleKeys key, bool includeVersion) : BasicMarkupExtension diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 6c9cec18a..756771de5 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -33,6 +33,7 @@ using Ryujinx.Common.Utilities; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Graphics.Gpu; +using Ryujinx.Graphics.Gpu.Overlay; using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE.FileSystem; @@ -129,6 +130,7 @@ namespace Ryujinx.Ava.Systems private readonly bool _isFirmwareTitle; private readonly Lock _lockObject = new(); + private ControllerOverlay _controllerOverlay; public event EventHandler AppExit; public event EventHandler StatusUpdatedEvent; @@ -932,6 +934,18 @@ namespace Ryujinx.Ava.Systems _viewModel.UiHandler ) ); + + // Initialize controller overlay with localization + var localization = new ControllerOverlayLocalization + { + TitleText = LocaleManager.Instance[LocaleKeys.ControllerOverlayTitle], + NoControllerText = LocaleManager.Instance[LocaleKeys.ControllerOverlayNoController], + KeyboardText = LocaleManager.Instance[LocaleKeys.ControllerOverlayKeyboard], + ControllerText = LocaleManager.Instance[LocaleKeys.ControllerOverlayController], + UnknownText = LocaleManager.Instance[LocaleKeys.ControllerOverlayUnknown] + }; + _controllerOverlay = new ControllerOverlay(localization); + Device.Gpu.Window.AddOverlay(_controllerOverlay); } private static IHardwareDeviceDriver InitializeAudio() @@ -1350,6 +1364,21 @@ namespace Ryujinx.Ava.Systems case KeyboardHotkeyState.CycleInputDevicePlayer4: CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player4); break; + case KeyboardHotkeyState.CycleInputDevicePlayer5: + CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player5); + break; + case KeyboardHotkeyState.CycleInputDevicePlayer6: + CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player6); + break; + case KeyboardHotkeyState.CycleInputDevicePlayer7: + CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player7); + break; + case KeyboardHotkeyState.CycleInputDevicePlayer8: + CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player8); + break; + case KeyboardHotkeyState.CycleInputDeviceHandheld: + CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Handheld); + break; case KeyboardHotkeyState.None: (_keyboardInterface as AvaloniaKeyboard).Clear(); break; @@ -1458,7 +1487,7 @@ namespace Ryujinx.Ava.Systems NpadManager.ReloadConfiguration(currentConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); // Show controller overlay - ShowControllerOverlay(currentConfig); + ShowControllerOverlay(currentConfig, ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value); } private InputConfig CreateDefaultInputConfig((DeviceType Type, string Id, string Name) device, PlayerIndex playerIndex) @@ -1476,21 +1505,17 @@ namespace Ryujinx.Ava.Systems return null; } - - - private void ShowControllerOverlay(List inputConfigs) + public void ShowControllerOverlay(List inputConfigs, int duration) { try { - // Show overlay through the GPU context window directly - if (Device?.Gpu?.Window != null) + if (_controllerOverlay != null) { - int duration = ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value; - Device.Gpu.Window.ShowControllerBindings(inputConfigs, duration); + _controllerOverlay.ShowControllerBindings(inputConfigs, duration); } else { - Logger.Warning?.Print(LogClass.Application, "AppHost: Cannot show overlay - Device.Gpu.Window is null"); + Logger.Warning?.Print(LogClass.Application, "AppHost: Cannot show overlay - ControllerOverlay is null"); } } catch (Exception ex) @@ -1567,6 +1592,26 @@ namespace Ryujinx.Ava.Systems { state = KeyboardHotkeyState.CycleInputDevicePlayer4; } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer5)) + { + state = KeyboardHotkeyState.CycleInputDevicePlayer5; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer6)) + { + state = KeyboardHotkeyState.CycleInputDevicePlayer6; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer7)) + { + state = KeyboardHotkeyState.CycleInputDevicePlayer7; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer8)) + { + state = KeyboardHotkeyState.CycleInputDevicePlayer8; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDeviceHandheld)) + { + state = KeyboardHotkeyState.CycleInputDeviceHandheld; + } return state; } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index 0f45d04d1..eaa92c92a 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 71; + public const int CurrentVersion = 72; /// /// Version of the configuration file format diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 6ad687688..3cf2e04d7 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -486,6 +486,34 @@ namespace Ryujinx.Ava.Systems.Configuration { cff.ControllerOverlayGameStartDuration = 3; cff.ControllerOverlayInputCycleDuration = 2; + }), + (72, static cff => + { + cff.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = cff.Hotkeys.ToggleVSyncMode, + Screenshot = cff.Hotkeys.Screenshot, + ShowUI = cff.Hotkeys.ShowUI, + Pause = cff.Hotkeys.Pause, + ToggleMute = cff.Hotkeys.ToggleMute, + ResScaleUp = cff.Hotkeys.ResScaleUp, + ResScaleDown = cff.Hotkeys.ResScaleDown, + VolumeUp = cff.Hotkeys.VolumeUp, + VolumeDown = cff.Hotkeys.VolumeDown, + CustomVSyncIntervalIncrement = cff.Hotkeys.CustomVSyncIntervalIncrement, + CustomVSyncIntervalDecrement = cff.Hotkeys.CustomVSyncIntervalDecrement, + TurboMode = cff.Hotkeys.TurboMode, + TurboModeWhileHeld = cff.Hotkeys.TurboModeWhileHeld, + CycleInputDevicePlayer1 = Key.Unbound, + CycleInputDevicePlayer2 = Key.Unbound, + CycleInputDevicePlayer3 = Key.Unbound, + CycleInputDevicePlayer4 = Key.Unbound, + CycleInputDevicePlayer5 = Key.Unbound, + CycleInputDevicePlayer6 = Key.Unbound, + CycleInputDevicePlayer7 = Key.Unbound, + CycleInputDevicePlayer8 = Key.Unbound, + CycleInputDeviceHandheld = Key.Unbound + }; }) ); } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs index 71f73bc65..0d4211e1b 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs @@ -273,7 +273,16 @@ namespace Ryujinx.Ava.Systems.Configuration CustomVSyncIntervalIncrement = Key.Unbound, CustomVSyncIntervalDecrement = Key.Unbound, TurboMode = Key.Unbound, - TurboModeWhileHeld = false + TurboModeWhileHeld = false, + CycleInputDevicePlayer1 = Key.Unbound, + CycleInputDevicePlayer2 = Key.Unbound, + CycleInputDevicePlayer3 = Key.Unbound, + CycleInputDevicePlayer4 = Key.Unbound, + CycleInputDevicePlayer5 = Key.Unbound, + CycleInputDevicePlayer6 = Key.Unbound, + CycleInputDevicePlayer7 = Key.Unbound, + CycleInputDevicePlayer8 = Key.Unbound, + CycleInputDeviceHandheld = Key.Unbound }; Hid.RainbowSpeed.Value = 1f; Hid.InputConfig.Value = diff --git a/src/Ryujinx/UI/Helpers/Converters/PlayerHotkeyLabelConverter.cs b/src/Ryujinx/UI/Helpers/Converters/PlayerHotkeyLabelConverter.cs new file mode 100644 index 000000000..9ac7392a6 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/Converters/PlayerHotkeyLabelConverter.cs @@ -0,0 +1,28 @@ +using Avalonia.Data.Converters; +using Ryujinx.Ava.Common.Locale; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class PlayerHotkeyLabelConverter : IValueConverter + { + public static readonly PlayerHotkeyLabelConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is string playerName && !string.IsNullOrEmpty(playerName)) + { + string baseText = LocaleManager.Instance[LocaleKeys.SettingsTabHotkeysCycleInputDevicePlayerX]; + return string.Format(baseText, playerName); + } + + return string.Empty; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs b/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs index 78ab40678..b6de26aa0 100644 --- a/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs +++ b/src/Ryujinx/UI/Models/Input/HotkeyConfig.cs @@ -40,6 +40,16 @@ namespace Ryujinx.Ava.UI.Models.Input [ObservableProperty] private Key _cycleInputDevicePlayer4; + [ObservableProperty] private Key _cycleInputDevicePlayer5; + + [ObservableProperty] private Key _cycleInputDevicePlayer6; + + [ObservableProperty] private Key _cycleInputDevicePlayer7; + + [ObservableProperty] private Key _cycleInputDevicePlayer8; + + [ObservableProperty] private Key _cycleInputDeviceHandheld; + public HotkeyConfig(KeyboardHotkeys config) { if (config == null) @@ -62,6 +72,11 @@ namespace Ryujinx.Ava.UI.Models.Input CycleInputDevicePlayer2 = config.CycleInputDevicePlayer2; CycleInputDevicePlayer3 = config.CycleInputDevicePlayer3; CycleInputDevicePlayer4 = config.CycleInputDevicePlayer4; + CycleInputDevicePlayer5 = config.CycleInputDevicePlayer5; + CycleInputDevicePlayer6 = config.CycleInputDevicePlayer6; + CycleInputDevicePlayer7 = config.CycleInputDevicePlayer7; + CycleInputDevicePlayer8 = config.CycleInputDevicePlayer8; + CycleInputDeviceHandheld = config.CycleInputDeviceHandheld; } public KeyboardHotkeys GetConfig() => @@ -83,7 +98,12 @@ namespace Ryujinx.Ava.UI.Models.Input CycleInputDevicePlayer1 = CycleInputDevicePlayer1, CycleInputDevicePlayer2 = CycleInputDevicePlayer2, CycleInputDevicePlayer3 = CycleInputDevicePlayer3, - CycleInputDevicePlayer4 = CycleInputDevicePlayer4 + CycleInputDevicePlayer4 = CycleInputDevicePlayer4, + CycleInputDevicePlayer5 = CycleInputDevicePlayer5, + CycleInputDevicePlayer6 = CycleInputDevicePlayer6, + CycleInputDevicePlayer7 = CycleInputDevicePlayer7, + CycleInputDevicePlayer8 = CycleInputDevicePlayer8, + CycleInputDeviceHandheld = CycleInputDeviceHandheld }; } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index cfd8a6835..e14b81f4f 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1596,9 +1596,9 @@ namespace Ryujinx.Ava.UI.ViewModels // Code where conditions will be executed after loading user configuration if (ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() != backendThreadingInit) { - Rebooter.RebootAppWithGame(application.Path, + Rebooter.RebootAppWithGame(application.Path, [ - "--bt", + "--bt", ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() ]); @@ -1706,10 +1706,7 @@ namespace Ryujinx.Ava.UI.ViewModels // Always show overlay - if no configs, it will show test data int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value; // Show overlay through the GPU context window directly - if (AppHost?.Device?.Gpu?.Window != null) - { - AppHost.Device.Gpu.Window.ShowControllerBindings(inputConfigs, duration); - } + AppHost.ShowControllerOverlay(inputConfigs, duration); } catch (Exception ex) { diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml index ba82812b9..e120a4ef1 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml @@ -121,29 +121,59 @@ - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs index 7f481f82e..e547f2e70 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs @@ -86,7 +86,12 @@ namespace Ryujinx.Ava.UI.Views.Settings { "CycleInputDevicePlayer1", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer1 = Key.Unbound }, { "CycleInputDevicePlayer2", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer2 = Key.Unbound }, { "CycleInputDevicePlayer3", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer3 = Key.Unbound }, - { "CycleInputDevicePlayer4", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer4 = Key.Unbound } + { "CycleInputDevicePlayer4", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer4 = Key.Unbound }, + { "CycleInputDevicePlayer5", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer5 = Key.Unbound }, + { "CycleInputDevicePlayer6", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer6 = Key.Unbound }, + { "CycleInputDevicePlayer7", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer7 = Key.Unbound }, + { "CycleInputDevicePlayer8", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer8 = Key.Unbound }, + { "CycleInputDeviceHandheld", () => viewModel.KeyboardHotkey.CycleInputDeviceHandheld = Key.Unbound } }; if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action)) @@ -178,6 +183,21 @@ namespace Ryujinx.Ava.UI.Views.Settings case "CycleInputDevicePlayer4": ViewModel.KeyboardHotkey.CycleInputDevicePlayer4 = buttonValue.AsHidType(); break; + case "CycleInputDevicePlayer5": + ViewModel.KeyboardHotkey.CycleInputDevicePlayer5 = buttonValue.AsHidType(); + break; + case "CycleInputDevicePlayer6": + ViewModel.KeyboardHotkey.CycleInputDevicePlayer6 = buttonValue.AsHidType(); + break; + case "CycleInputDevicePlayer7": + ViewModel.KeyboardHotkey.CycleInputDevicePlayer7 = buttonValue.AsHidType(); + break; + case "CycleInputDevicePlayer8": + ViewModel.KeyboardHotkey.CycleInputDevicePlayer8 = buttonValue.AsHidType(); + break; + case "CycleInputDeviceHandheld": + ViewModel.KeyboardHotkey.CycleInputDeviceHandheld = buttonValue.AsHidType(); + break; } }); }