using System.Windows.Threading; using CursorLang.Interop; using CursorLang.Models; namespace CursorLang.Services; /// /// Настройки слежения за раскладкой. /// public sealed class KeyboardLayoutOptions { /// Как часто проверять раскладку активного окна. public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(150); } /// /// Опрашивает активное окно по таймеру. /// /// /// Опрос выбран не от простоты: событийных способов узнать о смене раскладки /// в чужом процессе из managed-кода нет. HSHELL_LANGUAGE от RegisterShellHookWindow /// в Windows 10/11 не приходит, а уведомления TSF (ITfLanguageProfileNotifySink) /// сообщают только о смене языка внутри своего процесса — оба варианта проверены /// и не сработали. Один тик — три вызова Win32, читающих данные из памяти ядра. /// 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; // Переход в другое приложение со своей раскладкой — не то же самое, // что переключение раскладки пользователем, и подписчики вправе // реагировать на эти случаи по-разному LayoutChangeReason reason = appSwitched ? LayoutChangeReason.ApplicationSwitched : LayoutChangeReason.UserSwitched; LayoutChanged?.Invoke(this, new LayoutChangedEventArgs(KeyboardLayout.FromLocaleId(localeId), reason)); } }