mirror of
https://github.com/Hofknecht/SystemTrayMenu.git
synced 2024-09-16 17:27:30 +12:00
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:
parent
60329d524f
commit
8ece540881
4 changed files with 136 additions and 146 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
private static readonly List<HotkeyRegistration> Registrations = new();
|
||||||
|
private static readonly object CriticalSectionLock = new();
|
||||||
|
|
||||||
|
static GlobalHotkeys()
|
||||||
|
{
|
||||||
|
HWnd = HwndSource.FromHwnd(new WindowInteropHelper(Window).EnsureHandle());
|
||||||
|
HWnd.AddHook(Hook);
|
||||||
|
}
|
||||||
|
|
||||||
/// <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="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 RegisterHotkeyGlobal(IntPtr hWnd, ModifierKeys modifiers, Key key)
|
internal static HotkeyRegistrationHandle Register(ModifierKeys modifiers, Key key)
|
||||||
{
|
|
||||||
lock (LastIdLock)
|
|
||||||
{
|
|
||||||
lastId = RegisterHotkeyLocal(hWnd, lastId + 1, modifiers, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers a global hotkey in a thread safe manner.
|
|
||||||
/// 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="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="key">Hotkey major key.</param>
|
|
||||||
/// <returns>ID of registered key.</returns>
|
|
||||||
internal static int RegisterHotkeyLocal(IntPtr hWnd, int id, 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)
|
||||||
|
{
|
||||||
|
foreach (var reg in Registrations)
|
||||||
|
{
|
||||||
|
if (id < reg.Id)
|
||||||
|
{
|
||||||
|
id = reg.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NativeMethods.User32RegisterHotKey(HWnd.Handle, id, (uint)modifiers, (uint)virtualKeyCode))
|
||||||
{
|
{
|
||||||
string errorHint = NativeMethods.GetLastErrorHint();
|
string errorHint = NativeMethods.GetLastErrorHint();
|
||||||
throw new InvalidOperationException(Translator.GetText("Could not register the hot key.") + " (" + errorHint + ")");
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue