using System.Windows.Threading; using CursorLang.Interop; using CursorLang.Models; namespace CursorLang.Services; /// /// Держит системный перехват Caps Lock и отличает короткое нажатие от удержания. /// /// /// Различить их можно только по факту отпускания клавиши, поэтому перехватываются /// оба события — и нажатие, и отпускание. Заодно это единственный способ отменить /// смену регистра: Windows переключает Caps Lock по событию нажатия, и пропустить /// его «на всякий случай» нельзя. /// 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); } } }