87 lines
3.3 KiB
C#
87 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.GetLocaleIdOf(KeyboardLayoutNative.GetForegroundWindow()));
|
|
|
|
public void Start()
|
|
{
|
|
_lastForegroundWindow = KeyboardLayoutNative.GetForegroundWindow();
|
|
_lastLocaleId = KeyboardLayoutNative.GetLocaleIdOf(_lastForegroundWindow);
|
|
_pollTimer.Start();
|
|
}
|
|
|
|
public void Stop() => _pollTimer.Stop();
|
|
|
|
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.GetLocaleIdOf(foreground);
|
|
if (localeId == _lastLocaleId)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_lastLocaleId = localeId;
|
|
|
|
// Переход в другое приложение со своей раскладкой — не то же самое,
|
|
// что переключение раскладки пользователем, и подписчики вправе
|
|
// реагировать на эти случаи по-разному
|
|
LayoutChangeReason reason = appSwitched
|
|
? LayoutChangeReason.ApplicationSwitched
|
|
: LayoutChangeReason.UserSwitched;
|
|
|
|
LayoutChanged?.Invoke(this, new LayoutChangedEventArgs(KeyboardLayout.FromLocaleId(localeId), reason));
|
|
}
|
|
}
|