From fb7c1fde11d33e0884bee192d4b95083181d500c Mon Sep 17 00:00:00 2001 From: Babib3l Date: Tue, 26 May 2026 17:54:55 +0000 Subject: [PATCH] Input: Refactor Keyboard Handling To Use Physical Keys (#13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR refactors keyboard handling to use physical key mappings for all gameplay input, ensuring controls remain consistent across different OS keyboard layouts. I'd like to give out an ENORMOUS thank you to @Neo for his very generous help on getting MacOS caps lock behaviour working, but as well for taking the time for extensive testing, planning and discussions, and finally for writing this PR message :) Keep being awsome pal 👊 ### New Features : * **New**: Gameplay input now uses physical key positions instead of OS layouts, ensuring the same physical key triggers the same action across keyboard layouts. * Key rebinding stores physical keys and config compatibility is preserved, with physical keys now the primary gameplay‑binding format. * Physical‑key model is now consistent across platforms, including updated SDL/headless behavior. * **Added**: New Input setting "Reset keybinds to default", with a new confirmation dialog appears when changes are being overwritten. * **Fractured**: Keyboard‑related locales to the newly created `KeyboardLayout.json`. * New input device settings/actions use clearer labels and tooltips. * UI Key Labels (such as Left Shift and Right Shift) are more accurate and standardized, with clearer symbols, consistent naming, dynamic learning of printable labels from real key events, and persistence across restarts. ### Improvements : * **Reduced**: Incorrect key labels by using observed host symbols instead of language assumptions. * **Reduced**: Stuck/stale keys by using binary pressed‑key tracking, fixing rebinding/gameplay paths, better held‑key recovery after focus changes, and clearing keyboard state when Ryujinx/settings windows lose focus. * **Improved**: Device handling → refreshing no longer clears the selector, disconnect fallback is consistent, reconnect restores controllers automatically, and the UI avoids invalid/empty device states. * **Improved**: Async input‑assignment callbacks are now guarded when switching views/devices, preventing stale callbacks from hitting detached views. * **Adjusted**: Input visualiser to be more robust when switching sources or handling controller disconnect/reconnect, without needing to reopen settings. * **Improved**: Modification (changes to input controls) tracking * Rebinding to the same value, reverting to original config, restoring defaults without differences, or reloading equivalent profiles no longer leaves Player marked as modified. * **Reduced**: Keyboard LED noise in logs and added optional UI keyboard‑state/rebinding diagnostics. ### Fixes : * **Special Keys**: * AltGr and other special keys behave correctly, including proper Ctrl+Alt → AltRight handling and more consistent normalization of special/synthetic keys. * Caps Lock is now reliably bindable on all platforms (Windows/Linux register every press; macOS every other). * **Fixed**: Certain cases where keyboard input broke after pointer interactions ### Current Limitations These are planned on being fixed/improved upon in future PRs: * Hotkeys still use semantic (Key) mappings. * Software keyboard / text input still uses the semantic path * Printable key labels may fall back to defaults until observed from host input. * Full semantic/physical split currently implemented only in the Avalonia driver. Co-authored-by: _Neo_ Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/13 --- .gitignore | 3 + assets/Locales/KeyboardLayout.json | 1904 +++++++++++++++ assets/Locales/Root.json | 2066 +---------------- .../Keyboard/StandardKeyboardInputConfig.cs | 2 +- .../Configuration/Hid/PhysicalKey.cs | 142 ++ src/Ryujinx.Input.SDL3/SDL3Gamepad.cs | 3 + src/Ryujinx.Input.SDL3/SDL3Keyboard.cs | 108 +- src/Ryujinx.Input.SDL3/SDLKeyboardDriver.cs | 2 +- .../Assigner/KeyboardKeyAssigner.cs | 67 +- src/Ryujinx.Input/HLE/NpadController.cs | 5 +- src/Ryujinx.Input/HLE/NpadManager.cs | 101 +- src/Ryujinx.Input/IKeyboard.cs | 18 +- src/Ryujinx.Input/IKeyboardModeDriver.cs | 7 + src/Ryujinx.Input/InputConfigDefaults.cs | 148 ++ .../KeyboardInputMappingHelper.cs | 78 + src/Ryujinx.Input/KeyboardInputMode.cs | 8 + src/Ryujinx.Input/KeyboardStateSnapshot.cs | 4 + src/Ryujinx/Headless/HeadlessRyujinx.Init.cs | 151 +- src/Ryujinx/Input/AvaloniaKeyboard.cs | 120 +- src/Ryujinx/Input/AvaloniaKeyboardDriver.cs | 512 +++- .../Input/AvaloniaKeyboardMappingHelper.cs | 198 +- src/Ryujinx/Systems/AppHost.cs | 5 + .../ConfigurationState.Migration.cs | 66 +- .../Configuration/ConfigurationState.cs | 66 +- .../Applet/AvaloniaDynamicTextInputHandler.cs | 14 +- src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs | 15 + .../Helpers/Converters/KeyValueConverter.cs | 148 +- .../UI/Helpers/InputDeviceNameConverter.cs | 28 + .../UI/Helpers/KeyboardLayoutLocaleHelper.cs | 142 ++ .../UI/Helpers/PhysicalKeyLabelHelper.cs | 234 ++ .../UI/Models/Input/KeyboardInputConfig.cs | 96 +- .../UI/Models/Input/StickVisualizer.cs | 138 +- src/Ryujinx/UI/Renderer/RendererHost.cs | 1 - .../Input/ControllerInputViewModel.cs | 6 +- .../UI/ViewModels/Input/InputViewModel.cs | 850 ++++--- .../Views/Input/ControllerInputView.axaml.cs | 17 +- src/Ryujinx/UI/Views/Input/InputView.axaml | 32 +- src/Ryujinx/UI/Views/Input/InputView.axaml.cs | 76 +- .../UI/Views/Input/KeyboardInputView.axaml.cs | 132 +- .../Settings/SettingsHotkeysView.axaml.cs | 3 +- .../Views/Settings/SettingsInputView.axaml.cs | 50 +- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 5 +- 42 files changed, 4788 insertions(+), 2983 deletions(-) create mode 100644 assets/Locales/KeyboardLayout.json create mode 100644 src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs create mode 100644 src/Ryujinx.Input/IKeyboardModeDriver.cs create mode 100644 src/Ryujinx.Input/InputConfigDefaults.cs create mode 100644 src/Ryujinx.Input/KeyboardInputMappingHelper.cs create mode 100644 src/Ryujinx.Input/KeyboardInputMode.cs create mode 100644 src/Ryujinx/UI/Helpers/InputDeviceNameConverter.cs create mode 100644 src/Ryujinx/UI/Helpers/KeyboardLayoutLocaleHelper.cs create mode 100644 src/Ryujinx/UI/Helpers/PhysicalKeyLabelHelper.cs diff --git a/.gitignore b/.gitignore index 6f887e638..caa75657f 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,9 @@ ipch/ _ReSharper*/ *.[Rr]e[Ss]harper +# .NET +.dotnet-home/ + # TeamCity is a build add-in _TeamCity* diff --git a/assets/Locales/KeyboardLayout.json b/assets/Locales/KeyboardLayout.json new file mode 100644 index 000000000..7a4ffbbca --- /dev/null +++ b/assets/Locales/KeyboardLayout.json @@ -0,0 +1,1904 @@ +{ + "Locales": [ + { + "ID": "KeyUnknown", + "Translations": { + "ar_SA": "مجهول", + "de_DE": "Unbekannt", + "el_GR": "Άγνωστο", + "en_US": "Unknown", + "es_ES": "Desconocido", + "fr_FR": "Inconnu", + "he_IL": "לא ידוע", + "it_IT": "Sconosciuto", + "ja_JP": "不明", + "ko_KR": "알 수 없음", + "no_NO": "Ukjent", + "pl_PL": "Nieznany", + "pt_BR": "Desconhecido", + "ru_RU": "Неизвестно", + "sv_SE": "Okänd", + "th_TH": "ไม่รู้จัก", + "tr_TR": "Bilinmeyen", + "uk_UA": "Невідомо", + "zh_CN": "未知", + "zh_TW": "未知" + } + }, + { + "ID": "KeyShiftLeft", + "Translations": { + "ar_SA": "⇧ يسار", + "de_DE": "⇧ Links", + "el_GR": "⇧ Αριστερό", + "en_US": "⇧ Left", + "es_ES": "⇧ Izquierda", + "fr_FR": "⇧ Gauche", + "he_IL": "⇧ שמאל", + "it_IT": "⇧ Sinistro", + "ja_JP": "⇧左", + "ko_KR": "좌측 ⇧", + "no_NO": "⇧ Venstre", + "pl_PL": "⇧ Lewy", + "pt_BR": "⇧ Esquerdo", + "ru_RU": "Левый ⇧", + "sv_SE": "⇧ Vänster", + "th_TH": "⇧ ซ้าย", + "tr_TR": "⇧ Sol", + "uk_UA": "⇧ Лівий", + "zh_CN": "左侧⇧", + "zh_TW": "左 ⇧" + } + }, + { + "ID": "KeyShiftRight", + "Translations": { + "ar_SA": "⇧ يمين", + "de_DE": "⇧ Rechts", + "el_GR": "⇧ Δεξί", + "en_US": "⇧ Right", + "es_ES": "⇧ Derecha", + "fr_FR": "⇧ Droit", + "he_IL": "⇧ ימין", + "it_IT": "⇧ Destro", + "ja_JP": "⇧右", + "ko_KR": "우측 ⇧", + "no_NO": "⇧ Høyre", + "pl_PL": "⇧ Prawy", + "pt_BR": "⇧ Direito", + "ru_RU": "Правый ⇧", + "sv_SE": "⇧ Höger", + "th_TH": "⇧ ขวา", + "tr_TR": "⇧ Sağ", + "uk_UA": "⇧ Правий", + "zh_CN": "右侧⇧", + "zh_TW": "右 ⇧" + } + }, + { + "ID": "KeyControlLeft", + "Translations": { + "ar_SA": "Ctrl الأيسر", + "de_DE": "Ctrl Links", + "el_GR": "Ctrl Αριστερά", + "en_US": "Ctrl Left", + "es_ES": "Alt Gr", + "fr_FR": "Alt Gr", + "he_IL": "Ctrl שמאל", + "it_IT": "Ctrl Sinistro", + "ja_JP": "左Ctrl", + "ko_KR": "좌측 Ctrl", + "no_NO": "Ctrl Venstre", + "pl_PL": "Ctrl Lewy", + "pt_BR": "Ctrl Esquerdo", + "ru_RU": "Левый Ctrl", + "sv_SE": "Ctrl Vänster", + "th_TH": "Ctrl ซ้าย", + "tr_TR": "Sol Ctrl", + "uk_UA": "Ctrl Лівий", + "zh_CN": "左侧Ctrl", + "zh_TW": "左 Ctrl" + } + }, + { + "ID": "KeyMacControlLeft", + "Translations": { + "ar_SA": "⌃ الأيسر", + "de_DE": "⌃ Links", + "el_GR": "⌃ Αριστερά", + "en_US": "⌃ Left", + "es_ES": "⌃ Izquierdo", + "fr_FR": "⌃ Gauche", + "he_IL": "⌃ שמאל", + "it_IT": "⌃ Sinistro", + "ja_JP": "左⌃", + "ko_KR": "좌측 ⌃", + "no_NO": "⌃ Venstre", + "pl_PL": "⌃ Lewy", + "pt_BR": "⌃ Esquerda", + "ru_RU": "Левый ⌃", + "sv_SE": "^ Vänster", + "th_TH": "^ ซ้าย", + "tr_TR": "⌃ Sol", + "uk_UA": "⌃ Лівий", + "zh_CN": "左侧⌃", + "zh_TW": "左 ⌃" + } + }, + { + "ID": "KeyControlRight", + "Translations": { + "ar_SA": "Ctrl الأيمن", + "de_DE": "Ctrl Rechts", + "el_GR": "Ctrl Δεξιά", + "en_US": "Ctrl Right", + "es_ES": "Ctrl Derecho", + "fr_FR": "Ctrl Droite", + "he_IL": "Ctrl ימין", + "it_IT": "Ctrl Destro", + "ja_JP": "右Ctrl", + "ko_KR": "우측 Ctrl", + "no_NO": "Ctrl Høyre", + "pl_PL": "Ctrl Prawy", + "pt_BR": "Ctrl Direito", + "ru_RU": "Правый Ctrl", + "sv_SE": "Ctrl Höger", + "th_TH": "Ctrl ขวา", + "tr_TR": "Sağ Ctrl", + "uk_UA": "Ctrl Правий", + "zh_CN": "右侧Ctrl", + "zh_TW": "右 Ctrl" + } + }, + { + "ID": "KeyMacControlRight", + "Translations": { + "ar_SA": "⌃ الأيمن", + "de_DE": "⌃ Rechts", + "el_GR": "⌃ Δεξιά", + "en_US": "⌃ Right", + "es_ES": "⌃ Derecho", + "fr_FR": "⌃ Droite", + "he_IL": "⌃ ימין", + "it_IT": "⌃ Destro", + "ja_JP": "右⌃", + "ko_KR": "우측 ⌃", + "no_NO": "⌃ Høyre", + "pl_PL": "⌃ Prawy", + "pt_BR": "⌃ Direito", + "ru_RU": "Правый ⌃", + "sv_SE": "^ Höger", + "th_TH": "⌃ ขวา", + "tr_TR": "⌃ Sağ", + "uk_UA": "⌃ Правий", + "zh_CN": "右侧⌃", + "zh_TW": "右 ⌃" + } + }, + { + "ID": "KeyAltLeft", + "Translations": { + "ar_SA": "Alt الأيسر", + "de_DE": "Alt Links", + "el_GR": "Alt Αριστερά", + "en_US": "Alt Left", + "es_ES": "Alt Izquierdo", + "fr_FR": "Alt Gauche", + "he_IL": "Alt שמאל", + "it_IT": "Alt Sinistro", + "ja_JP": "左Alt", + "ko_KR": "좌측 Alt", + "no_NO": "Alt Venstre", + "pl_PL": "Alt Lewy", + "pt_BR": "Alt Esquerdo", + "ru_RU": "Левый Alt", + "sv_SE": "Alt Vänster", + "th_TH": "Alt ซ้าย", + "tr_TR": "Sol Alt", + "uk_UA": "Alt Лівий", + "zh_CN": "左侧Alt", + "zh_TW": "左 Alt" + } + }, + { + "ID": "KeyMacAltLeft", + "Translations": { + "ar_SA": "⌥ الأيسر", + "de_DE": "⌥ Links", + "el_GR": "⌥ Αριστερά", + "en_US": "⌥ Left", + "es_ES": "⌥ Izquierdo", + "fr_FR": "⌥ Gauche", + "he_IL": "⌥ שמאל", + "it_IT": "⌥ Sinistro", + "ja_JP": "左⌥", + "ko_KR": "좌측 ⌥", + "no_NO": "⌥ Venstre", + "pl_PL": "⌥ Lewy", + "pt_BR": "⌥ Esquerda", + "ru_RU": "Левый ⌥", + "sv_SE": "⌥ Vänster", + "th_TH": "⌥ ซ้าย", + "tr_TR": "⌥ Sol", + "uk_UA": "⌥ Лівий", + "zh_CN": "左侧⌥", + "zh_TW": "左 ⌥" + } + }, + { + "ID": "KeyAltRight", + "Translations": { + "ar_SA": "Alt الأيمن", + "de_DE": "Alt Rechts", + "el_GR": "Alt Δεξιά", + "en_US": "Alt Right", + "es_ES": "Alt Derecho", + "fr_FR": "Alt Droite", + "he_IL": "Alt ימין", + "it_IT": "Alt Destro", + "ja_JP": "右Alt", + "ko_KR": "우측 Alt", + "no_NO": "Alt Høyre", + "pl_PL": "Alt Prawy", + "pt_BR": "Alt Direito", + "ru_RU": "Правый Alt", + "sv_SE": "Alt Höger", + "th_TH": "Alt ขวา", + "tr_TR": "Sağ Alt", + "uk_UA": "Alt Правий", + "zh_CN": "右侧Alt", + "zh_TW": "右 Alt" + } + }, + { + "ID": "KeyMacAltRight", + "Translations": { + "ar_SA": "⌥ الأيمن", + "de_DE": "⌥ Rechts", + "el_GR": "⌥ Δεξιά", + "en_US": "⌥ Right", + "es_ES": "⌥ Derecho", + "fr_FR": "⌥ Droite", + "he_IL": "⌥ ימין", + "it_IT": "⌥ Destro", + "ja_JP": "右⌥", + "ko_KR": "우측 ⌥", + "no_NO": "⌥ Høyre", + "pl_PL": "⌥ Prawy", + "pt_BR": "⌥ Direito", + "ru_RU": "Правый ⌥", + "sv_SE": "⌥ Höger", + "th_TH": "⌥ ขวา", + "tr_TR": "⌥ Sağ", + "uk_UA": "⌥ Правий", + "zh_CN": "右侧⌥", + "zh_TW": "右⌥" + } + }, + { + "ID": "KeyWinLeft", + "Translations": { + "ar_SA": "⊞ الأيسر", + "de_DE": "⊞ Links", + "el_GR": "⊞ Αριστερά", + "en_US": "⊞ Left", + "es_ES": "⊞ Izquierdo", + "fr_FR": "⊞ Gauche", + "he_IL": "⊞ שמאל", + "it_IT": "⊞ Sinistro", + "ja_JP": "左⊞", + "ko_KR": "좌측 ⊞", + "no_NO": "⊞ Venstre", + "pl_PL": "⊞ Lewy", + "pt_BR": "⊞ Esquerdo", + "ru_RU": "Левый ⊞", + "sv_SE": "⊞ Vänster", + "th_TH": "⊞ ซ้าย", + "tr_TR": "⊞ Sol", + "uk_UA": "⊞ Лівий", + "zh_CN": "左侧⊞", + "zh_TW": "左 ⊞" + } + }, + { + "ID": "KeyMacWinLeft", + "Translations": { + "ar_SA": "⌘ الأيسر", + "de_DE": "⌘ Links", + "el_GR": "⌘ Αριστερά", + "en_US": "⌘ Left", + "es_ES": "⌘ Izquierdo", + "fr_FR": "⌘ Gauche", + "he_IL": "⌘ שמאל", + "it_IT": "⌘ Sinistro", + "ja_JP": "左⌘", + "ko_KR": "좌측 ⌘", + "no_NO": "⌘ Venstre", + "pl_PL": "⌘ Lewy", + "pt_BR": "⌘ Esquerdo", + "ru_RU": "Левый ⌘", + "sv_SE": "⌘ Vänster", + "th_TH": "⌘ ซ้าย", + "tr_TR": "⌘ Sol", + "uk_UA": "⌘ Лівий", + "zh_CN": "左侧⌘", + "zh_TW": "左⌘" + } + }, + { + "ID": "KeyWinRight", + "Translations": { + "ar_SA": "⊞ الأيمن", + "de_DE": "⊞ Rechts", + "el_GR": "⊞ Δεξιά", + "en_US": "⊞ Right", + "es_ES": "⊞ Derecho", + "fr_FR": "⊞ Droite", + "he_IL": "⊞ ימין", + "it_IT": "⊞ Destro", + "ja_JP": "右⊞", + "ko_KR": "우측 ⊞", + "no_NO": "⊞ Høyre", + "pl_PL": "⊞ Prawy", + "pt_BR": "⊞ Direito", + "ru_RU": "Правый ⊞", + "sv_SE": "⊞ Höger", + "th_TH": "⊞ ขวา", + "tr_TR": "⊞ Sağ", + "uk_UA": "⊞ Правий", + "zh_CN": "右侧⊞", + "zh_TW": "右⊞" + } + }, + { + "ID": "KeyMacWinRight", + "Translations": { + "ar_SA": "⌘ الأيمن", + "de_DE": "⌘ Rechts", + "el_GR": "⌘ Δεξιά", + "en_US": "⌘ Right", + "es_ES": "⌘ Derecho", + "fr_FR": "⌘ Droite", + "he_IL": "⌘ ימין", + "it_IT": "⌘ Destro", + "ja_JP": "左⌘", + "ko_KR": "우측 ⌘", + "no_NO": "⌘ Høyre", + "pl_PL": "⌘ Lewy", + "pt_BR": "⌘ Direito", + "ru_RU": "Правый ⌘", + "sv_SE": "⌘ Höger", + "th_TH": "⌘ ขวา", + "tr_TR": "⌘ Sol", + "uk_UA": "⌘ Лівий", + "zh_CN": "左侧⌘", + "zh_TW": "左⌘" + } + }, + { + "ID": "KeyUp", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "↑", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyDown", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "↓", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyLeft", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "←", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyRight", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "→", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyEnter", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "⏎", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyEscape", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "Esc", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeySpace", + "Translations": { + "ar_SA": "مسافة", + "de_DE": "Leertaste", + "el_GR": "Διάστημα", + "en_US": "Space", + "es_ES": "Espacio", + "fr_FR": "Espace", + "he_IL": "מקש רווח", + "it_IT": "Spazio", + "ja_JP": "スペース", + "ko_KR": "스페이스", + "no_NO": "Mellomrom", + "pl_PL": "Spacja", + "pt_BR": "Espaço", + "ru_RU": "Пробел", + "sv_SE": "Blanksteg", + "th_TH": "สเปซ", + "tr_TR": "Boşluk", + "uk_UA": "Пробіл", + "zh_CN": "空格键", + "zh_TW": "空白鍵" + } + }, + { + "ID": "KeyTab", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "⇥", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyBackSpace", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "⌫", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyInsert", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "Insert", + "es_ES": null, + "fr_FR": "Inser", + "he_IL": null, + "it_IT": "Ins", + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyDelete", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "⌦", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyPageUp", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "Pg. Up", + "es_ES": "Re Pág", + "fr_FR": "Pg.Suiv", + "he_IL": null, + "it_IT": "Pag. Su", + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyPageDown", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "Pg. Down", + "es_ES": "Av Pág", + "fr_FR": "Pg.Préc", + "he_IL": null, + "it_IT": "Pag. Giù", + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyHome", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "↖", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyEnd", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "↘", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyCapsLock", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "⇪", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyScrollLock", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "Scroll Lock", + "es_ES": "Bloq Despl", + "fr_FR": "Arr Déf", + "he_IL": null, + "it_IT": "Bloc Scorr", + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyPrintScreen", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "PrtSc", + "es_ES": "Impr Pant", + "fr_FR": "Impr Écran", + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyPause", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "Pause", + "es_ES": "Pausa", + "fr_FR": null, + "he_IL": null, + "it_IT": "Pausa", + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumLock", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "Num Lock", + "es_ES": "Bloq Num", + "fr_FR": "Verr Num", + "he_IL": null, + "it_IT": "Bloc Num", + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyClear", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "⌧", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad0", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "0 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad1", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "1 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad2", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "2 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad3", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "3 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad4", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "4 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad5", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "5 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad6", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "6 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad7", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "7 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad8", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "8 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypad9", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "9 (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypadDivide", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "/ (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypadMultiply", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "* (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypadSubtract", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "- (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypadAdd", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "+ (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypadDecimal", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": ". (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyKeypadEnter", + "Translations": { + "ar_SA": null, + "de_DE": null, + "el_GR": null, + "en_US": "⏎ (#)", + "es_ES": null, + "fr_FR": null, + "he_IL": null, + "it_IT": null, + "ja_JP": null, + "ko_KR": null, + "no_NO": null, + "pl_PL": null, + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": null, + "tr_TR": null, + "uk_UA": null, + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber0", + "Translations": { + "ar_SA": "٠", + "de_DE": "", + "el_GR": "", + "en_US": "0", + "es_ES": null, + "fr_FR": "à", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber1", + "Translations": { + "ar_SA": "١", + "de_DE": "", + "el_GR": "", + "en_US": "1", + "es_ES": null, + "fr_FR": "&", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber2", + "Translations": { + "ar_SA": "٢", + "de_DE": "", + "el_GR": "", + "en_US": "2", + "es_ES": null, + "fr_FR": "é", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber3", + "Translations": { + "ar_SA": "٣", + "de_DE": "", + "el_GR": "", + "en_US": "3", + "es_ES": null, + "fr_FR": "\"", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber4", + "Translations": { + "ar_SA": "٤", + "de_DE": "", + "el_GR": "", + "en_US": "4", + "es_ES": null, + "fr_FR": "'", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber5", + "Translations": { + "ar_SA": "٥", + "de_DE": "", + "el_GR": "", + "en_US": "5", + "es_ES": null, + "fr_FR": "(", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber6", + "Translations": { + "ar_SA": "٦", + "de_DE": "", + "el_GR": "", + "en_US": "6", + "es_ES": null, + "fr_FR": "-", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber7", + "Translations": { + "ar_SA": "٧", + "de_DE": "", + "el_GR": "", + "en_US": "7", + "es_ES": null, + "fr_FR": "è", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber8", + "Translations": { + "ar_SA": "٨", + "de_DE": "", + "el_GR": "", + "en_US": "8", + "es_ES": null, + "fr_FR": "_", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyNumber9", + "Translations": { + "ar_SA": "٩", + "de_DE": "", + "el_GR": "", + "en_US": "9", + "es_ES": null, + "fr_FR": "ç", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyTilde", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "~", + "es_ES": "ñ", + "fr_FR": "ù", + "he_IL": "", + "it_IT": "ò", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyGrave", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "`", + "es_ES": "º", + "fr_FR": "<", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyMinus", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "-", + "es_ES": null, + "fr_FR": null, + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyPlus", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "+", + "es_ES": null, + "fr_FR": "=", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyBracketLeft", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "[", + "es_ES": "'", + "fr_FR": ")", + "he_IL": "", + "it_IT": "'", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyBracketRight", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "]", + "es_ES": "¡", + "fr_FR": "^", + "he_IL": "", + "it_IT": "ì", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeySemicolon", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": ";", + "es_ES": "`", + "fr_FR": "$", + "he_IL": "", + "it_IT": "è", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyQuote", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "\"", + "es_ES": "´", + "fr_FR": "²", + "he_IL": "", + "it_IT": "à", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyComma", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": ",", + "es_ES": null, + "fr_FR": null, + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyPeriod", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": ".", + "es_ES": null, + "fr_FR": ";", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeySlash", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "/", + "es_ES": "ç", + "fr_FR": ":", + "he_IL": "", + "it_IT": "ù", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyBackSlash", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "\\", + "es_ES": "<", + "fr_FR": "*", + "he_IL": "", + "it_IT": "<", + "ja_JP": "", + "ko_KR": null, + "no_NO": "", + "pl_PL": "", + "pt_BR": null, + "ru_RU": null, + "sv_SE": null, + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": null, + "zh_TW": null + } + }, + { + "ID": "KeyUnbound", + "Translations": { + "ar_SA": "غير مرتبط", + "de_DE": "Ungebunden", + "el_GR": "Αδιάδεσμο", + "en_US": "Unbound", + "es_ES": "Libre", + "fr_FR": "Libre", + "he_IL": "לא מוקצה", + "it_IT": "Libero", + "ja_JP": "未割り当て", + "ko_KR": "설정 안함", + "no_NO": "Ubundet", + "pl_PL": "Nieprzypisany", + "pt_BR": "Não Atribuído", + "ru_RU": "Не привязано", + "sv_SE": "Obunden", + "th_TH": "ยังไม่กำหนดปุ่ม", + "tr_TR": "Bağlanmamış", + "uk_UA": "Незв’язаний", + "zh_CN": "未分配", + "zh_TW": "未分配" + } + }, + { + "ID": "KeyboardInputMode", + "Translations": { + "ar_SA": "لوحة المفاتيح", + "de_DE": "Tastatur", + "el_GR": "Πληκτρολόγιο", + "en_US": "Keyboard", + "es_ES": "Teclado", + "fr_FR": "Clavier", + "he_IL": "מקלדת", + "it_IT": "Tastiera", + "ja_JP": "キーボード", + "ko_KR": "키보드", + "no_NO": "Tastatur", + "pl_PL": "Klawiatura", + "pt_BR": "Teclado", + "ru_RU": "Клавиатура", + "sv_SE": "Tangentbord", + "th_TH": "คีย์บอร์ด", + "tr_TR": "Klavyeler", + "uk_UA": "Клавіатура", + "zh_CN": "键盘", + "zh_TW": "鍵盤" + } + } + ] +} diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index ccd4368ab..e98a49293 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -1,4 +1,4 @@ -{ +{ "Locales": [ { "ID": "SettingsTabInputDirectMouseAccess", @@ -6403,26 +6403,51 @@ { "ID": "ControllerSettingsRefresh", "Translations": { - "ar_SA": "تحديث", - "de_DE": "Aktualisieren", - "el_GR": "Ανανέωση", - "en_US": "Refresh", - "es_ES": "Actualizar", - "fr_FR": "Actualiser", - "he_IL": "רענון", - "it_IT": "Ricarica", - "ja_JP": "更新", - "ko_KR": "새로 고침", - "no_NO": "Oppdater", - "pl_PL": "Odśwież", - "pt_BR": "Atualizar", - "ru_RU": "Обновить", - "sv_SE": "Uppdatera", - "th_TH": "รีเฟรช", - "tr_TR": "Yenile", - "uk_UA": "Оновити", - "zh_CN": "刷新", - "zh_TW": "重新整理" + "ar_SA": "تحديث أجهزة الإدخال", + "de_DE": "Eingabegeräte aktualisieren", + "el_GR": "Ανανέωση συσκευών εισόδου", + "en_US": "Refresh input devices", + "es_ES": "Actualizar dispositivos de entrada", + "fr_FR": "Actualiser les périphériques d'entrée", + "he_IL": "רענון התקני קלט", + "it_IT": "Aggiorna dispositivi di input", + "ja_JP": "入力デバイスを更新", + "ko_KR": "입력 장치 새로 고침", + "no_NO": "Oppdater inndataenheter", + "pl_PL": "Odśwież urządzenia wejściowe", + "pt_BR": "Atualizar dispositivos de entrada", + "ru_RU": "Обновить устройства ввода", + "sv_SE": "Uppdatera inmatningsenheter", + "th_TH": "รีเฟรชอุปกรณ์ป้อนข้อมูล", + "tr_TR": "Giriş cihazlarını yenile", + "uk_UA": "Оновити пристрої введення", + "zh_CN": "刷新输入设备", + "zh_TW": "重新整理輸入裝置" + } + }, + { + "ID": "ControllerSettingsResetKeybindsToDefault", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Reset keybinds to default", + "es_ES": "Restablecer asignaciones por defecto", + "fr_FR": "Réinitialiser les assignations par défaut", + "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": "" } }, { @@ -8175,1906 +8200,6 @@ "zh_TW": "關閉" } }, - { - "ID": "KeyUnknown", - "Translations": { - "ar_SA": "مجهول", - "de_DE": "Unbekannt", - "el_GR": "", - "en_US": "Unknown", - "es_ES": "Desconocido", - "fr_FR": "Inconnu", - "he_IL": "", - "it_IT": "Sconosciuto", - "ja_JP": "", - "ko_KR": "알 수 없음", - "no_NO": "Ukjent", - "pl_PL": "", - "pt_BR": "Desconhecido", - "ru_RU": "Неизвестно", - "sv_SE": "Okänd", - "th_TH": "ไม่รู้จัก", - "tr_TR": "", - "uk_UA": "Невизначено", - "zh_CN": "未知", - "zh_TW": "未知" - } - }, - { - "ID": "KeyShiftLeft", - "Translations": { - "ar_SA": "زر ‫Shift الأيسر", - "de_DE": "", - "el_GR": "", - "en_US": "Shift Left", - "es_ES": "Mayús Izquierda", - "fr_FR": "Maj Gauche", - "he_IL": "", - "it_IT": "Maiusc sinistro", - "ja_JP": "", - "ko_KR": "좌측 Shift", - "no_NO": "Skift venstre", - "pl_PL": "", - "pt_BR": "Shift Esquerdo", - "ru_RU": "Левый Shift", - "sv_SE": "Skift vänster", - "th_TH": "Shift ซ้าย", - "tr_TR": "Sol Shift", - "uk_UA": "Shift Лівий", - "zh_CN": "左侧Shift", - "zh_TW": "左 Shift" - } - }, - { - "ID": "KeyShiftRight", - "Translations": { - "ar_SA": "زر ‫Shift الأيمن", - "de_DE": "", - "el_GR": "", - "en_US": "Shift Right", - "es_ES": "Mayús Derecha", - "fr_FR": "Maj Droite", - "he_IL": "", - "it_IT": "Maiusc destro", - "ja_JP": "", - "ko_KR": "우측 Shift", - "no_NO": "Skift høyre", - "pl_PL": "", - "pt_BR": "Shift Direito", - "ru_RU": "Правый Shift", - "sv_SE": "Skift höger", - "th_TH": "Shift ขวา", - "tr_TR": "Sağ Shift", - "uk_UA": "Shift Правий", - "zh_CN": "右侧Shift", - "zh_TW": "右 Shift" - } - }, - { - "ID": "KeyControlLeft", - "Translations": { - "ar_SA": "زر ‫Ctrl الأيسر", - "de_DE": "", - "el_GR": "", - "en_US": "Ctrl Left", - "es_ES": "Alt Gr", - "fr_FR": "Alt Gr", - "he_IL": "", - "it_IT": "Ctrl sinistro", - "ja_JP": "", - "ko_KR": "좌측 Ctrl", - "no_NO": "Ctrl venstre", - "pl_PL": "", - "pt_BR": "Ctrl Esquerdo", - "ru_RU": "Левый Ctrl", - "sv_SE": "Ctrl vänster", - "th_TH": "Ctrl ซ้าย", - "tr_TR": "Sol Ctrl", - "uk_UA": "Ctrl Лівий", - "zh_CN": "左侧Ctrl", - "zh_TW": "左 Ctrl" - } - }, - { - "ID": "KeyMacControlLeft", - "Translations": { - "ar_SA": "زر ⌃ الأيسر", - "de_DE": "", - "el_GR": "", - "en_US": "⌃ Left", - "es_ES": "⌃ Izquierdo", - "fr_FR": "⌃ Gauche", - "he_IL": "", - "it_IT": "⌃ sinistro", - "ja_JP": "", - "ko_KR": "좌측 ⌃", - "no_NO": "⌃ Venstre", - "pl_PL": "", - "pt_BR": "⌃ Esquerda", - "ru_RU": "Левый ⌃", - "sv_SE": "^ Vänster", - "th_TH": "^ ซ้าย", - "tr_TR": "⌃ Sol", - "uk_UA": "⌃ Лівий", - "zh_CN": "左侧⌃", - "zh_TW": "左 ⌃" - } - }, - { - "ID": "KeyControlRight", - "Translations": { - "ar_SA": "زر ‫Ctrl الأيمن", - "de_DE": "", - "el_GR": "", - "en_US": "Ctrl Right", - "es_ES": "Ctrl Derecho", - "fr_FR": "Ctrl Droite", - "he_IL": "", - "it_IT": "Ctrl destro", - "ja_JP": "", - "ko_KR": "우측 Ctrl", - "no_NO": "Ctrl høyre", - "pl_PL": "", - "pt_BR": "Ctrl Direito", - "ru_RU": "Правый Ctrl", - "sv_SE": "Ctrl höger", - "th_TH": "Ctrl ขวา", - "tr_TR": "Sağ Control", - "uk_UA": "Ctrl Правий", - "zh_CN": "右侧Ctrl", - "zh_TW": "右 Ctrl" - } - }, - { - "ID": "KeyMacControlRight", - "Translations": { - "ar_SA": "زر ⌃ الأيمن", - "de_DE": "", - "el_GR": "", - "en_US": "⌃ Right", - "es_ES": "⌃ Derecho", - "fr_FR": "⌃ Droite", - "he_IL": "", - "it_IT": "⌃ destro", - "ja_JP": "", - "ko_KR": "우측 ⌃", - "no_NO": "⌃ Høyre", - "pl_PL": "", - "pt_BR": "⌃ Direito", - "ru_RU": "Правый ⌃", - "sv_SE": "^ Höger", - "th_TH": "⌃ ขวา", - "tr_TR": "⌃ Sağ", - "uk_UA": "⌃ Правий", - "zh_CN": "右侧⌃", - "zh_TW": "右 ⌃" - } - }, - { - "ID": "KeyAltLeft", - "Translations": { - "ar_SA": "زر ‫Alt الأيسر", - "de_DE": "", - "el_GR": "", - "en_US": "Alt Left", - "es_ES": "Alt Izquierdo", - "fr_FR": "Alt Gauche", - "he_IL": "", - "it_IT": "Alt sinistro", - "ja_JP": "", - "ko_KR": "좌측 Alt", - "no_NO": "Alt Venstre", - "pl_PL": "", - "pt_BR": "Alt Esquerdo", - "ru_RU": "Левый Alt", - "sv_SE": "Alt vänster", - "th_TH": "Alt ซ้าย", - "tr_TR": "Sol Alt", - "uk_UA": "Alt Лівий", - "zh_CN": "左侧Alt", - "zh_TW": "左 Alt" - } - }, - { - "ID": "KeyMacAltLeft", - "Translations": { - "ar_SA": "زر ⌥ الأيسر", - "de_DE": "", - "el_GR": "", - "en_US": "⌥ Left", - "es_ES": "⌥ Izquierdo", - "fr_FR": "⌥ Gauche", - "he_IL": "", - "it_IT": "⌥ sinistro", - "ja_JP": "", - "ko_KR": "좌측 ⌥", - "no_NO": "⌥ Venstre", - "pl_PL": "", - "pt_BR": "⌥ Esquerda", - "ru_RU": "Левый ⌥", - "sv_SE": "⌥ vänster", - "th_TH": "⌥ ซ้าย", - "tr_TR": "⌥ Sol", - "uk_UA": "⌥ Лівий", - "zh_CN": "左侧⌥", - "zh_TW": "左 ⌥" - } - }, - { - "ID": "KeyAltRight", - "Translations": { - "ar_SA": "زر ‫Alt الأيمن", - "de_DE": "", - "el_GR": "", - "en_US": "Alt Right", - "es_ES": "Alt Derecho", - "fr_FR": "Alt Droite", - "he_IL": "", - "it_IT": "Alt destro", - "ja_JP": "", - "ko_KR": "우측 Alt", - "no_NO": "Alt høyre", - "pl_PL": "", - "pt_BR": "Alt Direito", - "ru_RU": "Правый Alt", - "sv_SE": "Alt höger", - "th_TH": "Alt ขวา", - "tr_TR": "Sağ Alt", - "uk_UA": "Alt Правий", - "zh_CN": "右侧Alt", - "zh_TW": "右 Alt" - } - }, - { - "ID": "KeyMacAltRight", - "Translations": { - "ar_SA": "زر ⌥ الأيمن", - "de_DE": "", - "el_GR": "", - "en_US": "⌥ Right", - "es_ES": "⌥ Derecho", - "fr_FR": "⌥ Droite", - "he_IL": "", - "it_IT": "⌥ destro", - "ja_JP": "", - "ko_KR": "우측 ⌥", - "no_NO": "⌥ Høyre", - "pl_PL": "", - "pt_BR": "⌥ Direito", - "ru_RU": "Правый ⌥", - "sv_SE": "⌥ höger", - "th_TH": "⌥ ขวา", - "tr_TR": "⌥ Sağ", - "uk_UA": "⌥ Правий", - "zh_CN": "右侧⌥", - "zh_TW": "右 ⌥" - } - }, - { - "ID": "KeyWinLeft", - "Translations": { - "ar_SA": "زر ⊞ الأيسر", - "de_DE": "", - "el_GR": "", - "en_US": "⊞ Left", - "es_ES": "⊞ Izquierdo", - "fr_FR": "⊞ Gauche", - "he_IL": "", - "it_IT": "⊞ sinistro", - "ja_JP": "", - "ko_KR": "좌측 ⊞", - "no_NO": "⊞ Venstre", - "pl_PL": "", - "pt_BR": "⊞ Esquerdo", - "ru_RU": "Левый ⊞", - "sv_SE": "⊞ vänster", - "th_TH": "⊞ ซ้าย", - "tr_TR": "⊞ Sol", - "uk_UA": "⊞ Лівий", - "zh_CN": "左侧⊞", - "zh_TW": "左 ⊞" - } - }, - { - "ID": "KeyMacWinLeft", - "Translations": { - "ar_SA": "زر ⌘ الأيسر", - "de_DE": "", - "el_GR": "", - "en_US": "⌘ Left", - "es_ES": "⌘ Izqierdo", - "fr_FR": "⌘ Gauche", - "he_IL": "", - "it_IT": "⌘ sinistro", - "ja_JP": "", - "ko_KR": "좌측 ⌘", - "no_NO": "⌘ Venstre", - "pl_PL": "", - "pt_BR": "⌘ Esquerdo", - "ru_RU": "Левый ⌘", - "sv_SE": "⌘ vänster", - "th_TH": "⌘ ซ้าย", - "tr_TR": "⌘ Sol", - "uk_UA": "⌘ Лівий", - "zh_CN": "左侧⌘", - "zh_TW": "左 ⌘" - } - }, - { - "ID": "KeyWinRight", - "Translations": { - "ar_SA": "زر ⊞ الأيمن", - "de_DE": "", - "el_GR": "", - "en_US": "⊞ Right", - "es_ES": "⊞ Derecho", - "fr_FR": "⊞ Droite", - "he_IL": "", - "it_IT": "⊞ destro", - "ja_JP": "", - "ko_KR": "우측 ⊞", - "no_NO": "⊞ Høyre", - "pl_PL": "", - "pt_BR": "⊞ Direito", - "ru_RU": "Правый ⊞", - "sv_SE": "⊞ höger", - "th_TH": "⊞ ขวา", - "tr_TR": "⊞ Sağ", - "uk_UA": "⊞ Правий", - "zh_CN": "右侧⊞", - "zh_TW": "右 ⊞" - } - }, - { - "ID": "KeyMacWinRight", - "Translations": { - "ar_SA": "زر ⌘ الأيمن", - "de_DE": "", - "el_GR": "", - "en_US": "⌘ Right", - "es_ES": "⌘ Derecho", - "fr_FR": "⌘ Droite", - "he_IL": "", - "it_IT": "⌘ destro", - "ja_JP": "", - "ko_KR": "우측 ⌘", - "no_NO": "⌘ Høyre", - "pl_PL": "", - "pt_BR": "⌘ Direito", - "ru_RU": "Правый ⌘", - "sv_SE": "⌘ höger", - "th_TH": "⌘ ขวา", - "tr_TR": "⌘ Sağ", - "uk_UA": "⌘ Правий", - "zh_CN": "右侧⌘", - "zh_TW": "右 ⌘" - } - }, - { - "ID": "KeyMenu", - "Translations": { - "ar_SA": "زر القائمة", - "de_DE": "", - "el_GR": "", - "en_US": "Menu", - "es_ES": null, - "fr_FR": null, - "he_IL": "", - "it_IT": "Menù", - "ja_JP": "", - "ko_KR": "메뉴", - "no_NO": "Meny", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Меню", - "sv_SE": "Meny", - "th_TH": "เมนู", - "tr_TR": "Menü", - "uk_UA": "Меню", - "zh_CN": "菜单键", - "zh_TW": "功能表鍵" - } - }, - { - "ID": "KeyUp", - "Translations": { - "ar_SA": "فوق", - "de_DE": "", - "el_GR": "", - "en_US": "Up", - "es_ES": "Arriba", - "fr_FR": "Haut", - "he_IL": "", - "it_IT": "Su", - "ja_JP": "", - "ko_KR": "↑", - "no_NO": "Opp", - "pl_PL": "", - "pt_BR": "Cima", - "ru_RU": "Вверх", - "sv_SE": "Upp", - "th_TH": "ขึ้น", - "tr_TR": "Yukarı", - "uk_UA": "Вгору ↑", - "zh_CN": "上", - "zh_TW": "上" - } - }, - { - "ID": "KeyDown", - "Translations": { - "ar_SA": "اسفل", - "de_DE": "", - "el_GR": "", - "en_US": "Down", - "es_ES": "Abajo", - "fr_FR": "Bas", - "he_IL": "", - "it_IT": "Giù", - "ja_JP": "", - "ko_KR": "↓", - "no_NO": "Ned", - "pl_PL": "", - "pt_BR": "Baixo", - "ru_RU": "Вниз", - "sv_SE": "Ner", - "th_TH": "ลง", - "tr_TR": "Aşağı", - "uk_UA": "Вниз ↓", - "zh_CN": "下", - "zh_TW": "下" - } - }, - { - "ID": "KeyLeft", - "Translations": { - "ar_SA": "يسار", - "de_DE": "", - "el_GR": "", - "en_US": "Left", - "es_ES": "Izquierda", - "fr_FR": "Gauche", - "he_IL": "", - "it_IT": "Sinistra", - "ja_JP": "", - "ko_KR": "←", - "no_NO": "Venstre", - "pl_PL": "", - "pt_BR": "Esquerda", - "ru_RU": "Влево", - "sv_SE": "Vänster", - "th_TH": "ซ้าย", - "tr_TR": "Sol", - "uk_UA": "Вліво ←", - "zh_CN": "左", - "zh_TW": "左" - } - }, - { - "ID": "KeyRight", - "Translations": { - "ar_SA": "يمين", - "de_DE": "", - "el_GR": "", - "en_US": "Right", - "es_ES": "Derecha", - "fr_FR": "Droite", - "he_IL": "", - "it_IT": "Destra", - "ja_JP": "", - "ko_KR": "→", - "no_NO": "Høyre", - "pl_PL": "", - "pt_BR": "Direita", - "ru_RU": "Вправо", - "sv_SE": "Höger", - "th_TH": "ขวา", - "tr_TR": "Sağ", - "uk_UA": "Вправо →", - "zh_CN": "右", - "zh_TW": "右" - } - }, - { - "ID": "KeyEnter", - "Translations": { - "ar_SA": "مفتاح الإدخال", - "de_DE": "", - "el_GR": "", - "en_US": "Enter", - "es_ES": "Intro", - "fr_FR": "Entrée", - "he_IL": "", - "it_IT": "Invio", - "ja_JP": "", - "ko_KR": "엔터", - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Enter", - "tr_TR": "", - "uk_UA": "", - "zh_CN": "回车键", - "zh_TW": "Enter 鍵" - } - }, - { - "ID": "KeyEscape", - "Translations": { - "ar_SA": "زر ‫Escape", - "de_DE": "", - "el_GR": "", - "en_US": "Escape", - "es_ES": "Esc", - "fr_FR": "Esc", - "he_IL": "", - "it_IT": "Esc", - "ja_JP": "", - "ko_KR": "Esc", - "no_NO": "Esc-tast", - "pl_PL": "", - "pt_BR": "Esc", - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Escape", - "tr_TR": "Esc", - "uk_UA": "Esc", - "zh_CN": "Esc", - "zh_TW": "Esc 鍵" - } - }, - { - "ID": "KeySpace", - "Translations": { - "ar_SA": "مسافة", - "de_DE": "", - "el_GR": "", - "en_US": "Space", - "es_ES": "Espacio", - "fr_FR": "Espace", - "he_IL": "", - "it_IT": "Spazio", - "ja_JP": "", - "ko_KR": "스페이스", - "no_NO": "Mellomrom", - "pl_PL": "", - "pt_BR": "Espaço", - "ru_RU": "Пробел", - "sv_SE": "Blanksteg", - "th_TH": "ปุ่ม Spacebar", - "tr_TR": "", - "uk_UA": "Пробіл", - "zh_CN": "空格键", - "zh_TW": "空白鍵" - } - }, - { - "ID": "KeyTab", - "Translations": { - "ar_SA": "زر ‫Tab", - "de_DE": "", - "el_GR": "", - "en_US": "Tab", - "es_ES": null, - "fr_FR": null, - "he_IL": "", - "it_IT": null, - "ja_JP": "", - "ko_KR": "탭", - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Tab", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "Tab 鍵" - } - }, - { - "ID": "KeyBackSpace", - "Translations": { - "ar_SA": "زر المسح للخلف", - "de_DE": "", - "el_GR": "", - "en_US": "Backspace", - "es_ES": "Retroceso", - "fr_FR": "Retour arrière", - "he_IL": "", - "it_IT": "Canc", - "ja_JP": "", - "ko_KR": "백스페이스", - "no_NO": "Tilbaketast", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": "Backsteg", - "th_TH": "ปุ่ม Backspace", - "tr_TR": "Geri tuşu", - "uk_UA": "", - "zh_CN": "退格键", - "zh_TW": "Backspace 鍵" - } - }, - { - "ID": "KeyInsert", - "Translations": { - "ar_SA": "زر Insert", - "de_DE": "", - "el_GR": "", - "en_US": "Insert", - "es_ES": null, - "fr_FR": "Inser", - "he_IL": "", - "it_IT": "Ins", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Sett inn", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Insert", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "Insert 鍵" - } - }, - { - "ID": "KeyDelete", - "Translations": { - "ar_SA": "زر الحذف", - "de_DE": "", - "el_GR": "", - "en_US": "Delete", - "es_ES": "Supr", - "fr_FR": "Suppr", - "he_IL": "", - "it_IT": "Canc", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Slett", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Delete", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "Delete 鍵" - } - }, - { - "ID": "KeyPageUp", - "Translations": { - "ar_SA": "زر ‫Page Up", - "de_DE": "", - "el_GR": "", - "en_US": "Page Up", - "es_ES": "Re Pág", - "fr_FR": "Pg.Suiv", - "he_IL": "", - "it_IT": "Pag. Su", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Side opp", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Page Up", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "向上捲頁鍵" - } - }, - { - "ID": "KeyPageDown", - "Translations": { - "ar_SA": "زر ‫Page Down", - "de_DE": "", - "el_GR": "", - "en_US": "Page Down", - "es_ES": "Av Pág", - "fr_FR": "Pg.Préc", - "he_IL": "", - "it_IT": "Pag. Giù", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Side ned", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Page Down", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "向下捲頁鍵" - } - }, - { - "ID": "KeyHome", - "Translations": { - "ar_SA": "زر ‫Home", - "de_DE": "", - "el_GR": "", - "en_US": "Home", - "es_ES": "Inicio", - "fr_FR": null, - "he_IL": "", - "it_IT": "Inizio", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Hjem", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Home", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "Home 鍵" - } - }, - { - "ID": "KeyEnd", - "Translations": { - "ar_SA": "زر ‫End", - "de_DE": "", - "el_GR": "", - "en_US": "End", - "es_ES": "Fin", - "fr_FR": "Fin", - "he_IL": "", - "it_IT": "Fine", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Avslutt", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม End", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "End 鍵" - } - }, - { - "ID": "KeyCapsLock", - "Translations": { - "ar_SA": "زر الحروف الكبيرة", - "de_DE": "", - "el_GR": "", - "en_US": "Caps Lock", - "es_ES": "Bloq Mayús", - "fr_FR": "Verr. Maj", - "he_IL": "", - "it_IT": "Bloc Maiusc", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Skiftelås", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Caps Lock", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "Caps Lock 鍵" - } - }, - { - "ID": "KeyScrollLock", - "Translations": { - "ar_SA": "زر ‫Scroll Lock", - "de_DE": "", - "el_GR": "", - "en_US": "Scroll Lock", - "es_ES": "Bloq Despl", - "fr_FR": "Arr. Déf.", - "he_IL": "", - "it_IT": "Bloc Scorr", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Rullelås", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Scroll Lock", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "Scroll Lock 鍵" - } - }, - { - "ID": "KeyPrintScreen", - "Translations": { - "ar_SA": "زر ‫Print Screen", - "de_DE": "", - "el_GR": "", - "en_US": "Print Screen", - "es_ES": "Impr Pant", - "fr_FR": "Impr Écran", - "he_IL": "", - "it_IT": "Stamp", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Skjermbilde", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Print Screen", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "Print Screen 鍵" - } - }, - { - "ID": "KeyPause", - "Translations": { - "ar_SA": "زر Pause", - "de_DE": "", - "el_GR": "", - "en_US": "Pause", - "es_ES": "Pausa", - "fr_FR": null, - "he_IL": "", - "it_IT": "Pausa", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Stans midlertidig", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Pause", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "Pause 鍵" - } - }, - { - "ID": "KeyNumLock", - "Translations": { - "ar_SA": "زر Num Lock", - "de_DE": "", - "el_GR": "", - "en_US": "Num Lock", - "es_ES": "Bloq Num", - "fr_FR": "Verr. Num", - "he_IL": "", - "it_IT": "Bloc Num", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Numerisk Lås", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "ปุ่ม Num Lock", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": "Num Lock 鍵" - } - }, - { - "ID": "KeyClear", - "Translations": { - "ar_SA": "زر Clear", - "de_DE": "", - "el_GR": "", - "en_US": "Clear", - "es_ES": "Borrar", - "fr_FR": "Effacer", - "he_IL": "", - "it_IT": "Cancella", - "ja_JP": "", - "ko_KR": null, - "no_NO": "Tøm", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": "Töm", - "th_TH": "ล้าง", - "tr_TR": "", - "uk_UA": "Очистити", - "zh_CN": "清除键", - "zh_TW": "清除" - } - }, - { - "ID": "KeyKeypad0", - "Translations": { - "ar_SA": "لوحة الأرقام 0", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 0", - "es_ES": "Num. 0", - "fr_FR": "Num. 0", - "he_IL": "", - "it_IT": "Tast. num. 0", - "ja_JP": "", - "ko_KR": "키패드 0", - "no_NO": "Numerisk 0", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 0", - "sv_SE": "Numerisk 0", - "th_TH": "ปุ่ม 0 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "Блок цифр 0", - "zh_CN": "小键盘0", - "zh_TW": "數字鍵 0" - } - }, - { - "ID": "KeyKeypad1", - "Translations": { - "ar_SA": "لوحة الأرقام 1", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 1", - "es_ES": "Num. 1", - "fr_FR": "Num. 1", - "he_IL": "", - "it_IT": "Tast. num. 1", - "ja_JP": "", - "ko_KR": "키패드 1", - "no_NO": "Numerisk 1", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 1", - "sv_SE": "Numerisk 1", - "th_TH": "ปุ่ม 1 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "Блок цифр 1", - "zh_CN": "小键盘1", - "zh_TW": "數字鍵 1" - } - }, - { - "ID": "KeyKeypad2", - "Translations": { - "ar_SA": "لوحة الأرقام 2", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 2", - "es_ES": "Num. 2", - "fr_FR": "Num. 2", - "he_IL": "", - "it_IT": "Tast. num. 2", - "ja_JP": "", - "ko_KR": "키패드 2", - "no_NO": "Numerisk 2", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 2", - "sv_SE": "Numerisk 2", - "th_TH": "ปุ่ม 2 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "2 (цифровий блок)", - "zh_CN": "小键盘2", - "zh_TW": "數字鍵 2" - } - }, - { - "ID": "KeyKeypad3", - "Translations": { - "ar_SA": "لوحة الأرقام 3", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 3", - "es_ES": "Num. 3", - "fr_FR": "Num. 3", - "he_IL": "", - "it_IT": "Tast. num. 3", - "ja_JP": "", - "ko_KR": "키패드 3", - "no_NO": "Numerisk 3", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 3", - "sv_SE": "Numerisk 3", - "th_TH": "ปุ่ม 3 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "3 (цифровий блок)", - "zh_CN": "小键盘3", - "zh_TW": "數字鍵 3" - } - }, - { - "ID": "KeyKeypad4", - "Translations": { - "ar_SA": "لوحة الأرقام 4", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 4", - "es_ES": "Num. 4", - "fr_FR": "Num. 4", - "he_IL": "", - "it_IT": "Tast. num. 4", - "ja_JP": "", - "ko_KR": "키패드 4", - "no_NO": "Numerisk 4", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 4", - "sv_SE": "Numerisk 4", - "th_TH": "ปุ่ม 4 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "4 (цифровий блок)", - "zh_CN": "小键盘4", - "zh_TW": "數字鍵 4" - } - }, - { - "ID": "KeyKeypad5", - "Translations": { - "ar_SA": "لوحة الأرقام 5", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 5", - "es_ES": "Num. 5", - "fr_FR": "Num. 5", - "he_IL": "", - "it_IT": "Tast. num. 5", - "ja_JP": "", - "ko_KR": "키패드 5", - "no_NO": "Numerisk 5", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 5", - "sv_SE": "Numerisk 5", - "th_TH": "ปุ่ม 5 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "5 (цифровий блок)", - "zh_CN": "小键盘5", - "zh_TW": "數字鍵 5" - } - }, - { - "ID": "KeyKeypad6", - "Translations": { - "ar_SA": "لوحة الأرقام 6", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 6", - "es_ES": "Num. 6", - "fr_FR": "Num. 6", - "he_IL": "", - "it_IT": "Tast. num. 6", - "ja_JP": "", - "ko_KR": "키패드 6", - "no_NO": "Numerisk 6", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 6", - "sv_SE": "Numerisk 6", - "th_TH": "ปุ่ม 6 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "6 (цифровий блок)", - "zh_CN": "小键盘6", - "zh_TW": "數字鍵 6" - } - }, - { - "ID": "KeyKeypad7", - "Translations": { - "ar_SA": "لوحة الأرقام 7", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 7", - "es_ES": "Num. 7", - "fr_FR": "Num. 7", - "he_IL": "", - "it_IT": "Tast. num. 7", - "ja_JP": "", - "ko_KR": "키패드 7", - "no_NO": "Numerisk 7", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 7", - "sv_SE": "Numerisk 7", - "th_TH": "ปุ่ม 7 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "7 (цифровий блок)", - "zh_CN": "小键盘7", - "zh_TW": "數字鍵 7" - } - }, - { - "ID": "KeyKeypad8", - "Translations": { - "ar_SA": "لوحة الأرقام 8", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 8", - "es_ES": "Num. 8", - "fr_FR": "Num. 8", - "he_IL": "", - "it_IT": "Tast. num. 8", - "ja_JP": "", - "ko_KR": "키패드 8", - "no_NO": "Numerisk 8", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 8", - "sv_SE": "Numerisk 8", - "th_TH": "ปุ่ม 8 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "8 (цифровий блок)", - "zh_CN": "小键盘8", - "zh_TW": "數字鍵 8" - } - }, - { - "ID": "KeyKeypad9", - "Translations": { - "ar_SA": "لوحة الأرقام 9", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad 9", - "es_ES": "Num. 9", - "fr_FR": "Num. 9", - "he_IL": "", - "it_IT": "Tast. num. 9", - "ja_JP": "", - "ko_KR": "키패드 9", - "no_NO": "Numerisk 9", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Блок цифр 9", - "sv_SE": "Numerisk 9", - "th_TH": "ปุ่ม 9 บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "9 (цифровий блок)", - "zh_CN": "小键盘9", - "zh_TW": "數字鍵 9" - } - }, - { - "ID": "KeyKeypadDivide", - "Translations": { - "ar_SA": "لوحة الأرقام علامة القسمة", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad Divide", - "es_ES": "Num. /", - "fr_FR": "Num. Diviser", - "he_IL": "", - "it_IT": "Tast. num. /", - "ja_JP": "", - "ko_KR": "키패드 분할", - "no_NO": "Numerisk Dele", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "/ (блок цифр)", - "sv_SE": "Keypad /", - "th_TH": "ปุ่ม / บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "/ (цифровий блок)", - "zh_CN": "小键盘/", - "zh_TW": "數字鍵除號" - } - }, - { - "ID": "KeyKeypadMultiply", - "Translations": { - "ar_SA": "لوحة الأرقام علامة الضرب", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad Multiply", - "es_ES": "Num. *", - "fr_FR": "Num. Multiplier", - "he_IL": "", - "it_IT": "Tast. num. *", - "ja_JP": "", - "ko_KR": "키패드 멀티플", - "no_NO": "Numerisk Multiplisere", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "* (блок цифр)", - "sv_SE": "Keypad *", - "th_TH": "ปุ่ม * บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "* (цифровий блок)", - "zh_CN": "小键盘*", - "zh_TW": "數字鍵乘號" - } - }, - { - "ID": "KeyKeypadSubtract", - "Translations": { - "ar_SA": "لوحة الأرقام علامة الطرح\n", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad Subtract", - "es_ES": "Num. -", - "fr_FR": "Num. Soustraire", - "he_IL": "", - "it_IT": "Tast. num. -", - "ja_JP": "", - "ko_KR": "키패드 빼기", - "no_NO": "Numerisk Subtrakt", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "- (блок цифр)", - "sv_SE": "Keypad -", - "th_TH": "ปุ่ม - บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "- (цифровий блок)", - "zh_CN": "小键盘-", - "zh_TW": "數字鍵減號" - } - }, - { - "ID": "KeyKeypadAdd", - "Translations": { - "ar_SA": "لوحة الأرقام علامة الزائد", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad Add", - "es_ES": "Num. +", - "fr_FR": "Num. Ajouter", - "he_IL": "", - "it_IT": "Tast. num. +", - "ja_JP": "", - "ko_KR": "키패드 추가", - "no_NO": "Numerisk Legg til", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "+ (блок цифр)", - "sv_SE": "Keypad +", - "th_TH": "ปุ่ม + บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "+ (цифровий блок)", - "zh_CN": "小键盘+", - "zh_TW": "數字鍵加號" - } - }, - { - "ID": "KeyKeypadDecimal", - "Translations": { - "ar_SA": "لوحة الأرقام الفاصلة العشرية", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad Decimal", - "es_ES": "Num. .", - "fr_FR": "Num. Point", - "he_IL": "", - "it_IT": "Tast. num. sep. decimale", - "ja_JP": "", - "ko_KR": "숫자 키패드", - "no_NO": "Numerisk Desimal", - "pl_PL": "", - "pt_BR": null, - "ru_RU": ". (блок цифр)", - "sv_SE": "Keypad ,", - "th_TH": "ปุ่ม . บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": ". (цифровий блок)", - "zh_CN": "小键盘.", - "zh_TW": "數字鍵小數點" - } - }, - { - "ID": "KeyKeypadEnter", - "Translations": { - "ar_SA": "لوحة الأرقام زر الإدخال", - "de_DE": "", - "el_GR": "", - "en_US": "Keypad Enter", - "es_ES": "Num. Intro", - "fr_FR": "Num. Ent", - "he_IL": "", - "it_IT": "Tast. num. Invio", - "ja_JP": "", - "ko_KR": "키패드 엔터", - "no_NO": "Numerisk Enter", - "pl_PL": "", - "pt_BR": null, - "ru_RU": "Enter (блок цифр)", - "sv_SE": "Enter (numerisk)", - "th_TH": "ปุ่ม Enter บนแป้นตัวเลข", - "tr_TR": "", - "uk_UA": "Enter (цифровий блок)", - "zh_CN": "小键盘回车键", - "zh_TW": "數字鍵 Enter" - } - }, - { - "ID": "KeyNumber0", - "Translations": { - "ar_SA": "٠", - "de_DE": "", - "el_GR": "", - "en_US": "0", - "es_ES": null, - "fr_FR": "à", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyNumber1", - "Translations": { - "ar_SA": "١", - "de_DE": "", - "el_GR": "", - "en_US": "1", - "es_ES": null, - "fr_FR": "&", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyNumber2", - "Translations": { - "ar_SA": "٢", - "de_DE": "", - "el_GR": "", - "en_US": "2", - "es_ES": null, - "fr_FR": "é", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyNumber3", - "Translations": { - "ar_SA": "٣", - "de_DE": "", - "el_GR": "", - "en_US": "3", - "es_ES": null, - "fr_FR": "\"", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyNumber4", - "Translations": { - "ar_SA": "٤", - "de_DE": "", - "el_GR": "", - "en_US": "4", - "es_ES": null, - "fr_FR": "'", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyNumber5", - "Translations": { - "ar_SA": "٥", - "de_DE": "", - "el_GR": "", - "en_US": "5", - "es_ES": null, - "fr_FR": "(", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyNumber6", - "Translations": { - "ar_SA": "٦", - "de_DE": "", - "el_GR": "", - "en_US": "6", - "es_ES": null, - "fr_FR": "-", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyNumber7", - "Translations": { - "ar_SA": "٧", - "de_DE": "", - "el_GR": "", - "en_US": "7", - "es_ES": null, - "fr_FR": "è", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyNumber8", - "Translations": { - "ar_SA": "٨", - "de_DE": "", - "el_GR": "", - "en_US": "8", - "es_ES": null, - "fr_FR": "_", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyNumber9", - "Translations": { - "ar_SA": "٩", - "de_DE": "", - "el_GR": "", - "en_US": "9", - "es_ES": null, - "fr_FR": "ç", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyTilde", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "~", - "es_ES": "ñ", - "fr_FR": "ù", - "he_IL": "", - "it_IT": "ò", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyGrave", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "`", - "es_ES": "º", - "fr_FR": null, - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyMinus", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "-", - "es_ES": null, - "fr_FR": null, - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyPlus", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "+", - "es_ES": null, - "fr_FR": "=", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyBracketLeft", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "[", - "es_ES": "'", - "fr_FR": ")", - "he_IL": "", - "it_IT": "'", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyBracketRight", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "]", - "es_ES": "¡", - "fr_FR": "^", - "he_IL": "", - "it_IT": "ì", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeySemicolon", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": ";", - "es_ES": "`", - "fr_FR": "$", - "he_IL": "", - "it_IT": "è", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyQuote", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "\"", - "es_ES": "´", - "fr_FR": "²", - "he_IL": "", - "it_IT": "à", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyComma", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": ",", - "es_ES": null, - "fr_FR": null, - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyPeriod", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": ".", - "es_ES": null, - "fr_FR": ";", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeySlash", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "/", - "es_ES": "ç", - "fr_FR": ":", - "he_IL": "", - "it_IT": "ù", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyBackSlash", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "\\", - "es_ES": "<", - "fr_FR": "<", - "he_IL": "", - "it_IT": "<", - "ja_JP": "", - "ko_KR": null, - "no_NO": "", - "pl_PL": "", - "pt_BR": null, - "ru_RU": null, - "sv_SE": null, - "th_TH": "", - "tr_TR": "", - "uk_UA": "", - "zh_CN": null, - "zh_TW": null - } - }, - { - "ID": "KeyUnbound", - "Translations": { - "ar_SA": "غير مرتبط", - "de_DE": "", - "el_GR": "", - "en_US": "Unbound", - "es_ES": "Sin Asignar", - "fr_FR": "Non Attribuée", - "he_IL": "", - "it_IT": "Non assegnato", - "ja_JP": "", - "ko_KR": "설정 안함", - "no_NO": "Ikke bundet", - "pl_PL": "", - "pt_BR": "Não Atribuído", - "ru_RU": "Не привязано", - "sv_SE": "Obunden", - "th_TH": "ยังไม่กำหนดปุ่ม", - "tr_TR": "", - "uk_UA": "Відв'язати", - "zh_CN": "未分配", - "zh_TW": "未分配" - } - }, { "ID": "GamepadLeftStick", "Translations": { @@ -13525,11 +11650,11 @@ "zh_TW": "目前控制器設定已更新。" } }, - { - "ID": "DialogControllerSettingsModifiedConfirmSubMessage", - "Translations": { - "ar_SA": "هل تريد الحفظ ؟", - "de_DE": "Controller-Einstellungen speichern?", + { + "ID": "DialogControllerSettingsModifiedConfirmSubMessage", + "Translations": { + "ar_SA": "هل تريد الحفظ ؟", + "de_DE": "Controller-Einstellungen speichern?", "el_GR": "Θέλετε να αποθηκεύσετε;", "en_US": "Do you want to save?", "es_ES": "¿Guardar cambios?", @@ -13546,13 +11671,63 @@ "th_TH": "คุณต้องการบันทึกหรือไม่?", "tr_TR": "Kaydetmek istiyor musunuz?", "uk_UA": "Ви хочете зберегти?", - "zh_CN": "是否保存?", - "zh_TW": "您想要儲存嗎?" - } - }, - { - "ID": "DialogLoadFileErrorMessage", - "Translations": { + "zh_CN": "是否保存?", + "zh_TW": "您想要儲存嗎?" + } + }, + { + "ID": "DialogControllerSettingsResetKeybindsConfirmMessage", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Reset the selected device keybinds to their default values?", + "es_ES": "¿Restablecer las asignaciones del dispositivo seleccionado a sus valores predeterminados?", + "fr_FR": "Réinitialiser les assignations du périphérique sélectionné à leurs valeurs par défaut ?", + "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": "DialogControllerSettingsResetKeybindsConfirmSubMessage", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "This is a destructive action and will overwrite the current bindings for this device.", + "es_ES": "Esta acción es destructiva y sobrescribirá las asignaciones actuales de este dispositivo.", + "fr_FR": "Cette action est destructive et écrasera les assignations actuelles de ce périphérique.", + "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": "DialogLoadFileErrorMessage", + "Translations": { "ar_SA": "{0}. ملف خاطئ: {1}", "de_DE": "{0}. Fehlerhafte Datei: {1}", "el_GR": "", @@ -18025,31 +16200,6 @@ "zh_TW": "正在編譯著色器" } }, - { - "ID": "AllKeyboards", - "Translations": { - "ar_SA": "كل لوحات المفاتيح", - "de_DE": "Alle Tastaturen", - "el_GR": "Όλα τα πληκτρολόγια", - "en_US": "All Keyboards", - "es_ES": "Todos los Teclados", - "fr_FR": "Tous les Claviers", - "he_IL": "כל המקלדות", - "it_IT": "Tutte le tastiere", - "ja_JP": "すべてのキーボード", - "ko_KR": "모든 키보드", - "no_NO": "Alle tastaturer", - "pl_PL": "Wszystkie klawiatury", - "pt_BR": "Todos os Teclados", - "ru_RU": "Все клавиатуры", - "sv_SE": "Alla tangentbord", - "th_TH": "คีย์บอร์ดทั้งหมด", - "tr_TR": "Tüm Klavyeler", - "uk_UA": "Усі клавіатури", - "zh_CN": "所有键盘", - "zh_TW": "所有鍵盤" - } - }, { "ID": "OpenFileDialogTitle", "Translations": { diff --git a/src/Ryujinx.Common/Configuration/Hid/Keyboard/StandardKeyboardInputConfig.cs b/src/Ryujinx.Common/Configuration/Hid/Keyboard/StandardKeyboardInputConfig.cs index 1e8b188e7..548b0762c 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Keyboard/StandardKeyboardInputConfig.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Keyboard/StandardKeyboardInputConfig.cs @@ -1,4 +1,4 @@ namespace Ryujinx.Common.Configuration.Hid.Keyboard { - public class StandardKeyboardInputConfig : GenericKeyboardInputConfig { } + public class StandardKeyboardInputConfig : GenericKeyboardInputConfig { } } diff --git a/src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs b/src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs new file mode 100644 index 000000000..c9cc1b8cd --- /dev/null +++ b/src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs @@ -0,0 +1,142 @@ +using System.Text.Json.Serialization; + +namespace Ryujinx.Common.Configuration.Hid +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum PhysicalKey + { + Unknown, + ShiftLeft, + ShiftRight, + ControlLeft, + ControlRight, + AltLeft, + AltRight, + WinLeft, + WinRight, + Menu, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + F21, + F22, + F23, + F24, + F25, + F26, + F27, + F28, + F29, + F30, + F31, + F32, + F33, + F34, + F35, + Up, + Down, + Left, + Right, + Enter, + Escape, + Space, + Tab, + BackSpace, + Insert, + Delete, + PageUp, + PageDown, + Home, + End, + CapsLock, + ScrollLock, + PrintScreen, + Pause, + NumLock, + Clear, + Keypad0, + Keypad1, + Keypad2, + Keypad3, + Keypad4, + Keypad5, + Keypad6, + Keypad7, + Keypad8, + Keypad9, + KeypadDivide, + KeypadMultiply, + KeypadSubtract, + KeypadAdd, + KeypadDecimal, + KeypadEnter, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + Number0, + Number1, + Number2, + Number3, + Number4, + Number5, + Number6, + Number7, + Number8, + Number9, + Tilde, + Grave, + Minus, + Plus, + BracketLeft, + BracketRight, + Semicolon, + Quote, + Comma, + Period, + Slash, + BackSlash, + Unbound, + + Count, + } +} diff --git a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs index 57f2940c8..421e9beff 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs @@ -165,6 +165,9 @@ namespace Ryujinx.Input.SDL3 public string Id { get; } public string Name { get; } + // Expose vendor id for higher-fidelity device detection in UI + public ushort VendorId => _gamepadHandle != null ? SDL_GetGamepadVendor(_gamepadHandle) : (ushort)0; + public bool IsConnected => SDL_GamepadConnected(_gamepadHandle); protected virtual void Dispose(bool disposing) diff --git a/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs b/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs index 8b179f43f..cf3f0c9d8 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs @@ -9,25 +9,15 @@ using System.Runtime.CompilerServices; using System.Threading; using SDL; using static SDL.SDL3; - -using ConfigKey = Ryujinx.Common.Configuration.Hid.Key; +using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; namespace Ryujinx.Input.SDL3 { class SDL3Keyboard : IKeyboard { - private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, Key From) - { - public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not Key.Unbound; - } - private readonly Lock _userMappingLock = new(); - -#pragma warning disable IDE0052 // Remove unread private member - private readonly SDL3KeyboardDriver _driver; -#pragma warning restore IDE0052 private StandardKeyboardInputConfig _configuration; - private readonly List _buttonsUserMapping; + private readonly List _buttonsUserMapping; private static readonly SDL_Keycode[] _keysDriverMapping = @@ -172,9 +162,8 @@ namespace Ryujinx.Input.SDL3 SDL_Keycode.SDLK_0 ]; - public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name) + public SDL3Keyboard(string id, string name) { - _driver = driver; Id = id; Name = name; _buttonsUserMapping = []; @@ -196,9 +185,9 @@ namespace Ryujinx.Input.SDL3 } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe static int ToSDL3Scancode(Key key) + private unsafe static int ToSDL3Scancode(ConfigPhysicalKey key) { - if (key is >= Key.Unknown and <= Key.Menu) + if (key is >= ConfigPhysicalKey.Unknown and <= ConfigPhysicalKey.Menu) { return -1; } @@ -206,18 +195,18 @@ namespace Ryujinx.Input.SDL3 return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null); } - private static SDL_Keymod GetKeyboardModifierMask(Key key) + private static SDL_Keymod GetKeyboardModifierMask(ConfigPhysicalKey key) { return key switch { - Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT, - Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT, - Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL, - Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL, - Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT, - Key.AltRight => SDL_Keymod.SDL_KMOD_RALT, - Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI, - Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI, + ConfigPhysicalKey.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT, + ConfigPhysicalKey.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT, + ConfigPhysicalKey.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL, + ConfigPhysicalKey.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL, + ConfigPhysicalKey.AltLeft => SDL_Keymod.SDL_KMOD_LALT, + ConfigPhysicalKey.AltRight => SDL_Keymod.SDL_KMOD_RALT, + ConfigPhysicalKey.WinLeft => SDL_Keymod.SDL_KMOD_LGUI, + ConfigPhysicalKey.WinRight => SDL_Keymod.SDL_KMOD_RGUI, // NOTE: Menu key isn't supported by SDL3. _ => SDL_Keymod.SDL_KMOD_NONE }; @@ -233,9 +222,9 @@ namespace Ryujinx.Input.SDL3 rawKeyboardState = SDL_GetKeyboardState(null); } - bool[] keysState = new bool[(int)Key.Count]; + bool[] keysState = new bool[(int)ConfigPhysicalKey.Count]; - for (Key key = 0; key < Key.Count; key++) + for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++) { int index = ToSDL3Scancode(key); if (index == -1) @@ -265,36 +254,6 @@ namespace Ryujinx.Input.SDL3 return value * ConvertRate; } - private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick stickConfig) - { - short stickX = 0; - short stickY = 0; - - if (snapshot.IsPressed((Key)stickConfig.StickUp)) - { - stickY += 1; - } - - if (snapshot.IsPressed((Key)stickConfig.StickDown)) - { - stickY -= 1; - } - - if (snapshot.IsPressed((Key)stickConfig.StickRight)) - { - stickX += 1; - } - - if (snapshot.IsPressed((Key)stickConfig.StickLeft)) - { - stickX -= 1; - } - - Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY)); - - return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue)); - } - public GamepadStateSnapshot GetMappedStateSnapshot() { KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot(); @@ -307,9 +266,9 @@ namespace Ryujinx.Input.SDL3 return result; } - foreach (ButtonMappingEntry entry in _buttonsUserMapping) + foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping) { - if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound) + if (!entry.IsValid) { continue; } @@ -321,8 +280,8 @@ namespace Ryujinx.Input.SDL3 } } - (short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick); - (short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick); + (short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick); + (short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick); result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY)); result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY)); @@ -358,38 +317,15 @@ namespace Ryujinx.Input.SDL3 { _configuration = (StandardKeyboardInputConfig)configuration; - // First clear the buttons mapping _buttonsUserMapping.Clear(); - // Then configure left joycon - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl)); - - // Finally configure right joycon - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl)); + _buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration)); } } public void SetLed(uint packedRgb) { - Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard"); + Logger.Debug?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard"); } public void SetTriggerThreshold(float triggerThreshold) diff --git a/src/Ryujinx.Input.SDL3/SDLKeyboardDriver.cs b/src/Ryujinx.Input.SDL3/SDLKeyboardDriver.cs index cd2a067be..00d5d6899 100644 --- a/src/Ryujinx.Input.SDL3/SDLKeyboardDriver.cs +++ b/src/Ryujinx.Input.SDL3/SDLKeyboardDriver.cs @@ -50,7 +50,7 @@ namespace Ryujinx.Input.SDL3 return null; } - return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards"); + return new SDL3Keyboard(_keyboardIdentifers[0], "All keyboards"); } public IEnumerable GetGamepads() diff --git a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs index 3c011a63b..9d3939653 100644 --- a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs +++ b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Logging; + namespace Ryujinx.Input.Assigner { /// @@ -8,22 +10,40 @@ namespace Ryujinx.Input.Assigner private readonly IKeyboard _keyboard; private KeyboardStateSnapshot _keyboardState; + private Button? _pressedButton; public KeyboardKeyAssigner(IKeyboard keyboard) { _keyboard = keyboard; } - public void Initialize() { } + public void Initialize() + { + _pressedButton = null; + } public void ReadInput() { _keyboardState = _keyboard.GetKeyboardStateSnapshot(); + + if (_pressedButton is null) + { + Button? buttonFromState = GetPressedButtonFromState(); + Button? buttonFromBufferedPress = buttonFromState is null ? GetPressedButtonFromBufferedPress() : null; + + _pressedButton = buttonFromState ?? buttonFromBufferedPress; + } + + if (_pressedButton is not null) + { + string source = _pressedButton.HasValue && GetPressedButtonFromState() is not null ? "state" : "buffered-press"; + Logger.Debug?.Print(LogClass.UI, $"Keyboard assigner registered key={_pressedButton.Value.AsHidType()}, source={source}, cancelPressed={ShouldCancel()}"); + } } public bool IsAnyButtonPressed() { - return GetPressedButton() is not null; + return _pressedButton is not null; } public bool ShouldCancel() @@ -33,18 +53,53 @@ namespace Ryujinx.Input.Assigner public Button? GetPressedButton() { - Button? keyPressed = null; + return !ShouldCancel() ? _pressedButton : null; + } + + private Button? GetPressedButtonFromState() + { + Key aliasedKey = GetAliasedPressedKey(); + + if (aliasedKey != Key.Unknown) + { + return new Button(aliasedKey); + } for (Key key = Key.Unknown; key < Key.Count; key++) { if (_keyboardState.IsPressed(key)) { - keyPressed = new(key); - break; + return new Button(key); } } - return !ShouldCancel() ? keyPressed : null; + return null; + } + + private Button? GetPressedButtonFromBufferedPress() + { + return _keyboard.TryConsumePressedKey(out Key key) ? new Button(key) : null; + } + + private Key GetAliasedPressedKey() + { + // On some layouts (for example AltGr on Windows), Right Alt is reported as Ctrl+Alt. + // Prefer AltRight in that case so the binding reflects the physical key used. + if (_keyboardState.IsPressed(Key.ControlLeft) && _keyboardState.IsPressed(Key.AltRight)) + { + return Key.AltRight; + } + + // On some Copilot keyboards, the key in the right-control position is reported as + // ShiftLeft+Win+F23. Prefer ControlRight so the binding reflects that physical key. + if (_keyboardState.IsPressed(Key.ShiftLeft) && + _keyboardState.IsPressed(Key.F23) && + (_keyboardState.IsPressed(Key.WinLeft) || _keyboardState.IsPressed(Key.WinRight))) + { + return Key.ControlRight; + } + + return Key.Unknown; } } } diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index 85ca5ffcb..ee34e9eb8 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -2,6 +2,7 @@ using Ryujinx.Common; 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 Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Hid; using System; @@ -233,7 +234,9 @@ namespace Ryujinx.Input.HLE _gamepad?.Dispose(); Id = config.Id; - _gamepad = GamepadDriver.GetGamepad(Id); + _gamepad = config is StandardKeyboardInputConfig && GamepadDriver is IKeyboardModeDriver keyboardModeDriver + ? keyboardModeDriver.GetKeyboard(Id, KeyboardInputMode.Physical) + : GamepadDriver.GetGamepad(Id); UpdateUserConfiguration(config); diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs index f2936aa72..bf483e2a7 100644 --- a/src/Ryujinx.Input/HLE/NpadManager.cs +++ b/src/Ryujinx.Input/HLE/NpadManager.cs @@ -24,7 +24,7 @@ namespace Ryujinx.Input.HLE private readonly Lock _lock = new(); - private bool _blockInputUpdates; + private int _inputUpdateBlockCount; private const int MaxControllers = 9; @@ -36,6 +36,7 @@ namespace Ryujinx.Input.HLE private bool _isDisposed; private List _inputConfig; + private List _requestedInputConfig; private bool _enableKeyboard; private bool _enableMouse; private Switch _device; @@ -52,6 +53,7 @@ namespace Ryujinx.Input.HLE _gamepadDriver = gamepadDriver; _mouseDriver = mouseDriver; _inputConfig = []; + _requestedInputConfig = []; _gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; @@ -89,29 +91,23 @@ namespace Ryujinx.Input.HLE } } - ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); + ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse); } } - private void HandleOnGamepadConnected(string id) + private void HandleOnGamepadConnected(string _) { // Force input reload - ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); + ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config) { - IGamepadDriver targetDriver = _gamepadDriver; - - if (config is StandardControllerInputConfig) - { - targetDriver = _gamepadDriver; - } - else if (config is StandardKeyboardInputConfig) - { - targetDriver = _keyboardDriver; - } + IGamepadDriver targetDriver = + config is StandardKeyboardInputConfig + ? _keyboardDriver + : _gamepadDriver; Debug.Assert(targetDriver != null, "Unknown input configuration!"); @@ -127,11 +123,13 @@ namespace Ryujinx.Input.HLE { lock (_lock) { + _requestedInputConfig = inputConfig?.ToList() ?? []; + NpadController[] oldControllers = _controllers.ToArray(); List validInputs = []; - foreach (InputConfig inputConfigEntry in inputConfig) + foreach (InputConfig inputConfigEntry in _requestedInputConfig) { NpadController controller; int index = (int)inputConfigEntry.PlayerIndex; @@ -147,7 +145,16 @@ namespace Ryujinx.Input.HLE controller = new(_cemuHookClient); } - bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry); + InputConfig activeConfig = inputConfigEntry; + bool isValid = DriverConfigurationUpdate(ref controller, activeConfig); + + if (!isValid && + inputConfigEntry is StandardControllerInputConfig && + TryGetKeyboardFallback(inputConfigEntry, out StandardKeyboardInputConfig fallbackConfig)) + { + activeConfig = fallbackConfig; + isValid = DriverConfigurationUpdate(ref controller, activeConfig); + } if (!isValid) { @@ -157,7 +164,7 @@ namespace Ryujinx.Input.HLE else { _controllers[index] = controller; - validInputs.Add(inputConfigEntry); + validInputs.Add(activeConfig); } } @@ -169,7 +176,7 @@ namespace Ryujinx.Input.HLE oldControllers[i] = null; } - _inputConfig = inputConfig; + _inputConfig = validInputs; _enableKeyboard = enableKeyboard; _enableMouse = enableMouse; @@ -177,16 +184,58 @@ namespace Ryujinx.Input.HLE } } + private bool TryGetKeyboardFallback(InputConfig inputConfig, out StandardKeyboardInputConfig fallbackConfig) + { + fallbackConfig = null; + + ReadOnlySpan keyboardIds = _keyboardDriver.GamepadsIds; + + if (keyboardIds.IsEmpty) + { + return false; + } + + string keyboardId = keyboardIds[0]; + + using IGamepad keyboard = _keyboardDriver.GetGamepad(keyboardId); + + if (keyboard == null) + { + return false; + } + + fallbackConfig = InputConfigDefaults.CreateDefaultKeyboardConfiguration( + keyboardId, + keyboard.Name, + inputConfig.ControllerType, + inputConfig.PlayerIndex); + + return true; + } + + private void ClearInputDriverStates() + { + foreach (InputConfig inputConfig in _inputConfig) + { + _controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear(); + } + } + public void UnblockInputUpdates() { lock (_lock) { - foreach (InputConfig inputConfig in _inputConfig) + if (_inputUpdateBlockCount == 0) { - _controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear(); + return; } - _blockInputUpdates = false; + _inputUpdateBlockCount--; + + if (_inputUpdateBlockCount == 0) + { + ClearInputDriverStates(); + } } } @@ -195,7 +244,7 @@ namespace Ryujinx.Input.HLE get { lock (_lock) - return _blockInputUpdates; + return _inputUpdateBlockCount > 0; } } @@ -203,7 +252,7 @@ namespace Ryujinx.Input.HLE { lock (_lock) { - _blockInputUpdates = true; + _inputUpdateBlockCount++; } } @@ -235,7 +284,7 @@ namespace Ryujinx.Input.HLE bool isJoyconPair = false; // Do we allow input updates and is a controller connected? - if (!_blockInputUpdates && controller != null) + if (_inputUpdateBlockCount == 0 && controller != null) { DriverConfigurationUpdate(ref controller, inputConfig); @@ -273,7 +322,7 @@ namespace Ryujinx.Input.HLE } } - if (!_blockInputUpdates && _enableKeyboard) + if (_inputUpdateBlockCount == 0 && _enableKeyboard) { hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver); } @@ -334,7 +383,7 @@ namespace Ryujinx.Input.HLE } } - internal InputConfig GetPlayerInputConfigByIndex(int index) + public InputConfig GetPlayerInputConfigByIndex(int index) { lock (_lock) { diff --git a/src/Ryujinx.Input/IKeyboard.cs b/src/Ryujinx.Input/IKeyboard.cs index c51d5aea3..9476fb1aa 100644 --- a/src/Ryujinx.Input/IKeyboard.cs +++ b/src/Ryujinx.Input/IKeyboard.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Runtime.CompilerServices; +using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; namespace Ryujinx.Input { @@ -33,15 +34,26 @@ namespace Ryujinx.Input { if (_keyState is null) { - _keyState = new bool[(int)Key.Count]; + _keyState = new bool[(int)ConfigPhysicalKey.Count]; } - for (Key key = 0; key < Key.Count; key++) + for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++) { - _keyState[(int)key] = keyboard.IsPressed(key); + _keyState[(int)key] = keyboard.IsPressed((Key)(int)key); } return new KeyboardStateSnapshot(_keyState); } + + /// + /// Try to consume a recently pressed key. + /// + /// The pressed key, if available. + /// True if a key press was consumed. + bool TryConsumePressedKey(out Key key) + { + key = Key.Unknown; + return false; + } } } diff --git a/src/Ryujinx.Input/IKeyboardModeDriver.cs b/src/Ryujinx.Input/IKeyboardModeDriver.cs new file mode 100644 index 000000000..62a835eb6 --- /dev/null +++ b/src/Ryujinx.Input/IKeyboardModeDriver.cs @@ -0,0 +1,7 @@ +namespace Ryujinx.Input +{ + public interface IKeyboardModeDriver : IGamepadDriver + { + IKeyboard GetKeyboard(string id, KeyboardInputMode mode); + } +} diff --git a/src/Ryujinx.Input/InputConfigDefaults.cs b/src/Ryujinx.Input/InputConfigDefaults.cs new file mode 100644 index 000000000..5608923f7 --- /dev/null +++ b/src/Ryujinx.Input/InputConfigDefaults.cs @@ -0,0 +1,148 @@ +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 ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; +using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; +using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; + +namespace Ryujinx.Input +{ + public static class InputConfigDefaults + { + public static StandardKeyboardInputConfig CreateDefaultKeyboardConfiguration( + string id, + string name, + ControllerType controllerType, + PlayerIndex playerIndex) + { + return new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = id, + Name = name, + PlayerIndex = playerIndex, + ControllerType = controllerType, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = ConfigPhysicalKey.Up, + DpadDown = ConfigPhysicalKey.Down, + DpadLeft = ConfigPhysicalKey.Left, + DpadRight = ConfigPhysicalKey.Right, + ButtonMinus = ConfigPhysicalKey.Minus, + ButtonL = ConfigPhysicalKey.E, + ButtonZl = ConfigPhysicalKey.Q, + ButtonSl = ConfigPhysicalKey.Unbound, + ButtonSr = ConfigPhysicalKey.Unbound, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = ConfigPhysicalKey.W, + StickDown = ConfigPhysicalKey.S, + StickLeft = ConfigPhysicalKey.A, + StickRight = ConfigPhysicalKey.D, + StickButton = ConfigPhysicalKey.F, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = ConfigPhysicalKey.Z, + ButtonB = ConfigPhysicalKey.X, + ButtonX = ConfigPhysicalKey.C, + ButtonY = ConfigPhysicalKey.V, + ButtonPlus = ConfigPhysicalKey.Plus, + ButtonR = ConfigPhysicalKey.U, + ButtonZr = ConfigPhysicalKey.O, + ButtonSl = ConfigPhysicalKey.Unbound, + ButtonSr = ConfigPhysicalKey.Unbound, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = ConfigPhysicalKey.I, + StickDown = ConfigPhysicalKey.K, + StickLeft = ConfigPhysicalKey.J, + StickRight = ConfigPhysicalKey.L, + StickButton = ConfigPhysicalKey.H, + }, + }; + } + + public static StandardControllerInputConfig CreateDefaultControllerConfiguration( + string id, + string name, + ControllerType controllerType, + PlayerIndex playerIndex, + bool isNintendoStyle) + { + return new StandardControllerInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.GamepadSDL3, + Id = id, + Name = name, + PlayerIndex = playerIndex, + ControllerType = controllerType, + 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.SingleLeftTrigger0, + ButtonSr = ConfigGamepadInputId.SingleRightTrigger0, + }, + LeftJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Left, + StickButton = ConfigGamepadInputId.LeftStick, + InvertStickX = false, + InvertStickY = false, + Rotate90CW = 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.SingleLeftTrigger1, + ButtonSr = ConfigGamepadInputId.SingleRightTrigger1, + }, + RightJoyconStick = new JoyconConfigControllerStick + { + Joystick = ConfigStickInputId.Right, + StickButton = ConfigGamepadInputId.RightStick, + InvertStickX = false, + InvertStickY = false, + Rotate90CW = false, + }, + Motion = new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + EnableMotion = true, + Sensitivity = 100, + GyroDeadzone = 1, + }, + Rumble = new RumbleConfigController + { + StrongRumble = 1f, + WeakRumble = 1f, + EnableRumble = false, + UseHDRumble = true, + }, + }; + } + } +} diff --git a/src/Ryujinx.Input/KeyboardInputMappingHelper.cs b/src/Ryujinx.Input/KeyboardInputMappingHelper.cs new file mode 100644 index 000000000..8c2827efd --- /dev/null +++ b/src/Ryujinx.Input/KeyboardInputMappingHelper.cs @@ -0,0 +1,78 @@ +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using System.Numerics; + +using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; + +namespace Ryujinx.Input +{ + public static class KeyboardInputMappingHelper + { + public readonly record struct KeyboardButtonMapping(GamepadButtonInputId To, ConfigPhysicalKey From) + { + public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound; + } + + public static KeyboardButtonMapping[] BuildButtonMappings(StandardKeyboardInputConfig configuration) => + [ + // Left JoyCon + new(GamepadButtonInputId.LeftStick, configuration.LeftJoyconStick.StickButton), + new(GamepadButtonInputId.DpadUp, configuration.LeftJoycon.DpadUp), + new(GamepadButtonInputId.DpadDown, configuration.LeftJoycon.DpadDown), + new(GamepadButtonInputId.DpadLeft, configuration.LeftJoycon.DpadLeft), + new(GamepadButtonInputId.DpadRight, configuration.LeftJoycon.DpadRight), + new(GamepadButtonInputId.Minus, configuration.LeftJoycon.ButtonMinus), + new(GamepadButtonInputId.LeftShoulder, configuration.LeftJoycon.ButtonL), + new(GamepadButtonInputId.LeftTrigger, configuration.LeftJoycon.ButtonZl), + new(GamepadButtonInputId.SingleRightTrigger0, configuration.LeftJoycon.ButtonSr), + new(GamepadButtonInputId.SingleLeftTrigger0, configuration.LeftJoycon.ButtonSl), + + // Right JoyCon + new(GamepadButtonInputId.RightStick, configuration.RightJoyconStick.StickButton), + new(GamepadButtonInputId.A, configuration.RightJoycon.ButtonA), + new(GamepadButtonInputId.B, configuration.RightJoycon.ButtonB), + new(GamepadButtonInputId.X, configuration.RightJoycon.ButtonX), + new(GamepadButtonInputId.Y, configuration.RightJoycon.ButtonY), + new(GamepadButtonInputId.Plus, configuration.RightJoycon.ButtonPlus), + new(GamepadButtonInputId.RightShoulder, configuration.RightJoycon.ButtonR), + new(GamepadButtonInputId.RightTrigger, configuration.RightJoycon.ButtonZr), + new(GamepadButtonInputId.SingleRightTrigger1, configuration.RightJoycon.ButtonSr), + new(GamepadButtonInputId.SingleLeftTrigger1, configuration.RightJoycon.ButtonSl), + ]; + + public static (short X, short Y) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick stickConfig) + { + short stickX = 0; + short stickY = 0; + + if (snapshot.IsPressed(stickConfig.StickUp)) + { + stickY += 1; + } + + if (snapshot.IsPressed(stickConfig.StickDown)) + { + stickY -= 1; + } + + if (snapshot.IsPressed(stickConfig.StickRight)) + { + stickX += 1; + } + + if (snapshot.IsPressed(stickConfig.StickLeft)) + { + stickX -= 1; + } + + if (stickX == 0 && stickY == 0) + { + return (0, 0); + } + + Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY)); + + return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue)); + } + } +} diff --git a/src/Ryujinx.Input/KeyboardInputMode.cs b/src/Ryujinx.Input/KeyboardInputMode.cs new file mode 100644 index 000000000..a46f3760d --- /dev/null +++ b/src/Ryujinx.Input/KeyboardInputMode.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.Input +{ + public enum KeyboardInputMode + { + Semantic, + Physical, + } +} diff --git a/src/Ryujinx.Input/KeyboardStateSnapshot.cs b/src/Ryujinx.Input/KeyboardStateSnapshot.cs index 9b40b46db..91765ceb8 100644 --- a/src/Ryujinx.Input/KeyboardStateSnapshot.cs +++ b/src/Ryujinx.Input/KeyboardStateSnapshot.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; namespace Ryujinx.Input { @@ -25,5 +26,8 @@ namespace Ryujinx.Input /// True if the given key is pressed [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsPressed(Key key) => KeysState[(int)key]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsPressed(ConfigPhysicalKey key) => KeysState[(int)key]; } } diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs index 3574c3061..ff7e56c52 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs @@ -7,8 +7,6 @@ using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Common.Configuration; 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 Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.Cpu; @@ -17,14 +15,12 @@ using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE; using Ryujinx.Input; +using Ryujinx.Input.SDL3; using Silk.NET.Vulkan; using System; using System.IO; using System.Text.Json; using System.Threading.Tasks; -using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; -using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; -using Key = Ryujinx.Common.Configuration.Hid.Key; namespace Ryujinx.Headless { @@ -90,6 +86,19 @@ namespace Ryujinx.Headless } string gamepadName = gamepad.Name; + + bool isNintendoStyle = false; + + if (gamepad is SDL3Gamepad sdlGp) + { + // Nintendo vendor ID is 0x057E + isNintendoStyle = sdlGp.VendorId == 0x057E; + } + else + { + // Fallback to name-based detection + isNintendoStyle = gamepadName.Contains("Nintendo", StringComparison.OrdinalIgnoreCase); + } gamepad.Dispose(); @@ -99,131 +108,21 @@ namespace Ryujinx.Headless { if (isKeyboard) { - config = new StandardKeyboardInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.WindowKeyboard, - Id = null, - ControllerType = ControllerType.JoyconPair, - 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 = InputConfigDefaults.CreateDefaultKeyboardConfiguration( + null, + null, + ControllerType.JoyconPair, + index); } else { - bool isNintendoStyle = gamepadName.Contains("Nintendo"); - config = new StandardControllerInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.GamepadSDL3, - Id = null, - ControllerType = ControllerType.JoyconPair, - 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.SingleLeftTrigger0, - ButtonSr = ConfigGamepadInputId.SingleRightTrigger0, - }, - - LeftJoyconStick = new JoyconConfigControllerStick - { - Joystick = ConfigStickInputId.Left, - StickButton = ConfigGamepadInputId.LeftStick, - InvertStickX = false, - InvertStickY = false, - Rotate90CW = 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.SingleLeftTrigger1, - ButtonSr = ConfigGamepadInputId.SingleRightTrigger1, - }, - - RightJoyconStick = new JoyconConfigControllerStick - { - Joystick = ConfigStickInputId.Right, - StickButton = ConfigGamepadInputId.RightStick, - InvertStickX = false, - InvertStickY = false, - Rotate90CW = false, - }, - - Motion = new StandardMotionConfigController - { - MotionBackend = MotionInputBackendType.GamepadDriver, - EnableMotion = true, - Sensitivity = 100, - GyroDeadzone = 1, - }, - Rumble = new RumbleConfigController - { - StrongRumble = 1f, - WeakRumble = 1f, - EnableRumble = false, - UseHDRumble = true - }, - }; + config = InputConfigDefaults.CreateDefaultControllerConfiguration( + null, + null, + ControllerType.JoyconPair, + index, + isNintendoStyle); } } else diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs index 704a15ba7..a9e9d400a 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboard.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs @@ -7,15 +7,15 @@ using System; using System.Collections.Generic; using System.Numerics; using System.Threading; -using ConfigKey = Ryujinx.Common.Configuration.Hid.Key; using Key = Ryujinx.Input.Key; namespace Ryujinx.Ava.Input { internal class AvaloniaKeyboard : IKeyboard { - private readonly List _buttonsUserMapping; + private readonly List _buttonsUserMapping; private readonly AvaloniaKeyboardDriver _driver; + private readonly KeyboardInputMode _mode; private StandardKeyboardInputConfig _configuration; private readonly Lock _userMappingLock = new(); @@ -25,18 +25,12 @@ namespace Ryujinx.Ava.Input public bool IsConnected => true; public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None; - - private class ButtonMappingEntry(GamepadButtonInputId to, Key from) - { - public readonly GamepadButtonInputId To = to; - public readonly Key From = from; - } - - public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name) + public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name, KeyboardInputMode mode) { _buttonsUserMapping = []; _driver = driver; + _mode = mode; Id = id; Name = name; } @@ -58,22 +52,18 @@ namespace Ryujinx.Ava.Input return result; } - foreach (ButtonMappingEntry entry in _buttonsUserMapping) + foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping) { - if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound) + if (!entry.IsValid || result.IsPressed(entry.To)) { continue; } - // NOTE: Do not touch state of the button already pressed. - if (!result.IsPressed(entry.To)) - { - result.SetPressed(entry.To, rawState.IsPressed(entry.From)); - } + result.SetPressed(entry.To, rawState.IsPressed(entry.From)); } - (short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick); - (short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick); + (short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick); + (short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick); result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY)); result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY)); @@ -101,7 +91,7 @@ namespace Ryujinx.Ava.Input { try { - return _driver.IsPressed(key); + return _driver.IsPressed(key, _mode); } catch { @@ -109,6 +99,19 @@ namespace Ryujinx.Ava.Input } } + public bool TryConsumePressedKey(out Key key) + { + try + { + return _driver.TryConsumePressedKey(_mode, out key); + } + catch + { + key = Key.Unknown; + return false; + } + } + public void SetConfiguration(InputConfig configuration) { lock (_userMappingLock) @@ -117,53 +120,20 @@ namespace Ryujinx.Ava.Input _buttonsUserMapping.Clear(); -#pragma warning disable IDE0055 // Disable formatting - // Left JoyCon - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl)); - - // Right JoyCon - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr)); - _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl)); -#pragma warning restore IDE0055 + _buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration)); } } public void SetLed(uint packedRgb) { - Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard"); + Logger.Debug?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard"); } - public void SetTriggerThreshold(float triggerThreshold) - { - // No operations - } + public bool HDRumble(VibrationValue left, VibrationValue right) => false; - public bool HDRumble(VibrationValue left, VibrationValue right) - { - return false; - } + public void SetTriggerThreshold(float triggerThreshold) { } - public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) - { - return false; - } + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) => false; public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero; @@ -174,41 +144,9 @@ namespace Ryujinx.Ava.Input return value * ConvertRate; } - private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick stickConfig) - { - short stickX = 0; - short stickY = 0; - - if (snapshot.IsPressed((Key)stickConfig.StickUp)) - { - stickY += 1; - } - - if (snapshot.IsPressed((Key)stickConfig.StickDown)) - { - stickY -= 1; - } - - if (snapshot.IsPressed((Key)stickConfig.StickRight)) - { - stickX += 1; - } - - if (snapshot.IsPressed((Key)stickConfig.StickLeft)) - { - stickX -= 1; - } - - Vector2 stick = new(stickX, stickY); - - stick = Vector2.Normalize(stick); - - return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue)); - } - public void Clear() { - _driver?.Clear(); + _driver?.Clear(_mode); } public void Dispose() { } diff --git a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs index 5fbfb1bbf..57fd05831 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs @@ -1,19 +1,55 @@ using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Systems.Configuration; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Common.Logging; using Ryujinx.Input; using System; using System.Collections.Generic; +using System.Threading; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; using AvaKey = Avalonia.Input.Key; +using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; using Key = Ryujinx.Input.Key; namespace Ryujinx.Ava.Input { - internal class AvaloniaKeyboardDriver : IGamepadDriver + internal class AvaloniaKeyboardDriver : IKeyboardModeDriver { + private enum PhysicalKeySource + { + Direct, + ObservedFallback, + Unknown, + } + + [Flags] + private enum CGEventFlags : ulong + { + AlphaShift = 1UL << 16 // CapsLock + } + + private enum CGEventSourceStateID : uint + { + HIDSystemState = 1 + } + + [DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")] + private static extern CGEventFlags CGEventSourceFlagsState(CGEventSourceStateID stateID); private static readonly string[] _keyboardIdentifers = ["0"]; private readonly Control _control; - private readonly HashSet _pressedKeys; + private readonly Window _window; + private readonly HashSet _semanticPressedKeys; + private readonly HashSet _physicalPressedKeys; + private readonly HashSet _keysToRestoreAfterActivation; + private readonly Dictionary _observedPhysicalKeysBySemanticKey; + private readonly Queue _semanticPressedKeyQueue; + private readonly Queue _physicalPressedKeyQueue; + private readonly Lock _pressedKeyQueueLock; + private readonly KeyboardInputMode _defaultMode; public event EventHandler KeyPressed; public event EventHandler KeyRelease; @@ -22,14 +58,41 @@ namespace Ryujinx.Ava.Input public string DriverName => "AvaloniaKeyboardDriver"; public ReadOnlySpan GamepadsIds => _keyboardIdentifers; - public AvaloniaKeyboardDriver(Control control) + public AvaloniaKeyboardDriver(Control control, KeyboardInputMode defaultMode = KeyboardInputMode.Semantic) { _control = control; - _pressedKeys = []; + _window = control as Window ?? TopLevel.GetTopLevel(control) as Window; + _semanticPressedKeys = []; + _physicalPressedKeys = []; + _keysToRestoreAfterActivation = []; + _observedPhysicalKeysBySemanticKey = []; + _semanticPressedKeyQueue = []; + _physicalPressedKeyQueue = []; + _pressedKeyQueueLock = new(); + _defaultMode = defaultMode; - _control.KeyDown += OnKeyPress; - _control.KeyUp += OnKeyRelease; + _control.AddHandler(InputElement.KeyDownEvent, OnKeyPress, RoutingStrategies.Tunnel, true); + _control.AddHandler(InputElement.KeyUpEvent, OnKeyRelease, RoutingStrategies.Tunnel, true); _control.TextInput += Control_TextInput; + _window?.Activated += Window_Activated; + _window?.Deactivated += Window_Deactivated; + } + + private void Window_Activated(object sender, EventArgs e) + { + RestorePressedKeysAfterActivation(); + } + + private void Window_Deactivated(object sender, EventArgs e) + { + lock (_pressedKeyQueueLock) + { + _keysToRestoreAfterActivation.Clear(); + _keysToRestoreAfterActivation.UnionWith(_semanticPressedKeys); + _observedPhysicalKeysBySemanticKey.Clear(); + } + + Clear(); } private void Control_TextInput(object sender, TextInputEventArgs e) @@ -50,13 +113,18 @@ namespace Ryujinx.Ava.Input } public IGamepad GetGamepad(string id) + { + return GetKeyboard(id, _defaultMode); + } + + public IKeyboard GetKeyboard(string id, KeyboardInputMode mode) { if (!_keyboardIdentifers[0].Equals(id)) { return null; } - return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]); + return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.KeyboardLayout_KeyboardInputMode], mode); } public IEnumerable GetGamepads() => [GetGamepad("0")]; @@ -65,40 +133,448 @@ namespace Ryujinx.Ava.Input { if (disposing) { - _control.KeyUp -= OnKeyPress; - _control.KeyDown -= OnKeyRelease; + _control.RemoveHandler(InputElement.KeyDownEvent, OnKeyPress); + _control.RemoveHandler(InputElement.KeyUpEvent, OnKeyRelease); + _control.TextInput -= Control_TextInput; + if (_window != null) + { + _window.Activated -= Window_Activated; + _window.Deactivated -= Window_Deactivated; + } + _observedPhysicalKeysBySemanticKey.Clear(); } } - protected void OnKeyPress(object sender, KeyEventArgs args) { - _pressedKeys.Add(args.Key); - + UpdateKeyStates(args, true); KeyPressed?.Invoke(this, args); } protected void OnKeyRelease(object sender, KeyEventArgs args) { - _pressedKeys.Remove(args.Key); - + UpdateKeyStates(args, false); KeyRelease?.Invoke(this, args); } - internal bool IsPressed(Key key) + internal bool IsPressed(Key key, KeyboardInputMode mode) { if (key is Key.Unbound or Key.Unknown) { return false; } - AvaloniaKeyboardMappingHelper.TryGetAvaKey(key, out AvaKey nativeKey); + if (key == Key.CapsLock) + { + return IsCapsLockOnMacOS(); + } - return _pressedKeys.Contains(nativeKey); + return mode == KeyboardInputMode.Physical + ? _physicalPressedKeys.Contains((ConfigPhysicalKey)(int)key) + : _semanticPressedKeys.Contains(key); + } + + private bool IsCapsLockOnMacOS() + { + bool currentState = false; + + try + { + if (OperatingSystem.IsMacOS()) + { + CGEventFlags flags = CGEventSourceFlagsState(CGEventSourceStateID.HIDSystemState); + currentState = (flags & CGEventFlags.AlphaShift) != 0; + } + else + { + // Fallback: use Avalonia's tracked key state (semantic CapsLock) + if (AvaloniaKeyboardMappingHelper.TryGetAvaKey(Key.CapsLock, out AvaKey nativeKey)) + { + currentState = _semanticPressedKeys.Contains(Key.CapsLock); + } + } + } + catch (Exception ex) + { + Logger.Debug?.Print(LogClass.UI, $"Failed to query CapsLock state: {ex}"); + } + + return currentState; + } + + internal void Clear(KeyboardInputMode mode) + { + lock (_pressedKeyQueueLock) + { + if (mode == KeyboardInputMode.Physical) + { + _physicalPressedKeys.Clear(); + _physicalPressedKeyQueue.Clear(); + } + else + { + _semanticPressedKeys.Clear(); + _semanticPressedKeyQueue.Clear(); + } + } } public void Clear() { - _pressedKeys.Clear(); + lock (_pressedKeyQueueLock) + { + _semanticPressedKeys.Clear(); + _physicalPressedKeys.Clear(); + _semanticPressedKeyQueue.Clear(); + _physicalPressedKeyQueue.Clear(); + } + } + + private void RestorePressedKeysAfterActivation() + { + if (!OperatingSystem.IsWindows()) + { + lock (_pressedKeyQueueLock) + { + _keysToRestoreAfterActivation.Clear(); + } + + return; + } + + lock (_pressedKeyQueueLock) + { + if (_keysToRestoreAfterActivation.Count == 0) + { + return; + } + + foreach (Key key in _keysToRestoreAfterActivation) + { + if (!TryGetWindowsVirtualKey(key, out int virtualKey) || + !IsWindowsKeyPressed(virtualKey)) + { + continue; + } + + _semanticPressedKeys.Add(key); + + ConfigPhysicalKey physicalKey = GetPhysicalKeyForSemanticKey(key); + + if (physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound) + { + _physicalPressedKeys.Add(physicalKey); + } + } + + _keysToRestoreAfterActivation.Clear(); + } + } + + private ConfigPhysicalKey GetPhysicalKeyForSemanticKey(Key key) + { + if (_observedPhysicalKeysBySemanticKey.TryGetValue(key, out ConfigPhysicalKey physicalKey)) + { + return physicalKey; + } + + return key is >= Key.Unknown and < Key.Count + ? (ConfigPhysicalKey)(int)key + : ConfigPhysicalKey.Unknown; + } + + [SupportedOSPlatform("windows")] + private static bool IsWindowsKeyPressed(int virtualKey) + { + return (Win32NativeInterop.GetAsyncKeyState(virtualKey) & 0x8000) != 0; + } + + private static bool TryGetWindowsVirtualKey(Key key, out int virtualKey) + { + switch (key) + { + case >= Key.A and <= Key.Z: + virtualKey = 'A' + (int)(key - Key.A); + return true; + case >= Key.Number0 and <= Key.Number9: + virtualKey = '0' + (int)(key - Key.Number0); + return true; + case >= Key.F1 and <= Key.F24: + virtualKey = 0x70 + (int)(key - Key.F1); + return true; + case Key.ShiftLeft: + virtualKey = 0xA0; + return true; + case Key.ShiftRight: + virtualKey = 0xA1; + return true; + case Key.ControlLeft: + virtualKey = 0xA2; + return true; + case Key.ControlRight: + virtualKey = 0xA3; + return true; + case Key.AltLeft: + virtualKey = 0xA4; + return true; + case Key.AltRight: + virtualKey = 0xA5; + return true; + case Key.WinLeft: + virtualKey = 0x5B; + return true; + case Key.WinRight: + virtualKey = 0x5C; + return true; + case Key.Menu: + virtualKey = 0x5D; + return true; + case Key.Up: + virtualKey = 0x26; + return true; + case Key.Down: + virtualKey = 0x28; + return true; + case Key.Left: + virtualKey = 0x25; + return true; + case Key.Right: + virtualKey = 0x27; + return true; + case Key.Enter: + virtualKey = 0x0D; + return true; + case Key.Escape: + virtualKey = 0x1B; + return true; + case Key.Space: + virtualKey = 0x20; + return true; + case Key.Tab: + virtualKey = 0x09; + return true; + case Key.BackSpace: + virtualKey = 0x08; + return true; + case Key.Insert: + virtualKey = 0x2D; + return true; + case Key.Delete: + virtualKey = 0x2E; + return true; + case Key.PageUp: + virtualKey = 0x21; + return true; + case Key.PageDown: + virtualKey = 0x22; + return true; + case Key.Home: + virtualKey = 0x24; + return true; + case Key.End: + virtualKey = 0x23; + return true; + case Key.CapsLock: + virtualKey = 0x14; + return true; + case Key.ScrollLock: + virtualKey = 0x91; + return true; + case Key.PrintScreen: + virtualKey = 0x2C; + return true; + case Key.Pause: + virtualKey = 0x13; + return true; + case Key.NumLock: + virtualKey = 0x90; + return true; + case Key.Clear: + virtualKey = 0x0C; + return true; + case >= Key.Keypad0 and <= Key.Keypad9: + virtualKey = 0x60 + (int)(key - Key.Keypad0); + return true; + case Key.KeypadDivide: + virtualKey = 0x6F; + return true; + case Key.KeypadMultiply: + virtualKey = 0x6A; + return true; + case Key.KeypadSubtract: + virtualKey = 0x6D; + return true; + case Key.KeypadAdd: + virtualKey = 0x6B; + return true; + case Key.KeypadDecimal: + virtualKey = 0x6E; + return true; + case Key.KeypadEnter: + virtualKey = 0x0D; + return true; + case Key.Tilde: + virtualKey = 0xC0; + return true; + case Key.Grave: + virtualKey = 0xE2; + return true; + case Key.Minus: + virtualKey = 0xBD; + return true; + case Key.Plus: + virtualKey = 0xBB; + return true; + case Key.BracketLeft: + virtualKey = 0xDB; + return true; + case Key.BracketRight: + virtualKey = 0xDD; + return true; + case Key.Semicolon: + virtualKey = 0xBA; + return true; + case Key.Quote: + virtualKey = 0xDE; + return true; + case Key.Comma: + virtualKey = 0xBC; + return true; + case Key.Period: + virtualKey = 0xBE; + return true; + case Key.Slash: + virtualKey = 0xBF; + return true; + case Key.BackSlash: + virtualKey = 0xDC; + return true; + default: + virtualKey = 0; + return false; + } + } + + internal bool TryConsumePressedKey(KeyboardInputMode mode, out Key key) + { + lock (_pressedKeyQueueLock) + { + Queue queue = mode == KeyboardInputMode.Physical ? _physicalPressedKeyQueue : _semanticPressedKeyQueue; + + if (queue.TryDequeue(out key)) + { + return true; + } + } + + key = Key.Unknown; + return false; + } + + private static void UpdateKeyState(HashSet pressedKeys, Key key, bool isPressed) + { + if (key is Key.Unknown or Key.Unbound) + { + return; + } + + if (isPressed) + { + pressedKeys.Add(key); + return; + } + + pressedKeys.Remove(key); + } + + private static void UpdateKeyState(HashSet pressedKeys, ConfigPhysicalKey key, bool isPressed) + { + if (key is ConfigPhysicalKey.Unknown or ConfigPhysicalKey.Unbound) + { + return; + } + + if (isPressed) + { + pressedKeys.Add(key); + return; + } + + pressedKeys.Remove(key); + } + + private void UpdateKeyStates(KeyEventArgs args, bool isPressed) + { + Key semanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.Key); + Key resolvedSemanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey, args.Key); + ConfigPhysicalKey physicalKey = GetPhysicalInputKey(args, semanticKey, out PhysicalKeySource physicalKeySource); + bool semanticWasPressed = _semanticPressedKeys.Contains(resolvedSemanticKey); + bool physicalWasPressed = _physicalPressedKeys.Contains(physicalKey); + bool semanticStateChanged = resolvedSemanticKey is not Key.Unknown and not Key.Unbound && semanticWasPressed != isPressed; + bool physicalStateChanged = physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound && physicalWasPressed != isPressed; + bool bufferedSemanticPress = false; + bool bufferedPhysicalPress = false; + + UpdateKeyState(_semanticPressedKeys, resolvedSemanticKey, isPressed); + UpdateKeyState(_physicalPressedKeys, physicalKey, isPressed); + + if (isPressed) + { + lock (_pressedKeyQueueLock) + { + if (!semanticWasPressed && resolvedSemanticKey is not Key.Unknown and not Key.Unbound) + { + _semanticPressedKeyQueue.Enqueue(resolvedSemanticKey); + bufferedSemanticPress = true; + } + + if (!physicalWasPressed && physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound) + { + _physicalPressedKeyQueue.Enqueue((Key)(int)physicalKey); + bufferedPhysicalPress = true; + } + } + } + + if (isPressed && + semanticKey is not Key.Unknown and not Key.Unbound && + physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound) + { + _observedPhysicalKeysBySemanticKey[semanticKey] = physicalKey; + } + + if (ConfigurationState.Instance.Logger.EnableAvaloniaLog && + (semanticStateChanged || physicalStateChanged)) + { + Logger.Info?.Print( + LogClass.UI, + $"Keyboard {(isPressed ? "down" : "up")}: avaloniaKey={args.Key}, avaloniaPhysical={args.PhysicalKey}, keySymbol={FormatKeySymbol(args.KeySymbol)}, modifiers={args.KeyModifiers}, semantic={semanticKey}, resolvedSemantic={resolvedSemanticKey}, physical={physicalKey}, physicalSource={physicalKeySource}, bufferedSemantic={bufferedSemanticPress}, bufferedPhysical={bufferedPhysicalPress}, semanticPressed={_semanticPressedKeys.Count}, physicalPressed={_physicalPressedKeys.Count}"); + } + } + + private ConfigPhysicalKey GetPhysicalInputKey(KeyEventArgs args, Key semanticKey, out PhysicalKeySource source) + { + Key key = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey); + + if (key is >= Key.Unknown and < Key.Count) + { + source = PhysicalKeySource.Direct; + return (ConfigPhysicalKey)(int)key; + } + + if (semanticKey is not Key.Unknown and not Key.Unbound && + _observedPhysicalKeysBySemanticKey.TryGetValue(semanticKey, out ConfigPhysicalKey observedPhysicalKey)) + { + source = PhysicalKeySource.ObservedFallback; + return observedPhysicalKey; + } + + source = PhysicalKeySource.Unknown; + return ConfigPhysicalKey.Unknown; + } + + private static string FormatKeySymbol(string keySymbol) + { + return string.IsNullOrEmpty(keySymbol) ? "" : keySymbol; } public void Dispose() diff --git a/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs b/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs index 4aa8692dd..b95b8c56f 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs @@ -2,6 +2,7 @@ using Ryujinx.Input; using System; using System.Collections.Generic; using AvaKey = Avalonia.Input.Key; +using AvaPhysicalKey = Avalonia.Input.PhysicalKey; namespace Ryujinx.Ava.Input { @@ -132,7 +133,8 @@ namespace Ryujinx.Ava.Input AvaKey.D8, AvaKey.D9, AvaKey.OemTilde, - AvaKey.OemTilde,AvaKey.OemMinus, + AvaKey.Oem102, + AvaKey.OemMinus, AvaKey.OemPlus, AvaKey.OemOpenBrackets, AvaKey.OemCloseBrackets, @@ -147,7 +149,149 @@ namespace Ryujinx.Ava.Input AvaKey.None ]; + private static readonly AvaPhysicalKey[] _physicalKeyMapping = + [ + // NOTE: Invalid + AvaPhysicalKey.None, + + AvaPhysicalKey.ShiftLeft, + AvaPhysicalKey.ShiftRight, + AvaPhysicalKey.ControlLeft, + AvaPhysicalKey.ControlRight, + AvaPhysicalKey.AltLeft, + AvaPhysicalKey.AltRight, + AvaPhysicalKey.MetaLeft, + AvaPhysicalKey.MetaRight, + AvaPhysicalKey.ContextMenu, + AvaPhysicalKey.F1, + AvaPhysicalKey.F2, + AvaPhysicalKey.F3, + AvaPhysicalKey.F4, + AvaPhysicalKey.F5, + AvaPhysicalKey.F6, + AvaPhysicalKey.F7, + AvaPhysicalKey.F8, + AvaPhysicalKey.F9, + AvaPhysicalKey.F10, + AvaPhysicalKey.F11, + AvaPhysicalKey.F12, + AvaPhysicalKey.F13, + AvaPhysicalKey.F14, + AvaPhysicalKey.F15, + AvaPhysicalKey.F16, + AvaPhysicalKey.F17, + AvaPhysicalKey.F18, + AvaPhysicalKey.F19, + AvaPhysicalKey.F20, + AvaPhysicalKey.F21, + AvaPhysicalKey.F22, + AvaPhysicalKey.F23, + AvaPhysicalKey.F24, + + AvaPhysicalKey.None, + AvaPhysicalKey.None, + AvaPhysicalKey.None, + AvaPhysicalKey.None, + AvaPhysicalKey.None, + AvaPhysicalKey.None, + AvaPhysicalKey.None, + AvaPhysicalKey.None, + AvaPhysicalKey.None, + AvaPhysicalKey.None, + AvaPhysicalKey.None, + + AvaPhysicalKey.ArrowUp, + AvaPhysicalKey.ArrowDown, + AvaPhysicalKey.ArrowLeft, + AvaPhysicalKey.ArrowRight, + AvaPhysicalKey.Enter, + AvaPhysicalKey.Escape, + AvaPhysicalKey.Space, + AvaPhysicalKey.Tab, + AvaPhysicalKey.Backspace, + AvaPhysicalKey.Insert, + AvaPhysicalKey.Delete, + AvaPhysicalKey.PageUp, + AvaPhysicalKey.PageDown, + AvaPhysicalKey.Home, + AvaPhysicalKey.End, + AvaPhysicalKey.CapsLock, + AvaPhysicalKey.ScrollLock, + AvaPhysicalKey.PrintScreen, + AvaPhysicalKey.Pause, + AvaPhysicalKey.NumLock, + AvaPhysicalKey.NumPadClear, + AvaPhysicalKey.NumPad0, + AvaPhysicalKey.NumPad1, + AvaPhysicalKey.NumPad2, + AvaPhysicalKey.NumPad3, + AvaPhysicalKey.NumPad4, + AvaPhysicalKey.NumPad5, + AvaPhysicalKey.NumPad6, + AvaPhysicalKey.NumPad7, + AvaPhysicalKey.NumPad8, + AvaPhysicalKey.NumPad9, + AvaPhysicalKey.NumPadDivide, + AvaPhysicalKey.NumPadMultiply, + AvaPhysicalKey.NumPadSubtract, + AvaPhysicalKey.NumPadAdd, + AvaPhysicalKey.NumPadDecimal, + AvaPhysicalKey.NumPadEnter, + AvaPhysicalKey.A, + AvaPhysicalKey.B, + AvaPhysicalKey.C, + AvaPhysicalKey.D, + AvaPhysicalKey.E, + AvaPhysicalKey.F, + AvaPhysicalKey.G, + AvaPhysicalKey.H, + AvaPhysicalKey.I, + AvaPhysicalKey.J, + AvaPhysicalKey.K, + AvaPhysicalKey.L, + AvaPhysicalKey.M, + AvaPhysicalKey.N, + AvaPhysicalKey.O, + AvaPhysicalKey.P, + AvaPhysicalKey.Q, + AvaPhysicalKey.R, + AvaPhysicalKey.S, + AvaPhysicalKey.T, + AvaPhysicalKey.U, + AvaPhysicalKey.V, + AvaPhysicalKey.W, + AvaPhysicalKey.X, + AvaPhysicalKey.Y, + AvaPhysicalKey.Z, + AvaPhysicalKey.Digit0, + AvaPhysicalKey.Digit1, + AvaPhysicalKey.Digit2, + AvaPhysicalKey.Digit3, + AvaPhysicalKey.Digit4, + AvaPhysicalKey.Digit5, + AvaPhysicalKey.Digit6, + AvaPhysicalKey.Digit7, + AvaPhysicalKey.Digit8, + AvaPhysicalKey.Digit9, + AvaPhysicalKey.Backquote, + AvaPhysicalKey.IntlBackslash, + AvaPhysicalKey.Minus, + AvaPhysicalKey.Equal, + AvaPhysicalKey.BracketLeft, + AvaPhysicalKey.BracketRight, + AvaPhysicalKey.Semicolon, + AvaPhysicalKey.Quote, + AvaPhysicalKey.Comma, + AvaPhysicalKey.Period, + AvaPhysicalKey.Slash, + AvaPhysicalKey.Backslash, + + // NOTE: invalid + AvaPhysicalKey.None + ]; + private static readonly Dictionary _avaKeyMapping; + private static readonly Dictionary _avaPhysicalKeyMapping; static AvaloniaKeyboardMappingHelper() { @@ -155,21 +299,42 @@ namespace Ryujinx.Ava.Input // NOTE: Avalonia.Input.Key is not contiguous and quite large, so use a dictionary instead of an array. _avaKeyMapping = new Dictionary(); + _avaPhysicalKeyMapping = new Dictionary(); foreach (Key key in inputKeys) { - if (TryGetAvaKey(key, out AvaKey index)) + if (TryGetAvaKey(key, out AvaKey avaKey)) { - _avaKeyMapping[index] = key; + _avaKeyMapping[avaKey] = key; + } + + if (TryGetAvaPhysicalKey(key, out AvaPhysicalKey avaPhysicalKey)) + { + _avaPhysicalKeyMapping[avaPhysicalKey] = key; } } + + // Alias additional Avalonia key values to improve non-US layout support. + _avaKeyMapping[AvaKey.Oem1] = Key.Semicolon; + _avaKeyMapping[AvaKey.Oem2] = Key.Slash; + _avaKeyMapping[AvaKey.Oem3] = Key.Tilde; + _avaKeyMapping[AvaKey.Oem4] = Key.BracketLeft; + _avaKeyMapping[AvaKey.Oem5] = Key.BackSlash; + _avaKeyMapping[AvaKey.Oem6] = Key.BracketRight; + _avaKeyMapping[AvaKey.Oem7] = Key.Quote; + _avaKeyMapping[AvaKey.OemBackslash] = Key.Grave; + _avaKeyMapping[AvaKey.Oem102] = Key.Grave; + + // Common alternates for non-US/JIS physical keys. + _avaPhysicalKeyMapping[AvaPhysicalKey.IntlRo] = Key.BackSlash; + _avaPhysicalKeyMapping[AvaPhysicalKey.IntlYen] = Key.BackSlash; } public static bool TryGetAvaKey(Key key, out AvaKey avaKey) { avaKey = AvaKey.None; - bool keyExist = (int)key < _keyMapping.Length; + bool keyExist = key < Key.Count && (int)key < _keyMapping.Length; if (keyExist) { avaKey = _keyMapping[(int)key]; @@ -178,9 +343,34 @@ namespace Ryujinx.Ava.Input return keyExist; } + public static bool TryGetAvaPhysicalKey(Key key, out AvaPhysicalKey avaPhysicalKey) + { + avaPhysicalKey = AvaPhysicalKey.None; + + bool keyExist = key < Key.Count && (int)key < _physicalKeyMapping.Length; + if (keyExist) + { + avaPhysicalKey = _physicalKeyMapping[(int)key]; + } + + return keyExist; + } + public static Key ToInputKey(AvaKey key) { return _avaKeyMapping.GetValueOrDefault(key, Key.Unknown); } + + public static Key ToInputKey(AvaPhysicalKey key) + { + return _avaPhysicalKeyMapping.GetValueOrDefault(key, Key.Unknown); + } + + public static Key ToInputKey(AvaPhysicalKey physicalKey, AvaKey key) + { + Key inputKey = ToInputKey(key); + + return inputKey != Key.Unknown ? inputKey : ToInputKey(physicalKey); + } } } diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 5d89ffdb1..9825612f6 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -1304,6 +1304,11 @@ namespace Ryujinx.Ava.Systems return false; } + if (!_viewModel.IsActive) + { + _inputManager.KeyboardDriver.Clear(); + } + NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); if (_viewModel.IsActive) diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 1b86e4f39..3fd132cff 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -13,6 +13,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Key = Ryujinx.Common.Configuration.Hid.Key; +using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; using RyuLogger = Ryujinx.Common.Logging.Logger; namespace Ryujinx.Ava.Systems.Configuration @@ -271,45 +273,45 @@ namespace Ryujinx.Ava.Systems.Configuration Id = "0", PlayerIndex = PlayerIndex.Player1, ControllerType = ControllerType.ProController, - LeftJoycon = new LeftJoyconCommonConfig + 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, + DpadUp = PhysicalKey.Up, + DpadDown = PhysicalKey.Down, + DpadLeft = PhysicalKey.Left, + DpadRight = PhysicalKey.Right, + ButtonMinus = PhysicalKey.Minus, + ButtonL = PhysicalKey.E, + ButtonZl = PhysicalKey.Q, + ButtonSl = PhysicalKey.Unbound, + ButtonSr = PhysicalKey.Unbound, }, - LeftJoyconStick = new JoyconConfigKeyboardStick + LeftJoyconStick = new JoyconConfigKeyboardStick { - StickUp = Key.W, - StickDown = Key.S, - StickLeft = Key.A, - StickRight = Key.D, - StickButton = Key.F, + StickUp = PhysicalKey.W, + StickDown = PhysicalKey.S, + StickLeft = PhysicalKey.A, + StickRight = PhysicalKey.D, + StickButton = PhysicalKey.F, }, - RightJoycon = new RightJoyconCommonConfig + 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, + ButtonA = PhysicalKey.Z, + ButtonB = PhysicalKey.X, + ButtonX = PhysicalKey.C, + ButtonY = PhysicalKey.V, + ButtonPlus = PhysicalKey.Plus, + ButtonR = PhysicalKey.U, + ButtonZr = PhysicalKey.O, + ButtonSl = PhysicalKey.Unbound, + ButtonSr = PhysicalKey.Unbound, }, - RightJoyconStick = new JoyconConfigKeyboardStick + RightJoyconStick = new JoyconConfigKeyboardStick { - StickUp = Key.I, - StickDown = Key.K, - StickLeft = Key.J, - StickRight = Key.L, - StickButton = Key.H, + StickUp = PhysicalKey.I, + StickDown = PhysicalKey.K, + StickLeft = PhysicalKey.J, + StickRight = PhysicalKey.L, + StickButton = PhysicalKey.H, }, } ]; diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs index b4b5e8029..acf8a6793 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs @@ -8,6 +8,8 @@ using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE; using System; using System.Linq; +using Key = Ryujinx.Common.Configuration.Hid.Key; +using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; namespace Ryujinx.Ava.Systems.Configuration { @@ -288,45 +290,45 @@ namespace Ryujinx.Ava.Systems.Configuration Name = "Keyboard", PlayerIndex = PlayerIndex.Player1, ControllerType = ControllerType.ProController, - LeftJoycon = new LeftJoyconCommonConfig + 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, + DpadUp = PhysicalKey.Up, + DpadDown = PhysicalKey.Down, + DpadLeft = PhysicalKey.Left, + DpadRight = PhysicalKey.Right, + ButtonMinus = PhysicalKey.Minus, + ButtonL = PhysicalKey.E, + ButtonZl = PhysicalKey.Q, + ButtonSl = PhysicalKey.Unbound, + ButtonSr = PhysicalKey.Unbound, }, - LeftJoyconStick = new JoyconConfigKeyboardStick + LeftJoyconStick = new JoyconConfigKeyboardStick { - StickUp = Key.W, - StickDown = Key.S, - StickLeft = Key.A, - StickRight = Key.D, - StickButton = Key.F, + StickUp = PhysicalKey.W, + StickDown = PhysicalKey.S, + StickLeft = PhysicalKey.A, + StickRight = PhysicalKey.D, + StickButton = PhysicalKey.F, }, - RightJoycon = new RightJoyconCommonConfig + 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, + ButtonA = PhysicalKey.Z, + ButtonB = PhysicalKey.X, + ButtonX = PhysicalKey.C, + ButtonY = PhysicalKey.V, + ButtonPlus = PhysicalKey.Plus, + ButtonR = PhysicalKey.U, + ButtonZr = PhysicalKey.O, + ButtonSl = PhysicalKey.Unbound, + ButtonSr = PhysicalKey.Unbound, }, - RightJoyconStick = new JoyconConfigKeyboardStick + RightJoyconStick = new JoyconConfigKeyboardStick { - StickUp = Key.I, - StickDown = Key.K, - StickLeft = Key.J, - StickRight = Key.L, - StickButton = Key.H, + StickUp = PhysicalKey.I, + StickDown = PhysicalKey.K, + StickLeft = PhysicalKey.J, + StickRight = PhysicalKey.L, + StickButton = PhysicalKey.H, }, } ]; diff --git a/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs b/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs index 397eab72c..0dcc86a8e 100644 --- a/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs @@ -15,6 +15,7 @@ namespace Ryujinx.Ava.UI.Applet class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler { private MainWindow _parent; + private AvaloniaKeyboardDriver _avaloniaKeyboardDriver; private readonly OffscreenTextBox _hiddenTextBox; private bool _canProcessInput; private IDisposable _textChangedSubscription; @@ -27,6 +28,7 @@ namespace Ryujinx.Ava.UI.Applet if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver) { + _avaloniaKeyboardDriver = avaloniaKeyboardDriver; avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed; avaloniaKeyboardDriver.KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease; avaloniaKeyboardDriver.TextInput += AvaloniaDynamicTextInputHandler_TextInput; @@ -65,7 +67,7 @@ namespace Ryujinx.Ava.UI.Applet private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e) { - HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key); + HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key); if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true)) { @@ -85,7 +87,7 @@ namespace Ryujinx.Ava.UI.Applet private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e) { - HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key); + HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key); if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true)) { @@ -115,11 +117,11 @@ namespace Ryujinx.Ava.UI.Applet public void Dispose() { - if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver) + if (_avaloniaKeyboardDriver != null) { - avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed; - avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease; - avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput; + _avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed; + _avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease; + _avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput; } _textChangedSubscription?.Dispose(); diff --git a/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs b/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs index a3939abf7..922897018 100644 --- a/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs +++ b/src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs @@ -1,5 +1,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Threading; +using Ryujinx.Ava.Input; using Ryujinx.Input; using Ryujinx.Input.Assigner; using System; @@ -25,6 +26,7 @@ namespace Ryujinx.Ava.UI.Helpers private bool _isWaitingForInput; private bool _shouldUnbind; + private IKeyboard _keyboard; public event EventHandler ButtonAssigned; public ButtonKeyAssigner(ToggleButton toggleButton) @@ -34,6 +36,9 @@ namespace Ryujinx.Ava.UI.Helpers public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null) { + _keyboard = keyboard; + ClearKeyboardState(_keyboard); + Dispatcher.UIThread.Post(() => { ToggledButton.IsChecked = true; @@ -82,6 +87,7 @@ namespace Ryujinx.Ava.UI.Helpers _isWaitingForInput = false; ToggledButton.IsChecked = false; + ClearKeyboardState(_keyboard); if (pressedButton.HasValue && pressedButton.Value.AsHidType() == Key.BackSpace) { @@ -98,6 +104,15 @@ namespace Ryujinx.Ava.UI.Helpers _isWaitingForInput = false; ToggledButton.IsChecked = false; _shouldUnbind = shouldUnbind; + ClearKeyboardState(_keyboard); + } + + private static void ClearKeyboardState(IKeyboard keyboard) + { + if (keyboard is AvaloniaKeyboard avaloniaKeyboard) + { + avaloniaKeyboard.Clear(); + } } } } diff --git a/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs b/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs index d153adc74..f6e042a6a 100644 --- a/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs +++ b/src/Ryujinx/UI/Helpers/Converters/KeyValueConverter.cs @@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller; using System; using System.Collections.Generic; using System.Globalization; +using Key = Ryujinx.Input.Key; namespace Ryujinx.Ava.UI.Helpers { @@ -12,79 +13,6 @@ namespace Ryujinx.Ava.UI.Helpers { public static readonly KeyValueConverter Instance = new(); - private static readonly Dictionary _keysMap = new() - { - { Key.Unknown, LocaleKeys.KeyUnknown }, - { Key.ShiftLeft, LocaleKeys.KeyShiftLeft }, - { Key.ShiftRight, LocaleKeys.KeyShiftRight }, - { Key.ControlLeft, LocaleKeys.KeyControlLeft }, - { Key.ControlRight, LocaleKeys.KeyControlRight }, - { Key.AltLeft, LocaleKeys.KeyAltLeft }, - { Key.AltRight, LocaleKeys.KeyAltRight }, - { Key.WinLeft, LocaleKeys.KeyWinLeft }, - { Key.WinRight, LocaleKeys.KeyWinRight }, - { Key.Up, LocaleKeys.KeyUp }, - { Key.Down, LocaleKeys.KeyDown }, - { Key.Left, LocaleKeys.KeyLeft }, - { Key.Right, LocaleKeys.KeyRight }, - { Key.Enter, LocaleKeys.KeyEnter }, - { Key.Escape, LocaleKeys.KeyEscape }, - { Key.Space, LocaleKeys.KeySpace }, - { Key.Tab, LocaleKeys.KeyTab }, - { Key.BackSpace, LocaleKeys.KeyBackSpace }, - { Key.Insert, LocaleKeys.KeyInsert }, - { Key.Delete, LocaleKeys.KeyDelete }, - { Key.PageUp, LocaleKeys.KeyPageUp }, - { Key.PageDown, LocaleKeys.KeyPageDown }, - { Key.Home, LocaleKeys.KeyHome }, - { Key.End, LocaleKeys.KeyEnd }, - { Key.CapsLock, LocaleKeys.KeyCapsLock }, - { Key.ScrollLock, LocaleKeys.KeyScrollLock }, - { Key.PrintScreen, LocaleKeys.KeyPrintScreen }, - { Key.Pause, LocaleKeys.KeyPause }, - { Key.NumLock, LocaleKeys.KeyNumLock }, - { Key.Clear, LocaleKeys.KeyClear }, - { Key.Keypad0, LocaleKeys.KeyKeypad0 }, - { Key.Keypad1, LocaleKeys.KeyKeypad1 }, - { Key.Keypad2, LocaleKeys.KeyKeypad2 }, - { Key.Keypad3, LocaleKeys.KeyKeypad3 }, - { Key.Keypad4, LocaleKeys.KeyKeypad4 }, - { Key.Keypad5, LocaleKeys.KeyKeypad5 }, - { Key.Keypad6, LocaleKeys.KeyKeypad6 }, - { Key.Keypad7, LocaleKeys.KeyKeypad7 }, - { Key.Keypad8, LocaleKeys.KeyKeypad8 }, - { Key.Keypad9, LocaleKeys.KeyKeypad9 }, - { Key.KeypadDivide, LocaleKeys.KeyKeypadDivide }, - { Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply }, - { Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract }, - { Key.KeypadAdd, LocaleKeys.KeyKeypadAdd }, - { Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal }, - { Key.KeypadEnter, LocaleKeys.KeyKeypadEnter }, - { Key.Number0, LocaleKeys.KeyNumber0 }, - { Key.Number1, LocaleKeys.KeyNumber1 }, - { Key.Number2, LocaleKeys.KeyNumber2 }, - { Key.Number3, LocaleKeys.KeyNumber3 }, - { Key.Number4, LocaleKeys.KeyNumber4 }, - { Key.Number5, LocaleKeys.KeyNumber5 }, - { Key.Number6, LocaleKeys.KeyNumber6 }, - { Key.Number7, LocaleKeys.KeyNumber7 }, - { Key.Number8, LocaleKeys.KeyNumber8 }, - { Key.Number9, LocaleKeys.KeyNumber9 }, - { Key.Tilde, LocaleKeys.KeyTilde }, - { Key.Grave, LocaleKeys.KeyGrave }, - { Key.Minus, LocaleKeys.KeyMinus }, - { Key.Plus, LocaleKeys.KeyPlus }, - { Key.BracketLeft, LocaleKeys.KeyBracketLeft }, - { Key.BracketRight, LocaleKeys.KeyBracketRight }, - { Key.Semicolon, LocaleKeys.KeySemicolon }, - { Key.Quote, LocaleKeys.KeyQuote }, - { Key.Comma, LocaleKeys.KeyComma }, - { Key.Period, LocaleKeys.KeyPeriod }, - { Key.Slash, LocaleKeys.KeySlash }, - { Key.BackSlash, LocaleKeys.KeyBackSlash }, - { Key.Unbound, LocaleKeys.KeyUnbound }, - }; - private static readonly Dictionary _gamepadInputIdMap = new() { { GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick }, @@ -110,78 +38,40 @@ namespace Ryujinx.Ava.UI.Helpers { GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0}, { GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1}, { GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1}, - { GamepadInputId.Unbound, LocaleKeys.KeyUnbound}, + { GamepadInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound}, }; private static readonly Dictionary _stickInputIdMap = new() { { StickInputId.Left, LocaleKeys.StickLeft}, { StickInputId.Right, LocaleKeys.StickRight}, - { StickInputId.Unbound, LocaleKeys.KeyUnbound}, + { StickInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound}, }; public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - string keyString = string.Empty; - LocaleKeys localeKey; - - switch (value) + return value switch { - case Key key: - if (_keysMap.TryGetValue(key, out localeKey)) - { - if (OperatingSystem.IsMacOS()) - { - localeKey = localeKey switch - { - LocaleKeys.KeyControlLeft => LocaleKeys.KeyMacControlLeft, - LocaleKeys.KeyControlRight => LocaleKeys.KeyMacControlRight, - LocaleKeys.KeyAltLeft => LocaleKeys.KeyMacAltLeft, - LocaleKeys.KeyAltRight => LocaleKeys.KeyMacAltRight, - LocaleKeys.KeyWinLeft => LocaleKeys.KeyMacWinLeft, - LocaleKeys.KeyWinRight => LocaleKeys.KeyMacWinRight, - _ => localeKey - }; - } - - keyString = LocaleManager.Instance[localeKey]; - } - else - { - keyString = key.ToString(); - } - - break; - case GamepadInputId gamepadInputId: - if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey)) - { - keyString = LocaleManager.Instance[localeKey]; - } - else - { - keyString = gamepadInputId.ToString(); - } - - break; - case StickInputId stickInputId: - if (_stickInputIdMap.TryGetValue(stickInputId, out localeKey)) - { - keyString = LocaleManager.Instance[localeKey]; - } - else - { - keyString = stickInputId.ToString(); - } - - break; - } - - return keyString; + Key key => KeyboardLayoutLocaleHelper.TryGetSemanticLabel(key, out string localizedKeyLabel) + ? localizedKeyLabel + : key.ToString(), + PhysicalKey physicalKey => PhysicalKeyLabelHelper.GetDisplayString(physicalKey), + GamepadInputId gamepadInputId => GetLocalizedMappedValue(gamepadInputId, _gamepadInputIdMap), + StickInputId stickInputId => GetLocalizedMappedValue(stickInputId, _stickInputIdMap), + _ => string.Empty, + }; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException(); } + + private static string GetLocalizedMappedValue(T value, IReadOnlyDictionary map) where T : notnull + { + return map.TryGetValue(value, out LocaleKeys localeKey) + ? LocaleManager.Instance[localeKey] + : value.ToString(); + } } } diff --git a/src/Ryujinx/UI/Helpers/InputDeviceNameConverter.cs b/src/Ryujinx/UI/Helpers/InputDeviceNameConverter.cs new file mode 100644 index 000000000..44abad47c --- /dev/null +++ b/src/Ryujinx/UI/Helpers/InputDeviceNameConverter.cs @@ -0,0 +1,28 @@ +using Avalonia.Data.Converters; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.UI.Models; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class InputDeviceNameConverter : MarkupExtension, IValueConverter + { + public static readonly InputDeviceNameConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is ValueTuple device ? device.Item3 : string.Empty; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return Instance; + } + } +} diff --git a/src/Ryujinx/UI/Helpers/KeyboardLayoutLocaleHelper.cs b/src/Ryujinx/UI/Helpers/KeyboardLayoutLocaleHelper.cs new file mode 100644 index 000000000..17f6a8b56 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/KeyboardLayoutLocaleHelper.cs @@ -0,0 +1,142 @@ +using Ryujinx.Ava.Common.Locale; +using System; +using System.Collections.Generic; +using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; +using InputKey = Ryujinx.Input.Key; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal static class KeyboardLayoutLocaleHelper + { + private static readonly Dictionary _sharedLocalizedKeysMap = new() + { + [InputKey.Unknown] = LocaleKeys.KeyboardLayout_KeyUnknown, + [InputKey.ShiftLeft] = LocaleKeys.KeyboardLayout_KeyShiftLeft, + [InputKey.ShiftRight] = LocaleKeys.KeyboardLayout_KeyShiftRight, + [InputKey.ControlLeft] = LocaleKeys.KeyboardLayout_KeyControlLeft, + [InputKey.ControlRight] = LocaleKeys.KeyboardLayout_KeyControlRight, + [InputKey.AltLeft] = LocaleKeys.KeyboardLayout_KeyAltLeft, + [InputKey.AltRight] = LocaleKeys.KeyboardLayout_KeyAltRight, + [InputKey.WinLeft] = LocaleKeys.KeyboardLayout_KeyWinLeft, + [InputKey.WinRight] = LocaleKeys.KeyboardLayout_KeyWinRight, + [InputKey.Up] = LocaleKeys.KeyboardLayout_KeyUp, + [InputKey.Down] = LocaleKeys.KeyboardLayout_KeyDown, + [InputKey.Left] = LocaleKeys.KeyboardLayout_KeyLeft, + [InputKey.Right] = LocaleKeys.KeyboardLayout_KeyRight, + [InputKey.Enter] = LocaleKeys.KeyboardLayout_KeyEnter, + [InputKey.Escape] = LocaleKeys.KeyboardLayout_KeyEscape, + [InputKey.Space] = LocaleKeys.KeyboardLayout_KeySpace, + [InputKey.Tab] = LocaleKeys.KeyboardLayout_KeyTab, + [InputKey.BackSpace] = LocaleKeys.KeyboardLayout_KeyBackSpace, + [InputKey.Insert] = LocaleKeys.KeyboardLayout_KeyInsert, + [InputKey.Delete] = LocaleKeys.KeyboardLayout_KeyDelete, + [InputKey.PageUp] = LocaleKeys.KeyboardLayout_KeyPageUp, + [InputKey.PageDown] = LocaleKeys.KeyboardLayout_KeyPageDown, + [InputKey.Home] = LocaleKeys.KeyboardLayout_KeyHome, + [InputKey.End] = LocaleKeys.KeyboardLayout_KeyEnd, + [InputKey.CapsLock] = LocaleKeys.KeyboardLayout_KeyCapsLock, + [InputKey.ScrollLock] = LocaleKeys.KeyboardLayout_KeyScrollLock, + [InputKey.PrintScreen] = LocaleKeys.KeyboardLayout_KeyPrintScreen, + [InputKey.Pause] = LocaleKeys.KeyboardLayout_KeyPause, + [InputKey.NumLock] = LocaleKeys.KeyboardLayout_KeyNumLock, + [InputKey.Clear] = LocaleKeys.KeyboardLayout_KeyClear, + [InputKey.Keypad0] = LocaleKeys.KeyboardLayout_KeyKeypad0, + [InputKey.Keypad1] = LocaleKeys.KeyboardLayout_KeyKeypad1, + [InputKey.Keypad2] = LocaleKeys.KeyboardLayout_KeyKeypad2, + [InputKey.Keypad3] = LocaleKeys.KeyboardLayout_KeyKeypad3, + [InputKey.Keypad4] = LocaleKeys.KeyboardLayout_KeyKeypad4, + [InputKey.Keypad5] = LocaleKeys.KeyboardLayout_KeyKeypad5, + [InputKey.Keypad6] = LocaleKeys.KeyboardLayout_KeyKeypad6, + [InputKey.Keypad7] = LocaleKeys.KeyboardLayout_KeyKeypad7, + [InputKey.Keypad8] = LocaleKeys.KeyboardLayout_KeyKeypad8, + [InputKey.Keypad9] = LocaleKeys.KeyboardLayout_KeyKeypad9, + [InputKey.KeypadDivide] = LocaleKeys.KeyboardLayout_KeyKeypadDivide, + [InputKey.KeypadMultiply] = LocaleKeys.KeyboardLayout_KeyKeypadMultiply, + [InputKey.KeypadSubtract] = LocaleKeys.KeyboardLayout_KeyKeypadSubtract, + [InputKey.KeypadAdd] = LocaleKeys.KeyboardLayout_KeyKeypadAdd, + [InputKey.KeypadDecimal] = LocaleKeys.KeyboardLayout_KeyKeypadDecimal, + [InputKey.KeypadEnter] = LocaleKeys.KeyboardLayout_KeyKeypadEnter, + [InputKey.Unbound] = LocaleKeys.KeyboardLayout_KeyUnbound, + }; + + private static readonly Dictionary _semanticPrintableKeysMap = new() + { + [InputKey.Number0] = LocaleKeys.KeyboardLayout_KeyNumber0, + [InputKey.Number1] = LocaleKeys.KeyboardLayout_KeyNumber1, + [InputKey.Number2] = LocaleKeys.KeyboardLayout_KeyNumber2, + [InputKey.Number3] = LocaleKeys.KeyboardLayout_KeyNumber3, + [InputKey.Number4] = LocaleKeys.KeyboardLayout_KeyNumber4, + [InputKey.Number5] = LocaleKeys.KeyboardLayout_KeyNumber5, + [InputKey.Number6] = LocaleKeys.KeyboardLayout_KeyNumber6, + [InputKey.Number7] = LocaleKeys.KeyboardLayout_KeyNumber7, + [InputKey.Number8] = LocaleKeys.KeyboardLayout_KeyNumber8, + [InputKey.Number9] = LocaleKeys.KeyboardLayout_KeyNumber9, + [InputKey.Tilde] = LocaleKeys.KeyboardLayout_KeyTilde, + [InputKey.Grave] = LocaleKeys.KeyboardLayout_KeyGrave, + [InputKey.Minus] = LocaleKeys.KeyboardLayout_KeyMinus, + [InputKey.Plus] = LocaleKeys.KeyboardLayout_KeyPlus, + [InputKey.BracketLeft] = LocaleKeys.KeyboardLayout_KeyBracketLeft, + [InputKey.BracketRight] = LocaleKeys.KeyboardLayout_KeyBracketRight, + [InputKey.Semicolon] = LocaleKeys.KeyboardLayout_KeySemicolon, + [InputKey.Quote] = LocaleKeys.KeyboardLayout_KeyQuote, + [InputKey.Comma] = LocaleKeys.KeyboardLayout_KeyComma, + [InputKey.Period] = LocaleKeys.KeyboardLayout_KeyPeriod, + [InputKey.Slash] = LocaleKeys.KeyboardLayout_KeySlash, + [InputKey.BackSlash] = LocaleKeys.KeyboardLayout_KeyBackSlash, + }; + + public static bool TryGetSemanticLabel(InputKey key, out string label) + { + if (TryGetSemanticLocaleKey(key, out LocaleKeys localeKey)) + { + label = GetLocalizedString(localeKey); + return true; + } + + label = string.Empty; + return false; + } + + public static bool TryGetPhysicalLabel(ConfigPhysicalKey key, out string label) + { + if (TryGetPhysicalLocaleKey(key, out LocaleKeys localeKey)) + { + label = GetLocalizedString(localeKey); + return true; + } + + label = string.Empty; + return false; + } + + public static bool TryGetPhysicalLocaleKey(ConfigPhysicalKey key, out LocaleKeys localeKey) + { + return _sharedLocalizedKeysMap.TryGetValue((InputKey)(int)key, out localeKey); + } + + private static bool TryGetSemanticLocaleKey(InputKey key, out LocaleKeys localeKey) + { + return _sharedLocalizedKeysMap.TryGetValue(key, out localeKey) || + _semanticPrintableKeysMap.TryGetValue(key, out localeKey); + } + + private static string GetLocalizedString(LocaleKeys localeKey) + { + if (OperatingSystem.IsMacOS()) + { + localeKey = localeKey switch + { + LocaleKeys.KeyboardLayout_KeyControlLeft => LocaleKeys.KeyboardLayout_KeyMacControlLeft, + LocaleKeys.KeyboardLayout_KeyControlRight => LocaleKeys.KeyboardLayout_KeyMacControlRight, + LocaleKeys.KeyboardLayout_KeyAltLeft => LocaleKeys.KeyboardLayout_KeyMacAltLeft, + LocaleKeys.KeyboardLayout_KeyAltRight => LocaleKeys.KeyboardLayout_KeyMacAltRight, + LocaleKeys.KeyboardLayout_KeyWinLeft => LocaleKeys.KeyboardLayout_KeyMacWinLeft, + LocaleKeys.KeyboardLayout_KeyWinRight => LocaleKeys.KeyboardLayout_KeyMacWinRight, + _ => localeKey + }; + } + + return LocaleManager.Instance[localeKey]; + } + } +} diff --git a/src/Ryujinx/UI/Helpers/PhysicalKeyLabelHelper.cs b/src/Ryujinx/UI/Helpers/PhysicalKeyLabelHelper.cs new file mode 100644 index 000000000..89a1e891b --- /dev/null +++ b/src/Ryujinx/UI/Helpers/PhysicalKeyLabelHelper.cs @@ -0,0 +1,234 @@ +using Avalonia.Input; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using AvaPhysicalKey = Avalonia.Input.PhysicalKey; +using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; +using InputKey = Ryujinx.Input.Key; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal static class PhysicalKeyLabelHelper + { + private const string ObservedLabelsFileName = "keyboard_layout_labels.json"; + private static readonly ConcurrentDictionary _observedLayoutLabels = new(); + private static readonly object _observedLayoutLabelsLock = new(); + private static readonly JsonSerializerOptions _serializerOptions = new() + { + WriteIndented = true, + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip + }; + private static bool _observedLayoutLabelsLoaded; + public static event Action LabelsChanged; + + public static string GetDisplayString(ConfigPhysicalKey key) + { + EnsureObservedLayoutLabelsLoaded(); + + if (KeyboardLayoutLocaleHelper.TryGetPhysicalLabel(key, out string localizedLabel)) + { + return localizedLabel; + } + + if (_observedLayoutLabels.TryGetValue(key, out string observedLabel)) + { + return observedLabel; + } + + if (TryGetFallbackPrintableKeyLabel(key, out string label)) + { + return label; + } + + return key.ToString(); + } + + public static void ObserveKeyPress(object sender, KeyEventArgs args) + { + EnsureObservedLayoutLabelsLoaded(); + + if (args.KeyModifiers != KeyModifiers.None) + { + return; + } + + InputKey inputKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey); + if (!TryConvertToConfigPhysicalKey(inputKey, out ConfigPhysicalKey physicalKey) || + KeyboardLayoutLocaleHelper.TryGetPhysicalLocaleKey(physicalKey, out _)) + { + return; + } + + if (TryNormalizeObservedPrintableLabel(args.KeySymbol, out string label)) + { + if (IsCapsLockOn() && !char.IsLetter(label[0])) + { + return; + } + + if (_observedLayoutLabels.TryGetValue(physicalKey, out string existingLabel) && existingLabel == label) + { + return; + } + + _observedLayoutLabels[physicalKey] = label; + SaveObservedLayoutLabels(); + LabelsChanged?.Invoke(); + } + } + + private static void EnsureObservedLayoutLabelsLoaded() + { + if (_observedLayoutLabelsLoaded) + { + return; + } + + lock (_observedLayoutLabelsLock) + { + if (_observedLayoutLabelsLoaded) + { + return; + } + + string labelsPath = GetObservedLabelsPath(); + if (!File.Exists(labelsPath)) + { + _observedLayoutLabelsLoaded = true; + return; + } + + try + { + string labelsJson = File.ReadAllText(labelsPath); + Dictionary? labels = JsonSerializer.Deserialize>(labelsJson, _serializerOptions); + + if (labels != null) + { + foreach ((string key, string value) in labels) + { + if (Enum.TryParse(key, out ConfigPhysicalKey physicalKey) && + !string.IsNullOrEmpty(value)) + { + _observedLayoutLabels[physicalKey] = value; + } + } + } + } + catch (Exception ex) + { + Logger.Warning?.Print(LogClass.UI, $"Unable to load observed keyboard layout labels from '{labelsPath}': {ex.Message}"); + } + finally + { + _observedLayoutLabelsLoaded = true; + } + } + } + + private static void SaveObservedLayoutLabels() + { + lock (_observedLayoutLabelsLock) + { + try + { + Dictionary labels = new(); + + foreach ((ConfigPhysicalKey key, string value) in _observedLayoutLabels) + { + labels[key.ToString()] = value; + } + + File.WriteAllText(GetObservedLabelsPath(), JsonSerializer.Serialize(labels, _serializerOptions)); + } + catch (Exception ex) + { + Logger.Warning?.Print(LogClass.UI, $"Unable to save observed keyboard layout labels: {ex.Message}"); + } + } + } + + private static string GetObservedLabelsPath() + { + return Path.Combine(AppDataManager.BaseDirPath, ObservedLabelsFileName); + } + + private static bool TryGetFallbackPrintableKeyLabel(ConfigPhysicalKey key, out string label) + { + // The legacy enum name for the ISO extra key is misleading, so give it a distinct physical label. + if (key == ConfigPhysicalKey.Grave) + { + label = "<>"; + return true; + } + + if (!AvaloniaKeyboardMappingHelper.TryGetAvaPhysicalKey((InputKey)(int)key, out AvaPhysicalKey avaPhysicalKey)) + { + label = string.Empty; + return false; + } + + label = PhysicalKeyExtensions.ToQwertyKeySymbol(avaPhysicalKey, false); + + if (string.IsNullOrEmpty(label) || label.Length != 1 || char.IsControl(label[0])) + { + label = string.Empty; + return false; + } + + if (char.IsLetter(label[0])) + { + label = char.ToUpperInvariant(label[0]).ToString(); + } + + return true; + } + + private static bool IsCapsLockOn() + { + try + { + return OperatingSystem.IsWindows() && Console.CapsLock; + } + catch (Exception ex) + { + Logger.Debug?.Print(LogClass.UI, $"CapsLock state query failed: {ex.Message}"); + return false; + } + } + + private static bool TryNormalizeObservedPrintableLabel(string keySymbol, out string label) + { + if (string.IsNullOrEmpty(keySymbol) || keySymbol.Length != 1 || char.IsControl(keySymbol[0])) + { + label = string.Empty; + return false; + } + + label = char.IsLetter(keySymbol[0]) + ? char.ToUpperInvariant(keySymbol[0]).ToString() + : keySymbol; + + return true; + } + + private static bool TryConvertToConfigPhysicalKey(InputKey key, out ConfigPhysicalKey physicalKey) + { + if (key is >= InputKey.Unknown and < InputKey.Count) + { + physicalKey = (ConfigPhysicalKey)(int)key; + return true; + } + + physicalKey = ConfigPhysicalKey.Unknown; + return false; + } + } +} diff --git a/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs b/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs index de51d9d70..6b27162fd 100644 --- a/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs +++ b/src/Ryujinx/UI/Models/Input/KeyboardInputConfig.cs @@ -13,88 +13,88 @@ namespace Ryujinx.Ava.UI.Models.Input public PlayerIndex PlayerIndex { get; set; } [ObservableProperty] - public partial Key LeftStickUp { get; set; } + public partial PhysicalKey LeftStickUp { get; set; } [ObservableProperty] - public partial Key LeftStickDown { get; set; } + public partial PhysicalKey LeftStickDown { get; set; } [ObservableProperty] - public partial Key LeftStickLeft { get; set; } + public partial PhysicalKey LeftStickLeft { get; set; } [ObservableProperty] - public partial Key LeftStickRight { get; set; } + public partial PhysicalKey LeftStickRight { get; set; } [ObservableProperty] - public partial Key LeftStickButton { get; set; } + public partial PhysicalKey LeftStickButton { get; set; } [ObservableProperty] - public partial Key RightStickUp { get; set; } + public partial PhysicalKey RightStickUp { get; set; } [ObservableProperty] - public partial Key RightStickDown { get; set; } + public partial PhysicalKey RightStickDown { get; set; } [ObservableProperty] - public partial Key RightStickLeft { get; set; } + public partial PhysicalKey RightStickLeft { get; set; } [ObservableProperty] - public partial Key RightStickRight { get; set; } + public partial PhysicalKey RightStickRight { get; set; } [ObservableProperty] - public partial Key RightStickButton { get; set; } + public partial PhysicalKey RightStickButton { get; set; } [ObservableProperty] - public partial Key DpadUp { get; set; } + public partial PhysicalKey DpadUp { get; set; } [ObservableProperty] - public partial Key DpadDown { get; set; } + public partial PhysicalKey DpadDown { get; set; } [ObservableProperty] - public partial Key DpadLeft { get; set; } + public partial PhysicalKey DpadLeft { get; set; } [ObservableProperty] - public partial Key DpadRight { get; set; } + public partial PhysicalKey DpadRight { get; set; } [ObservableProperty] - public partial Key ButtonMinus { get; set; } + public partial PhysicalKey ButtonMinus { get; set; } [ObservableProperty] - public partial Key ButtonPlus { get; set; } + public partial PhysicalKey ButtonPlus { get; set; } [ObservableProperty] - public partial Key ButtonA { get; set; } + public partial PhysicalKey ButtonA { get; set; } [ObservableProperty] - public partial Key ButtonB { get; set; } + public partial PhysicalKey ButtonB { get; set; } [ObservableProperty] - public partial Key ButtonX { get; set; } + public partial PhysicalKey ButtonX { get; set; } [ObservableProperty] - public partial Key ButtonY { get; set; } + public partial PhysicalKey ButtonY { get; set; } [ObservableProperty] - public partial Key ButtonL { get; set; } + public partial PhysicalKey ButtonL { get; set; } [ObservableProperty] - public partial Key ButtonR { get; set; } + public partial PhysicalKey ButtonR { get; set; } [ObservableProperty] - public partial Key ButtonZl { get; set; } + public partial PhysicalKey ButtonZl { get; set; } [ObservableProperty] - public partial Key ButtonZr { get; set; } + public partial PhysicalKey ButtonZr { get; set; } [ObservableProperty] - public partial Key LeftButtonSl { get; set; } + public partial PhysicalKey LeftButtonSl { get; set; } [ObservableProperty] - public partial Key LeftButtonSr { get; set; } + public partial PhysicalKey LeftButtonSr { get; set; } [ObservableProperty] - public partial Key RightButtonSl { get; set; } + public partial PhysicalKey RightButtonSl { get; set; } [ObservableProperty] - public partial Key RightButtonSr { get; set; } + public partial PhysicalKey RightButtonSr { get; set; } public KeyboardInputConfig(InputConfig config) { @@ -153,7 +153,7 @@ namespace Ryujinx.Ava.UI.Models.Input Backend = InputBackendType.WindowKeyboard, PlayerIndex = PlayerIndex, ControllerType = ControllerType, - LeftJoycon = new LeftJoyconCommonConfig + LeftJoycon = new LeftJoyconCommonConfig { DpadUp = DpadUp, DpadDown = DpadDown, @@ -165,7 +165,7 @@ namespace Ryujinx.Ava.UI.Models.Input ButtonSl = LeftButtonSl, ButtonSr = LeftButtonSr, }, - RightJoycon = new RightJoyconCommonConfig + RightJoycon = new RightJoyconCommonConfig { ButtonA = ButtonA, ButtonB = ButtonB, @@ -177,7 +177,7 @@ namespace Ryujinx.Ava.UI.Models.Input ButtonR = ButtonR, ButtonZr = ButtonZr, }, - LeftJoyconStick = new JoyconConfigKeyboardStick + LeftJoyconStick = new JoyconConfigKeyboardStick { StickUp = LeftStickUp, StickDown = LeftStickDown, @@ -185,7 +185,7 @@ namespace Ryujinx.Ava.UI.Models.Input StickLeft = LeftStickLeft, StickButton = LeftStickButton, }, - RightJoyconStick = new JoyconConfigKeyboardStick + RightJoyconStick = new JoyconConfigKeyboardStick { StickUp = RightStickUp, StickDown = RightStickDown, @@ -198,5 +198,37 @@ namespace Ryujinx.Ava.UI.Models.Input return config; } + + public void NotifyKeyLabelsChanged() + { + OnPropertiesChanged(nameof(LeftStickUp), + nameof(LeftStickDown), + nameof(LeftStickLeft), + nameof(LeftStickRight), + nameof(LeftStickButton), + nameof(RightStickUp), + nameof(RightStickDown), + nameof(RightStickLeft), + nameof(RightStickRight), + nameof(RightStickButton), + nameof(DpadUp), + nameof(DpadDown), + nameof(DpadLeft), + nameof(DpadRight), + nameof(ButtonMinus), + nameof(ButtonPlus), + nameof(ButtonA), + nameof(ButtonB), + nameof(ButtonX), + nameof(ButtonY), + nameof(ButtonL), + nameof(ButtonR), + nameof(ButtonZl), + nameof(ButtonZr), + nameof(LeftButtonSl), + nameof(LeftButtonSr), + nameof(RightButtonSl), + nameof(RightButtonSr)); + } } } diff --git a/src/Ryujinx/UI/Models/Input/StickVisualizer.cs b/src/Ryujinx/UI/Models/Input/StickVisualizer.cs index f88f4ea72..2a2c41bfc 100644 --- a/src/Ryujinx/UI/Models/Input/StickVisualizer.cs +++ b/src/Ryujinx/UI/Models/Input/StickVisualizer.cs @@ -1,5 +1,6 @@ using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Common.Logging; using Ryujinx.Input; using System; using System.Threading; @@ -117,6 +118,11 @@ namespace Ryujinx.Ava.UI.Models.Input public void UpdateConfig(object config) { + KeyboardConfig = null; + GamepadConfig = null; + UiStickLeft = (0f, 0f); + UiStickRight = (0f, 0f); + if (config is ControllerInputViewModel padConfig) { GamepadConfig = padConfig.Config; @@ -145,76 +151,86 @@ namespace Ryujinx.Ava.UI.Models.Input leftBuffer = (0f, 0f); rightBuffer = (0f, 0f); - switch (Type) + try { - case DeviceType.Keyboard: - IKeyboard keyboard = (IKeyboard)Parent.AvaloniaKeyboardDriver.GetGamepad("0"); + switch (Type) + { + case DeviceType.Keyboard: + IKeyboard keyboard = Parent?.AvaloniaKeyboardDriver?.GetGamepad("0") as IKeyboard; - if (keyboard != null) - { - KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot(); - - if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight)) + if (keyboard != null && KeyboardConfig != null) { - leftBuffer.Item1 += 1; + KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot(); + + if (snapshot.IsPressed(KeyboardConfig.LeftStickRight)) + { + leftBuffer.Item1 += 1; + } + + if (snapshot.IsPressed(KeyboardConfig.LeftStickLeft)) + { + leftBuffer.Item1 -= 1; + } + + if (snapshot.IsPressed(KeyboardConfig.LeftStickUp)) + { + leftBuffer.Item2 += 1; + } + + if (snapshot.IsPressed(KeyboardConfig.LeftStickDown)) + { + leftBuffer.Item2 -= 1; + } + + if (snapshot.IsPressed(KeyboardConfig.RightStickRight)) + { + rightBuffer.Item1 += 1; + } + + if (snapshot.IsPressed(KeyboardConfig.RightStickLeft)) + { + rightBuffer.Item1 -= 1; + } + + if (snapshot.IsPressed(KeyboardConfig.RightStickUp)) + { + rightBuffer.Item2 += 1; + } + + if (snapshot.IsPressed(KeyboardConfig.RightStickDown)) + { + rightBuffer.Item2 -= 1; + } + + UiStickLeft = leftBuffer; + UiStickRight = rightBuffer; } - if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft)) + break; + + case DeviceType.Controller: + IGamepad controller = Parent?.SelectedGamepad; + + if (controller is IKeyboard) { - leftBuffer.Item1 -= 1; + } + else if (controller != null && GamepadConfig != null) + { + leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick); + rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick); } - if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp)) - { - leftBuffer.Item2 += 1; - } + break; - if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown)) - { - leftBuffer.Item2 -= 1; - } - - if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight)) - { - rightBuffer.Item1 += 1; - } - - if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft)) - { - rightBuffer.Item1 -= 1; - } - - if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp)) - { - rightBuffer.Item2 += 1; - } - - if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown)) - { - rightBuffer.Item2 -= 1; - } - - UiStickLeft = leftBuffer; - UiStickRight = rightBuffer; - } - - break; - - case DeviceType.Controller: - IGamepad controller = Parent.SelectedGamepad; - - if (controller != null) - { - leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick); - rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick); - } - - break; - - case DeviceType.None: - break; - default: - throw new ArgumentException($"Unable to poll device type \"{Type}\""); + case DeviceType.None: + break; + default: + throw new ArgumentException($"Unable to poll device type \"{Type}\""); + } + } + catch (Exception ex) when (ex is ObjectDisposedException || ex is NullReferenceException || ex is NotSupportedException) + { + Logger.Debug?.Print(LogClass.UI, $"StickVisualizer polling failed: {ex}"); } UiStickLeft = leftBuffer; diff --git a/src/Ryujinx/UI/Renderer/RendererHost.cs b/src/Ryujinx/UI/Renderer/RendererHost.cs index 9d24fbbad..df1d77dc5 100644 --- a/src/Ryujinx/UI/Renderer/RendererHost.cs +++ b/src/Ryujinx/UI/Renderer/RendererHost.cs @@ -45,7 +45,6 @@ namespace Ryujinx.Ava.UI.Renderer Content = EmbeddedWindow; } - public void Dispose() { if (EmbeddedWindow != null) diff --git a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs index 2949b69a8..2df530cb7 100644 --- a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs @@ -88,19 +88,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public async void ShowMotionConfig() { await MotionInputView.Show(this); - ParentModel.IsModified = true; + ParentModel.RefreshModifiedState(); } public async void ShowRumbleConfig() { await RumbleInputView.Show(this); - ParentModel.IsModified = true; + ParentModel.RefreshModifiedState(); } public async void ShowLedConfig() { await LedInputView.Show(this); - ParentModel.IsModified = true; + ParentModel.RefreshModifiedState(); } public void OnParentModelChanged() diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 68559d6c1..945b84a33 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -1,6 +1,7 @@ using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Svg.Skia; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using Gommon; using Ryujinx.Ava.Common.Locale; @@ -14,11 +15,11 @@ using Ryujinx.Common; using Ryujinx.Common.Configuration; 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 Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.Input; +using Ryujinx.Input.SDL3; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -26,9 +27,7 @@ using System.Drawing; using System.IO; using System.Linq; using System.Text.Json; -using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; -using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; -using Key = Ryujinx.Common.Configuration.Hid.Key; +using System.Threading.Tasks; namespace Ryujinx.Ava.UI.ViewModels.Input { @@ -42,6 +41,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input private const string KeyboardString = "keyboard"; private const string ControllerString = "controller"; private readonly MainWindow _mainWindow; + private Control _keyboardDriverControl; private PlayerIndex _playerId; private PlayerIndex _playerIdChoose; @@ -65,18 +65,24 @@ namespace Ryujinx.Ava.UI.ViewModels.Input private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public IGamepadDriver AvaloniaKeyboardDriver { get; } + public IGamepadDriver AvaloniaKeyboardDriver { get; private set; } public IGamepad SelectedGamepad { get; private set { + if (!ReferenceEquals(field, value)) + { + field?.Dispose(); + } + Rainbow.Reset(); field = value; - if (ConfigViewModel is ControllerInputViewModel { Config.UseRainbowLed: true }) + if ((field?.Features & GamepadFeaturesFlag.Led) != 0 && + ConfigViewModel is ControllerInputViewModel { Config.UseRainbowLed: true }) Rainbow.Updated += (ref Color color) => field.SetLed((uint)color.ToArgb()); OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed)); @@ -89,7 +95,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; } internal ObservableCollection Controllers { get; set; } public AvaloniaList ProfilesList { get; set; } - public AvaloniaList DeviceList { get; set; } public bool UseGlobalConfig; @@ -99,7 +104,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public bool IsKeyboard => !IsController; public bool IsRight { get; set; } public bool IsLeft { get; set; } - public string RevertDeviceId { get; set; } public bool HasLed => (SelectedGamepad.Features & GamepadFeaturesFlag.Led) != 0; public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense"); @@ -163,7 +167,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input LoadDevice(); LoadProfiles(); - RevertDeviceId = Devices[Device].Id; _isLoaded = true; _isChangeTrackingActive = true; OnPropertyChanged(); @@ -175,52 +178,58 @@ namespace Ryujinx.Ava.UI.ViewModels.Input get => _controller; set { - MarkAsChanged(); + int controllerIndex = value < 0 ? 0 : value; - _controller = value; - - if (_controller == -1) + if (controllerIndex == _controller) { - _controller = 0; + return; } - if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1) - { - ControllerType controller = Controllers[_controller].Type; - - IsLeft = true; - IsRight = true; - - switch (controller) - { - case ControllerType.Handheld: - ControllerImage = JoyConPairResource; - break; - case ControllerType.ProController: - ControllerImage = ProControllerResource; - break; - case ControllerType.JoyconPair: - ControllerImage = JoyConPairResource; - break; - case ControllerType.JoyconLeft: - ControllerImage = JoyConLeftResource; - IsRight = false; - break; - case ControllerType.JoyconRight: - ControllerImage = JoyConRightResource; - IsLeft = false; - break; - } - - LoadInputDriver(); - LoadProfiles(); - } - - OnPropertyChanged(); - NotifyChanges(); + ApplyControllerSelection(controllerIndex); + RefreshModifiedState(); } } + private void ApplyControllerSelection(int controllerIndex) + { + _controller = controllerIndex; + + if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1) + { + ControllerType controller = Controllers[_controller].Type; + + IsLeft = true; + IsRight = true; + + switch (controller) + { + case ControllerType.Handheld: + ControllerImage = JoyConPairResource; + break; + case ControllerType.ProController: + ControllerImage = ProControllerResource; + break; + case ControllerType.JoyconPair: + ControllerImage = JoyConPairResource; + break; + case ControllerType.JoyconLeft: + ControllerImage = JoyConLeftResource; + IsRight = false; + break; + case ControllerType.JoyconRight: + ControllerImage = JoyConRightResource; + IsLeft = false; + break; + } + + LoadInputDriver(); + LoadProfiles(); + } + + OnPropertyChanged(nameof(Controller)); + NotifyChanges(); + } + public string ControllerImage { get => _controllerImage; @@ -255,33 +264,103 @@ namespace Ryujinx.Ava.UI.ViewModels.Input get => _device; set { - MarkAsChanged(); - - _device = value < 0 ? 0 : value; - - if (_device >= Devices.Count) + if (value < 0 || value >= Devices.Count) { return; } + _device = value; + DeviceType selected = Devices[_device].Type; if (selected != DeviceType.None) { - LoadControllers(); - if (_isLoaded) { - LoadConfiguration(LoadDefaultConfiguration()); + LoadSelectedDeviceDefaults(); + } + else if (_device < Devices.Count) + { + LoadControllers(); } } + RefreshModifiedState(); FindPairedDeviceInConfigFile(); OnPropertyChanged(); + OnPropertyChanged(nameof(SelectedDeviceItem)); NotifyChanges(); } } + public bool NeedsResetCurrentDeviceToDefaultsConfirmation() + { + if (_device <= 0 || _device >= Devices.Count || Devices[_device].Type == DeviceType.None) + { + return false; + } + + InputConfig defaultConfig = LoadDefaultConfiguration(); + InputConfig currentConfig = GetSelectedDeviceConfig(); + + return !ConfigsMatch(currentConfig, defaultConfig); + } + + public void ResetCurrentDeviceToDefaults() + { + RefreshAvailableDevices(); + + if (_device <= 0 || _device >= Devices.Count || Devices[_device].Type == DeviceType.None) + { + return; + } + + LoadSelectedDeviceDefaults(); + RefreshModifiedState(); + FindPairedDeviceInConfigFile(); + NotifyChanges(); + } + + public void RefreshInputDevices() + { + RefreshAvailableDevices(); + } + + public object SelectedDeviceItem + { + get => _device >= 0 && _device < Devices.Count ? Devices[_device] : null; + set + { + if (value is not ValueTuple selectedDevice) + { + return; + } + + int deviceIndex = -1; + for (int i = 0; i < Devices.Count; i++) + { + (DeviceType Type, string Id, string Name) device = Devices[i]; + if (device.Type == selectedDevice.Item1 && device.Id == selectedDevice.Item2) + { + deviceIndex = i; + break; + } + } + + if (deviceIndex < 0) + { + return; + } + + if (deviceIndex == _device) + { + return; + } + + Device = deviceIndex; + } + } + public InputConfig Config { get; set; } public InputViewModel(UserControl owner, bool useGlobal = false) : this() @@ -290,18 +369,17 @@ namespace Ryujinx.Ava.UI.ViewModels.Input { _mainWindow = RyujinxApp.MainWindow; - AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner); + ReplaceKeyboardDriver(owner); + PhysicalKeyLabelHelper.LabelsChanged += OnPhysicalKeyLabelsChanged; _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; - _mainWindow.ViewModel.AppHost?.NpadManager.BlockInputUpdates(); - UseGlobalConfig = useGlobal; _isLoaded = false; - LoadDevices(); + RefreshAvailableDevices(); PlayerId = PlayerIndex.Player1; } @@ -309,13 +387,22 @@ namespace Ryujinx.Ava.UI.ViewModels.Input _isChangeTrackingActive = true; } + public void RetargetKeyboardDriver(Control owner) + { + if (!Program.PreviewerDetached) + { + return; + } + + ReplaceKeyboardDriver(owner); + } + public InputViewModel() { PlayerIndexes = []; Controllers = []; Devices = []; ProfilesList = []; - DeviceList = []; VisualStick = new StickVisualizer(this); ControllerImage = ProControllerResource; @@ -333,17 +420,21 @@ namespace Ryujinx.Ava.UI.ViewModels.Input - private void LoadConfiguration(InputConfig inputConfig = null) + private InputConfig GetPersistedInputConfig() { if (UseGlobalConfig && Program.UseExtraConfig) { - Config = inputConfig ?? ConfigurationState.InstanceExtra.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId); - } - else - { - Config = inputConfig ?? ConfigurationState.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId); + return ConfigurationState.InstanceExtra.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId); } + return ConfigurationState.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId); + } + + private void LoadConfiguration(InputConfig inputConfig = null) + { + Config = inputConfig ?? GetDisplayedInputConfig(GetPersistedInputConfig()); + ConfigViewModel = null; + if (Config is StandardKeyboardInputConfig keyboardInputConfig) { ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick); @@ -355,13 +446,45 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } + private InputConfig GetDisplayedInputConfig(InputConfig persistedConfig) + { + if (persistedConfig is not StandardControllerInputConfig controllerConfig) + { + return persistedConfig; + } + + // If runtime has already fallen back to keyboard, reflect that active config in settings + // instead of showing the stale persisted controller config. + InputConfig activeConfig = _mainWindow?.ViewModel.AppHost?.NpadManager?.GetPlayerInputConfigByIndex((int)_playerId); + + if (activeConfig is StandardKeyboardInputConfig) + { + return activeConfig; + } + + // When no game is running (NpadManager unavailable) and the persisted controller + // device isn't currently connected, fall back to keyboard so the user isn't + // stuck on "Disabled". + if (activeConfig == null && + !Devices.Any(device => device.Type == DeviceType.Controller && device.Id == controllerConfig.Id) && + TryCreateKeyboardFallbackConfig(persistedConfig, out StandardKeyboardInputConfig fallbackConfig)) + { + return fallbackConfig; + } + + return persistedConfig; + } + private void FindPairedDeviceInConfigFile() { // This function allows you to output a message about the device configuration found in the file // NOTE: if the configuration is found, we display the message "Waiting for controller connection", // but only if the id gamepad belongs to the selected player - NotificationIsVisible = Config != null && Devices.FirstOrDefault(d => d.Id == Config.Id).Id != Config.Id && Config.PlayerIndex == PlayerId; + NotificationIsVisible = + Config != null && + !Devices.Any(device => device.Id == Config.Id) && + Config.PlayerIndex == PlayerId; if (NotificationIsVisible) { if (string.IsNullOrEmpty(Config.Name)) @@ -375,16 +498,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } - private void MarkAsChanged() - { - //If tracking is active, then allow changing the modifier - if (!IsModified && _isChangeTrackingActive) - { - RevertDeviceId = Devices[Device].Id; // Remember the device to undo changes - IsModified = true; - } - } - public void UnlinkDevice() { // "Disabled" mode is available after unbinding the device @@ -395,48 +508,148 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public void LoadDevice() { + int deviceIndex = 0; + if (Config == null || Config.Backend == InputBackendType.Invalid) { - Device = 0; + ApplyLoadedDevice(deviceIndex); + return; } - else + + DeviceType type = DeviceType.None; + + if (Config is StandardKeyboardInputConfig) { - DeviceType type = DeviceType.None; + type = DeviceType.Keyboard; + } - if (Config is StandardKeyboardInputConfig) - { - type = DeviceType.Keyboard; - } + if (Config is StandardControllerInputConfig) + { + type = DeviceType.Controller; + } - if (Config is StandardControllerInputConfig) + for (int i = 0; i < Devices.Count; i++) + { + if (Devices[i].Type == type && Devices[i].Id == Config.Id) { - type = DeviceType.Controller; - } - - (DeviceType Type, string Id, string Name) item = Devices.FirstOrDefault(x => x.Type == type && x.Id == Config.Id); - if (item != default) - { - Device = Devices.ToList().FindIndex(x => x.Id == item.Id); - } - else - { - Device = 0; + deviceIndex = i; + break; } } + + ApplyLoadedDevice(deviceIndex); } - private void LoadInputDriver() + private void ApplyLoadedDevice(int deviceIndex) { - if (_device < 0) + _device = deviceIndex is >= 0 and < int.MaxValue ? deviceIndex : 0; + + if (_device >= Devices.Count) + { + _device = 0; + } + + if (_device > 0 && Devices[_device].Type != DeviceType.None) + { + LoadControllers(); + } + + FindPairedDeviceInConfigFile(); + OnPropertyChanged(nameof(Device)); + OnPropertyChanged(nameof(SelectedDeviceItem)); + NotifyChanges(); + } + + private void LoadSelectedDeviceDefaults() + { + if (_device > 0 && _device < Devices.Count && Devices[_device].Type != DeviceType.None) + { + LoadControllers(); + } + + LoadConfiguration(LoadDefaultConfiguration()); + } + + public void RefreshModifiedState() + { + if (!_isChangeTrackingActive) { return; } - string id = GetCurrentGamepadId(); - DeviceType type = Devices[Device].Type; + IsModified = !ConfigsMatch(GetSelectedDeviceConfig(), GetDisplayedInputConfig(GetPersistedInputConfig())); + } + + private static bool ConfigsMatch(InputConfig currentConfig, InputConfig otherConfig) + { + if (currentConfig == null || otherConfig == null) + { + return currentConfig == otherConfig; + } + + return SerializeComparableConfig(currentConfig) == + SerializeComparableConfig(otherConfig); + } + + private static string SerializeComparableConfig(InputConfig config) + { + InputConfig comparableConfig = + JsonHelper.Deserialize( + JsonHelper.Serialize(config, _serializerContext.InputConfig), + _serializerContext.InputConfig); + + comparableConfig.Name = string.Empty; + + if (comparableConfig is StandardControllerInputConfig controllerConfig && + controllerConfig.Led is { EnableLed: false, TurnOffLed: false, UseRainbow: false, LedColor: 0 }) + { + controllerConfig.Led = null; + } + + return JsonHelper.Serialize(comparableConfig, _serializerContext.InputConfig); + } + + private InputConfig GetSelectedDeviceConfig() + { + if (!TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) device) || device.Type == DeviceType.None) + { + return null; + } + + InputConfig config = device.Type switch + { + DeviceType.Keyboard => (ConfigViewModel as KeyboardInputViewModel)?.Config.GetConfig(), + DeviceType.Controller => (ConfigViewModel as ControllerInputViewModel)?.Config.GetConfig(), + _ => null, + }; + + if (config == null) + { + return null; + } + + config.Id = GetConfigDeviceId(device); + config.Name = device.Name; + config.PlayerIndex = _playerId; + config.ControllerType = Controllers[_controller].Type; + + return config; + } + + private void LoadInputDriver() + { + if (!TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) device)) + { + SelectedGamepad = null; + return; + } + + string id = GetGamepadId(device); + DeviceType type = device.Type; if (type == DeviceType.None) { + SelectedGamepad = null; return; } @@ -462,44 +675,117 @@ namespace Ryujinx.Ava.UI.ViewModels.Input { _isChangeTrackingActive = false; // Disable configuration change tracking - LoadDevices(); + bool selectedControllerDisconnected = + TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) currentDevice) && + currentDevice.Type == DeviceType.Controller && + string.Equals(GetGamepadId(currentDevice), id, StringComparison.Ordinal); - IsModified = true; - RevertChanges(); - FindPairedDeviceInConfigFile(); + RefreshAvailableDevices(); + + InputConfig persistedConfig = GetPersistedInputConfig(); + InputConfig displayedConfig = GetDisplayedInputConfig(persistedConfig); + bool shouldApplyKeyboardFallback = + selectedControllerDisconnected || + displayedConfig is StandardKeyboardInputConfig; + + if (shouldApplyKeyboardFallback) + { + if (selectedControllerDisconnected && + displayedConfig is not StandardKeyboardInputConfig && + TryCreateKeyboardFallbackConfig(persistedConfig, out StandardKeyboardInputConfig fallbackConfig)) + { + displayedConfig = fallbackConfig; + } + + LoadConfiguration(displayedConfig); + LoadDevice(); + LoadProfiles(); + FindPairedDeviceInConfigFile(); + IsModified = false; + NotifyChanges(); + } + else + { + IsModified = true; + RevertChanges(); + FindPairedDeviceInConfigFile(); + } _isChangeTrackingActive = true; // Enable configuration change tracking } - private void HandleOnGamepadConnected(string id) + private async void HandleOnGamepadConnected(string id) { _isChangeTrackingActive = false; // Disable configuration change tracking - LoadDevices(); + try + { + InputConfig persistedConfig = GetPersistedInputConfig(); + bool shouldRestoreControllerAfterFallback = + Config is StandardKeyboardInputConfig && + persistedConfig is StandardControllerInputConfig; - IsModified = true; - RevertChanges(); + if (shouldRestoreControllerAfterFallback) + { + const int reconnectRestoreAttempts = 20; + const int reconnectRestoreDelayMs = 250; - _isChangeTrackingActive = true;// Enable configuration change tracking + string controllerId = persistedConfig.Id; + for (int attempt = 0; attempt < reconnectRestoreAttempts; attempt++) + { + RefreshAvailableDevices(); + + if (Devices.Any(device => device.Type == DeviceType.Controller && device.Id == controllerId)) + { + IsModified = true; + RevertChanges(); + return; + } + + await Task.Delay(reconnectRestoreDelayMs); + } + } + + RefreshAvailableDevices(); + + IsModified = true; + RevertChanges(); + } + finally + { + _isChangeTrackingActive = true;// Enable configuration change tracking + } } - private string GetCurrentGamepadId() + private bool TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) device) { - if (_device < 0) + if (_device < 0 || _device >= Devices.Count) { - return string.Empty; + device = default; + return false; } - (DeviceType Type, string Id, string Name) device = Devices[Device]; + device = Devices[_device]; + return true; + } - if (device.Type == DeviceType.None) + private static string GetGamepadId((DeviceType Type, string Id, string Name) device) + { + return device.Type == DeviceType.None ? null : device.Id.Split(" ")[0]; + } + + // Keyboard configs keep the full ID, while displayed controller entries include + // the user-facing numbered suffix and need normalization before persistence/lookup. + private static string GetConfigDeviceId((DeviceType Type, string Id, string Name) device) + { + return device.Type switch { - return null; - } - - return device.Id.Split(" ")[0]; + DeviceType.Keyboard => device.Id, + DeviceType.Controller => GetGamepadId(device), + _ => null, + }; } public void LoadControllers() @@ -510,7 +796,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input { Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeHandheld])); - Controller = 0; + ApplyControllerSelection(0); } else { @@ -519,23 +805,36 @@ namespace Ryujinx.Ava.UI.ViewModels.Input Controllers.Add(new(ControllerType.JoyconLeft, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConLeft])); Controllers.Add(new(ControllerType.JoyconRight, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConRight])); - if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1) + if (Config != null) { - int controllerIndex = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType); - - // Avalonia bug: setting a newly instanced ComboBox to 0 - // causes the selected item to show up blank - // Workaround: set the box to 1 and then 0 - if (controllerIndex == 0) + int controllerIndex = -1; + for (int i = 0; i < Controllers.Count; i++) { - Controller = 1; + if (Controllers[i].Type == Config.ControllerType) + { + controllerIndex = i; + break; + } } - Controller = controllerIndex; + if (controllerIndex != -1) + { + // Avalonia bug: setting a newly instanced ComboBox to 0 + // causes the selected item to show up blank. + // Workaround: set the box to 1 and then 0. + // See: https://github.com/AvaloniaUI/Avalonia/issues/4610 + // https://github.com/AvaloniaUI/Avalonia/discussions/18834 + if (controllerIndex == 0) + { + ApplyControllerSelection(1); + } + + ApplyControllerSelection(controllerIndex); + } } else { - Controller = 0; + ApplyControllerSelection(0); } } } @@ -553,16 +852,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input return str; } - private static string GetShortGamepadId(string str) + private void RefreshAvailableDevices() { - const string Hyphen = "-"; - const int Offset = 1; + int selectedDeviceIndex = 0; + (DeviceType Type, string Id, string Name) selectedDevice = default; - return str[(str.IndexOf(Hyphen) + Offset)..]; - } + if (_device >= 0 && _device < Devices.Count) + { + selectedDevice = Devices[_device]; + } - public void LoadDevices() - { string GetGamepadName(IGamepad gamepad, int controllerNumber) { return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})"; @@ -583,7 +882,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input lock (Devices) { Devices.Clear(); - DeviceList.Clear(); Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled])); @@ -609,9 +907,27 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } - DeviceList.AddRange(Devices.Select(x => x.Name)); - Device = Math.Min(Device, DeviceList.Count); + if (selectedDevice != default) + { + selectedDeviceIndex = -1; + for (int i = 0; i < Devices.Count; i++) + { + (DeviceType Type, string Id, string Name) device = Devices[i]; + if (device.Type == selectedDevice.Type && device.Id == selectedDevice.Id) + { + selectedDeviceIndex = i; + break; + } + } + } + + if (selectedDeviceIndex < 0) + { + selectedDeviceIndex = Math.Clamp(_device, 0, Devices.Count - 1); + } } + + ApplyLoadedDevice(selectedDeviceIndex); } private string GetProfileBasePath() @@ -667,131 +983,46 @@ 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 = InputConfigDefaults.CreateDefaultKeyboardConfiguration( + activeDevice.Id, + activeDevice.Name, + ControllerType.ProController, + _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 + bool isNintendoStyle = false; + + try { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.GamepadSDL3, - 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 + IGamepad gp = _mainWindow?.InputManager?.GamepadDriver?.GetGamepad(id); + + if (gp is SDL3Gamepad sdlGp) { - DpadUp = ConfigGamepadInputId.DpadUp, - DpadDown = ConfigGamepadInputId.DpadDown, - DpadLeft = ConfigGamepadInputId.DpadLeft, - DpadRight = ConfigGamepadInputId.DpadRight, - ButtonMinus = ConfigGamepadInputId.Minus, - ButtonL = ConfigGamepadInputId.LeftShoulder, - ButtonZl = ConfigGamepadInputId.LeftTrigger, - ButtonSl = ConfigGamepadInputId.SingleLeftTrigger0, - ButtonSr = ConfigGamepadInputId.SingleRightTrigger0, - }, - LeftJoyconStick = new JoyconConfigControllerStick + // Nintendo vendor ID is 0x057E + isNintendoStyle = sdlGp.VendorId == 0x057E; + } + else { - 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.SingleLeftTrigger1, - ButtonSr = ConfigGamepadInputId.SingleRightTrigger1, - }, - 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, - UseHDRumble = true - }, - }; + // Fallback to name-based detection + isNintendoStyle = name.Contains("Nintendo", StringComparison.OrdinalIgnoreCase); + } + } + catch (Exception ex) + { + Logger.Debug?.Print(LogClass.UI, $"Controller vendor detection failed for '{name}': {ex.Message}"); + isNintendoStyle = name.Contains("Nintendo", StringComparison.OrdinalIgnoreCase); + } + + config = InputConfigDefaults.CreateDefaultControllerConfiguration( + id, + name, + ControllerType.ProController, + _playerId, + isNintendoStyle); } else { @@ -803,10 +1034,32 @@ namespace Ryujinx.Ava.UI.ViewModels.Input return config; } + private bool TryCreateKeyboardFallbackConfig(InputConfig sourceConfig, out StandardKeyboardInputConfig fallbackConfig) + { + fallbackConfig = null; + + (DeviceType Type, string Id, string Name) keyboardDevice = + Devices.FirstOrDefault(device => device.Type == DeviceType.Keyboard); + + if (keyboardDevice == default) + { + return false; + } + + ControllerType controllerType = sourceConfig?.ControllerType ?? ControllerType.ProController; + PlayerIndex playerIndex = sourceConfig?.PlayerIndex ?? _playerId; + + fallbackConfig = InputConfigDefaults.CreateDefaultKeyboardConfiguration( + keyboardDevice.Id, + keyboardDevice.Name, + controllerType, + playerIndex); + return true; + } + public void LoadProfileButton() { LoadProfile(); - IsModified = true; } public async void LoadProfile() @@ -861,7 +1114,17 @@ namespace Ryujinx.Ava.UI.ViewModels.Input { _isLoaded = false; - config.Id = Config.Id; // Set current device id instead of changing device(independent profiles) + string currentDeviceId = Config?.Id ?? + (TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) currentDevice) + ? GetConfigDeviceId(currentDevice) + : null); + if (string.IsNullOrEmpty(currentDeviceId)) + { + Logger.Warning?.Print(LogClass.Configuration, $"Ignoring profile load for {ProfileName} because no active input device is selected."); + return; + } + + config.Id = currentDeviceId; // Set current device id instead of changing device(independent profiles) LoadConfiguration(config); @@ -869,6 +1132,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input _isLoaded = true; + RefreshModifiedState(); NotifyChanges(); } } @@ -959,9 +1223,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public void RevertChanges() { - LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad - Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId); - _isLoaded = false; LoadConfiguration(); LoadDevice(); @@ -981,8 +1242,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input IsModified = false; - RevertDeviceId = Devices[Device].Id; // Remember selected device after saving - List newConfig = []; if (UseGlobalConfig && Program.UseExtraConfig) @@ -994,33 +1253,15 @@ namespace Ryujinx.Ava.UI.ViewModels.Input newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); } - newConfig.Remove(newConfig.FirstOrDefault(x => x == null)); + newConfig.RemoveAll(static inputConfig => inputConfig == null); if (Device == 0) { - newConfig.Remove(newConfig.FirstOrDefault(x => x.PlayerIndex == this.PlayerId)); + newConfig.RemoveAll(inputConfig => inputConfig.PlayerIndex == PlayerId); } else { - (DeviceType Type, string Id, string Name) device = Devices[Device]; - - if (device.Type == DeviceType.Keyboard) - { - KeyboardInputConfig inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config; - inputConfig.Id = device.Id; - } - else - { - GamepadInputConfig inputConfig = (ConfigViewModel as ControllerInputViewModel).Config; - inputConfig.Id = device.Id.Split(" ")[0]; - } - - InputConfig config = !IsController - ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig() - : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); - config.ControllerType = Controllers[_controller].Type; - config.PlayerIndex = _playerId; - config.Name = device.Name; + InputConfig config = GetSelectedDeviceConfig(); int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId); if (i == -1) @@ -1061,20 +1302,55 @@ namespace Ryujinx.Ava.UI.ViewModels.Input NotifyChangesEvent?.Invoke(); } + private void OnPhysicalKeyLabelsChanged() + { + if (ConfigViewModel is KeyboardInputViewModel keyboardInputViewModel) + { + Dispatcher.UIThread.Post(keyboardInputViewModel.Config.NotifyKeyLabelsChanged); + } + } + + private void ReplaceKeyboardDriver(Control owner) + { + Control target = TopLevel.GetTopLevel(owner) as Control ?? owner; + + if (ReferenceEquals(_keyboardDriverControl, target)) + { + return; + } + + if (AvaloniaKeyboardDriver is AvaloniaKeyboardDriver oldKeyboardDriver) + { + oldKeyboardDriver.KeyPressed -= PhysicalKeyLabelHelper.ObserveKeyPress; + oldKeyboardDriver.Dispose(); + } + + _keyboardDriverControl = target; + + AvaloniaKeyboardDriver keyboardDriver = new(target, KeyboardInputMode.Physical); + keyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress; + AvaloniaKeyboardDriver = keyboardDriver; + + if (_isLoaded && Device > 0 && Device < Devices.Count && Devices[Device].Type == DeviceType.Keyboard) + { + SelectedGamepad?.Dispose(); + LoadInputDriver(); + } + } + public void Dispose() { GC.SuppressFinalize(this); + PhysicalKeyLabelHelper.LabelsChanged -= OnPhysicalKeyLabelsChanged; _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected; - _mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates(); - VisualStick.Dispose(); SelectedGamepad?.Dispose(); - AvaloniaKeyboardDriver.Dispose(); + AvaloniaKeyboardDriver?.Dispose(); } } } diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs index 02e6e99c9..6b3c691c3 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml.cs @@ -104,7 +104,7 @@ namespace Ryujinx.Ava.UI.Views.Input PointerPressed += MouseClick; - ControllerInputViewModel viewModel = (DataContext as ControllerInputViewModel); + ControllerInputViewModel viewModel = ViewModel; IKeyboard keyboard = (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver @@ -113,10 +113,9 @@ namespace Ryujinx.Ava.UI.Views.Input _currentAssigner.ButtonAssigned += (sender, e) => { - if (e.ButtonValue.HasValue) + if (e.ButtonValue.HasValue && IsActiveAssignmentContext(viewModel)) { Button buttonValue = e.ButtonValue.Value; - FlagInputConfigChanged(); switch (button.Name) { @@ -187,6 +186,8 @@ namespace Ryujinx.Ava.UI.Views.Input viewModel.Config.RightJoystick = buttonValue.AsHidType(); break; } + + FlagInputConfigChanged(); } }; @@ -212,7 +213,15 @@ namespace Ryujinx.Ava.UI.Views.Input private void FlagInputConfigChanged() { - (DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true; + if (DataContext is ControllerInputViewModel viewModel && VisualRoot is not null) + { + viewModel.ParentModel.RefreshModifiedState(); + } + } + + private bool IsActiveAssignmentContext(ControllerInputViewModel viewModel) + { + return VisualRoot is not null && ReferenceEquals(DataContext, viewModel); } private void MouseClick(object sender, PointerPressedEventArgs e) diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml b/src/Ryujinx/UI/Views/Input/InputView.axaml index c4f61e78e..5ff8e0618 100644 --- a/src/Ryujinx/UI/Views/Input/InputView.axaml +++ b/src/Ryujinx/UI/Views/Input/InputView.axaml @@ -3,6 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" + xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" @@ -77,7 +78,7 @@ ToolTip.Tip="{ext:Locale ControllerSettingsCancelCurrentChangesToolTip}" Command="{Binding RevertChanges}"> @@ -148,7 +149,7 @@ + HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto,Auto"> + ItemsSource="{Binding Devices}" + SelectedItem="{Binding SelectedDeviceItem, Mode=TwoWay}"> + + + + + + + diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs index f8ba04f5d..2879e4367 100644 --- a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs @@ -1,4 +1,8 @@ using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia; +using Avalonia.Layout; +using Avalonia.Media; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Systems.Configuration; @@ -15,9 +19,14 @@ namespace Ryujinx.Ava.UI.Views.Input public InputView() { - ViewModel = new InputViewModel(this, ConfigurationState.Instance.System.UseInputGlobalConfig); + ReplaceViewModel(ConfigurationState.Instance.System.UseInputGlobalConfig); + } - InitializeComponent(); + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + ViewModel?.RetargetKeyboardDriver(this); } public void SaveCurrentProfile() @@ -28,8 +37,18 @@ namespace Ryujinx.Ava.UI.Views.Input public void ToggleLocalGlobalInput(bool enableConfigGlobal) { Dispose(); - ViewModel = new InputViewModel(this, enableConfigGlobal); // Create new Input Page with global input configs + ReplaceViewModel(enableConfigGlobal); + } + + private void ReplaceViewModel(bool useGlobalConfig) + { + ViewModel = new InputViewModel(this, useGlobalConfig); // Create new Input Page with the selected input config scope. InitializeComponent(); + + if (VisualRoot is not null) + { + ViewModel.RetargetKeyboardDriver(this); + } } private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) @@ -83,7 +102,56 @@ namespace Ryujinx.Ava.UI.Views.Input if (sender is FAComboBox faComboBox) { faComboBox.IsDropDownOpen = false; - ViewModel.IsModified = true; + ViewModel.RefreshModifiedState(); + } + } + + private async void ResetCurrentDeviceToDefaultsButton_OnClick(object sender, RoutedEventArgs e) + { + if (!ViewModel.NeedsResetCurrentDeviceToDefaultsConfirmation()) + { + ViewModel.ResetCurrentDeviceToDefaults(); + return; + } + + Window owner = TopLevel.GetTopLevel(this) as Window; + + StackPanel content = new() + { + Spacing = 4, + MaxWidth = 360, + }; + + content.Children.Add(new TextBlock + { + Text = LocaleManager.Instance[LocaleKeys.DialogControllerSettingsResetKeybindsConfirmMessage], + TextWrapping = TextWrapping.Wrap, + MaxWidth = 360, + }); + + content.Children.Add(new TextBlock + { + Text = LocaleManager.Instance[LocaleKeys.DialogControllerSettingsResetKeybindsConfirmSubMessage], + TextWrapping = TextWrapping.Wrap, + MaxWidth = 360, + }); + + ContentDialog contentDialog = new ContentDialog + { + Title = LocaleManager.Instance[LocaleKeys.RyujinxConfirm], + PrimaryButtonText = LocaleManager.Instance[LocaleKeys.InputDialogYes], + CloseButtonText = LocaleManager.Instance[LocaleKeys.InputDialogNo], + DefaultButton = ContentDialogButton.Primary, + Content = content, + }.ApplyStyles(); + + ContentDialogResult result = owner is not null + ? await contentDialog.ShowAsync(owner) + : await ContentDialogHelper.ShowAsync(contentDialog); + + if (result == ContentDialogResult.Primary) + { + ViewModel.ResetCurrentDeviceToDefaults(); } } diff --git a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs index 668e1220c..ef800ed50 100644 --- a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml.cs @@ -12,7 +12,7 @@ using Ryujinx.Input.Assigner; using System; using System.Collections.Generic; using Button = Ryujinx.Input.Button; -using Key = Ryujinx.Common.Configuration.Hid.Key; +using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; namespace Ryujinx.Ava.UI.Views.Input { @@ -63,105 +63,108 @@ namespace Ryujinx.Ava.UI.Views.Input PointerPressed += MouseClick; + KeyboardInputViewModel viewModel = ViewModel; + IKeyboard keyboard = - (IKeyboard)ViewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. + (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. IButtonAssigner assigner = - new KeyboardKeyAssigner((IKeyboard)ViewModel.ParentModel.SelectedGamepad); + new KeyboardKeyAssigner((IKeyboard)viewModel.ParentModel.SelectedGamepad); _currentAssigner.ButtonAssigned += (_, be) => { - if (be.ButtonValue.HasValue) + if (be.ButtonValue.HasValue && IsActiveAssignmentContext(viewModel)) { Button buttonValue = be.ButtonValue.Value; - ViewModel.ParentModel.IsModified = true; switch (button.Name) { case "ButtonZl": - ViewModel.Config.ButtonZl = buttonValue.AsHidType(); + viewModel.Config.ButtonZl = buttonValue.AsHidType(); break; case "ButtonL": - ViewModel.Config.ButtonL = buttonValue.AsHidType(); + viewModel.Config.ButtonL = buttonValue.AsHidType(); break; case "ButtonMinus": - ViewModel.Config.ButtonMinus = buttonValue.AsHidType(); + viewModel.Config.ButtonMinus = buttonValue.AsHidType(); break; case "LeftStickButton": - ViewModel.Config.LeftStickButton = buttonValue.AsHidType(); + viewModel.Config.LeftStickButton = buttonValue.AsHidType(); break; case "LeftStickUp": - ViewModel.Config.LeftStickUp = buttonValue.AsHidType(); + viewModel.Config.LeftStickUp = buttonValue.AsHidType(); break; case "LeftStickDown": - ViewModel.Config.LeftStickDown = buttonValue.AsHidType(); + viewModel.Config.LeftStickDown = buttonValue.AsHidType(); break; case "LeftStickRight": - ViewModel.Config.LeftStickRight = buttonValue.AsHidType(); + viewModel.Config.LeftStickRight = buttonValue.AsHidType(); break; case "LeftStickLeft": - ViewModel.Config.LeftStickLeft = buttonValue.AsHidType(); + viewModel.Config.LeftStickLeft = buttonValue.AsHidType(); break; case "DpadUp": - ViewModel.Config.DpadUp = buttonValue.AsHidType(); + viewModel.Config.DpadUp = buttonValue.AsHidType(); break; case "DpadDown": - ViewModel.Config.DpadDown = buttonValue.AsHidType(); + viewModel.Config.DpadDown = buttonValue.AsHidType(); break; case "DpadLeft": - ViewModel.Config.DpadLeft = buttonValue.AsHidType(); + viewModel.Config.DpadLeft = buttonValue.AsHidType(); break; case "DpadRight": - ViewModel.Config.DpadRight = buttonValue.AsHidType(); + viewModel.Config.DpadRight = buttonValue.AsHidType(); break; case "LeftButtonSr": - ViewModel.Config.LeftButtonSr = buttonValue.AsHidType(); + viewModel.Config.LeftButtonSr = buttonValue.AsHidType(); break; case "LeftButtonSl": - ViewModel.Config.LeftButtonSl = buttonValue.AsHidType(); + viewModel.Config.LeftButtonSl = buttonValue.AsHidType(); break; case "RightButtonSr": - ViewModel.Config.RightButtonSr = buttonValue.AsHidType(); + viewModel.Config.RightButtonSr = buttonValue.AsHidType(); break; case "RightButtonSl": - ViewModel.Config.RightButtonSl = buttonValue.AsHidType(); + viewModel.Config.RightButtonSl = buttonValue.AsHidType(); break; case "ButtonZr": - ViewModel.Config.ButtonZr = buttonValue.AsHidType(); + viewModel.Config.ButtonZr = buttonValue.AsHidType(); break; case "ButtonR": - ViewModel.Config.ButtonR = buttonValue.AsHidType(); + viewModel.Config.ButtonR = buttonValue.AsHidType(); break; case "ButtonPlus": - ViewModel.Config.ButtonPlus = buttonValue.AsHidType(); + viewModel.Config.ButtonPlus = buttonValue.AsHidType(); break; case "ButtonA": - ViewModel.Config.ButtonA = buttonValue.AsHidType(); + viewModel.Config.ButtonA = buttonValue.AsHidType(); break; case "ButtonB": - ViewModel.Config.ButtonB = buttonValue.AsHidType(); + viewModel.Config.ButtonB = buttonValue.AsHidType(); break; case "ButtonX": - ViewModel.Config.ButtonX = buttonValue.AsHidType(); + viewModel.Config.ButtonX = buttonValue.AsHidType(); break; case "ButtonY": - ViewModel.Config.ButtonY = buttonValue.AsHidType(); + viewModel.Config.ButtonY = buttonValue.AsHidType(); break; case "RightStickButton": - ViewModel.Config.RightStickButton = buttonValue.AsHidType(); + viewModel.Config.RightStickButton = buttonValue.AsHidType(); break; case "RightStickUp": - ViewModel.Config.RightStickUp = buttonValue.AsHidType(); + viewModel.Config.RightStickUp = buttonValue.AsHidType(); break; case "RightStickDown": - ViewModel.Config.RightStickDown = buttonValue.AsHidType(); + viewModel.Config.RightStickDown = buttonValue.AsHidType(); break; case "RightStickRight": - ViewModel.Config.RightStickRight = buttonValue.AsHidType(); + viewModel.Config.RightStickRight = buttonValue.AsHidType(); break; case "RightStickLeft": - ViewModel.Config.RightStickLeft = buttonValue.AsHidType(); + viewModel.Config.RightStickLeft = buttonValue.AsHidType(); break; } + + viewModel.ParentModel.RefreshModifiedState(); } }; @@ -207,40 +210,40 @@ namespace Ryujinx.Ava.UI.Views.Input { Dictionary buttonActions = new() { - { "ButtonZl", () => ViewModel.Config.ButtonZl = Key.Unbound }, - { "ButtonL", () => ViewModel.Config.ButtonL = Key.Unbound }, - { "ButtonMinus", () => ViewModel.Config.ButtonMinus = Key.Unbound }, - { "LeftStickButton", () => ViewModel.Config.LeftStickButton = Key.Unbound }, - { "LeftStickUp", () => ViewModel.Config.LeftStickUp = Key.Unbound }, - { "LeftStickDown", () => ViewModel.Config.LeftStickDown = Key.Unbound }, - { "LeftStickRight", () => ViewModel.Config.LeftStickRight = Key.Unbound }, - { "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = Key.Unbound }, - { "DpadUp", () => ViewModel.Config.DpadUp = Key.Unbound }, - { "DpadDown", () => ViewModel.Config.DpadDown = Key.Unbound }, - { "DpadLeft", () => ViewModel.Config.DpadLeft = Key.Unbound }, - { "DpadRight", () => ViewModel.Config.DpadRight = Key.Unbound }, - { "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = Key.Unbound }, - { "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = Key.Unbound }, - { "RightButtonSr", () => ViewModel.Config.RightButtonSr = Key.Unbound }, - { "RightButtonSl", () => ViewModel.Config.RightButtonSl = Key.Unbound }, - { "ButtonZr", () => ViewModel.Config.ButtonZr = Key.Unbound }, - { "ButtonR", () => ViewModel.Config.ButtonR = Key.Unbound }, - { "ButtonPlus", () => ViewModel.Config.ButtonPlus = Key.Unbound }, - { "ButtonA", () => ViewModel.Config.ButtonA = Key.Unbound }, - { "ButtonB", () => ViewModel.Config.ButtonB = Key.Unbound }, - { "ButtonX", () => ViewModel.Config.ButtonX = Key.Unbound }, - { "ButtonY", () => ViewModel.Config.ButtonY = Key.Unbound }, - { "RightStickButton", () => ViewModel.Config.RightStickButton = Key.Unbound }, - { "RightStickUp", () => ViewModel.Config.RightStickUp = Key.Unbound }, - { "RightStickDown", () => ViewModel.Config.RightStickDown = Key.Unbound }, - { "RightStickRight", () => ViewModel.Config.RightStickRight = Key.Unbound }, - { "RightStickLeft", () => ViewModel.Config.RightStickLeft = Key.Unbound } + { "ButtonZl", () => ViewModel.Config.ButtonZl = PhysicalKey.Unbound }, + { "ButtonL", () => ViewModel.Config.ButtonL = PhysicalKey.Unbound }, + { "ButtonMinus", () => ViewModel.Config.ButtonMinus = PhysicalKey.Unbound }, + { "LeftStickButton", () => ViewModel.Config.LeftStickButton = PhysicalKey.Unbound }, + { "LeftStickUp", () => ViewModel.Config.LeftStickUp = PhysicalKey.Unbound }, + { "LeftStickDown", () => ViewModel.Config.LeftStickDown = PhysicalKey.Unbound }, + { "LeftStickRight", () => ViewModel.Config.LeftStickRight = PhysicalKey.Unbound }, + { "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = PhysicalKey.Unbound }, + { "DpadUp", () => ViewModel.Config.DpadUp = PhysicalKey.Unbound }, + { "DpadDown", () => ViewModel.Config.DpadDown = PhysicalKey.Unbound }, + { "DpadLeft", () => ViewModel.Config.DpadLeft = PhysicalKey.Unbound }, + { "DpadRight", () => ViewModel.Config.DpadRight = PhysicalKey.Unbound }, + { "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = PhysicalKey.Unbound }, + { "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = PhysicalKey.Unbound }, + { "RightButtonSr", () => ViewModel.Config.RightButtonSr = PhysicalKey.Unbound }, + { "RightButtonSl", () => ViewModel.Config.RightButtonSl = PhysicalKey.Unbound }, + { "ButtonZr", () => ViewModel.Config.ButtonZr = PhysicalKey.Unbound }, + { "ButtonR", () => ViewModel.Config.ButtonR = PhysicalKey.Unbound }, + { "ButtonPlus", () => ViewModel.Config.ButtonPlus = PhysicalKey.Unbound }, + { "ButtonA", () => ViewModel.Config.ButtonA = PhysicalKey.Unbound }, + { "ButtonB", () => ViewModel.Config.ButtonB = PhysicalKey.Unbound }, + { "ButtonX", () => ViewModel.Config.ButtonX = PhysicalKey.Unbound }, + { "ButtonY", () => ViewModel.Config.ButtonY = PhysicalKey.Unbound }, + { "RightStickButton", () => ViewModel.Config.RightStickButton = PhysicalKey.Unbound }, + { "RightStickUp", () => ViewModel.Config.RightStickUp = PhysicalKey.Unbound }, + { "RightStickDown", () => ViewModel.Config.RightStickDown = PhysicalKey.Unbound }, + { "RightStickRight", () => ViewModel.Config.RightStickRight = PhysicalKey.Unbound }, + { "RightStickLeft", () => ViewModel.Config.RightStickLeft = PhysicalKey.Unbound } }; if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action)) { action(); - ViewModel.ParentModel.IsModified = true; + ViewModel.ParentModel.RefreshModifiedState(); } } } @@ -251,5 +254,10 @@ namespace Ryujinx.Ava.UI.Views.Input _currentAssigner?.Cancel(); _currentAssigner = null; } + + private bool IsActiveAssignmentContext(KeyboardInputViewModel viewModel) + { + return VisualRoot is not null && ReferenceEquals(DataContext, viewModel); + } } } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs index b9a5462b2..2e1e452af 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs @@ -34,7 +34,8 @@ namespace Ryujinx.Ava.UI.Views.Settings } } - _avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this); + _avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this, KeyboardInputMode.Semantic); + _avaloniaKeyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress; } protected override void OnPointerReleased(PointerReleasedEventArgs e) diff --git a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs index 55b69af06..4ab7b181d 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs @@ -1,17 +1,65 @@ +using Avalonia; using Avalonia.Controls; +using Ryujinx.Ava.UI.Windows; namespace Ryujinx.Ava.UI.Views.Settings { public partial class SettingsInputView : UserControl { + private bool _inputUpdatesBlocked; + public SettingsInputView() { InitializeComponent(); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + SetInputUpdatesBlocked(true); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + SetInputUpdatesBlocked(false); + base.OnDetachedFromVisualTree(e); + } + public void Dispose() { - InputView.Dispose(); + try + { + InputView.Dispose(); + } + finally + { + SetInputUpdatesBlocked(false); + } + } + + private void SetInputUpdatesBlocked(bool blocked) + { + if (_inputUpdatesBlocked == blocked) + { + return; + } + + MainWindow? mainWindow = RyujinxApp.MainWindow; + if (mainWindow?.ViewModel?.AppHost?.NpadManager is not { } npadManager) + { + return; + } + + if (blocked) + { + npadManager.BlockInputUpdates(); + } + else + { + npadManager.UnblockInputUpdates(); + } + + _inputUpdatesBlocked = blocked; } } } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 1f20604f6..365b00b77 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -30,6 +30,7 @@ using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.Input.HLE; using Ryujinx.Input.SDL3; +using Ryujinx.Input; using System; using System.Collections.Generic; using System.Linq; @@ -107,7 +108,9 @@ namespace Ryujinx.Ava.UI.Windows if (Program.PreviewerDetached) { - InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL3GamepadDriver()); + AvaloniaKeyboardDriver keyboardDriver = new(this, KeyboardInputMode.Semantic); + keyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress; + InputManager = new InputManager(keyboardDriver, new SDL3GamepadDriver()); _ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it); this.ScalingChanged += OnScalingChanged;