2020-08-17 10:00:56 -07:00
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System ;
2020-10-08 08:45:09 -07:00
using Microsoft.PowerToys.Settings.UI.Helpers ;
2020-10-22 09:45:48 -07:00
using Microsoft.PowerToys.Settings.UI.Library ;
2022-04-19 22:00:28 +02:00
using Microsoft.UI.Xaml ;
using Microsoft.UI.Xaml.Automation ;
using Microsoft.UI.Xaml.Controls ;
using Microsoft.UI.Xaml.Input ;
2021-09-06 20:21:18 +02:00
using Windows.ApplicationModel.Resources ;
2022-04-19 22:00:28 +02:00
using Windows.System ;
2020-08-17 10:00:56 -07:00
namespace Microsoft.PowerToys.Settings.UI.Controls
{
2021-09-06 20:21:18 +02:00
public sealed partial class ShortcutControl : UserControl , IDisposable
2020-08-17 10:00:56 -07:00
{
2020-10-08 08:45:09 -07:00
private readonly UIntPtr ignoreKeyEventFlag = ( UIntPtr ) 0x5555 ;
2020-10-29 14:24:16 -07:00
private bool _shiftKeyDownOnEntering ;
private bool _shiftToggled ;
2021-09-06 20:21:18 +02:00
private bool _enabled ;
private HotkeySettings hotkeySettings ;
private HotkeySettings internalSettings ;
private HotkeySettings lastValidSettings ;
private HotkeySettingsControlHook hook ;
private bool _isActive ;
private bool disposedValue ;
2020-10-08 08:45:09 -07:00
2020-08-17 10:00:56 -07:00
public string Header { get ; set ; }
public string Keys { get ; set ; }
2021-09-06 20:21:18 +02:00
public static readonly DependencyProperty IsActiveProperty = DependencyProperty . Register ( "Enabled" , typeof ( bool ) , typeof ( ShortcutControl ) , null ) ;
public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty . Register ( "HotkeySettings" , typeof ( HotkeySettings ) , typeof ( ShortcutControl ) , null ) ;
2020-08-17 10:00:56 -07:00
2021-09-06 20:21:18 +02:00
private ShortcutDialogContentControl c = new ShortcutDialogContentControl ( ) ;
private ContentDialog shortcutDialog ;
2020-08-17 10:00:56 -07:00
public bool Enabled
{
get
{
return _enabled ;
}
set
{
SetValue ( IsActiveProperty , value ) ;
_enabled = value ;
if ( value )
{
2021-09-06 20:21:18 +02:00
EditButton . IsEnabled = true ;
2020-08-17 10:00:56 -07:00
}
else
{
2021-09-06 20:21:18 +02:00
EditButton . IsEnabled = false ;
2020-08-17 10:00:56 -07:00
}
}
}
public HotkeySettings HotkeySettings
{
get
{
return hotkeySettings ;
}
set
{
if ( hotkeySettings ! = value )
{
hotkeySettings = value ;
SetValue ( HotkeySettingsProperty , value ) ;
2021-09-06 20:21:18 +02:00
PreviewKeysControl . ItemsSource = HotkeySettings . GetKeysList ( ) ;
AutomationProperties . SetHelpText ( EditButton , HotkeySettings . ToString ( ) ) ;
c . Keys = HotkeySettings . GetKeysList ( ) ;
2020-08-17 10:00:56 -07:00
}
}
}
2021-09-06 20:21:18 +02:00
public ShortcutControl ( )
2020-08-17 10:00:56 -07:00
{
InitializeComponent ( ) ;
internalSettings = new HotkeySettings ( ) ;
2021-09-06 20:21:18 +02:00
this . Unloaded + = ShortcutControl_Unloaded ;
2020-10-08 08:45:09 -07:00
hook = new HotkeySettingsControlHook ( Hotkey_KeyDown , Hotkey_KeyUp , Hotkey_IsActive , FilterAccessibleKeyboardEvents ) ;
2022-04-19 22:00:28 +02:00
ResourceLoader resourceLoader = ResourceLoader . GetForViewIndependentUse ( ) ;
2021-09-06 20:21:18 +02:00
// We create the Dialog in C# because doing it in XAML is giving WinUI/XAML Island bugs when using dark theme.
shortcutDialog = new ContentDialog
{
XamlRoot = this . XamlRoot ,
Title = resourceLoader . GetString ( "Activation_Shortcut_Title" ) ,
Content = c ,
PrimaryButtonText = resourceLoader . GetString ( "Activation_Shortcut_Save" ) ,
CloseButtonText = resourceLoader . GetString ( "Activation_Shortcut_Cancel" ) ,
DefaultButton = ContentDialogButton . Primary ,
} ;
shortcutDialog . PrimaryButtonClick + = ShortcutDialog_PrimaryButtonClick ;
shortcutDialog . Opened + = ShortcutDialog_Opened ;
shortcutDialog . Closing + = ShortcutDialog_Closing ;
2022-12-13 15:14:09 +01:00
AutomationProperties . SetName ( EditButton , resourceLoader . GetString ( "Activation_Shortcut_Title" ) ) ;
2020-08-17 10:00:56 -07:00
}
2021-09-06 20:21:18 +02:00
private void ShortcutControl_Unloaded ( object sender , RoutedEventArgs e )
2020-08-17 10:00:56 -07:00
{
2021-09-06 20:21:18 +02:00
shortcutDialog . PrimaryButtonClick - = ShortcutDialog_PrimaryButtonClick ;
shortcutDialog . Opened - = ShortcutDialog_Opened ;
shortcutDialog . Closing - = ShortcutDialog_Closing ;
2020-08-17 10:00:56 -07:00
// Dispose the HotkeySettingsControlHook object to terminate the hook threads when the textbox is unloaded
hook . Dispose ( ) ;
}
2020-10-29 14:24:16 -07:00
private void KeyEventHandler ( int key , bool matchValue , int matchValueCode )
2020-08-17 10:00:56 -07:00
{
2022-04-19 22:00:28 +02:00
switch ( ( VirtualKey ) key )
2020-08-17 10:00:56 -07:00
{
2022-04-19 22:00:28 +02:00
case VirtualKey . LeftWindows :
case VirtualKey . RightWindows :
2020-08-17 10:00:56 -07:00
internalSettings . Win = matchValue ;
break ;
2022-04-19 22:00:28 +02:00
case VirtualKey . Control :
case VirtualKey . LeftControl :
case VirtualKey . RightControl :
2020-08-17 10:00:56 -07:00
internalSettings . Ctrl = matchValue ;
break ;
2022-04-19 22:00:28 +02:00
case VirtualKey . Menu :
case VirtualKey . LeftMenu :
case VirtualKey . RightMenu :
2020-08-17 10:00:56 -07:00
internalSettings . Alt = matchValue ;
break ;
2022-04-19 22:00:28 +02:00
case VirtualKey . Shift :
case VirtualKey . LeftShift :
case VirtualKey . RightShift :
2020-10-08 08:45:09 -07:00
_shiftToggled = true ;
2020-08-17 10:00:56 -07:00
internalSettings . Shift = matchValue ;
break ;
2022-04-19 22:00:28 +02:00
case VirtualKey . Escape :
2020-08-17 10:00:56 -07:00
internalSettings = new HotkeySettings ( ) ;
2021-09-06 20:21:18 +02:00
shortcutDialog . IsPrimaryButtonEnabled = false ;
2020-08-17 10:00:56 -07:00
return ;
default :
internalSettings . Code = matchValueCode ;
break ;
}
}
2020-10-08 08:45:09 -07:00
// Function to send a single key event to the system which would be ignored by the hotkey control.
private void SendSingleKeyboardInput ( short keyCode , uint keyStatus )
{
NativeKeyboardHelper . INPUT inputShift = new NativeKeyboardHelper . INPUT
{
type = NativeKeyboardHelper . INPUTTYPE . INPUT_KEYBOARD ,
data = new NativeKeyboardHelper . InputUnion
{
ki = new NativeKeyboardHelper . KEYBDINPUT
{
wVk = keyCode ,
dwFlags = keyStatus ,
// Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead.
dwExtraInfo = ignoreKeyEventFlag ,
} ,
} ,
} ;
NativeKeyboardHelper . INPUT [ ] inputs = new NativeKeyboardHelper . INPUT [ ] { inputShift } ;
_ = NativeMethods . SendInput ( 1 , inputs , NativeKeyboardHelper . INPUT . Size ) ;
}
private bool FilterAccessibleKeyboardEvents ( int key , UIntPtr extraInfo )
{
// A keyboard event sent with this value in the extra Information field should be ignored by the hook so that it can be captured by the system instead.
if ( extraInfo = = ignoreKeyEventFlag )
{
return false ;
}
// If the current key press is tab, based on the other keys ignore the key press so as to shift focus out of the hotkey control.
2022-04-19 22:00:28 +02:00
if ( ( VirtualKey ) key = = VirtualKey . Tab )
2020-10-08 08:45:09 -07:00
{
// Shift was not pressed while entering and Shift is not pressed while leaving the hotkey control, treat it as a normal tab key press.
if ( ! internalSettings . Shift & & ! _shiftKeyDownOnEntering & & ! internalSettings . Win & & ! internalSettings . Alt & & ! internalSettings . Ctrl )
{
return false ;
}
// Shift was not pressed while entering but it was pressed while leaving the hotkey, therefore simulate a shift key press as the system does not know about shift being pressed in the hotkey.
else if ( internalSettings . Shift & & ! _shiftKeyDownOnEntering & & ! internalSettings . Win & & ! internalSettings . Alt & & ! internalSettings . Ctrl )
{
// This is to reset the shift key press within the control as it was not used within the control but rather was used to leave the hotkey.
internalSettings . Shift = false ;
2022-04-19 22:00:28 +02:00
SendSingleKeyboardInput ( ( short ) VirtualKey . Shift , ( uint ) NativeKeyboardHelper . KeyEventF . KeyDown ) ;
2020-10-08 08:45:09 -07:00
return false ;
}
// Shift was pressed on entering and remained pressed, therefore only ignore the tab key so that it can be passed to the system.
// As the shift key is already assumed to be pressed by the system while it entered the hotkey control, shift would still remain pressed, hence ignoring the tab input would simulate a Shift+Tab key press.
else if ( ! internalSettings . Shift & & _shiftKeyDownOnEntering & & ! _shiftToggled & & ! internalSettings . Win & & ! internalSettings . Alt & & ! internalSettings . Ctrl )
{
return false ;
}
// Shift was pressed on entering but it was released and later pressed again.
// Ignore the tab key and the system already has the shift key pressed, therefore this would simulate Shift+Tab.
// However, since the last shift key was only used to move out of the control, reset the status of shift within the control.
else if ( internalSettings . Shift & & _shiftKeyDownOnEntering & & _shiftToggled & & ! internalSettings . Win & & ! internalSettings . Alt & & ! internalSettings . Ctrl )
{
internalSettings . Shift = false ;
return false ;
}
// Shift was pressed on entering and was later released.
// The system still has shift in the key pressed status, therefore pass a Shift KeyUp message to the system, to release the shift key, therefore simulating only the Tab key press.
else if ( ! internalSettings . Shift & & _shiftKeyDownOnEntering & & _shiftToggled & & ! internalSettings . Win & & ! internalSettings . Alt & & ! internalSettings . Ctrl )
{
2022-04-19 22:00:28 +02:00
SendSingleKeyboardInput ( ( short ) VirtualKey . Shift , ( uint ) NativeKeyboardHelper . KeyEventF . KeyUp ) ;
2020-10-08 08:45:09 -07:00
return false ;
}
}
2021-09-06 20:21:18 +02:00
// Either the cancel or save button has keyboard focus.
if ( FocusManager . GetFocusedElement ( LayoutRoot . XamlRoot ) . GetType ( ) = = typeof ( Button ) )
{
return false ;
}
2020-10-08 08:45:09 -07:00
return true ;
}
2022-04-19 22:00:28 +02:00
private void Hotkey_KeyDown ( int key )
2020-08-17 10:00:56 -07:00
{
2022-04-19 22:00:28 +02:00
KeyEventHandler ( key , true , key ) ;
c . Keys = internalSettings . GetKeysList ( ) ;
2020-10-08 08:45:09 -07:00
2022-04-19 22:00:28 +02:00
if ( internalSettings . GetKeysList ( ) . Count = = 0 )
{
// Empty, disable save button
shortcutDialog . IsPrimaryButtonEnabled = false ;
}
else if ( internalSettings . GetKeysList ( ) . Count = = 1 )
{
// 1 key, disable save button
shortcutDialog . IsPrimaryButtonEnabled = false ;
2021-09-06 20:21:18 +02:00
2022-04-19 22:00:28 +02:00
// Check if the one key is a hotkey
if ( internalSettings . Shift | | internalSettings . Win | | internalSettings . Alt | | internalSettings . Ctrl )
2021-09-06 20:21:18 +02:00
{
2022-04-19 22:00:28 +02:00
c . IsError = false ;
2021-09-06 20:21:18 +02:00
}
2022-04-19 22:00:28 +02:00
else
2021-09-06 20:21:18 +02:00
{
2022-04-19 22:00:28 +02:00
c . IsError = true ;
2021-09-06 20:21:18 +02:00
}
2022-04-19 22:00:28 +02:00
}
2021-09-06 20:21:18 +02:00
2022-04-19 22:00:28 +02:00
// Tab and Shift+Tab are accessible keys and should not be displayed in the hotkey control.
if ( internalSettings . Code > 0 & & ! internalSettings . IsAccessibleShortcut ( ) )
{
lastValidSettings = internalSettings . Clone ( ) ;
2021-09-06 20:21:18 +02:00
2022-04-19 22:00:28 +02:00
if ( ! ComboIsValid ( lastValidSettings ) )
{
DisableKeys ( ) ;
2020-08-17 10:00:56 -07:00
}
2022-04-19 22:00:28 +02:00
else
{
EnableKeys ( ) ;
}
}
2020-08-17 10:00:56 -07:00
}
2021-09-06 20:21:18 +02:00
private void EnableKeys ( )
{
shortcutDialog . IsPrimaryButtonEnabled = true ;
c . IsError = false ;
// WarningLabel.Style = (Style)App.Current.Resources["SecondaryTextStyle"];
}
private void DisableKeys ( )
{
shortcutDialog . IsPrimaryButtonEnabled = false ;
c . IsError = true ;
// WarningLabel.Style = (Style)App.Current.Resources["SecondaryWarningTextStyle"];
}
2022-04-19 22:00:28 +02:00
private void Hotkey_KeyUp ( int key )
2020-08-17 10:00:56 -07:00
{
2022-04-19 22:00:28 +02:00
KeyEventHandler ( key , false , 0 ) ;
2020-08-17 10:00:56 -07:00
}
private bool Hotkey_IsActive ( )
{
return _isActive ;
}
2021-09-06 20:21:18 +02:00
private void ShortcutDialog_Opened ( ContentDialog sender , ContentDialogOpenedEventArgs args )
2020-08-17 10:00:56 -07:00
{
2021-09-06 20:21:18 +02:00
if ( ! ComboIsValid ( hotkeySettings ) )
{
DisableKeys ( ) ;
}
else
{
EnableKeys ( ) ;
}
2020-10-08 08:45:09 -07:00
// Reset the status on entering the hotkey each time.
_shiftKeyDownOnEntering = false ;
_shiftToggled = false ;
// To keep track of the shift key, whether it was pressed on entering.
2022-04-19 22:00:28 +02:00
if ( ( NativeMethods . GetAsyncKeyState ( ( int ) VirtualKey . Shift ) & 0x8000 ) ! = 0 )
2020-10-08 08:45:09 -07:00
{
_shiftKeyDownOnEntering = true ;
}
2020-08-17 10:00:56 -07:00
_isActive = true ;
}
2021-09-06 20:21:18 +02:00
private async void OpenDialogButton_Click ( object sender , RoutedEventArgs e )
2020-08-17 10:00:56 -07:00
{
2021-09-06 20:21:18 +02:00
c . Keys = null ;
c . Keys = HotkeySettings . GetKeysList ( ) ;
shortcutDialog . XamlRoot = this . XamlRoot ;
await shortcutDialog . ShowAsync ( ) ;
}
private void ShortcutDialog_PrimaryButtonClick ( ContentDialog sender , ContentDialogButtonClickEventArgs args )
{
if ( ComboIsValid ( lastValidSettings ) )
2020-08-17 10:00:56 -07:00
{
HotkeySettings = lastValidSettings . Clone ( ) ;
}
2021-09-06 20:21:18 +02:00
PreviewKeysControl . ItemsSource = hotkeySettings . GetKeysList ( ) ;
AutomationProperties . SetHelpText ( EditButton , HotkeySettings . ToString ( ) ) ;
shortcutDialog . Hide ( ) ;
}
2021-07-15 13:22:23 +02:00
2021-09-06 20:21:18 +02:00
private static bool ComboIsValid ( HotkeySettings settings )
{
if ( settings ! = null & & ( settings . IsValid ( ) | | settings . IsEmpty ( ) ) )
{
return true ;
}
else
{
return false ;
2021-07-15 13:22:23 +02:00
}
2021-09-06 20:21:18 +02:00
}
2021-07-15 13:22:23 +02:00
2021-09-06 20:21:18 +02:00
private void ShortcutDialog_Closing ( ContentDialog sender , ContentDialogClosingEventArgs args )
{
2020-08-17 10:00:56 -07:00
_isActive = false ;
}
2020-10-29 14:24:16 -07:00
private void Dispose ( bool disposing )
{
if ( ! disposedValue )
{
if ( disposing )
{
hook . Dispose ( ) ;
}
disposedValue = true ;
}
}
public void Dispose ( )
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose ( disposing : true ) ;
GC . SuppressFinalize ( this ) ;
}
2020-08-17 10:00:56 -07:00
}
}