using System.Windows.Threading; using CursorLang.Interop; using CursorLang.Models; namespace CursorLang.Services; /// /// The settings of layout tracking. /// public sealed class KeyboardLayoutOptions { /// How often to check the layout of the active window. public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(150); } /// /// Polls the active window on a timer. /// /// /// Polling was chosen not for simplicity: there is no event-based way to learn about /// a layout change in another process from managed code. HSHELL_LANGUAGE from /// RegisterShellHookWindow does not arrive on Windows 10/11, and TSF notifications /// (ITfLanguageProfileNotifySink) report only language changes inside our own process /// — both options were tried and did not work. One tick is three Win32 calls reading /// data from kernel memory. /// public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable { private readonly DispatcherTimer _pollTimer; private int _lastLocaleId = -1; private IntPtr _lastForegroundWindow; public KeyboardLayoutService(KeyboardLayoutOptions options) { _pollTimer = new DispatcherTimer { Interval = options.PollInterval }; _pollTimer.Tick += OnTick; } public event EventHandler? LayoutChanged; public KeyboardLayout Current => KeyboardLayout.FromLocaleId(KeyboardLayoutNative.GetActiveLocaleId()); public void Start() { _lastForegroundWindow = KeyboardLayoutNative.GetForegroundWindow(); _lastLocaleId = KeyboardLayoutNative.GetActiveLocaleId(); _pollTimer.Start(); } public void Stop() => _pollTimer.Stop(); public void SwitchToNext() => KeyboardLayoutNative.RequestNextLayout(); public void Dispose() { _pollTimer.Stop(); _pollTimer.Tick -= OnTick; } private void OnTick(object? sender, EventArgs e) { IntPtr foreground = KeyboardLayoutNative.GetForegroundWindow(); if (foreground == IntPtr.Zero) { return; } bool appSwitched = foreground != _lastForegroundWindow; _lastForegroundWindow = foreground; int localeId = KeyboardLayoutNative.GetActiveLocaleId(); if (localeId == _lastLocaleId) { return; } _lastLocaleId = localeId; // Moving to another application with a layout of its own is not the same as // the user switching the layout, and the subscribers are free to react to // these cases differently LayoutChangeReason reason = appSwitched ? LayoutChangeReason.ApplicationSwitched : LayoutChangeReason.UserSwitched; LayoutChanged?.Invoke(this, new LayoutChangedEventArgs(KeyboardLayout.FromLocaleId(localeId), reason)); } }