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

136 lines
4.7 KiB
C#

using System.Windows.Threading;
using CursorLang.Interop;
using CursorLang.Models;
namespace CursorLang.Services;
/// <summary>
/// Держит системный перехват Caps Lock и отличает короткое нажатие от удержания.
/// </summary>
/// <remarks>
/// Различить их можно только по факту отпускания клавиши, поэтому перехватываются
/// оба события — и нажатие, и отпускание. Заодно это единственный способ отменить
/// смену регистра: Windows переключает Caps Lock по событию нажатия, и пропустить
/// его «на всякий случай» нельзя.
/// </remarks>
public sealed class CapsLockHotkeyService : ICapsLockHotkeyService, IDisposable
{
private const int VirtualKeyCapsLock = 0x14;
private readonly AppSettings _settings;
private readonly LowLevelKeyboardHook _hook;
private readonly DispatcherTimer _holdTimer = new();
private readonly Dispatcher _dispatcher = Dispatcher.CurrentDispatcher;
private bool _isPressed;
private bool _isHolding;
public CapsLockHotkeyService(AppSettings settings)
{
_settings = settings;
_hook = new LowLevelKeyboardHook(OnKeyEvent);
_holdTimer.Tick += OnHoldTimerTick;
}
public event EventHandler? Tapped;
public event EventHandler? HoldStarted;
public event EventHandler? HoldEnded;
public bool IsRunning => _hook.IsInstalled;
public void Start() => _hook.Install();
public void Stop()
{
_hook.Uninstall();
ResetPress();
}
// При закрытии приложения событий уже никто не ждёт, поэтому в отличие
// от Stop состояние сбрасывается молча: очередь диспетчера в этот момент
// может быть закрыта
public void Dispose()
{
_holdTimer.Tick -= OnHoldTimerTick;
_holdTimer.Stop();
_isPressed = false;
_isHolding = false;
_hook.Dispose();
}
// Вызывается системным хуком, то есть внутри разбора очереди сообщений.
// Здесь только учёт состояния: показывать окна и рассылать события отсюда
// нельзя — обработчик обязан вернуть управление за считанные миллисекунды
private bool OnKeyEvent(int virtualKey, bool isKeyDown)
{
if (virtualKey != VirtualKeyCapsLock)
{
return false;
}
if (isKeyDown)
{
// Пока клавишу держат, Windows повторяет нажатие: отсчёт удержания
// ведём от первого события и на повторы не реагируем
if (!_isPressed)
{
_isPressed = true;
// Порог читаем при каждом нажатии: его меняют в настройках на лету
_holdTimer.Interval = _settings.CapsLockHoldDelay;
_holdTimer.Start();
}
return true;
}
_isPressed = false;
_holdTimer.Stop();
if (_isHolding)
{
_isHolding = false;
Notify(HoldEnded);
}
else
{
Notify(Tapped);
}
return true;
}
private void OnHoldTimerTick(object? sender, EventArgs e)
{
_holdTimer.Stop();
_isHolding = true;
HoldStarted?.Invoke(this, EventArgs.Empty);
}
// Событие уходит подписчикам после возврата из хука: они вправе показывать
// окна и делать что угодно ещё, не задерживая обработку нажатия
private void Notify(EventHandler? handler)
{
if (handler is not null)
{
_dispatcher.BeginInvoke(() => handler(this, EventArgs.Empty));
}
}
// Перехват могли снять с зажатой клавишей — например, сняв галочку
// в настройках. Подсказку в этом случае нужно убрать
private void ResetPress()
{
_isPressed = false;
_holdTimer.Stop();
if (_isHolding)
{
_isHolding = false;
Notify(HoldEnded);
}
}
}