Centralized hotkey registration

Only one window handle is required for all hotkeys.
Simplifies maintaining multiple hotkeys spreaded over multiple windows.
This commit is contained in:
Peter Kirmeier 2023-05-27 16:37:39 +02:00
parent 60329d524f
commit 8ece540881
4 changed files with 136 additions and 146 deletions

View file

@ -13,7 +13,7 @@ namespace SystemTrayMenu.Handler
internal class KeyboardInput : IDisposable internal class KeyboardInput : IDisposable
{ {
private readonly KeyboardHook hook = new(); private GlobalHotkeys.HotkeyRegistrationHandle? hotkeyHandle;
private Menu? focussedMenu; private Menu? focussedMenu;
@ -27,7 +27,7 @@ namespace SystemTrayMenu.Handler
public void Dispose() public void Dispose()
{ {
hook.Dispose(); GlobalHotkeys.Unregister(hotkeyHandle);
} }
internal bool RegisterHotKey(string hotKeyString) internal bool RegisterHotKey(string hotKeyString)
@ -36,14 +36,15 @@ namespace SystemTrayMenu.Handler
{ {
try try
{ {
hook.RegisterHotKey(hotKeyString); hotkeyHandle = GlobalHotkeys.Register(hotKeyString);
hook.KeyPressed += (_, _) => HotKeyPressed?.Invoke();
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
{ {
Log.Warn($"Hotkey cannot be set: '{hotKeyString}'", ex); Log.Warn($"Hotkey cannot be set: '{hotKeyString}'", ex);
return false; return false;
} }
hotkeyHandle.KeyPressed += (_) => HotKeyPressed?.Invoke();
} }
return true; return true;

View file

@ -7,94 +7,109 @@
namespace SystemTrayMenu.Helpers namespace SystemTrayMenu.Helpers
{ {
using System; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Interop;
using SystemTrayMenu.DllImports; using SystemTrayMenu.DllImports;
using SystemTrayMenu.Utilities; using SystemTrayMenu.Utilities;
internal static class GlobalHotkeys internal static class GlobalHotkeys
{ {
private static readonly object LastIdLock = new(); private static readonly HwndSourceHook Hook = new (WndProc);
private static int lastId = 0; private static readonly Window Window = new();
private static readonly HwndSource HWnd;
/// <summary> private static readonly List<HotkeyRegistration> Registrations = new();
/// Registers a global hotkey in a thread safe manner. private static readonly object CriticalSectionLock = new();
/// Throws an InvalidOperationException on error.
/// The caller needs to call UnregisterHotkey to free up ressources. static GlobalHotkeys()
/// </summary>
/// <param name="hWnd">Window handle receiving the events.</param>
/// <param name="modifiers">Hotkey modifiers.</param>
/// <param name="key">Hotkey major key.</param>
/// <returns>ID of registered key.</returns>
internal static int RegisterHotkeyGlobal(IntPtr hWnd, ModifierKeys modifiers, Key key)
{ {
lock (LastIdLock) HWnd = HwndSource.FromHwnd(new WindowInteropHelper(Window).EnsureHandle());
{ HWnd.AddHook(Hook);
lastId = RegisterHotkeyLocal(hWnd, lastId + 1, modifiers, key);
}
return lastId;
} }
/// <summary> /// <summary>
/// Registers a global hotkey in a thread safe manner. /// Registers a global hotkey.
/// Function is thread safe.
/// Throws an InvalidOperationException on error. /// Throws an InvalidOperationException on error.
/// The caller needs to call UnregisterHotkey to free up ressources. /// The caller needs to call UnregisterHotkey to free up ressources.
/// </summary> /// </summary>
/// <param name="hWnd">Window handle receiving the events.</param>
/// <param name="hotKeyString">Hotkey string description.</param>
/// <returns>ID of registered key.</returns>
internal static int RegisterHotkeyGlobal(IntPtr hWnd, string hotKeyString)
{
ModifierKeys modifiers = ModifierKeysFromString(hotKeyString);
Key key = KeyFromString(hotKeyString);
return RegisterHotkeyGlobal(hWnd, modifiers, key);
}
/// <summary>
/// Registers a local hotkey (with given ID).
/// Throws an InvalidOperationException on error.
/// The caller needs to call UnregisterHotkey to free up ressources.
/// </summary>
/// <param name="hWnd">Window handle receiving the events.</param>
/// <param name="id">ID for the registration.</param>
/// <param name="modifiers">Hotkey modifiers.</param> /// <param name="modifiers">Hotkey modifiers.</param>
/// <param name="key">Hotkey major key.</param> /// <param name="key">Hotkey major key.</param>
/// <returns>ID of registered key.</returns> /// <returns>Handle of this registration.</returns>
internal static int RegisterHotkeyLocal(IntPtr hWnd, int id, ModifierKeys modifiers, Key key) internal static HotkeyRegistrationHandle Register(ModifierKeys modifiers, Key key)
{ {
int virtualKeyCode = KeyInterop.VirtualKeyFromKey(key); int virtualKeyCode = KeyInterop.VirtualKeyFromKey(key);
if (!NativeMethods.User32RegisterHotKey(hWnd, id, (uint)modifiers, (uint)virtualKeyCode)) int id = 0;
lock (CriticalSectionLock)
{ {
string errorHint = NativeMethods.GetLastErrorHint(); foreach (var reg in Registrations)
throw new InvalidOperationException(Translator.GetText("Could not register the hot key.") + " (" + errorHint + ")"); {
if (id < reg.Id)
{
id = reg.Id;
}
}
if (!NativeMethods.User32RegisterHotKey(HWnd.Handle, id, (uint)modifiers, (uint)virtualKeyCode))
{
string errorHint = NativeMethods.GetLastErrorHint();
throw new InvalidOperationException(Translator.GetText("Could not register the hot key.") + " (" + errorHint + ")");
}
} }
return id; HotkeyRegistration regHandle = new()
{
Id = id,
Modifiers = modifiers,
Key = key,
};
Registrations.Add(regHandle);
return regHandle;
} }
/// <summary> /// <summary>
/// Registers a local hotkey (with given ID). /// Registers a global hotkey.
/// Function is thread safe.
/// Throws an InvalidOperationException on error. /// Throws an InvalidOperationException on error.
/// The caller needs to call UnregisterHotkey to free up ressources. /// The caller needs to call UnregisterHotkey to free up ressources.
/// </summary> /// </summary>
/// <param name="hWnd">Window handle receiving the events.</param> /// <param name="hotKeyString">Hotkey string representation.</param>
/// <param name="id">ID for the registration.</param> /// <returns>Handle of this registration.</returns>
/// <param name="hotKeyString">Hotkey string description.</param> internal static HotkeyRegistrationHandle Register(string hotKeyString)
/// <returns>ID of registered key.</returns>
internal static int RegisterHotkeyLocal(IntPtr hWnd, int id, string hotKeyString)
{ {
ModifierKeys modifiers = ModifierKeysFromString(hotKeyString); var (modifiers, key) = ParseKeysAndModifiersFromString(hotKeyString);
Key key = KeyFromString(hotKeyString); return Register(modifiers, key);
return RegisterHotkeyLocal(hWnd, id, modifiers, key);
} }
/// <summary> /// <summary>
/// Unregisters a local or global hotkey. /// Unregisters a global hotkey in a thread safe manner.
/// Function is thread safe.
/// </summary> /// </summary>
/// <param name="hWnd">Window handle.</param> /// <param name="regHandle">Handle of the registration.</param>
/// <param name="id">ID for the registration.</param> /// <returns>true: Success or false: Failure.</returns>
internal static void UnregisterHotkey(IntPtr hWnd, int id) => NativeMethods.User32UnregisterHotKey(hWnd, id); internal static bool Unregister(HotkeyRegistrationHandle? regHandle)
{
if (regHandle == null || regHandle is not HotkeyRegistration reg || Registrations.Contains(reg))
{
return true;
}
lock (CriticalSectionLock)
{
if (!NativeMethods.User32UnregisterHotKey(HWnd.Handle, reg.Id))
{
return false;
}
Registrations.Remove(reg);
}
return true;
}
internal static ModifierKeys ModifierKeysFromString(string modifiersString) internal static ModifierKeys ModifierKeysFromString(string modifiersString)
{ {
@ -264,5 +279,56 @@ namespace SystemTrayMenu.Helpers
return key.ToString(); return key.ToString();
} }
} }
private static (ModifierKeys, Key) ParseKeysAndModifiersFromString(string hotKeyString) => (ModifierKeysFromString(hotKeyString), KeyFromString(hotKeyString));
private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int WmHotkey = 0x0312;
// check if we got a hot key pressed.
if (msg == WmHotkey)
{
ModifierKeys modifiers = (ModifierKeys)((int)lParam & 0xFFFF);
int virtualKeyCode = ((int)lParam >> 16) & 0xFFFF;
Key key = KeyInterop.KeyFromVirtualKey(virtualKeyCode);
HotkeyRegistration? reg = null;
lock (CriticalSectionLock)
{
foreach (var regHandle in Registrations)
{
if (modifiers == regHandle.Modifiers && key == regHandle.Key)
{
reg = regHandle;
break;
}
}
}
reg?.OnKeyPressed();
}
handled = false;
return IntPtr.Zero;
}
internal abstract class HotkeyRegistrationHandle
{
internal event Action<HotkeyRegistrationHandle>? KeyPressed;
protected void RiseKeyPressed() => KeyPressed?.Invoke(this);
}
private class HotkeyRegistration : HotkeyRegistrationHandle
{
internal int Id { get; init; }
internal ModifierKeys Modifiers { get; set; }
internal Key Key { get; set; }
internal void OnKeyPressed() => RiseKeyPressed();
}
} }
} }

View file

@ -1,73 +0,0 @@
// <copyright file="KeyboardHook.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace SystemTrayMenu.Helpers
{
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
// TODO: Move this whole class into mainMenu to spare creating additional Win32 window handles by using Menu's Window directly.
internal class KeyboardHook : IDisposable
{
private readonly HwndSourceHook hook;
private readonly HwndSource hwnd;
private readonly Window window = new();
private int currentId;
public KeyboardHook()
{
hwnd = HwndSource.FromHwnd(new WindowInteropHelper(window).EnsureHandle());
hook = new HwndSourceHook(WndProc);
hwnd.AddHook(hook);
}
/// <summary>
/// A hot key has been pressed.
/// </summary>
internal event Action<Key, ModifierKeys>? KeyPressed;
public void Dispose()
{
// On regular App.Dispose the handle was already invalidated
if (hwnd.Handle != IntPtr.Zero)
{
// unregister all the registered hot keys.
for (int i = currentId; i > 0; i--)
{
GlobalHotkeys.UnregisterHotkey(hwnd.Handle, i);
}
hwnd.RemoveHook(hook);
}
hwnd.Dispose();
}
internal void RegisterHotKey(ModifierKeys modifiers, Key key) => currentId = GlobalHotkeys.RegisterHotkeyLocal(hwnd.Handle, currentId + 1, modifiers, key);
internal void RegisterHotKey(string hotKeyString) => currentId = GlobalHotkeys.RegisterHotkeyLocal(hwnd.Handle, currentId + 1, hotKeyString);
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int WmHotkey = 0x0312;
// check if we got a hot key pressed.
if (msg == WmHotkey)
{
// get the keys.
ModifierKeys modifiers = (ModifierKeys)((int)lParam & 0xFFFF);
int virtualKeyCode = ((int)lParam >> 16) & 0xFFFF;
Key key = KeyInterop.KeyFromVirtualKey(virtualKeyCode);
// invoke the event to notify the parent.
KeyPressed?.Invoke(key, modifiers);
}
handled = false;
return IntPtr.Zero;
}
}
}

