added layour swithing by caps key
This commit is contained in:
@@ -7,6 +7,14 @@ namespace CursorLang.Interop;
|
||||
/// </summary>
|
||||
internal static class KeyboardLayoutNative
|
||||
{
|
||||
private const uint WmInputLangChangeRequest = 0x0050;
|
||||
|
||||
/// <summary>Взять следующую раскладку из системного списка.</summary>
|
||||
private static readonly IntPtr InputLangChangeForward = new(0x0002);
|
||||
|
||||
/// <summary>HKL_NEXT — та же просьба на языке старых версий Windows.</summary>
|
||||
private static readonly IntPtr HklNext = new(1);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern IntPtr GetForegroundWindow();
|
||||
|
||||
@@ -16,24 +24,53 @@ internal static class KeyboardLayoutNative
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetKeyboardLayout(uint idThread);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
/// <summary>
|
||||
/// Раскладка, которой сейчас печатает пользователь.
|
||||
/// </summary>
|
||||
internal static int GetActiveLocaleId() => GetLocaleIdOf(GetInputWindow());
|
||||
|
||||
/// <summary>
|
||||
/// Просит активное приложение перейти на следующую раскладку из системного списка.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Синтезировать системное сочетание вроде Alt+Shift не годится: его назначение
|
||||
/// пользователь меняет в настройках Windows и может отключить вовсе. Просьба
|
||||
/// сообщением от таких настроек не зависит и работает в чужом процессе.
|
||||
/// </remarks>
|
||||
internal static void RequestNextLayout()
|
||||
{
|
||||
IntPtr target = GetInputWindow();
|
||||
if (target == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Оба параметра означают одно и то же: разные версии Windows и разные
|
||||
// библиотеки интерфейса смотрят то на флаг, то на lParam
|
||||
PostMessage(target, WmInputLangChangeRequest, InputLangChangeForward, HklNext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Окно, которому принадлежит ввод с клавиатуры.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Спрашиваем окно с фокусом клавиатуры, а не окно верхнего плана: у Блокнота
|
||||
/// Windows 11, меню «Пуск» и прочих приложений на WinUI поле ввода живёт в
|
||||
/// отдельном потоке, и раскладка меняется только у него. У потока главного
|
||||
/// окна она остаётся прежней, и переключение проходит незамеченным.
|
||||
/// </remarks>
|
||||
internal static int GetActiveLocaleId()
|
||||
private static IntPtr GetInputWindow()
|
||||
{
|
||||
if (ForegroundInputNative.TryGetInfo(out ForegroundInputNative.GuiThreadInfo info) &&
|
||||
info.hwndFocus != IntPtr.Zero)
|
||||
{
|
||||
return GetLocaleIdOf(info.hwndFocus);
|
||||
return info.hwndFocus;
|
||||
}
|
||||
|
||||
return GetLocaleIdOf(GetForegroundWindow());
|
||||
return GetForegroundWindow();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Системный перехватчик клавиатуры (WH_KEYBOARD_LL): видит нажатия во всех
|
||||
/// приложениях и умеет не пропускать их дальше.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ставится только низкоуровневый хук: обычный WH_KEYBOARD требует внедрения
|
||||
/// DLL в чужие процессы, что для managed-кода невозможно. Обратный вызов
|
||||
/// приходит на тот поток, который поставил хук, и поток обязан крутить цикл
|
||||
/// сообщений — отсюда требование ставить хук из потока пользовательского
|
||||
/// интерфейса. Возврат управления затягивать нельзя: по истечении системного
|
||||
/// тайм-аута Windows молча снимает хук.
|
||||
/// </remarks>
|
||||
internal sealed class LowLevelKeyboardHook : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Обработчик события клавиши. Возвращает <c>true</c>, если событие нужно
|
||||
/// проглотить — тогда приложение переднего плана его не увидит.
|
||||
/// </summary>
|
||||
internal delegate bool KeyFilter(int virtualKey, bool isKeyDown);
|
||||
|
||||
private const int WhKeyboardLowLevel = 13;
|
||||
private const int HcAction = 0;
|
||||
|
||||
private const int WmKeyDown = 0x0100;
|
||||
private const int WmKeyUp = 0x0101;
|
||||
private const int WmSysKeyDown = 0x0104;
|
||||
private const int WmSysKeyUp = 0x0105;
|
||||
|
||||
/// <summary>Событие пришло не от живого нажатия, а из SendInput.</summary>
|
||||
private const uint LowLevelKeyHookFlagInjected = 0x10;
|
||||
|
||||
private readonly KeyFilter _filter;
|
||||
|
||||
// Делегат живёт в поле не для удобства: ссылку на него держит только Win32,
|
||||
// о которой сборщик мусора не знает, и без поля хук перестаёт работать
|
||||
// через случайное время
|
||||
private readonly HookProc _callback;
|
||||
|
||||
private IntPtr _handle;
|
||||
|
||||
internal LowLevelKeyboardHook(KeyFilter filter)
|
||||
{
|
||||
_filter = filter;
|
||||
_callback = OnHookEvent;
|
||||
}
|
||||
|
||||
private delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
internal bool IsInstalled => _handle != IntPtr.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Ставит хук. Возвращает <c>false</c>, если система отказала.
|
||||
/// </summary>
|
||||
internal bool Install()
|
||||
{
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_handle = SetWindowsHookEx(WhKeyboardLowLevel, _callback, GetModuleHandle(null), 0);
|
||||
return _handle != IntPtr.Zero;
|
||||
}
|
||||
|
||||
internal void Uninstall()
|
||||
{
|
||||
if (_handle == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnhookWindowsHookEx(_handle);
|
||||
_handle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
public void Dispose() => Uninstall();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr CallNextHookEx(IntPtr hhk, int code, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr GetModuleHandle(string? lpModuleName);
|
||||
|
||||
private IntPtr OnHookEvent(int code, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
if (code != HcAction)
|
||||
{
|
||||
return CallNextHookEx(_handle, code, wParam, lParam);
|
||||
}
|
||||
|
||||
var message = (int)wParam;
|
||||
bool isKeyDown = message is WmKeyDown or WmSysKeyDown;
|
||||
bool isKeyUp = message is WmKeyUp or WmSysKeyUp;
|
||||
|
||||
var data = Marshal.PtrToStructure<KeyboardHookData>(lParam);
|
||||
|
||||
// Синтетический ввод присылают экранные клавиатуры, программы
|
||||
// автозамены и средства автоматизации: подменять их работу не наше дело
|
||||
bool injected = (data.flags & LowLevelKeyHookFlagInjected) != 0;
|
||||
|
||||
if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown))
|
||||
{
|
||||
// Ненулевой результат вместо CallNextHookEx обрывает цепочку: событие
|
||||
// не дойдёт ни до приложения, ни до обработчика регистра в Windows
|
||||
return 1;
|
||||
}
|
||||
|
||||
return CallNextHookEx(_handle, code, wParam, lParam);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct KeyboardHookData
|
||||
{
|
||||
public uint vkCode;
|
||||
public uint scanCode;
|
||||
public uint flags;
|
||||
public uint time;
|
||||
public IntPtr dwExtraInfo;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user