138 lines
4.0 KiB
C#
138 lines
4.0 KiB
C#
using System.Windows.Threading;
|
|
using CursorLang.Interop;
|
|
using CursorLang.Models;
|
|
|
|
namespace CursorLang.Services;
|
|
|
|
/// <summary>
|
|
/// Holds the system Caps Lock hook and tells a short tap from a hold.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// They can be told apart only by the key being released, so both events are
|
|
/// intercepted — the press and the release. That is also the only way to cancel the
|
|
/// case change: Windows toggles Caps Lock on the press event, and letting it through
|
|
/// "just in case" is not an option.
|
|
/// </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(HandleKeyEvent);
|
|
_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();
|
|
}
|
|
|
|
// When the application is closing, nobody is waiting for events any more, so
|
|
// unlike in Stop the state is reset quietly: the dispatcher queue may be shut
|
|
// down by that moment
|
|
public void Dispose()
|
|
{
|
|
_holdTimer.Tick -= OnHoldTimerTick;
|
|
_holdTimer.Stop();
|
|
_isPressed = false;
|
|
_isHolding = false;
|
|
_hook.Dispose();
|
|
}
|
|
|
|
// Called by the system hook, that is, inside message queue processing. Only state
|
|
// tracking belongs here: showing windows and raising events from here is not
|
|
// allowed — the handler must return control within a few milliseconds.
|
|
// In tests the key presses are fed here as well: there is no need to install a
|
|
// real keyboard hook just to check how presses are interpreted
|
|
internal bool HandleKeyEvent(int virtualKey, bool isKeyDown)
|
|
{
|
|
if (virtualKey != VirtualKeyCapsLock)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (isKeyDown)
|
|
{
|
|
// While the key is held, Windows repeats the press: the hold is counted
|
|
// from the first event and the repeats are ignored
|
|
if (!_isPressed)
|
|
{
|
|
_isPressed = true;
|
|
|
|
// The threshold is read on every press: it is changed in the settings on the fly
|
|
_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);
|
|
}
|
|
|
|
// The event reaches the subscribers after the hook returns: they are free to show
|
|
// windows and do anything else without holding up the handling of the key press
|
|
private void Notify(EventHandler? handler)
|
|
{
|
|
if (handler is not null)
|
|
{
|
|
_dispatcher.BeginInvoke(() => handler(this, EventArgs.Empty));
|
|
}
|
|
}
|
|
|
|
// The hook may have been removed with the key held down — by clearing the
|
|
// checkbox in the settings, for instance. The popup has to be taken down then
|
|
private void ResetPress()
|
|
{
|
|
_isPressed = false;
|
|
_holdTimer.Stop();
|
|
|
|
if (_isHolding)
|
|
{
|
|
_isHolding = false;
|
|
Notify(HoldEnded);
|
|
}
|
|
}
|
|
}
|