9b0dc11ee6
- Replace PTT key dropdown with live key-capture textbox - Use low-level keyboard hook for capture (distinguishes Left/Right Ctrl) - Support mouse buttons as PTT (Left/Right/Middle/X1/X2 via WH_MOUSE_LL) - Filter mouse clicks from capture, allow Left/Right Ctrl - Add Noise/Speed/NoiseW sliders with live labels, reinit on MouseUp - Stream audio via BufferedWaveProvider (no temp WAV files) - Add text input field with Speak button - Fix sentence terminator, UTF-8 marshaling, voice catalogue JSON
187 lines
6.1 KiB
C#
187 lines
6.1 KiB
C#
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<Keys>? 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);
|
|
}
|