using System.Runtime.InteropServices; using System.Windows.Forms; namespace Robovoice.App; internal sealed class PttHotkey : IDisposable { private const int WH_KEYBOARD_LL = 13; private const int WH_MOUSE_LL = 14; private const int WM_KEYDOWN = 0x0100; private const int WM_KEYUP = 0x0101; private const int WM_SYSKEYDOWN = 0x0104; private const int WM_SYSKEYUP = 0x0105; private const int WM_LBUTTONDOWN = 0x0201; private const int WM_LBUTTONUP = 0x0202; private const int WM_RBUTTONDOWN = 0x0204; private const int WM_RBUTTONUP = 0x0205; private const int WM_MBUTTONDOWN = 0x0207; private const int WM_MBUTTONUP = 0x0208; private const int WM_XBUTTONDOWN = 0x020B; private const int WM_XBUTTONUP = 0x020C; private readonly LowLevelKeyboardProc _kbProc; private readonly LowLevelMouseProc _msProc; private IntPtr _kbHook = IntPtr.Zero; private IntPtr _msHook = IntPtr.Zero; private bool _isDown; private bool _disposed; public Keys Key { get; set; } = Keys.F8; public static bool IsMouseButton(Keys key) => key is Keys.LButton or Keys.RButton or Keys.MButton or Keys.XButton1 or Keys.XButton2; public event EventHandler? Pressed; public event EventHandler? Released; public event Action? CaptureKeyPressed; public void InstallCaptureHook() { ObjectDisposedException.ThrowIf(_disposed, this); if (_kbHook != IntPtr.Zero) return; IntPtr hModule = GetModuleHandle(null); _kbHook = SetWindowsHookEx(WH_KEYBOARD_LL, _kbProc, hModule, 0); } public PttHotkey() { _kbProc = KeyboardCallback; _msProc = MouseCallback; } public void Install() { ObjectDisposedException.ThrowIf(_disposed, this); if (_kbHook != IntPtr.Zero || _msHook != IntPtr.Zero) return; IntPtr hModule = GetModuleHandle(null); if (IsMouseButton(Key)) _msHook = SetWindowsHookEx(WH_MOUSE_LL, _msProc, hModule, 0); else _kbHook = SetWindowsHookEx(WH_KEYBOARD_LL, _kbProc, hModule, 0); } public void Uninstall() { if (_kbHook != IntPtr.Zero) { UnhookWindowsHookEx(_kbHook); _kbHook = IntPtr.Zero; } if (_msHook != IntPtr.Zero) { UnhookWindowsHookEx(_msHook); _msHook = IntPtr.Zero; } _isDown = false; } private IntPtr KeyboardCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { int vkCode = Marshal.ReadInt32(lParam); Keys key = (Keys)vkCode; bool isDown = wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN; bool isUp = wParam == WM_KEYUP || wParam == WM_SYSKEYUP; if (CaptureKeyPressed is not null && isDown) { CaptureKeyPressed.Invoke(key); } else if (key == Key) { if (isDown && !_isDown) { _isDown = true; Pressed?.Invoke(this, EventArgs.Empty); } else if (isUp && _isDown) { _isDown = false; Released?.Invoke(this, EventArgs.Empty); } } } return CallNextHookEx(_kbHook, nCode, wParam, lParam); } private IntPtr MouseCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { Keys? button = null; switch ((int)wParam) { case WM_LBUTTONDOWN: button = Keys.LButton; break; case WM_LBUTTONUP: button = Keys.LButton; break; case WM_RBUTTONDOWN: button = Keys.RButton; break; case WM_RBUTTONUP: button = Keys.RButton; break; case WM_MBUTTONDOWN: button = Keys.MButton; break; case WM_MBUTTONUP: button = Keys.MButton; break; case WM_XBUTTONDOWN: case WM_XBUTTONUP: { int xButton = Marshal.ReadInt32(lParam + 8) >> 16; button = xButton == 1 ? Keys.XButton1 : Keys.XButton2; break; } } if (button is Keys btn && btn == Key) { bool isDown = wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN || wParam == WM_MBUTTONDOWN || wParam == WM_XBUTTONDOWN; bool isUp = wParam == WM_LBUTTONUP || wParam == WM_RBUTTONUP || wParam == WM_MBUTTONUP || wParam == WM_XBUTTONUP; if (isDown && !_isDown) { _isDown = true; Pressed?.Invoke(this, EventArgs.Empty); } else if (isUp && _isDown) { _isDown = false; Released?.Invoke(this, EventArgs.Empty); } } } return CallNextHookEx(_msHook, nCode, wParam, lParam); } public void Dispose() { if (_disposed) return; Uninstall(); _disposed = true; } private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam); private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string? lpModuleName); }