// // Copyright (c) PlaceholderCompany. All rights reserved. // // // Origin of some parts: http://www.codeproject.com/KB/buttons/hotkeycontrol.aspx namespace SystemTrayMenu.UserInterface { using System; using System.Collections.Generic; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using SystemTrayMenu.Helpers; using SystemTrayMenu.Utilities; using static SystemTrayMenu.Helpers.GlobalHotkeys; public sealed class HotkeySelector : TextBox { // ArrayLists used to enforce the use of proper modifiers. // Shift+A isn't a valid hotkey, for instance, as it would screw up when the user is typing. private readonly IList needNonShiftModifier = new List(); private readonly IList needNonAltGrModifier = new List(); // These variables store the current hotkey and modifier(s) private IHotkeyRegistration? hotkeyRegistration; private Key hotkey = Key.None; private ModifierKeys modifiers = ModifierKeys.None; private Action? handler; /// /// Initializes a new instance of the class. /// public HotkeySelector() { // Disable right-clicking ContextMenu = new() { Visibility = System.Windows.Visibility.Collapsed, IsEnabled = false, }; // Handle events that occurs when keys are pressed KeyUp += HotkeyControl_KeyUp; KeyDown += HotkeyControl_KeyDown; PreviewKeyDown += HandlePreviewKeyDown; PreviewTextInput += HandlePreviewTextInput; GotFocus += (_, _) => GlobalHotkeys.IsEnabled = false; LostFocus += (_, _) => { #if TODO // HOTKEY Settings.Default.HotKey = new KeysConverter().ConvertToInvariantString( textBoxHotkey.Hotkey | textBoxHotkey.HotkeyModifiers); #endif #if TODO // HOTKEY /// /// Registers all hotkeys as configured, displaying a dialog in case of hotkey conflicts with other tools. /// /// Whether the hotkeys could be registered to the users content. This also applies if conflicts arise and the user decides to ignore these (i.e. not to register the conflicting hotkey). RegisterHotkeys(false); #endif GlobalHotkeys.IsEnabled = true; }; PopulateModifierLists(); SetHotkeyRegistration((IHotkeyRegistration?)null); } public static string HotkeyToString(ModifierKeys modifierKeyCode, Key key) { StringBuilder hotkeyString = new(); if ((modifierKeyCode & ModifierKeys.Alt) != 0) { hotkeyString.Append("Alt").Append(" + "); } if ((modifierKeyCode & ModifierKeys.Control) != 0) { hotkeyString.Append("Ctrl").Append(" + "); } if ((modifierKeyCode & ModifierKeys.Shift) != 0) { hotkeyString.Append("Shift").Append(" + "); } if ((modifierKeyCode & ModifierKeys.Windows) != 0) { hotkeyString.Append("Win").Append(" + "); } return hotkeyString.ToString() + key.ToString(); } public override string ToString() => HotkeyToString(modifiers, hotkey); /// /// Set the registration interface the control is working on. /// /// Registration interface. internal void SetHotkeyRegistration(IHotkeyRegistration? registration) { hotkeyRegistration = registration; if (hotkeyRegistration != null) { hotkey = hotkeyRegistration.GetKey(); modifiers = hotkeyRegistration.GetModifierKeys(); Background = Brushes.LightGreen; } else { hotkey = Key.None; modifiers = ModifierKeys.None; Background = SystemColors.ControlBrush; } Text = HotkeyToLocalizedString(modifiers, hotkey); } /// /// Set the registration interface the control is working on. /// The registration interface is looked up by given hotkey combination string. /// /// Hotkey combination string. internal void SetHotkeyRegistration(string hotkeyString) => SetHotkeyRegistration(FindRegistration(hotkeyString)); /// /// Change the hotkey to given combination. /// /// Hotkey combination string. internal void ChangeHotkey(string hotkeyString) => Reassign(hotkeyRegistration, hotkeyString); /// /// Register a hotkey. /// /// The key modifiers . /// The virtual key code. /// A HotKeyHandler, this will be called to handle the hotkey press. /// the hotkey number, -1 if failed. internal int RegisterHotKey(ModifierKeys modifiers, Key key, Action handler) { if (key == Key.None) { Background = SystemColors.ControlBrush; return 0; } try { hotkeyRegistration = Register(modifiers, key); } catch (InvalidOperationException ex) { Background = Brushes.IndianRed; Log.Info($"Couldn't register hotkey modifier {modifiers} key {key} ex: " + ex.ToString()); return -1; } this.handler = handler; hotkeyRegistration.KeyPressed += (_) => handler.Invoke(); Background = Brushes.LightGreen; return 1; } /// /// Clears the current hotkey and resets the TextBox. /// private void ResetHotkey() { hotkey = Key.None; modifiers = ModifierKeys.None; Redraw(false); } private void HandlePreviewKeyDown(object sender, KeyEventArgs e) { // Handle some misc keys, such as Delete and Shift+Insert ModifierKeys modifiers = Keyboard.Modifiers; switch (e.Key) { case Key.Delete: ResetHotkey(); e.Handled = true; break; case Key.Insert: if (modifiers == ModifierKeys.Shift) { e.Handled = true; // Don't allow } break; default: break; } } /// /// Redraws the TextBox when necessary. /// /// Specifies whether this function was called by the Hotkey/HotkeyModifiers properties or by the user. private void Redraw(bool bCalledProgramatically) { // No hotkey set if (hotkey == Key.None) { Text = string.Empty; return; } // Only validate input if it comes from the user if (bCalledProgramatically == false) { // No modifier or shift only, AND a hotkey that needs another modifier if ((modifiers == ModifierKeys.Shift || modifiers == ModifierKeys.None) && needNonShiftModifier.Contains((int)hotkey)) { if (modifiers == ModifierKeys.None) { // Set Ctrl+Alt as the modifier unless Ctrl+Alt+ won't work... if (needNonAltGrModifier.Contains((int)hotkey) == false) { modifiers = ModifierKeys.Alt | ModifierKeys.Control; } else { // ... in that case, use Shift+Alt instead. modifiers = ModifierKeys.Alt | ModifierKeys.Shift; } } else { // User pressed Shift and an invalid key (e.g. a letter or a number), // that needs another set of modifier keys hotkey = Key.None; Text = string.Empty; return; } } // Check all Ctrl+Alt keys if (modifiers == (ModifierKeys.Alt | ModifierKeys.Control) && needNonAltGrModifier.Contains((int)hotkey)) { // Ctrl+Alt+4 etc won't work; reset hotkey and tell the user hotkey = Key.None; Text = string.Empty; return; } } // I have no idea why this is needed, but it is. Without this code, pressing only Ctrl // will show up as "Control + ControlKey", etc. if (hotkey == Key.LeftAlt || hotkey == Key.RightAlt || hotkey == Key.LeftShift || hotkey == Key.RightShift || hotkey == Key.LeftCtrl || hotkey == Key.RightCtrl) { hotkey = Key.None; } Text = HotkeyToLocalizedString(modifiers, hotkey); } /// /// Populates the ArrayLists specifying disallowed hotkeys /// such as Shift+A, Ctrl+Alt+4 (would produce a dollar sign) etc. /// private void PopulateModifierLists() { // Shift + 0 - 9, A - Z for (Key k = Key.D0; k <= Key.Z; k++) { needNonShiftModifier.Add((int)k); } // Shift + Numpad keys for (Key k = Key.NumPad0; k <= Key.NumPad9; k++) { needNonShiftModifier.Add((int)k); } // Shift + Misc (,;<./ etc) for (Key k = Key.Oem1; k <= Key.OemBackslash; k++) { needNonShiftModifier.Add((int)k); } // Shift + Space, PgUp, PgDn, End, Home for (Key k = Key.Space; k <= Key.Home; k++) { needNonShiftModifier.Add((int)k); } // Misc keys that we can't loop through needNonShiftModifier.Add((int)Key.Insert); needNonShiftModifier.Add((int)Key.Help); needNonShiftModifier.Add((int)Key.Multiply); needNonShiftModifier.Add((int)Key.Add); needNonShiftModifier.Add((int)Key.Subtract); needNonShiftModifier.Add((int)Key.Divide); needNonShiftModifier.Add((int)Key.Decimal); needNonShiftModifier.Add((int)Key.Return); needNonShiftModifier.Add((int)Key.Escape); needNonShiftModifier.Add((int)Key.NumLock); // Ctrl+Alt + 0 - 9 for (Key k = Key.D0; k <= Key.D9; k++) { needNonAltGrModifier.Add((int)k); } } /// /// Fires when a key is pushed down. Here, we'll want to update the text in the box /// to notify the user what combination is currently pressed. /// private void HotkeyControl_KeyDown(object sender, KeyEventArgs e) { // Clear the current hotkey if (e.Key == Key.Back || e.Key == Key.Delete) { ResetHotkey(); } else { modifiers = Keyboard.Modifiers; hotkey = e.Key; Reassign(hotkeyRegistration, modifiers, hotkey); Redraw(false); } } /// /// Fires when all keys are released. If the current hotkey isn't valid, reset it. /// Otherwise, do nothing and keep the text and hotkey as it was. /// private void HotkeyControl_KeyUp(object sender, KeyEventArgs e) { // Somehow the PrintScreen only comes as a keyup, therefore we handle it here. if (e.Key == Key.PrintScreen) { modifiers = Keyboard.Modifiers; hotkey = e.Key; Redraw(false); } else if (hotkey == Key.None && modifiers == ModifierKeys.None) { ResetHotkey(); } } /// /// Prevents the letter/whatever entered to show up in the TextBox /// Without this, a "A" key press would appear as "aControl, Alt + A". /// private void HandlePreviewTextInput(object sender, TextCompositionEventArgs e) { e.Handled = true; } #if TODO // HOTKEY /// /// Helper method to cleanly register a hotkey. /// /// failedKeys. /// hotkeyString. /// handler. /// bool success. private static bool RegisterHotkey(StringBuilder failedKeys, string hotkeyString, HotKeyHandler handler) { Keys modifierKeyCode = HotkeyModifiersFromString(hotkeyString); Keys virtualKeyCode = HotkeyFromString(hotkeyString); if (!Keys.None.Equals(virtualKeyCode)) { if (RegisterHotKey(modifierKeyCode, virtualKeyCode, handler) < 0) { if (failedKeys.Length > 0) { failedKeys.Append(", "); } failedKeys.Append(hotkeyString); return false; } } return true; } /// /// Registers all hotkeys as configured, displaying a dialog in case of hotkey conflicts with other tools. /// /// if true, a failed hotkey registration will not be reported to the user - the hotkey will simply not be registered. /// Whether the hotkeys could be registered to the users content. This also applies if conflicts arise and the user decides to ignore these (i.e. not to register the conflicting hotkey). private static bool RegisterHotkeys(bool ignoreFailedRegistration) { bool success = true; StringBuilder failedKeys = new(); if (!RegisterHotkey( failedKeys, Settings.Default.HotKey, handler)) { success = false; } if (!success) { if (!ignoreFailedRegistration) { success = HandleFailedHotkeyRegistration(failedKeys.ToString()); } } return success || ignoreFailedRegistration; } /// /// Displays a dialog for the user to choose how to handle hotkey registration failures: /// retry (allowing to shut down the conflicting application before), /// ignore (not registering the conflicting hotkey and resetting the respective config to "None", i.e. not trying to register it again on next startup) /// abort (do nothing about it). /// /// comma separated list of the hotkeys that could not be registered, for display in dialog text. /// bool success. private static bool HandleFailedHotkeyRegistration(string failedKeys) { bool success = false; string warningTitle = Translator.GetText("Warning"); string message = Translator.GetText("Could not register the hot key.") + failedKeys; DialogResult dr = MessageBox.Show(message, warningTitle, MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Exclamation); if (dr == DialogResult.Retry) { UnregisterHotKey(); success = RegisterHotkeys(false); // TODO: This may end up in endless recursion, better use a loop at caller } else if (dr == DialogResult.Ignore) { UnregisterHotKey(); success = RegisterHotkeys(true); } return success; } #endif } }