View file

@ -14,14 +14,14 @@ namespace SystemTrayMenu.UserInterface
using SystemTrayMenu.Helpers; using SystemTrayMenu.Helpers;
using SystemTrayMenu.Utilities; using SystemTrayMenu.Utilities;
public sealed class HotkeySelector : TextBox, IDisposable public sealed class HotkeySelector : TextBox
{ {
// ArrayLists used to enforce the use of proper modifiers. // ArrayLists used to enforce the use of proper modifiers.
// Shift+A isn't a valid hotkey, for instance, as it would screw up when the user is typing. // Shift+A isn't a valid hotkey, for instance, as it would screw up when the user is typing.
private readonly IList<int> needNonShiftModifier = new List<int>(); private readonly IList<int> needNonShiftModifier = new List<int>();
private readonly IList<int> needNonAltGrModifier = new List<int>(); private readonly IList<int> needNonAltGrModifier = new List<int>();
private KeyboardHook hook = new(); private GlobalHotkeys.HotkeyRegistrationHandle? hotkeyHandle;
// These variables store the current hotkey and modifier(s) // These variables store the current hotkey and modifier(s)
private Key hotkey = Key.None; private Key hotkey = Key.None;
@ -75,7 +75,10 @@ namespace SystemTrayMenu.UserInterface
PopulateModifierLists(); PopulateModifierLists();
} }
~HotkeySelector() => Dispose(); ~HotkeySelector()
{
GlobalHotkeys.Unregister(hotkeyHandle);
}
/// <summary> /// <summary>
/// Gets or sets used to get/set the hotkey (e.g. Key.A). /// Gets or sets used to get/set the hotkey (e.g. Key.A).
@ -131,12 +134,6 @@ namespace SystemTrayMenu.UserInterface
return hotkeyString.ToString() + key.ToString(); return hotkeyString.ToString() + key.ToString();
} }
public void Dispose()
{
hook.Dispose();
GC.SuppressFinalize(this);
}
public override string ToString() => HotkeyToString(modifiers, hotkey); public override string ToString() => HotkeyToString(modifiers, hotkey);
/// <summary> /// <summary>
@ -166,7 +163,7 @@ namespace SystemTrayMenu.UserInterface
try try
{ {
hook.RegisterHotKey(modifiers, key); hotkeyHandle = GlobalHotkeys.Register(modifiers, key);
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
{ {
@ -175,7 +172,7 @@ namespace SystemTrayMenu.UserInterface
} }
this.handler = handler; this.handler = handler;
hook.KeyPressed += (_, _) => handler.Invoke(); hotkeyHandle.KeyPressed += (_) => handler.Invoke();
return 1; return 1;
} }
@ -189,9 +186,8 @@ namespace SystemTrayMenu.UserInterface
private void UnregisterHotKey() private void UnregisterHotKey()
{ {
// TODO: Rework to allow deregistration? GlobalHotkeys.Unregister(hotkeyHandle);
hook.Dispose(); hotkeyHandle = null;
hook = new();
} }
/// <summary> /// <summary>