moved to MVVM architecture
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
using CursorLang.Models;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Следит за раскладкой активного окна — в том числе в чужих приложениях.
|
||||
/// </summary>
|
||||
public interface IKeyboardLayoutService
|
||||
{
|
||||
/// <summary>Раскладка активного окна на текущий момент.</summary>
|
||||
KeyboardLayout Current { get; }
|
||||
|
||||
event EventHandler<LayoutChangedEventArgs>? LayoutChanged;
|
||||
|
||||
void Start();
|
||||
|
||||
void Stop();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using CursorLang.Models;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Показывает подсказку с раскладкой у курсора.
|
||||
/// </summary>
|
||||
public interface ILayoutPopupService
|
||||
{
|
||||
void Show(KeyboardLayout layout);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Windows.Threading;
|
||||
using CursorLang.Models;
|
||||
using CursorLang.ViewModels;
|
||||
using CursorLang.Views;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Настройки подсказки у курсора.
|
||||
/// </summary>
|
||||
public sealed class PopupOptions
|
||||
{
|
||||
/// <summary>Сколько подсказка держится на экране после последнего переключения.</summary>
|
||||
public TimeSpan Duration { get; init; } = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
/// <summary>Отступ от курсора в единицах WPF, чтобы подсказка не оказалась под ним.</summary>
|
||||
public double CursorOffset { get; init; } = 16;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Управляет временем жизни подсказки: окно отвечает только за показ,
|
||||
/// а решение «когда показать и когда убрать» принимается здесь.
|
||||
/// </summary>
|
||||
public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
||||
{
|
||||
private readonly LayoutPopupWindow _window;
|
||||
private readonly LayoutPopupViewModel _viewModel;
|
||||
private readonly DispatcherTimer _hideTimer;
|
||||
|
||||
public LayoutPopupService(LayoutPopupWindow window, LayoutPopupViewModel viewModel, PopupOptions options)
|
||||
{
|
||||
_window = window;
|
||||
_viewModel = viewModel;
|
||||
|
||||
_hideTimer = new DispatcherTimer { Interval = options.Duration };
|
||||
_hideTimer.Tick += OnHideTimerTick;
|
||||
}
|
||||
|
||||
public void Show(KeyboardLayout layout)
|
||||
{
|
||||
_viewModel.ShortName = layout.ShortName;
|
||||
_window.ShowAtCursor();
|
||||
|
||||
// Перезапускаем таймер, чтобы быстрые переключения продлевали показ
|
||||
_hideTimer.Stop();
|
||||
_hideTimer.Start();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_hideTimer.Stop();
|
||||
_hideTimer.Tick -= OnHideTimerTick;
|
||||
_window.Close();
|
||||
}
|
||||
|
||||
private void OnHideTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
_hideTimer.Stop();
|
||||
_window.Hide();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user