diff --git a/MainWindow.xaml b/MainWindow.xaml
index a506b86..d831c6e 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -81,7 +81,8 @@
-
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 6648409..94dcf98 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -312,13 +312,78 @@ public partial class MainWindow : Window
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
? mode
: LauncherDisplayMode.LargeIconWithText;
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 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()
{
isLoadingStartupState = true;
@@ -569,9 +634,24 @@ public partial class MainWindow : Window
return;
}
- ApplyConfigFields();
- ConfigService.Save(controller.Config);
- controller.ReloadConfig();
+ string previousHotkey = controller.Config.Hotkey;
+ try
+ {
+ 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);
RefreshView(reloadedSelection);
StatusText.Text = reloadedSelection is null
diff --git a/Services/HotkeyService.cs b/Services/HotkeyService.cs
index 55a4234..7a96b3f 100644
--- a/Services/HotkeyService.cs
+++ b/Services/HotkeyService.cs
@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
+using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
@@ -17,6 +18,7 @@ public sealed class HotkeyService : IDisposable
public void Register(string hotkey)
{
+ ParseHotkey(hotkey, out uint modifiers, out uint key);
Unregister();
window = new Window
@@ -37,8 +39,13 @@ public sealed class HotkeyService : IDisposable
source = HwndSource.FromHwnd(helper.Handle);
source?.AddHook(WndProc);
- ParseHotkey(hotkey, out uint modifiers, out uint key);
- RegisterHotKey(helper.Handle, HotkeyId, modifiers, key);
+ if (!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();
@@ -78,12 +85,47 @@ public sealed class HotkeyService : IDisposable
return IntPtr.Zero;
}
+ public static string Normalize(string hotkey)
+ {
+ ParseHotkey(hotkey, out uint modifiers, out uint key);
+ Key parsedKey = KeyInterop.KeyFromVirtualKey((int)key);
+ List 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)
{
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))
{
@@ -101,11 +143,45 @@ public sealed class HotkeyService : IDisposable
{
modifiers |= 0x0008;
}
- else if (Enum.TryParse(part, true, out Key parsedKey))
+ else if (TryParseKey(part, out Key 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)]