136 lines
4.7 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|