feat: add global hotkey to toggle refresh rate#17
Conversation
- Add HotkeyManager class using RegisterHotKey/UnregisterHotKey Win32 API - Add configurable hotkey in config.json (default: Ctrl+Shift+R) - Add hotkey menu item in tray context menu - Add hotkey configuration dialog with modifier checkboxes and key picker - Register/Unregister hotkey on startup/dispose Closes #3
There was a problem hiding this comment.
Pull request overview
Adds a global, configurable hotkey to toggle the refresh rate from anywhere (using Win32 RegisterHotKey), integrating it into the tray menu and persisting settings in config.json (Issue #3).
Changes:
- Introduces
HotkeyManagerto register/unregister a global hotkey and raise a managed event onWM_HOTKEY. - Adds hotkey settings (
HotkeyModifiers,HotkeyKey) toAppConfigwith defaults. - Extends the tray context menu with a Hotkey item and a configuration dialog; registers the hotkey on startup and disposes it on exit.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| TrayApp.cs | Wires up global hotkey registration, tray menu item, and a dialog to configure/save the hotkey. |
| HotkeyManager.cs | Implements Win32 hotkey registration and config parsing/display helpers. |
| Config.cs | Adds persisted hotkey configuration fields with defaults. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var keyLabel = new Label | ||
| { | ||
| AutoSize = true, | ||
| Text = "Key:", | ||
| Left = 12, | ||
| Top = 100 | ||
| }; | ||
|
|
||
| var keyCombo = new ComboBox | ||
| { | ||
| Left = 50, | ||
| Top = 97, |
| if (!RegisterHotKey(_window.Handle, HOTKEY_ID, (uint)modifiers, (uint)key)) | ||
| { | ||
| var errorCode = Marshal.GetLastWin32Error(); | ||
| error = $"RegisterHotKey failed (error {errorCode}). The combination may be in use by another application."; | ||
| return false; | ||
| } |
| modifiers |= part.ToLowerInvariant() switch | ||
| { | ||
| "ctrl" or "control" => ModifierKeys.Control, | ||
| "alt" => ModifierKeys.Alt, | ||
| "shift" => ModifierKeys.Shift, | ||
| "win" or "windows" => ModifierKeys.Windows, | ||
| _ => throw new ArgumentException($"Unknown modifier: {part}") | ||
| }; |
| var instructionLabel = new Label | ||
| { | ||
| AutoSize = true, | ||
| Text = "Press the desired key combination:", |
| var modifiers = ModifierKeys.None; | ||
| if (ctrlCheckBox.Checked) modifiers |= ModifierKeys.Control; | ||
| if (altCheckBox.Checked) modifiers |= ModifierKeys.Alt; | ||
| if (shiftCheckBox.Checked) modifiers |= ModifierKeys.Shift; | ||
|
|
||
| if (modifiers == 0) | ||
| { | ||
| return null; | ||
| } |
| return false; | ||
| } | ||
|
|
||
| // Strip modifier flags that may be part of the Keys enum value (e.g. "D" vs "D1") |
| public string HotkeyModifiers { get; set; } = "Ctrl+Shift"; | ||
| public string HotkeyKey { get; set; } = "R"; |
| _hotkeyManager.TryRegister(ModifierKeys.Control | ModifierKeys.Shift, Keys.R, out _); | ||
| return; | ||
| } | ||
|
|
||
| _hotkeyManager.TryRegister(modifiers, key, out _); |
| if (!HotkeyManager.TryParse(_config.HotkeyModifiers, _config.HotkeyKey, | ||
| out var modifiers, out var key, out var error)) | ||
| { | ||
| // Fall back to Ctrl+Shift+R if config is invalid. | ||
| _hotkeyManager.TryRegister(ModifierKeys.Control | ModifierKeys.Shift, Keys.R, out _); | ||
| return; | ||
| } |
- HotkeyManager: make TryParse non-throwing (returns false instead of ArgumentException)
- HotkeyManager: add MOD_NOREPEAT flag to prevent rapid repeat on hold
- HotkeyManager: fix misleading comment about modifier flag stripping
- Config.cs: normalize null/empty HotkeyModifiers/HotkeyKey in Load()
- TrayApp: add Win modifier checkbox in hotkey dialog
- TrayApp: fix dialog instruction text ('Press' → 'Select')
- TrayApp: surface registration error to user on startup
- TrayApp: reset invalid hotkey config to defaults instead of keeping broken values
There was a problem hiding this comment.
Pull request overview
Adds a configurable global hotkey (via Win32 RegisterHotKey) to toggle refresh rate from anywhere, integrating it into the tray menu and persisting it in config.json.
Changes:
- Introduce
HotkeyManagerto register/unregister a global hotkey and raise aHotkeyPressedevent. - Extend
AppConfigto persist hotkey modifiers/key with defaults (Ctrl+Shift+R). - Add tray UI for showing/configuring the hotkey, plus a small configuration dialog.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| TrayApp.cs | Wires up global hotkey registration, adds tray menu entry + configuration dialog, triggers toggle on hotkey press. |
| HotkeyManager.cs | New Win32-backed manager for registering/unregistering and dispatching WM_HOTKEY to managed events. |
| Config.cs | Adds hotkey fields to config and normalizes missing/blank values on load. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (dialog.ShowDialog() != DialogResult.OK) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var modifiers = ModifierKeys.None; | ||
| if (ctrlCheckBox.Checked) modifiers |= ModifierKeys.Control; | ||
| if (altCheckBox.Checked) modifiers |= ModifierKeys.Alt; | ||
| if (shiftCheckBox.Checked) modifiers |= ModifierKeys.Shift; | ||
| if (winCheckBox.Checked) modifiers |= ModifierKeys.Windows; | ||
|
|
||
| if (modifiers == ModifierKeys.None) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| if (keyCombo.SelectedIndex < 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var selectedKey = commonKeys[keyCombo.SelectedIndex]; | ||
| return (modifiers, selectedKey); |
| // into non-nullable string properties, which would cause NullReferenceException | ||
| // downstream when HotkeyManager.TryParse calls Split(). |
| @@ -0,0 +1,190 @@ | |||
| using System.Diagnostics; | |||
| { | ||
| _config.HotkeyModifiers = previousModifiers; | ||
| _config.HotkeyKey = previousKey; | ||
| ShowError($"Could not register hotkey: {hotkeyError}"); |
| RegisterConfiguredHotkey(); | ||
|
|
Closes #3