Rework hotkey registration for sharing hotkeys between objects.

Use colors for working, failing or unset hotkeys in HotkeySelector.
This commit is contained in:
Peter Kirmeier 2023-05-27 18:23:01 +02:00
parent 8ece540881
commit ce2d9772f6
5 changed files with 104 additions and 72 deletions

View file

@ -13,7 +13,7 @@ namespace SystemTrayMenu.Handler
internal class KeyboardInput : IDisposable internal class KeyboardInput : IDisposable
{ {
private GlobalHotkeys.HotkeyRegistrationHandle? hotkeyHandle; private GlobalHotkeys.IHotkeyRegistration? hotkeyRegistration;
private Menu? focussedMenu; private Menu? focussedMenu;
@ -27,7 +27,7 @@ namespace SystemTrayMenu.Handler
public void Dispose() public void Dispose()
{ {
GlobalHotkeys.Unregister(hotkeyHandle); GlobalHotkeys.Unregister(hotkeyRegistration);
} }
internal bool RegisterHotKey(string hotKeyString) internal bool RegisterHotKey(string hotKeyString)
@ -36,7 +36,7 @@ namespace SystemTrayMenu.Handler
{ {
try try
{ {
hotkeyHandle = GlobalHotkeys.Register(hotKeyString); hotkeyRegistration = GlobalHotkeys.Register(hotKeyString);
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
{ {
@ -44,7 +44,7 @@ namespace SystemTrayMenu.Handler
return false; return false;
} }
hotkeyHandle.KeyPressed += (_) => HotKeyPressed?.Invoke(); hotkeyRegistration.KeyPressed += (_) => HotKeyPressed?.Invoke();
} }
return true; return true;

View file

@ -30,6 +30,15 @@ namespace SystemTrayMenu.Helpers
HWnd.AddHook(Hook); HWnd.AddHook(Hook);
} }
internal interface IHotkeyRegistration
{
event Action<IHotkeyRegistration>? KeyPressed;
ModifierKeys GetModifierKeys();
Key GetKey();
}
/// <summary> /// <summary>
/// Registers a global hotkey. /// Registers a global hotkey.
/// Function is thread safe. /// Function is thread safe.
@ -38,8 +47,8 @@ namespace SystemTrayMenu.Helpers
/// </summary> /// </summary>
/// <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>Handle of this registration.</returns> /// <returns>Registration interface.</returns>
internal static HotkeyRegistrationHandle Register(ModifierKeys modifiers, Key key) internal static IHotkeyRegistration Register(ModifierKeys modifiers, Key key)
{ {
int virtualKeyCode = KeyInterop.VirtualKeyFromKey(key); int virtualKeyCode = KeyInterop.VirtualKeyFromKey(key);
int id = 0; int id = 0;
@ -61,14 +70,14 @@ namespace SystemTrayMenu.Helpers
} }
} }
HotkeyRegistration regHandle = new() HotkeyRegistration registration = new()
{ {
Id = id, Id = id,
Modifiers = modifiers, Modifiers = modifiers,
Key = key, Key = key,
}; };
Registrations.Add(regHandle); Registrations.Add(registration);
return regHandle; return registration;
} }
/// <summary> /// <summary>
@ -78,8 +87,8 @@ namespace SystemTrayMenu.Helpers
/// The caller needs to call UnregisterHotkey to free up ressources. /// The caller needs to call UnregisterHotkey to free up ressources.
/// </summary> /// </summary>
/// <param name="hotKeyString">Hotkey string representation.</param> /// <param name="hotKeyString">Hotkey string representation.</param>
/// <returns>Handle of this registration.</returns> /// <returns>Registration interface.</returns>
internal static HotkeyRegistrationHandle Register(string hotKeyString) internal static IHotkeyRegistration Register(string hotKeyString)
{ {
var (modifiers, key) = ParseKeysAndModifiersFromString(hotKeyString); var (modifiers, key) = ParseKeysAndModifiersFromString(hotKeyString);
return Register(modifiers, key); return Register(modifiers, key);
@ -89,11 +98,11 @@ namespace SystemTrayMenu.Helpers
/// Unregisters a global hotkey in a thread safe manner. /// Unregisters a global hotkey in a thread safe manner.
/// Function is thread safe. /// Function is thread safe.
/// </summary> /// </summary>
/// <param name="regHandle">Handle of the registration.</param> /// <param name="registration">Registration interface.</param>
/// <returns>true: Success or false: Failure.</returns> /// <returns>true: Success or false: Failure.</returns>
internal static bool Unregister(HotkeyRegistrationHandle? regHandle) internal static bool Unregister(IHotkeyRegistration? registration)
{ {
if (regHandle == null || regHandle is not HotkeyRegistration reg || Registrations.Contains(reg)) if (registration == null || registration is not HotkeyRegistration reg || Registrations.Contains(reg))
{ {
return true; return true;
} }
@ -111,6 +120,26 @@ namespace SystemTrayMenu.Helpers
return true; return true;
} }
// 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 ModifierKeys ModifierKeysFromString(string modifiersString) internal static ModifierKeys ModifierKeysFromString(string modifiersString)
{ {
ModifierKeys modifiers = ModifierKeys.None; ModifierKeys modifiers = ModifierKeys.None;
@ -296,11 +325,11 @@ namespace SystemTrayMenu.Helpers
HotkeyRegistration? reg = null; HotkeyRegistration? reg = null;
lock (CriticalSectionLock) lock (CriticalSectionLock)
{ {
foreach (var regHandle in Registrations) foreach (var registration in Registrations)
{ {
if (modifiers == regHandle.Modifiers && key == regHandle.Key) if (modifiers == registration.Modifiers && key == registration.Key)
{ {
reg = regHandle; reg = registration;
break; break;
} }
} }
@ -313,22 +342,21 @@ namespace SystemTrayMenu.Helpers
return IntPtr.Zero; return IntPtr.Zero;
} }
internal abstract class HotkeyRegistrationHandle private class HotkeyRegistration : IHotkeyRegistration
{ {
internal event Action<HotkeyRegistrationHandle>? KeyPressed; public event Action<IHotkeyRegistration>? KeyPressed;
protected void RiseKeyPressed() => KeyPressed?.Invoke(this);
}
private class HotkeyRegistration : HotkeyRegistrationHandle
{
internal int Id { get; init; } internal int Id { get; init; }
internal ModifierKeys Modifiers { get; set; } internal ModifierKeys Modifiers { get; set; }
internal Key Key { get; set; } internal Key Key { get; set; }
internal void OnKeyPressed() => RiseKeyPressed(); public ModifierKeys GetModifierKeys() => Modifiers;
public Key GetKey() => Key;
internal void OnKeyPressed() => KeyPressed?.Invoke(this);
} }
} }
} }

