using System.Runtime.InteropServices; using System.ComponentModel; using System.Windows; using System.Windows.Input; using System.Windows.Interop; namespace TaskbarLauncher.Services; public sealed class HotkeyService : IDisposable { private const int WmHotkey = 0x0312; private const int HotkeyId = 9001; private HwndSource? source; private Window? window; public event EventHandler? Pressed; public void Register(string hotkey) { ParseHotkey(hotkey, out uint modifiers, out uint key); Unregister(); window = new Window { Width = 0, Height = 0, ShowInTaskbar = false, WindowStyle = WindowStyle.None, AllowsTransparency = true, Opacity = 0, Left = -10000, Top = -10000 }; window.SourceInitialized += (_, _) => { var helper = new WindowInteropHelper(window); source = HwndSource.FromHwnd(helper.Handle); source?.AddHook(WndProc); if (!RegisterHotKey(helper.Handle, HotkeyId, modifiers, key)) { int error = Marshal.GetLastWin32Error(); throw new InvalidOperationException( $"Windows could not register hotkey '{Normalize(hotkey)}'. It may already be used by another app.", new Win32Exception(error)); } }; window.Show(); window.Hide(); } public void Unregister() { if (window is not null) { var helper = new WindowInteropHelper(window); if (helper.Handle != IntPtr.Zero) { UnregisterHotKey(helper.Handle, HotkeyId); } } source?.RemoveHook(WndProc); source = null; window?.Close(); window = null; } public void Dispose() { Unregister(); } private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WmHotkey && wParam.ToInt32() == HotkeyId) { Pressed?.Invoke(this, EventArgs.Empty); handled = true; } return IntPtr.Zero; } public static string Normalize(string hotkey) { ParseHotkey(hotkey, out uint modifiers, out uint key); Key parsedKey = KeyInterop.KeyFromVirtualKey((int)key); List parts = []; if ((modifiers & 0x0002) != 0) { parts.Add("Ctrl"); } if ((modifiers & 0x0001) != 0) { parts.Add("Alt"); } if ((modifiers & 0x0004) != 0) { parts.Add("Shift"); } if ((modifiers & 0x0008) != 0) { parts.Add("Win"); } parts.Add(parsedKey.ToString()); return string.Join("+", parts); } private static void ParseHotkey(string hotkey, out uint modifiers, out uint key) { modifiers = 0; key = 0; string[] parts = hotkey .Replace(",", "+", StringComparison.Ordinal) .Replace(" ", "+", StringComparison.Ordinal) .Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); foreach (string part in parts) { if (part.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || part.Equals("Control", 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 (TryParseKey(part, out Key parsedKey)) { key = (uint)KeyInterop.VirtualKeyFromKey(parsedKey); } else { throw new FormatException($"'{part}' is not a valid hotkey key."); } } if (modifiers == 0) { throw new FormatException("Hotkeys must include at least one modifier: Ctrl, Alt, Shift, or Win."); } if (key == 0) { throw new FormatException("Hotkeys must include a final key, such as Space, S, F12, or D1."); } } private static bool TryParseKey(string value, out Key key) { if (Enum.TryParse(value, true, out key)) { return true; } if (value.Length == 1 && char.IsLetter(value[0])) { return Enum.TryParse(value.ToUpperInvariant(), out key); } if (value.Length == 1 && char.IsDigit(value[0])) { return Enum.TryParse($"D{value}", out key); } return false; } [DllImport("user32.dll", SetLastError = true)] private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); [DllImport("user32.dll", SetLastError = true)] private static extern bool UnregisterHotKey(IntPtr hWnd, int id); }