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;