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