Refactor hotkey registration

Allow function registrations besides the actual hotkey registration.
This commit is contained in:
Peter Kirmeier 2023-05-29 23:56:52 +02:00
parent 637cc46e7e
commit 215c263052
4 changed files with 219 additions and 237 deletions

View file

@ -7,16 +7,21 @@ namespace SystemTrayMenu.Handler
using System;
using System.Windows.Input;
using SystemTrayMenu.DataClasses;
using SystemTrayMenu.Helpers;
using SystemTrayMenu.UserInterface;
using SystemTrayMenu.Utilities;
using static SystemTrayMenu.Helpers.GlobalHotkeys;
internal class KeyboardInput : IDisposable
{
private GlobalHotkeys.IHotkeyRegistration? hotkeyRegistration;
private readonly IHotkeyFunction hotkeyFunction = Create();
private Menu? focussedMenu;
public KeyboardInput()
{
hotkeyFunction.KeyPressed += (_) => HotKeyPressed?.Invoke();
}
internal event Action? HotKeyPressed;
internal event Action<RowData?>? RowSelectionChanged;
@ -27,7 +32,7 @@ namespace SystemTrayMenu.Handler
public void Dispose()
{
GlobalHotkeys.Unregister(hotkeyRegistration);
hotkeyFunction.Unregister();
}
internal bool RegisterHotKey(string hotKeyString)
@ -36,15 +41,13 @@ namespace SystemTrayMenu.Handler
{
try
{
hotkeyRegistration = GlobalHotkeys.Register(hotKeyString);
hotkeyFunction.Register(hotKeyString);
}
catch (InvalidOperationException ex)
{
Log.Warn($"Hotkey cannot be set: '{hotKeyString}'", ex);
return false;
}
hotkeyRegistration.KeyPressed += (_) => HotKeyPressed?.Invoke();
}
return true;

View file

@ -21,22 +21,30 @@ namespace SystemTrayMenu.Helpers
private static readonly Window Window = new();
private static readonly HwndSource HWnd;
private static readonly List<HotkeyRegistration> Registrations = new();
private static readonly Dictionary<int, HotkeyRegistration> Registrations = new();
private static readonly object CriticalSectionLock = new();
private static IHotkeyFunction? lastCreatedHotkeyFunction; // TODO: Remove this hack! See: GetLastCreatedHotkeyFunction
static GlobalHotkeys()
{
HWnd = HwndSource.FromHwnd(new WindowInteropHelper(Window).EnsureHandle());
HWnd.AddHook(Hook);
}
internal interface IHotkeyRegistration
internal interface IHotkeyFunction
{
event Action<IHotkeyRegistration>? KeyPressed;
event Action<IHotkeyFunction>? KeyPressed;
ModifierKeys GetModifierKeys();
Key GetKey();
void Register(ModifierKeys modifiers, Key key);
void Register(string hotKeyString);
void Unregister();
}
/// <summary>
@ -45,161 +53,11 @@ namespace SystemTrayMenu.Helpers
/// </summary>
internal static bool IsEnabled { get; set; } = true;
/// <summary>
/// Registers a global hotkey.
/// Function is thread safe.
/// Throws an InvalidOperationException on error.
/// The caller needs to call UnregisterHotkey to free up ressources.
/// </summary>
/// <param name="modifiers">Hotkey modifiers.</param>
/// <param name="key">Hotkey major key.</param>
/// <returns>Registration interface.</returns>
internal static IHotkeyRegistration Register(ModifierKeys modifiers, Key key)
{
int virtualKeyCode = KeyInterop.VirtualKeyFromKey(key);
int id = 0;
lock (CriticalSectionLock)
{
foreach (var reg in Registrations)
{
if (id <= reg.Id)
{
id = reg.Id + 1; // TODO: Rework to re-use gaps
}
}
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 + ")");
}
}
HotkeyRegistration registration = new()
{
Id = id,
Modifiers = modifiers,
Key = key,
};
Registrations.Add(registration);
return registration;
}
/// <summary>
/// Registers a global hotkey.
/// Function is thread safe.
/// Throws an InvalidOperationException on error.
/// The caller needs to call UnregisterHotkey to free up ressources.
/// </summary>
/// <param name="hotKeyString">Hotkey string representation.</param>
/// <returns>Registration interface.</returns>
internal static IHotkeyRegistration Register(string hotKeyString)
{
var (modifiers, key) = ParseKeysAndModifiersFromString(hotKeyString);
return Register(modifiers, key);
}
/// <summary>
/// Unregisters a global hotkey in a thread safe manner.
/// Function is thread safe.
/// </summary>
/// <param name="registration">Registration interface.</param>
/// <returns>true: Success or false: Failure.</returns>
internal static bool Unregister(IHotkeyRegistration? registration)
{
if (registration == null || registration 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 bool Reassign(IHotkeyRegistration? registration, ModifierKeys modifiers, Key key)
{
if (registration == null || registration is not HotkeyRegistration reg || !Registrations.Contains(reg))
{
return false;
}
if (modifiers == reg.Modifiers && key == reg.Key)
{
return true; // Yes, nothing changed, but we return true as requested key is properly registered even when unchanged.
}
int virtualKeyCode = KeyInterop.VirtualKeyFromKey(key);
int id = 0;
lock (CriticalSectionLock)
{
foreach (var regs in Registrations)
{
if (id <= regs.Id)
{
id = reg.Id + 1; // TODO: Rework to re-use gaps
}
}
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 + ")");
}
// In case unregister failes, unfortunately registration remains
// but will not trigger anything as we change our hotkey registration.
// However, this means the hotkey keeps being registered with this application
// and the key combination will not be availalbe for re-registration till app restart.
// TODO: Decide how to handle this? Restart App? Try keep old registartion and not update it?
if (!NativeMethods.User32UnregisterHotKey(HWnd.Handle, reg.Id))
{
Log.Info("Hotkey registration cannot unregister key " + reg.Modifiers.ToString() + " with modifiers " + reg.Modifiers.ToString());
}
reg.Id = id;
reg.Modifiers = modifiers;
reg.Key = key;
}
return true;
}
internal static bool Reassign(IHotkeyRegistration? registration, string hotKeyString)
{
var (modifiers, key) = ParseKeysAndModifiersFromString(hotKeyString);
return Reassign(registration, modifiers, key);
}
internal static IHotkeyFunction Create() => lastCreatedHotkeyFunction = new HotkeyFunction();
// TODO: Instead of searching for the registration, it should be passed to the caller instead.
// Only this ensures caller and registrator are talking about the SAME registration.
internal static IHotkeyRegistration? FindRegistration(string hotKeyString)
{
var (modifiers, key) = ParseKeysAndModifiersFromString(hotKeyString);
lock (CriticalSectionLock)
{
foreach (var registration in Registrations)
{
if (modifiers == registration.Modifiers && key == registration.Key)
{
return registration;
}
}
}
return null;
}
internal static IHotkeyFunction? GetLastCreatedHotkeyFunction() => lastCreatedHotkeyFunction;
internal static ModifierKeys ModifierKeysFromString(string modifiersString)
{
@ -386,7 +244,7 @@ namespace SystemTrayMenu.Helpers
HotkeyRegistration? reg = null;
lock (CriticalSectionLock)
{
foreach (var registration in Registrations)
foreach (var (id, registration) in Registrations)
{
if (modifiers == registration.Modifiers && key == registration.Key)
{
@ -403,21 +261,152 @@ namespace SystemTrayMenu.Helpers
return IntPtr.Zero;
}
private class HotkeyRegistration : IHotkeyRegistration
/// <summary>
/// Registers a global hotkey.
/// Function is thread safe.
/// Throws an InvalidOperationException on error.
/// The caller needs to call Unregister to free up ressources.
/// </summary>
/// <param name="modifiers">Hotkey modifiers.</param>
/// <param name="key">Hotkey major key.</param>
/// <returns>Hotkey registration.</returns>
private static HotkeyRegistration Register(ModifierKeys modifiers, Key key)
{
public event Action<IHotkeyRegistration>? KeyPressed;
HotkeyRegistration registration;
int virtualKeyCode = KeyInterop.VirtualKeyFromKey(key);
int id = 0;
internal int Id { get; set; }
lock (CriticalSectionLock)
{
while (Registrations.ContainsKey(id))
{
id++;
}
internal ModifierKeys Modifiers { get; set; }
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 + ")");
}
internal Key Key { get; set; }
registration = new()
{
Id = id,
Modifiers = modifiers,
Key = key,
};
Registrations.Add(id, registration);
}
public ModifierKeys GetModifierKeys() => Modifiers;
return registration;
}
public Key GetKey() => Key;
/// <summary>
/// Unregisters a global hotkey.
/// Function is thread safe.
/// </summary>
/// <param name="registration">Hotkey registration.</param>
private static void Unregister(HotkeyRegistration registration)
{
lock (CriticalSectionLock)
{
if (Registrations.ContainsValue(registration))
{
if (!NativeMethods.User32UnregisterHotKey(HWnd.Handle, registration.Id))
{
Log.Info("Hotkey registration cannot unregister key " + registration.Modifiers.ToString() + " with modifiers " + registration.Modifiers.ToString());
}
internal void OnKeyPressed() => KeyPressed?.Invoke(this);
// In case unregister failes, unfortunately registration remains
// but will not trigger anything as we remove the hotkey registration.
// However, this means the hotkey keeps being registered with this application
// and the key combination will not be availalbe for re-registration till app restart.
// TODO: Decide how to handle this? Restart App? Try keep old registartion and not update it?
Registrations.Remove(registration.Id);
}
}
}
/// <summary>
/// Registers a new global hotkey and unregisters the old one.
/// Function is thread safe.
/// Throws an InvalidOperationException on error.
/// The caller needs to call Unregister to free up ressources in case a new registration is returned.
/// </summary>
/// <param name="registration">Old hotkey registration.</param>
/// <param name="modifiers">Hotkey modifiers.</param>
/// <param name="key">Hotkey key.</param>
/// <returns>New hotkey registration or null (nothing changed).</returns>
private static HotkeyRegistration? Reassign(HotkeyRegistration registration, ModifierKeys modifiers, Key key)
{
if (!Registrations.ContainsValue(registration) ||
(modifiers == registration.Modifiers && key == registration.Key))
{
// Either registration is not valid or
// nothing would as requested key is already properly registered.
return null;
}
HotkeyRegistration reg = Register(modifiers, key);
Unregister(registration);
return reg;
}
private class HotkeyRegistration
{
public event Action? KeyPressed;
internal int Id { get; init; }
internal ModifierKeys Modifiers { get; init; }
internal Key Key { get; init; }
internal void OnKeyPressed() => KeyPressed?.Invoke();
}
private class HotkeyFunction : IHotkeyFunction
{
public event Action<IHotkeyFunction>? KeyPressed;
internal HotkeyRegistration? Registration { get; set; }
public void Unregister()
{
if (Registration != null)
{
GlobalHotkeys.Unregister(Registration);
Registration = null;
}
}
public ModifierKeys GetModifierKeys() => Registration?.Modifiers ?? ModifierKeys.None;
public Key GetKey() => Registration?.Key ?? Key.None;
public void Register(ModifierKeys modifiers, Key key)
{
if (Registration == null)
{
Registration = GlobalHotkeys.Register(modifiers, key);
Registration.KeyPressed += () => KeyPressed?.Invoke(this);
}
else
{
HotkeyRegistration? reg = Reassign(Registration, modifiers, key);
if (reg != null)
{
Registration = reg;
Registration.KeyPressed += () => KeyPressed?.Invoke(this);
}
}
}
public void Register(string hotKeyString)
{
var (modifiers, key) = ParseKeysAndModifiersFromString(hotKeyString);
Register(modifiers, key);
}
}
}
}

