106 lines
4.9 KiB
C#
106 lines
4.9 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);
|
|
}
|
|
}
|
|
|
|
internal 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 if (VirtualKeys.TryGetValue(part, out var virtualKey)) key = virtualKey;
|
|
else return false;
|
|
}
|
|
return key != 0;
|
|
}
|
|
|
|
private static readonly Dictionary<string, uint> VirtualKeys = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["Backspace"] = 0x08, ["Tab"] = 0x09, ["Enter"] = 0x0D, ["Pause"] = 0x13,
|
|
["CapsLock"] = 0x14, ["Escape"] = 0x1B, ["Space"] = 0x20, ["PageUp"] = 0x21,
|
|
["PageDown"] = 0x22, ["End"] = 0x23, ["Home"] = 0x24, ["Left"] = 0x25,
|
|
["Up"] = 0x26, ["Right"] = 0x27, ["Down"] = 0x28, ["Insert"] = 0x2D,
|
|
["Delete"] = 0x2E, ["NumLock"] = 0x90, ["ScrollLock"] = 0x91
|
|
};
|
|
|
|
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);
|
|
}
|