Lumi/companion/plugins/Lumi.Companion.Overlay/GlobalHotkeyManager.cs
2026-07-26 22:06:34 +02:00

96 lines
4.3 KiB
C#

using System.Runtime.InteropServices;
namespace Lumi.Companion.Overlay;
public sealed class GlobalHotkeyManager : IDisposable
{
private const uint WmHotkey = 0x0312;
private const uint WmQuit = 0x0012;
private readonly Dictionary<int, Action> _actions = [];
private readonly ManualResetEventSlim _ready = new();
private readonly Thread _thread;
private IntPtr _window;
private uint _threadId;
private int _nextId = 0x4C00;
public GlobalHotkeyManager()
{
_thread = new Thread(MessageLoop) { IsBackground = true, Name = "Lumi Overlay hotkeys" };
_thread.Start();
_ready.Wait(TimeSpan.FromSeconds(3));
}
public bool Register(string chord, Action action)
{
if (!TryParse(chord, out var modifiers, out var key) || _window == IntPtr.Zero) return false;
var id = Interlocked.Increment(ref _nextId);
if (!RegisterHotKey(_window, id, modifiers | 0x4000, key)) return false;
lock (_actions) _actions[id] = action;
return true;
}
public void Clear()
{
lock (_actions)
{
foreach (var id in _actions.Keys) UnregisterHotKey(_window, id);
_actions.Clear();
}
}
private void MessageLoop()
{
_threadId = GetCurrentThreadId();
_window = CreateWindowEx(0, "STATIC", "LumiOverlayHotkeys", 0, 0, 0, 0, 0, new IntPtr(-3), IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
_ready.Set();
while (GetMessage(out var message, IntPtr.Zero, 0, 0) > 0)
{
if (message.message == WmHotkey)
{
Action? action;
lock (_actions) _actions.TryGetValue(message.wParam.ToInt32(), out action);
try { action?.Invoke(); } catch { }
}
TranslateMessage(ref message);
DispatchMessage(ref message);
}
}
private static bool TryParse(string chord, out uint modifiers, out uint key)
{
modifiers = 0;
key = 0;
var parts = (chord ?? string.Empty).Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
if (part.Equals("Ctrl", StringComparison.OrdinalIgnoreCase)) modifiers |= 0x0002;
else if (part.Equals("Alt", StringComparison.OrdinalIgnoreCase)) modifiers |= 0x0001;
else if (part.Equals("Shift", StringComparison.OrdinalIgnoreCase)) modifiers |= 0x0004;
else if (part.Equals("Win", StringComparison.OrdinalIgnoreCase)) modifiers |= 0x0008;
else if (part.Length == 1 && char.IsAsciiLetterOrDigit(part[0])) key = char.ToUpperInvariant(part[0]);
else if (part.StartsWith('F') && int.TryParse(part[1..], out var function) && function is >= 1 and <= 24) key = (uint)(0x70 + function - 1);
else return false;
}
return key != 0;
}
public void Dispose()
{
Clear();
if (_threadId != 0) PostThreadMessage(_threadId, WmQuit, IntPtr.Zero, IntPtr.Zero);
_thread.Join(TimeSpan.FromSeconds(1));
_ready.Dispose();
}
[StructLayout(LayoutKind.Sequential)]
private struct Msg { public IntPtr hwnd; public uint message; public IntPtr wParam; public IntPtr lParam; public uint time; public int x; public int y; }
[DllImport("user32.dll", SetLastError = true)] private static extern bool RegisterHotKey(IntPtr hwnd, int id, uint modifiers, uint key);
[DllImport("user32.dll", SetLastError = true)] private static extern bool UnregisterHotKey(IntPtr hwnd, int id);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr CreateWindowEx(uint exStyle, string className, string windowName, uint style, int x, int y, int width, int height, IntPtr parent, IntPtr menu, IntPtr instance, IntPtr parameter);
[DllImport("user32.dll")] private static extern int GetMessage(out Msg message, IntPtr hwnd, uint min, uint max);
[DllImport("user32.dll")] private static extern bool TranslateMessage(ref Msg message);
[DllImport("user32.dll")] private static extern IntPtr DispatchMessage(ref Msg message);
[DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")] private static extern bool PostThreadMessage(uint threadId, uint message, IntPtr wParam, IntPtr lParam);
}