View file

@ -9,10 +9,13 @@ namespace SystemTrayMenu.UserInterface
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using SystemTrayMenu.Helpers; using SystemTrayMenu.Helpers;
using SystemTrayMenu.Utilities; using SystemTrayMenu.Utilities;
using static SystemTrayMenu.Helpers.GlobalHotkeys;
public sealed class HotkeySelector : TextBox public sealed class HotkeySelector : TextBox
{ {
@ -21,9 +24,8 @@ namespace SystemTrayMenu.UserInterface
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 GlobalHotkeys.HotkeyRegistrationHandle? hotkeyHandle;
// These variables store the current hotkey and modifier(s) // These variables store the current hotkey and modifier(s)
private IHotkeyRegistration? hotkeyHandle;
private Key hotkey = Key.None; private Key hotkey = Key.None;
private ModifierKeys modifiers = ModifierKeys.None; private ModifierKeys modifiers = ModifierKeys.None;
private Action? handler; private Action? handler;
@ -40,8 +42,6 @@ namespace SystemTrayMenu.UserInterface
IsEnabled = false, IsEnabled = false,
}; };
Text = string.Empty;
// Handle events that occurs when keys are pressed // Handle events that occurs when keys are pressed
KeyUp += HotkeyControl_KeyUp; KeyUp += HotkeyControl_KeyUp;
KeyDown += HotkeyControl_KeyDown; KeyDown += HotkeyControl_KeyDown;
@ -73,37 +73,7 @@ namespace SystemTrayMenu.UserInterface
}; };
PopulateModifierLists(); PopulateModifierLists();
} SetHotkeyRegistration((IHotkeyRegistration?)null);
~HotkeySelector()
{
GlobalHotkeys.Unregister(hotkeyHandle);
}
/// <summary>
/// Gets or sets used to get/set the hotkey (e.g. Key.A).
/// </summary>
public Key Hotkey
{
get => hotkey;
set
{
hotkey = value;
Redraw(true);
}
}
/// <summary>
/// Gets or sets used to get/set the modifier keys (e.g. Alt | Control).
/// </summary>
public ModifierKeys HotkeyModifiers
{
get => modifiers;
set
{
modifiers = value;
Redraw(true);
}
} }
internal bool Reassigning { get; private set; } internal bool Reassigning { get; private set; }
@ -137,13 +107,43 @@ namespace SystemTrayMenu.UserInterface
public override string ToString() => HotkeyToString(modifiers, hotkey); public override string ToString() => HotkeyToString(modifiers, hotkey);
/// <summary> /// <summary>
/// Used to get/set the hotkey (e.g. Key.A). /// Set the registration interface the control is working on.
/// </summary> /// </summary>
/// <param name="hotkey">hotkey.</param> /// <param name="registration">Registration interface.</param>
public void SetHotkey(string hotkey) internal void SetHotkeyRegistration(IHotkeyRegistration? registration)
{ {
this.hotkey = GlobalHotkeys.KeyFromString(hotkey); hotkeyHandle = registration;
modifiers = GlobalHotkeys.ModifierKeysFromString(hotkey); if (hotkeyHandle != null)
{
hotkey = hotkeyHandle.GetKey();
modifiers = hotkeyHandle.GetModifierKeys();
Background = Brushes.LightGreen;
}
else
{
hotkey = Key.None;
modifiers = ModifierKeys.None;
Background = SystemColors.ControlBrush;
}
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)
{
hotkey = KeyFromString(hotkeyString);
modifiers = ModifierKeysFromString(hotkeyString);
Redraw(true); Redraw(true);
} }
@ -154,25 +154,29 @@ namespace SystemTrayMenu.UserInterface
/// <param name="key">The virtual key code.</param> /// <param name="key">The virtual key code.</param>
/// <param name="handler">A HotKeyHandler, this will be called to handle the hotkey press.</param> /// <param name="handler">A HotKeyHandler, this will be called to handle the hotkey press.</param>
/// <returns>the hotkey number, -1 if failed.</returns> /// <returns>the hotkey number, -1 if failed.</returns>
public int RegisterHotKey(ModifierKeys modifiers, Key key, Action handler) internal int RegisterHotKey(ModifierKeys modifiers, Key key, Action handler)
{ {
if (key == Key.None) if (key == Key.None)
{ {
Background = SystemColors.ControlBrush;
return 0; return 0;
} }
try try
{ {
hotkeyHandle = GlobalHotkeys.Register(modifiers, key); hotkeyHandle = Register(modifiers, key);
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
{ {
Background = Brushes.IndianRed;
Log.Info($"Couldn't register hotkey modifier {modifiers} key {key} ex: " + ex.ToString()); Log.Info($"Couldn't register hotkey modifier {modifiers} key {key} ex: " + ex.ToString());
return -1; return -1;
} }
this.handler = handler; this.handler = handler;
hotkeyHandle.KeyPressed += (_) => handler.Invoke(); hotkeyHandle.KeyPressed += (_) => handler.Invoke();
Background = Brushes.LightGreen;
return 1; return 1;
} }
@ -186,7 +190,7 @@ namespace SystemTrayMenu.UserInterface
private void UnregisterHotKey() private void UnregisterHotKey()
{ {
GlobalHotkeys.Unregister(hotkeyHandle); Unregister(hotkeyHandle);
hotkeyHandle = null; hotkeyHandle = null;
} }
@ -283,7 +287,7 @@ namespace SystemTrayMenu.UserInterface
hotkey = Key.None; hotkey = Key.None;
} }
Text = GlobalHotkeys.HotkeyToLocalizedString(modifiers, hotkey); Text = HotkeyToLocalizedString(modifiers, hotkey);
} }
/// <summary> /// <summary>

