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)]