added layour swithing by caps key
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.ComponentModel;
|
||||
using CursorLang.Models;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Превращает нажатия Caps Lock в действия: короткое — переключает раскладку,
|
||||
/// длинное — только показывает подсказку. Он же включает и выключает перехват
|
||||
/// вслед за галочкой в настройках.
|
||||
/// </summary>
|
||||
public sealed class CapsLockSwitchCoordinator : IDisposable
|
||||
{
|
||||
private readonly ICapsLockHotkeyService _hotkeyService;
|
||||
private readonly IKeyboardLayoutService _layoutService;
|
||||
private readonly ILayoutPopupService _popupService;
|
||||
private readonly AppSettings _settings;
|
||||
|
||||
public CapsLockSwitchCoordinator(
|
||||
ICapsLockHotkeyService hotkeyService,
|
||||
IKeyboardLayoutService layoutService,
|
||||
ILayoutPopupService popupService,
|
||||
AppSettings settings)
|
||||
{
|
||||
_hotkeyService = hotkeyService;
|
||||
_layoutService = layoutService;
|
||||
_popupService = popupService;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_hotkeyService.Tapped += OnTapped;
|
||||
_hotkeyService.HoldStarted += OnHoldStarted;
|
||||
_hotkeyService.HoldEnded += OnHoldEnded;
|
||||
_settings.PropertyChanged += OnSettingsChanged;
|
||||
|
||||
ApplySetting();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_settings.PropertyChanged -= OnSettingsChanged;
|
||||
_hotkeyService.Tapped -= OnTapped;
|
||||
_hotkeyService.HoldStarted -= OnHoldStarted;
|
||||
_hotkeyService.HoldEnded -= OnHoldEnded;
|
||||
_hotkeyService.Stop();
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(AppSettings.UseCapsLockHotkey))
|
||||
{
|
||||
ApplySetting();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySetting()
|
||||
{
|
||||
if (_settings.UseCapsLockHotkey)
|
||||
{
|
||||
_hotkeyService.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
_hotkeyService.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTapped(object? sender, EventArgs e) => _layoutService.SwitchToNext();
|
||||
|
||||
// Раскладку не меняем, но и молчать нельзя: без подсказки долгое нажатие
|
||||
// выглядит как то, что клавиша просто не сработала
|
||||
private void OnHoldStarted(object? sender, EventArgs e) =>
|
||||
_popupService.ShowUntilHidden(_layoutService.Current);
|
||||
|
||||
private void OnHoldEnded(object? sender, EventArgs e) => _popupService.Hide();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Перехватывает Caps Lock на уровне системы и делит нажатия на короткие
|
||||
/// и длинные. Что делать с ними — решают подписчики.
|
||||
/// </summary>
|
||||
public interface ICapsLockHotkeyService
|
||||
{
|
||||
/// <summary>Короткое нажатие: клавишу отпустили до порога удержания.</summary>
|
||||
event EventHandler? Tapped;
|
||||
|
||||
/// <summary>Порог удержания пройден, клавишу всё ещё держат.</summary>
|
||||
event EventHandler? HoldStarted;
|
||||
|
||||
/// <summary>Удержание закончилось: клавишу отпустили.</summary>
|
||||
event EventHandler? HoldEnded;
|
||||
|
||||
/// <summary>Стоит ли перехват прямо сейчас.</summary>
|
||||
bool IsRunning { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Начинает перехват. Вызывать нужно из потока пользовательского интерфейса:
|
||||
/// системный хук клавиатуры работает только на потоке с циклом сообщений.
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>Снимает перехват, возвращая клавише её обычное поведение.</summary>
|
||||
void Stop();
|
||||
}
|
||||
@@ -3,7 +3,8 @@ using CursorLang.Models;
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Следит за раскладкой активного окна — в том числе в чужих приложениях.
|
||||
/// Следит за раскладкой активного окна — в том числе в чужих приложениях —
|
||||
/// и умеет её переключать.
|
||||
/// </summary>
|
||||
public interface IKeyboardLayoutService
|
||||
{
|
||||
@@ -15,4 +16,6 @@ public interface IKeyboardLayoutService
|
||||
void Start();
|
||||
|
||||
void Stop();
|
||||
|
||||
void SwitchToNext();
|
||||
}
|
||||
|
||||
@@ -7,5 +7,14 @@ namespace CursorLang.Services;
|
||||
/// </summary>
|
||||
public interface ILayoutPopupService
|
||||
{
|
||||
/// <summary>Показывает подсказку и убирает её через заданное в настройках время.</summary>
|
||||
void Show(KeyboardLayout layout);
|
||||
|
||||
/// <summary>
|
||||
/// Показывает подсказку до явного вызова <see cref="Hide"/>: нужна там, где
|
||||
/// время показа задаёт не таймер, а действие пользователя.
|
||||
/// </summary>
|
||||
void ShowUntilHidden(KeyboardLayout layout);
|
||||
|
||||
void Hide();
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ public sealed class KeyboardLayoutService : IKeyboardLayoutService, IDisposable
|
||||
|
||||
public void Stop() => _pollTimer.Stop();
|
||||
|
||||
public void SwitchToNext() => KeyboardLayoutNative.RequestNextLayout();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pollTimer.Stop();
|
||||
|
||||
@@ -27,16 +27,28 @@ public sealed class LayoutPopupService : ILayoutPopupService, IDisposable
|
||||
|
||||
public void Show(KeyboardLayout layout)
|
||||
{
|
||||
_viewModel.ShortName = layout.ShortName;
|
||||
_window.ShowPopup();
|
||||
ShowUntilHidden(layout);
|
||||
|
||||
// Длительность читаем при каждом показе: её меняют в настройках на лету.
|
||||
// Перезапуск таймера заодно продлевает показ при быстрых переключениях
|
||||
_hideTimer.Stop();
|
||||
_hideTimer.Interval = _settings.Duration;
|
||||
_hideTimer.Start();
|
||||
}
|
||||
|
||||
public void ShowUntilHidden(KeyboardLayout layout)
|
||||
{
|
||||
_hideTimer.Stop();
|
||||
|
||||
_viewModel.ShortName = layout.ShortName;
|
||||
_window.ShowPopup();
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
_hideTimer.Stop();
|
||||
_window.Hide();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_hideTimer.Stop();
|
||||
|
||||
Reference in New Issue
Block a user