2023-05-26 10:34:31 +12:00
// <copyright file="HotkeySelector.cs" company="PlaceholderCompany">
2023-05-22 07:13:18 +12:00
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
2023-05-26 10:34:31 +12:00
//
// Origin of some parts: http://www.codeproject.com/KB/buttons/hotkeycontrol.aspx
2023-05-22 07:13:18 +12:00
2023-05-26 06:52:31 +12:00
namespace SystemTrayMenu.UserInterface
2023-05-22 07:13:18 +12:00
{
using System ;
using System.Collections.Generic ;
using System.Text ;
2023-05-28 04:23:01 +12:00
using System.Windows ;
2023-05-22 07:13:18 +12:00
using System.Windows.Controls ;
using System.Windows.Input ;
2023-05-28 04:23:01 +12:00
using System.Windows.Media ;
2023-05-26 10:34:31 +12:00
using SystemTrayMenu.Helpers ;
2023-05-22 07:13:18 +12:00
using SystemTrayMenu.Utilities ;
2023-05-28 04:23:01 +12:00
using static SystemTrayMenu . Helpers . GlobalHotkeys ;
2023-05-22 07:13:18 +12:00
2023-05-28 02:37:39 +12:00
public sealed class HotkeySelector : TextBox
2023-05-22 07:13:18 +12:00
{
// 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.
2023-06-01 10:32:28 +12:00
private readonly List < Key > needNonShiftModifier = new ( ) ;
private readonly List < Key > needNonAltGrModifier = new ( ) ;
2023-05-22 07:13:18 +12:00
// These variables store the current hotkey and modifier(s)
private Key hotkey = Key . None ;
private ModifierKeys modifiers = ModifierKeys . None ;
/// <summary>
2023-05-26 10:34:31 +12:00
/// Initializes a new instance of the <see cref="HotkeySelector"/> class.
2023-05-22 07:13:18 +12:00
/// </summary>
2023-05-26 10:34:31 +12:00
public HotkeySelector ( )
2023-05-22 07:13:18 +12:00
{
// Disable right-clicking
2023-05-26 10:34:31 +12:00
ContextMenu = new ( )
{
2023-06-04 02:56:17 +12:00
Visibility = Visibility . Collapsed ,
2023-05-26 10:34:31 +12:00
IsEnabled = false ,
} ;
2023-05-22 07:13:18 +12:00
// Handle events that occurs when keys are pressed
KeyUp + = HotkeyControl_KeyUp ;
KeyDown + = HotkeyControl_KeyDown ;
PreviewKeyDown + = HandlePreviewKeyDown ;
PreviewTextInput + = HandlePreviewTextInput ;
2023-05-30 06:48:46 +12:00
GotFocus + = ( _ , _ ) = > GlobalHotkeys . IsEnabled = false ;
2023-06-01 10:32:28 +12:00
LostFocus + = ( _ , _ ) = > GlobalHotkeys . IsEnabled = true ;
2023-05-26 10:34:31 +12:00
2023-05-22 07:13:18 +12:00
PopulateModifierLists ( ) ;
2023-06-04 02:56:17 +12:00
SetHotkeyRegistration ( null ) ;
2023-05-22 07:13:18 +12:00
}
2023-06-04 02:56:17 +12:00
internal bool WasHotkeyChanged { get ; private set ; }
2023-06-01 08:32:19 +12:00
internal IHotkeyFunction ? HotkeyFunction { get ; private set ; }
2023-05-26 06:52:31 +12:00
public static string HotkeyToString ( ModifierKeys modifierKeyCode , Key key )
2023-05-22 07:13:18 +12:00
{
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 ( " + " ) ;
}
2023-05-26 06:52:31 +12:00
return hotkeyString . ToString ( ) + key . ToString ( ) ;
2023-05-22 07:13:18 +12:00
}
2023-05-27 06:51:42 +12:00
public override string ToString ( ) = > HotkeyToString ( modifiers , hotkey ) ;
/// <summary>
2023-05-30 09:56:52 +12:00
/// Set the hotkey function the control is working on.
2023-05-27 06:51:42 +12:00
/// </summary>
2023-05-30 09:56:52 +12:00
/// <param name="hotkeyFunction">Hotkey function interface.</param>
internal void SetHotkeyRegistration ( IHotkeyFunction ? hotkeyFunction )
2023-05-27 06:51:42 +12:00
{
2023-06-01 08:32:19 +12:00
HotkeyFunction = hotkeyFunction ;
2023-06-04 02:56:17 +12:00
WasHotkeyChanged = false ;
2023-05-30 09:56:52 +12:00
UpdateHotkeyRegistration ( ) ;
}
/// <summary>
2023-06-01 08:32:19 +12:00
/// Update the UI based on the hotkey registration.
2023-05-30 09:56:52 +12:00
/// </summary>
internal void UpdateHotkeyRegistration ( )
{
2023-06-01 08:32:19 +12:00
hotkey = HotkeyFunction ? . GetKey ( ) ? ? Key . None ;
modifiers = HotkeyFunction ? . GetModifierKeys ( ) ? ? ModifierKeys . None ;
2023-05-30 09:56:52 +12:00
if ( modifiers = = ModifierKeys . None & & hotkey = = Key . None )
2023-05-28 04:23:01 +12:00
{
2023-05-30 09:56:52 +12:00
Background = SystemColors . ControlBrush ;
2023-05-28 04:23:01 +12:00
}
2023-06-01 08:32:19 +12:00
else if ( HotkeyFunction ! = null )
2023-05-28 04:23:01 +12:00
{
2023-05-30 09:56:52 +12:00
Background = Brushes . LightGreen ;
2023-05-28 04:23:01 +12:00
}
2023-06-01 08:32:19 +12:00
else
{
Background = Brushes . IndianRed ;
}
2023-05-28 04:23:01 +12:00
2023-06-01 10:32:28 +12:00
Text = ModifiersAndKeyToString ( modifiers , hotkey ) ;
2023-05-28 04:23:01 +12:00
}
/// <summary>
/// Change the hotkey to given combination.
/// </summary>
/// <param name="hotkeyString">Hotkey combination string.</param>
2023-06-01 08:32:19 +12:00
internal void ChangeHotkey ( string hotkeyString )
{
2023-06-04 02:56:17 +12:00
try
{
HotkeyFunction ? . Register ( hotkeyString ) ;
WasHotkeyChanged = true ;
}
catch ( InvalidOperationException ex )
{
Log . Warn ( $"Hotkey failed resetting to default: '{hotkeyString}'" , ex ) ;
return ;
}
2023-06-01 08:32:19 +12:00
UpdateHotkeyRegistration ( ) ;
}
2023-05-27 06:51:42 +12:00
2023-05-22 07:13:18 +12:00
/// <summary>
2023-06-01 08:32:19 +12:00
/// Change the hotkey to given combination.
/// Sets background accordingly.
2023-05-22 07:13:18 +12:00
/// </summary>
2023-06-01 08:32:19 +12:00
/// <param name="modifiers">Hotkey modifiers.</param>
/// <param name="key">Hotkey key.</param>
internal void ChangeHotkey ( ModifierKeys modifiers , Key key )
2023-05-22 07:13:18 +12:00
{
2023-05-26 06:52:31 +12:00
if ( key = = Key . None )
2023-05-22 07:13:18 +12:00
{
2023-06-01 10:32:28 +12:00
HotkeyFunction ? . Unregister ( ) ;
2023-06-04 02:56:17 +12:00
WasHotkeyChanged = true ;
2023-05-22 07:13:18 +12:00
}
2023-06-01 08:32:19 +12:00
else
2023-05-22 07:13:18 +12:00
{
2023-06-01 08:32:19 +12:00
try
{
HotkeyFunction ? . Register ( modifiers , key ) ;
2023-06-04 02:56:17 +12:00
WasHotkeyChanged = true ;
2023-06-01 08:32:19 +12:00
}
catch ( InvalidOperationException ex )
{
Log . Info ( $"Couldn't register hotkey modifier {modifiers} key {key} ex: " + ex . ToString ( ) ) ;
}
2023-05-30 09:56:52 +12:00
}
2023-05-22 07:13:18 +12:00
}
private void HandlePreviewKeyDown ( object sender , KeyEventArgs e )
{
ModifierKeys modifiers = Keyboard . Modifiers ;
switch ( e . Key )
{
2023-05-30 09:56:52 +12:00
case Key . Back :
2023-05-22 07:13:18 +12:00
case Key . Delete :
2023-06-01 10:32:28 +12:00
HotkeyFunction ? . Unregister ( ) ;
2023-06-04 02:56:17 +12:00
WasHotkeyChanged = true ;
2023-06-01 10:32:28 +12:00
UpdateHotkeyRegistration ( ) ;
2023-05-22 07:13:18 +12:00
e . Handled = true ;
break ;
case Key . Insert :
if ( modifiers = = ModifierKeys . Shift )
{
e . Handled = true ; // Don't allow
}
break ;
default :
break ;
}
}
2023-06-01 10:32:28 +12:00
private void FilterCombinations ( ref ModifierKeys modifiers , ref Key key )
2023-05-22 07:13:18 +12:00
{
2023-05-30 09:56:52 +12:00
// No modifier or shift only, AND a hotkey that needs another modifier
2023-06-01 10:32:28 +12:00
if ( ( modifiers = = ModifierKeys . Shift | | modifiers = = ModifierKeys . None ) & & needNonShiftModifier . Contains ( key ) )
2023-05-22 07:13:18 +12:00
{
2023-05-30 09:56:52 +12:00
if ( modifiers = = ModifierKeys . None )
2023-05-22 07:13:18 +12:00
{
2023-05-30 09:56:52 +12:00
// Set Ctrl+Alt as the modifier unless Ctrl+Alt+<key> won't work...
2023-06-01 10:32:28 +12:00
if ( needNonAltGrModifier . Contains ( key ) = = false )
2023-05-22 07:13:18 +12:00
{
2023-05-30 09:56:52 +12:00
modifiers = ModifierKeys . Alt | ModifierKeys . Control ;
2023-05-22 07:13:18 +12:00
}
else
{
2023-05-30 09:56:52 +12:00
// ... in that case, use Shift+Alt instead.
modifiers = ModifierKeys . Alt | ModifierKeys . Shift ;
2023-05-22 07:13:18 +12:00
}
}
2023-05-30 09:56:52 +12:00
else
2023-05-22 07:13:18 +12:00
{
2023-05-30 09:56:52 +12:00
// User pressed Shift and an invalid key (e.g. a letter or a number),
// that needs another set of modifier keys
2023-06-01 10:32:28 +12:00
key = Key . None ;
2023-05-22 07:13:18 +12:00
return ;
}
}
2023-05-30 09:56:52 +12:00
// Check all Ctrl+Alt keys
2023-06-01 10:32:28 +12:00
if ( modifiers = = ( ModifierKeys . Alt | ModifierKeys . Control ) & & needNonAltGrModifier . Contains ( key ) )
2023-05-30 09:56:52 +12:00
{
// Ctrl+Alt+4 etc won't work; reset hotkey and tell the user
2023-06-01 10:32:28 +12:00
key = Key . None ;
2023-05-30 09:56:52 +12:00
return ;
}
2023-05-22 07:13:18 +12:00
// I have no idea why this is needed, but it is. Without this code, pressing only Ctrl
// will show up as "Control + ControlKey", etc.
2023-06-01 10:32:28 +12:00
if ( key = = Key . LeftAlt | | key = = Key . RightAlt | |
key = = Key . LeftShift | | key = = Key . RightShift | |
key = = Key . LeftCtrl | | key = = Key . RightCtrl )
2023-05-22 07:13:18 +12:00
{
2023-06-01 10:32:28 +12:00
key = Key . None ;
2023-05-22 07:13:18 +12:00
}
}
/// <summary>
/// Populates the ArrayLists specifying disallowed hotkeys
/// such as Shift+A, Ctrl+Alt+4 (would produce a dollar sign) etc.
/// </summary>
private void PopulateModifierLists ( )
{
2023-06-01 10:32:28 +12:00
// Ctrl+Alt + 0 - 9
// Shift + 0 - 9
for ( Key k = Key . D0 ; k < = Key . D9 ; k + + )
{
needNonAltGrModifier . Add ( k ) ;
needNonShiftModifier . Add ( k ) ;
}
// Shift + A - Z
for ( Key k = Key . A ; k < = Key . Z ; k + + )
2023-05-22 07:13:18 +12:00
{
2023-06-01 10:32:28 +12:00
needNonShiftModifier . Add ( k ) ;
2023-05-22 07:13:18 +12:00
}
// Shift + Numpad keys
for ( Key k = Key . NumPad0 ; k < = Key . NumPad9 ; k + + )
{
2023-06-01 10:32:28 +12:00
needNonShiftModifier . Add ( k ) ;
2023-05-22 07:13:18 +12:00
}
// Shift + Misc (,;<./ etc)
for ( Key k = Key . Oem1 ; k < = Key . OemBackslash ; k + + )
{
2023-06-01 10:32:28 +12:00
needNonShiftModifier . Add ( k ) ;
2023-05-22 07:13:18 +12:00
}
// Shift + Space, PgUp, PgDn, End, Home
for ( Key k = Key . Space ; k < = Key . Home ; k + + )
{
2023-06-01 10:32:28 +12:00
needNonShiftModifier . Add ( k ) ;
2023-05-22 07:13:18 +12:00
}
// Misc keys that we can't loop through
2023-06-01 10:32:28 +12:00
needNonShiftModifier . Add ( Key . Insert ) ;
needNonShiftModifier . Add ( Key . Help ) ;
needNonShiftModifier . Add ( Key . Multiply ) ;
needNonShiftModifier . Add ( Key . Add ) ;
needNonShiftModifier . Add ( Key . Subtract ) ;
needNonShiftModifier . Add ( Key . Divide ) ;
needNonShiftModifier . Add ( Key . Decimal ) ;
needNonShiftModifier . Add ( Key . Return ) ;
needNonShiftModifier . Add ( Key . Escape ) ;
needNonShiftModifier . Add ( Key . NumLock ) ;
2023-05-22 07:13:18 +12:00
}
/// <summary>
/// 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.
/// </summary>
private void HotkeyControl_KeyDown ( object sender , KeyEventArgs e )
{
// Clear the current hotkey
if ( e . Key = = Key . Back | | e . Key = = Key . Delete )
{
2023-06-01 10:32:28 +12:00
HotkeyFunction ? . Unregister ( ) ;
2023-06-04 02:56:17 +12:00
WasHotkeyChanged = true ;
2023-05-22 07:13:18 +12:00
}
else
{
modifiers = Keyboard . Modifiers ;
hotkey = e . Key ;
2023-06-01 10:32:28 +12:00
FilterCombinations ( ref modifiers , ref hotkey ) ;
2023-06-01 08:32:19 +12:00
ChangeHotkey ( modifiers , hotkey ) ;
2023-05-22 07:13:18 +12:00
}
2023-06-01 10:32:28 +12:00
UpdateHotkeyRegistration ( ) ;
2023-05-22 07:13:18 +12:00
}
/// <summary>
/// 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.
/// </summary>
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 ;
2023-06-01 10:32:28 +12:00
FilterCombinations ( ref modifiers , ref hotkey ) ;
2023-06-01 08:32:19 +12:00
ChangeHotkey ( modifiers , hotkey ) ;
2023-05-30 09:56:52 +12:00
UpdateHotkeyRegistration ( ) ;
2023-05-22 07:13:18 +12:00
}
2023-06-01 10:32:28 +12:00
else if ( hotkey = = Key . None )
2023-05-22 07:13:18 +12:00
{
2023-06-01 10:32:28 +12:00
HotkeyFunction ? . Unregister ( ) ;
2023-06-04 02:56:17 +12:00
WasHotkeyChanged = true ;
2023-06-01 10:32:28 +12:00
UpdateHotkeyRegistration ( ) ;
2023-05-22 07:13:18 +12:00
}
}
/// <summary>
/// Prevents the letter/whatever entered to show up in the TextBox
/// Without this, a "A" key press would appear as "aControl, Alt + A".
/// </summary>
2023-06-01 08:32:19 +12:00
private void HandlePreviewTextInput ( object sender , TextCompositionEventArgs e ) = > e . Handled = true ;
2023-05-26 10:34:31 +12:00
#if TODO // HOTKEY
/// <summary>
/// Helper method to cleanly register a hotkey.
/// </summary>
/// <param name="failedKeys">failedKeys.</param>
/// <param name="hotkeyString">hotkeyString.</param>
/// <param name="handler">handler.</param>
/// <returns>bool success.</returns>
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 ;
}
/// <summary>
/// Registers all hotkeys as configured, displaying a dialog in case of hotkey conflicts with other tools.
/// </summary>
/// <param name="ignoreFailedRegistration">if true, a failed hotkey registration will not be reported to the user - the hotkey will simply not be registered.</param>
/// <returns>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).</returns>
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 ;
}
/// <summary>
/// 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).
/// </summary>
/// <param name="failedKeys">comma separated list of the hotkeys that could not be registered, for display in dialog text.</param>
/// <returns>bool success.</returns>
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
2023-05-22 07:13:18 +12:00
}
}