View file

@ -25,7 +25,7 @@ namespace SystemTrayMenu.UserInterface
private readonly IList<int> needNonAltGrModifier = new List<int>();
// These variables store the current hotkey and modifier(s)
private IHotkeyRegistration? hotkeyRegistration;
private IHotkeyFunction? hotkeyFunction;
private Key hotkey = Key.None;
private ModifierKeys modifiers = ModifierKeys.None;
private Action? handler;
@ -67,7 +67,7 @@ namespace SystemTrayMenu.UserInterface
};
PopulateModifierLists();
SetHotkeyRegistration((IHotkeyRegistration?)null);
SetHotkeyRegistration((IHotkeyFunction?)null);
}
public static string HotkeyToString(ModifierKeys modifierKeyCode, Key key)
@ -99,40 +99,41 @@ namespace SystemTrayMenu.UserInterface
public override string ToString() => HotkeyToString(modifiers, hotkey);
/// <summary>
/// Set the registration interface the control is working on.
/// Set the hotkey function the control is working on.
/// </summary>
/// <param name="registration">Registration interface.</param>
internal void SetHotkeyRegistration(IHotkeyRegistration? registration)
/// <param name="hotkeyFunction">Hotkey function interface.</param>
internal void SetHotkeyRegistration(IHotkeyFunction? hotkeyFunction)
{
hotkeyRegistration = registration;
if (hotkeyRegistration != null)
this.hotkeyFunction = hotkeyFunction;
UpdateHotkeyRegistration();
}
/// <summary>
/// Set the hotkey function the control is working on.
/// </summary>
/// <param name="hotkeyFunction">Hotkey function interface.</param>
internal void UpdateHotkeyRegistration()
{
hotkey = hotkeyFunction?.GetKey() ?? Key.None;
modifiers = hotkeyFunction?.GetModifierKeys() ?? ModifierKeys.None;
if (modifiers == ModifierKeys.None && hotkey == Key.None)
{
hotkey = hotkeyRegistration.GetKey();
modifiers = hotkeyRegistration.GetModifierKeys();
Background = Brushes.LightGreen;
Background = SystemColors.ControlBrush;
}
else
{
hotkey = Key.None;
modifiers = ModifierKeys.None;
Background = SystemColors.ControlBrush;
Background = Brushes.LightGreen;
}
Text = HotkeyToLocalizedString(modifiers, hotkey);
}
/// <summary>
/// Set the registration interface the control is working on.
/// The registration interface is looked up by given hotkey combination string.
/// </summary>
/// <param name="hotkeyString">Hotkey combination string.</param>
internal void SetHotkeyRegistration(string hotkeyString) => SetHotkeyRegistration(FindRegistration(hotkeyString));
/// <summary>
/// Change the hotkey to given combination.
/// </summary>
/// <param name="hotkeyString">Hotkey combination string.</param>
internal void ChangeHotkey(string hotkeyString) => Reassign(hotkeyRegistration, hotkeyString);
internal void ChangeHotkey(string hotkeyString) => hotkeyFunction?.Register(hotkeyString);
/// <summary>
/// Register a hotkey.
@ -151,7 +152,7 @@ namespace SystemTrayMenu.UserInterface
try
{
hotkeyRegistration = Register(modifiers, key);
hotkeyFunction?.Register(modifiers, key);
}
catch (InvalidOperationException ex)
{
@ -161,7 +162,10 @@ namespace SystemTrayMenu.UserInterface
}
this.handler = handler;
hotkeyRegistration.KeyPressed += (_) => handler.Invoke();
if (hotkeyFunction != null)
{
hotkeyFunction.KeyPressed += (_) => handler.Invoke();
}
Background = Brushes.LightGreen;
return 1;
@ -172,17 +176,16 @@ namespace SystemTrayMenu.UserInterface
/// </summary>
private void ResetHotkey()
{
hotkey = Key.None;
modifiers = ModifierKeys.None;
Redraw(false);
hotkeyFunction?.Unregister();
UpdateHotkeyRegistration();
}
private void HandlePreviewKeyDown(object sender, KeyEventArgs e)
{
// Handle some misc keys, such as Delete and Shift+Insert
ModifierKeys modifiers = Keyboard.Modifiers;
switch (e.Key)
{
case Key.Back:
case Key.Delete:
ResetHotkey();
e.Handled = true;
@ -199,58 +202,45 @@ namespace SystemTrayMenu.UserInterface
}
}
/// <summary>
/// Redraws the TextBox when necessary.
/// </summary>
/// <param name="bCalledProgramatically">Specifies whether this function was called by the Hotkey/HotkeyModifiers properties or by the user.</param>
private void Redraw(bool bCalledProgramatically)
// TODO: Instead of Redraw this seem to act more like an input filter for valid combinations?
// Maybe move to places right before Register() calls of the hotkeyFunction
private void Redraw()
{
// No hotkey set
if (hotkey == Key.None)
// No modifier or shift only, AND a hotkey that needs another modifier
if ((modifiers == ModifierKeys.Shift || modifiers == ModifierKeys.None) && needNonShiftModifier.Contains((int)hotkey))
{
Text = string.Empty;
return;
}
// Only validate input if it comes from the user
if (bCalledProgramatically == false)
{
// No modifier or shift only, AND a hotkey that needs another modifier
if ((modifiers == ModifierKeys.Shift || modifiers == ModifierKeys.None) && needNonShiftModifier.Contains((int)hotkey))
if (modifiers == ModifierKeys.None)
{
if (modifiers == ModifierKeys.None)
// Set Ctrl+Alt as the modifier unless Ctrl+Alt+<key> won't work...
if (needNonAltGrModifier.Contains((int)hotkey) == false)
{
// Set Ctrl+Alt as the modifier unless Ctrl+Alt+<key> won't work...
if (needNonAltGrModifier.Contains((int)hotkey) == false)
{
modifiers = ModifierKeys.Alt | ModifierKeys.Control;
}
else
{
// ... in that case, use Shift+Alt instead.
modifiers = ModifierKeys.Alt | ModifierKeys.Shift;
}
modifiers = ModifierKeys.Alt | ModifierKeys.Control;
}
else
{
// User pressed Shift and an invalid key (e.g. a letter or a number),
// that needs another set of modifier keys
hotkey = Key.None;
Text = string.Empty;
return;
// ... in that case, use Shift+Alt instead.
modifiers = ModifierKeys.Alt | ModifierKeys.Shift;
}
}
// Check all Ctrl+Alt keys
if (modifiers == (ModifierKeys.Alt | ModifierKeys.Control) && needNonAltGrModifier.Contains((int)hotkey))
else
{
// Ctrl+Alt+4 etc won't work; reset hotkey and tell the user
// User pressed Shift and an invalid key (e.g. a letter or a number),
// that needs another set of modifier keys
hotkey = Key.None;
Text = string.Empty;
return;
}
}
// Check all Ctrl+Alt keys
if (modifiers == (ModifierKeys.Alt | ModifierKeys.Control) && needNonAltGrModifier.Contains((int)hotkey))
{
// Ctrl+Alt+4 etc won't work; reset hotkey and tell the user
hotkey = Key.None;
Text = string.Empty;
return;
}
// I have no idea why this is needed, but it is. Without this code, pressing only Ctrl
// will show up as "Control + ControlKey", etc.
if (hotkey == Key.LeftAlt || hotkey == Key.RightAlt ||
@ -327,8 +317,8 @@ namespace SystemTrayMenu.UserInterface
{
modifiers = Keyboard.Modifiers;
hotkey = e.Key;
Reassign(hotkeyRegistration, modifiers, hotkey);
Redraw(false);
hotkeyFunction?.Register(modifiers, hotkey);
UpdateHotkeyRegistration();
}
}
@ -343,7 +333,7 @@ namespace SystemTrayMenu.UserInterface
{
modifiers = Keyboard.Modifiers;
hotkey = e.Key;
Redraw(false);
UpdateHotkeyRegistration();
}
else if (hotkey == Key.None && modifiers == ModifierKeys.None)
{

View file

@ -87,7 +87,7 @@ namespace SystemTrayMenu.UserInterface
checkBoxCheckForUpdates.IsChecked = Settings.Default.CheckForUpdates;
textBoxHotkey.SetHotkeyRegistration(Settings.Default.HotKey);
textBoxHotkey.SetHotkeyRegistration(GlobalHotkeys.GetLastCreatedHotkeyFunction());
InitializeLanguage();
void InitializeLanguage()