View file

@ -50,7 +50,7 @@
</GroupBox> </GroupBox>
<GroupBox Header="{u:Translate 'Hotkey'}"> <GroupBox Header="{u:Translate 'Hotkey'}">
<DockPanel LastChildFill="False"> <DockPanel LastChildFill="False">
<local:HotkeySelector x:Name="textBoxHotkey" Text="None" AcceptsTab="False" MinWidth="200" VerticalAlignment="Center"/> <local:HotkeySelector x:Name="textBoxHotkey" AcceptsTab="False" MinWidth="200" VerticalAlignment="Center"/>
<Button x:Name="buttonHotkeyDefault" DockPanel.Dock="Right" Content="{u:Translate 'Default'}" Margin="3" MinWidth="76" Click="ButtonHotkeyDefault_Click" VerticalAlignment="Center"/> <Button x:Name="buttonHotkeyDefault" DockPanel.Dock="Right" Content="{u:Translate 'Default'}" Margin="3" MinWidth="76" Click="ButtonHotkeyDefault_Click" VerticalAlignment="Center"/>
</DockPanel> </DockPanel>
</GroupBox> </GroupBox>

View file

@ -86,7 +86,7 @@ namespace SystemTrayMenu.UserInterface
checkBoxCheckForUpdates.IsChecked = Settings.Default.CheckForUpdates; checkBoxCheckForUpdates.IsChecked = Settings.Default.CheckForUpdates;
textBoxHotkey.SetHotkey(Settings.Default.HotKey); textBoxHotkey.SetHotkeyRegistration(Settings.Default.HotKey);
InitializeLanguage(); InitializeLanguage();
void InitializeLanguage() void InitializeLanguage()
@ -737,7 +737,7 @@ namespace SystemTrayMenu.UserInterface
private void ButtonHotkeyDefault_Click(object sender, RoutedEventArgs e) private void ButtonHotkeyDefault_Click(object sender, RoutedEventArgs e)
{ {
textBoxHotkey.SetHotkey((string)Settings.Default.Properties["HotKey"].DefaultValue); // see Settings.Default.HotKey textBoxHotkey.ChangeHotkey((string)Settings.Default.Properties["HotKey"].DefaultValue); // see Settings.Default.HotKey
} }
private void ButtonGeneralDefault_Click(object sender, RoutedEventArgs e) private void ButtonGeneralDefault_Click(object sender, RoutedEventArgs e)