Files
Taskbar-Launcher/Services/HotkeyService.cs
2026-06-02 21:47:00 -04:00

193 lines
5.2 KiB
C#

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<string> 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);
}