Files
cursor-lang/CursorLang/Services/KeyboardLayoutService.cs
T

88 lines
3.3 KiB
C#

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