Improve hotkey capture and validation
This commit is contained in:
@@ -81,7 +81,8 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="Hotkey" VerticalAlignment="Center" Margin="0,0,8,8"/>
|
<TextBlock Text="Hotkey" VerticalAlignment="Center" Margin="0,0,8,8"/>
|
||||||
<TextBox x:Name="HotkeyBox" Grid.Column="1" Margin="0,0,0,8"/>
|
<TextBox x:Name="HotkeyBox" Grid.Column="1" Margin="0,0,0,8"
|
||||||
|
PreviewKeyDown="HotkeyBox_PreviewKeyDown"/>
|
||||||
|
|
||||||
<TextBlock Text="Default for New Items" Grid.Row="1" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
<TextBlock Text="Default for New Items" Grid.Row="1" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
<ComboBox x:Name="DisplayModeBox" Grid.Row="1" Grid.Column="1"/>
|
<ComboBox x:Name="DisplayModeBox" Grid.Row="1" Grid.Column="1"/>
|
||||||
|
|||||||
@@ -312,13 +312,78 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.Config.Hotkey = string.IsNullOrWhiteSpace(HotkeyBox.Text) ? "Ctrl+Alt+Space" : HotkeyBox.Text.Trim();
|
controller.Config.Hotkey = string.IsNullOrWhiteSpace(HotkeyBox.Text)
|
||||||
|
? "Ctrl+Alt+Space"
|
||||||
|
: HotkeyService.Normalize(HotkeyBox.Text.Trim());
|
||||||
controller.Config.DisplayMode = DisplayModeBox.SelectedItem is LauncherDisplayMode mode
|
controller.Config.DisplayMode = DisplayModeBox.SelectedItem is LauncherDisplayMode mode
|
||||||
? mode
|
? mode
|
||||||
: LauncherDisplayMode.LargeIconWithText;
|
: LauncherDisplayMode.LargeIconWithText;
|
||||||
ApplyCurrentItem();
|
ApplyCurrentItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HotkeyBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||||
|
{
|
||||||
|
Key key = e.Key == Key.System ? e.SystemKey : e.Key;
|
||||||
|
if (key == Key.ImeProcessed)
|
||||||
|
{
|
||||||
|
key = e.ImeProcessedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key is Key.LeftCtrl or Key.RightCtrl or Key.LeftAlt or Key.RightAlt or Key.LeftShift or Key.RightShift
|
||||||
|
or Key.LWin or Key.RWin)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key is Key.Back or Key.Delete or Key.Escape)
|
||||||
|
{
|
||||||
|
HotkeyBox.Text = "";
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModifierKeys modifiers = Keyboard.Modifiers;
|
||||||
|
if (modifiers == ModifierKeys.None)
|
||||||
|
{
|
||||||
|
StatusText.Text = "Press a hotkey with Ctrl, Alt, Shift, or Win plus another key.";
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HotkeyBox.Text = FormatHotkey(modifiers, key);
|
||||||
|
HotkeyBox.CaretIndex = HotkeyBox.Text.Length;
|
||||||
|
StatusText.Text = "Click Save to apply the new hotkey.";
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatHotkey(ModifierKeys modifiers, Key key)
|
||||||
|
{
|
||||||
|
List<string> parts = [];
|
||||||
|
if (modifiers.HasFlag(ModifierKeys.Control))
|
||||||
|
{
|
||||||
|
parts.Add("Ctrl");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiers.HasFlag(ModifierKeys.Alt))
|
||||||
|
{
|
||||||
|
parts.Add("Alt");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiers.HasFlag(ModifierKeys.Shift))
|
||||||
|
{
|
||||||
|
parts.Add("Shift");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiers.HasFlag(ModifierKeys.Windows))
|
||||||
|
{
|
||||||
|
parts.Add("Win");
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.Add(key.ToString());
|
||||||
|
return string.Join("+", parts);
|
||||||
|
}
|
||||||
|
|
||||||
private void LoadStartupState()
|
private void LoadStartupState()
|
||||||
{
|
{
|
||||||
isLoadingStartupState = true;
|
isLoadingStartupState = true;
|
||||||
@@ -569,9 +634,24 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyConfigFields();
|
string previousHotkey = controller.Config.Hotkey;
|
||||||
ConfigService.Save(controller.Config);
|
try
|
||||||
controller.ReloadConfig();
|
{
|
||||||
|
ApplyConfigFields();
|
||||||
|
ConfigService.Save(controller.Config);
|
||||||
|
controller.ReloadConfig();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is FormatException or InvalidOperationException)
|
||||||
|
{
|
||||||
|
controller.Config.Hotkey = previousHotkey;
|
||||||
|
ConfigService.Save(controller.Config);
|
||||||
|
controller.ReloadConfig();
|
||||||
|
HotkeyBox.Text = previousHotkey;
|
||||||
|
MessageBox.Show($"Could not apply hotkey.\n\n{ex.Message}", "Taskbar Launcher");
|
||||||
|
StatusText.Text = $"Hotkey was not changed. Current hotkey: {previousHotkey}";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LauncherItem? reloadedSelection = selectedPath is null ? null : FindItemByPath(controller.Config.Items, selectedPath);
|
LauncherItem? reloadedSelection = selectedPath is null ? null : FindItemByPath(controller.Config.Items, selectedPath);
|
||||||
RefreshView(reloadedSelection);
|
RefreshView(reloadedSelection);
|
||||||
StatusText.Text = reloadedSelection is null
|
StatusText.Text = reloadedSelection is null
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
@@ -17,6 +18,7 @@ public sealed class HotkeyService : IDisposable
|
|||||||
|
|
||||||
public void Register(string hotkey)
|
public void Register(string hotkey)
|
||||||
{
|
{
|
||||||
|
ParseHotkey(hotkey, out uint modifiers, out uint key);
|
||||||
Unregister();
|
Unregister();
|
||||||
|
|
||||||
window = new Window
|
window = new Window
|
||||||
@@ -37,8 +39,13 @@ public sealed class HotkeyService : IDisposable
|
|||||||
source = HwndSource.FromHwnd(helper.Handle);
|
source = HwndSource.FromHwnd(helper.Handle);
|
||||||
source?.AddHook(WndProc);
|
source?.AddHook(WndProc);
|
||||||
|
|
||||||
ParseHotkey(hotkey, out uint modifiers, out uint key);
|
if (!RegisterHotKey(helper.Handle, HotkeyId, modifiers, key))
|
||||||
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.Show();
|
||||||
@@ -78,12 +85,47 @@ public sealed class HotkeyService : IDisposable
|
|||||||
return IntPtr.Zero;
|
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)
|
private static void ParseHotkey(string hotkey, out uint modifiers, out uint key)
|
||||||
{
|
{
|
||||||
modifiers = 0;
|
modifiers = 0;
|
||||||
key = (uint)KeyInterop.VirtualKeyFromKey(Key.Space);
|
key = 0;
|
||||||
|
|
||||||
foreach (string part in hotkey.Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
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))
|
if (part.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) || part.Equals("Control", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -101,11 +143,45 @@ public sealed class HotkeyService : IDisposable
|
|||||||
{
|
{
|
||||||
modifiers |= 0x0008;
|
modifiers |= 0x0008;
|
||||||
}
|
}
|
||||||
else if (Enum.TryParse(part, true, out Key parsedKey))
|
else if (TryParseKey(part, out Key parsedKey))
|
||||||
{
|
{
|
||||||
key = (uint)KeyInterop.VirtualKeyFromKey(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)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
|||||||
Reference in New Issue
Block a user