diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 7ed0ea7e59..20e1e5dcad 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1938,6 +1938,7 @@ Subdir subfolder subkey SUBLANG +submenu subquery substr Sul diff --git a/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs b/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs index 3daf0f97f7..da3c147572 100644 --- a/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs +++ b/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs @@ -5,6 +5,7 @@ using System; using System.ComponentModel.Composition; using System.Windows; +using System.Windows.Interop; using ColorPicker.Settings; using ColorPicker.ViewModelContracts; using Common.UI; @@ -21,6 +22,9 @@ namespace ColorPicker.Helpers private bool _colorPickerShown; private object _colorPickerVisibilityLock = new object(); + private HwndSource _hwndSource; + private const int _globalHotKeyId = 0x0001; + // Blocks using the escape key to close the color picker editor when the adjust color flyout is open. public static bool BlockEscapeKeyClosingColorPickerEditor { get; set; } @@ -56,6 +60,12 @@ namespace ColorPicker.Helpers { ShowColorPicker(); } + + // Handle the escape key to close Color Picker locally when being spawn from PowerToys, since Keyboard Hooks from the KeyboardMonitor are heavy. + if (!(System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys()) + { + SetupEscapeGlobalKeyShortcut(); + } } } @@ -74,6 +84,12 @@ namespace ColorPicker.Helpers HideColorPicker(); } + // Handle the escape key to close Color Picker locally when being spawn from PowerToys, since Keyboard Hooks from the KeyboardMonitor are heavy. + if (!(System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys()) + { + ClearEscapeGlobalKeyShortcut(); + } + SessionEventHelper.End(); return true; @@ -190,5 +206,67 @@ namespace ColorPicker.Helpers { SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ColorPicker); } + + internal void RegisterWindowHandle(System.Windows.Interop.HwndSource hwndSource) + { + _hwndSource = hwndSource; + } + +#pragma warning disable CA1801 // Review unused parameters + public IntPtr ProcessWindowMessages(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) +#pragma warning restore CA1801 // Review unused parameters + { + switch (msg) + { + case NativeMethods.WM_HOTKEY: + if (!BlockEscapeKeyClosingColorPickerEditor) + { + handled = EndUserSession(); + } + else + { + // If escape key is blocked it means a submenu is open. + // Send the escape key to the Window to close that submenu. + // Description for LPARAM in https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keyup#parameters + // It's basically some modifiers + scancode for escape (1) + number of repetitions (1) + handled = true; + handled &= NativeMethods.PostMessage(_hwndSource.Handle, NativeMethods.WM_KEYDOWN, (IntPtr)NativeMethods.VK_ESCAPE, (IntPtr)0x00010001); + handled &= NativeMethods.PostMessage(_hwndSource.Handle, NativeMethods.WM_KEYUP, (IntPtr)NativeMethods.VK_ESCAPE, (IntPtr)0xC0010001); + } + + break; + } + + return IntPtr.Zero; + } + + public void SetupEscapeGlobalKeyShortcut() + { + if (_hwndSource == null) + { + return; + } + + _hwndSource.AddHook(ProcessWindowMessages); + if (!NativeMethods.RegisterHotKey(_hwndSource.Handle, _globalHotKeyId, NativeMethods.MOD_NOREPEAT, NativeMethods.VK_ESCAPE)) + { + Logger.LogWarning("Couldn't register the hotkey for Esc."); + } + } + + public void ClearEscapeGlobalKeyShortcut() + { + if (_hwndSource == null) + { + return; + } + + if (!NativeMethods.UnregisterHotKey(_hwndSource.Handle, _globalHotKeyId)) + { + Logger.LogWarning("Couldn't unregister the hotkey for Esc."); + } + + _hwndSource.RemoveHook(ProcessWindowMessages); + } } } diff --git a/src/modules/colorPicker/ColorPickerUI/Keyboard/KeyboardMonitor.cs b/src/modules/colorPicker/ColorPickerUI/Keyboard/KeyboardMonitor.cs index 1a659fecaa..8daf16a734 100644 --- a/src/modules/colorPicker/ColorPickerUI/Keyboard/KeyboardMonitor.cs +++ b/src/modules/colorPicker/ColorPickerUI/Keyboard/KeyboardMonitor.cs @@ -83,40 +83,37 @@ namespace ColorPicker.Keyboard } } - if ((System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys()) + var name = Helper.GetKeyName((uint)virtualCode); + + // If the last key pressed is a modifier key, then currentlyPressedKeys cannot possibly match with _activationKeys + // because _activationKeys contains exactly 1 non-modifier key. Hence, there's no need to check if `name` is a + // modifier key or to do any additional processing on it. + if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown || e.KeyboardState == GlobalKeyboardHook.KeyboardState.SysKeyDown) { - var name = Helper.GetKeyName((uint)virtualCode); + // Check pressed modifier keys. + AddModifierKeys(currentlyPressedKeys); - // If the last key pressed is a modifier key, then currentlyPressedKeys cannot possibly match with _activationKeys - // because _activationKeys contains exactly 1 non-modifier key. Hence, there's no need to check if `name` is a - // modifier key or to do any additional processing on it. - if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown || e.KeyboardState == GlobalKeyboardHook.KeyboardState.SysKeyDown) + currentlyPressedKeys.Add(name); + } + + currentlyPressedKeys.Sort(); + + if (currentlyPressedKeys.Count == 0 && _previouslyPressedKeys.Count != 0) + { + // no keys pressed, we can enable activation shortcut again + _activationShortcutPressed = false; + } + + _previouslyPressedKeys = currentlyPressedKeys; + + if (ArraysAreSame(currentlyPressedKeys, _activationKeys)) + { + // avoid triggering this action multiple times as this will be called nonstop while keys are pressed + if (!_activationShortcutPressed) { - // Check pressed modifier keys. - AddModifierKeys(currentlyPressedKeys); + _activationShortcutPressed = true; - currentlyPressedKeys.Add(name); - } - - currentlyPressedKeys.Sort(); - - if (currentlyPressedKeys.Count == 0 && _previouslyPressedKeys.Count != 0) - { - // no keys pressed, we can enable activation shortcut again - _activationShortcutPressed = false; - } - - _previouslyPressedKeys = currentlyPressedKeys; - - if (ArraysAreSame(currentlyPressedKeys, _activationKeys)) - { - // avoid triggering this action multiple times as this will be called nonstop while keys are pressed - if (!_activationShortcutPressed) - { - _activationShortcutPressed = true; - - _appStateHandler.StartUserSession(); - } + _appStateHandler.StartUserSession(); } } } @@ -169,7 +166,7 @@ namespace ColorPicker.Keyboard if (disposing) { // TODO: dispose managed state (managed objects) - _keyboardHook.Dispose(); + _keyboardHook?.Dispose(); } // TODO: free unmanaged resources (unmanaged objects) and override finalizer diff --git a/src/modules/colorPicker/ColorPickerUI/MainWindow.xaml b/src/modules/colorPicker/ColorPickerUI/MainWindow.xaml index 469d7db375..208e1fcda0 100644 --- a/src/modules/colorPicker/ColorPickerUI/MainWindow.xaml +++ b/src/modules/colorPicker/ColorPickerUI/MainWindow.xaml @@ -14,6 +14,7 @@ Background="Transparent" SizeToContent="WidthAndHeight" AllowsTransparency="True" + SourceInitialized="MainWindowSourceInitialized" AutomationProperties.Name="Color Picker"> diff --git a/src/modules/colorPicker/ColorPickerUI/MainWindow.xaml.cs b/src/modules/colorPicker/ColorPickerUI/MainWindow.xaml.cs index fa76fcd67c..c3df6f54c7 100644 --- a/src/modules/colorPicker/ColorPickerUI/MainWindow.xaml.cs +++ b/src/modules/colorPicker/ColorPickerUI/MainWindow.xaml.cs @@ -4,6 +4,7 @@ using System.ComponentModel.Composition; using System.Windows; +using System.Windows.Interop; using ColorPicker.ViewModelContracts; namespace ColorPicker @@ -19,6 +20,7 @@ namespace ColorPicker Bootstrapper.InitializeContainer(this); InitializeComponent(); DataContext = this; + Show(); // Call show just to make sure source is initialized at startup. Hide(); } @@ -30,5 +32,10 @@ namespace ColorPicker Closing -= MainWindow_Closing; Bootstrapper.Dispose(); } + + private void MainWindowSourceInitialized(object sender, System.EventArgs e) + { + this.MainViewModel.RegisterWindowHandle(HwndSource.FromHwnd(new WindowInteropHelper(this).Handle)); + } } } diff --git a/src/modules/colorPicker/ColorPickerUI/NativeMethods.cs b/src/modules/colorPicker/ColorPickerUI/NativeMethods.cs index 39332bd1f9..3b24508d30 100644 --- a/src/modules/colorPicker/ColorPickerUI/NativeMethods.cs +++ b/src/modules/colorPicker/ColorPickerUI/NativeMethods.cs @@ -46,6 +46,26 @@ namespace ColorPicker [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Interop")] public const int VK_RWIN = 0x5C; + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Interop")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Interop")] + public const int VK_ESCAPE = 0x1B; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Interop")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Interop")] + public const int WM_HOTKEY = 0x0312; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Interop")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Interop")] + public const int WM_KEYDOWN = 0x0100; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Interop")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Interop")] + public const int WM_KEYUP = 0x0101; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Interop")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Interop")] + public const uint MOD_NOREPEAT = 0x4000; + public delegate bool MonitorEnumProc( IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lParam); @@ -88,6 +108,15 @@ namespace ColorPicker [DllImport("user32.dll", EntryPoint = "SystemParametersInfo")] internal static extern bool SystemParametersInfo(int uiAction, int uiParam, IntPtr pvParam, int fWinIni); + [DllImport("user32.dll")] + internal static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); + + [DllImport("user32.dll")] + internal static extern bool UnregisterHotKey(IntPtr hWnd, int id); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Interop object")] [StructLayout(LayoutKind.Sequential)] internal struct POINT diff --git a/src/modules/colorPicker/ColorPickerUI/ViewModelContracts/IMainViewModel.cs b/src/modules/colorPicker/ColorPickerUI/ViewModelContracts/IMainViewModel.cs index 59c5fd8d25..49dea7ed69 100644 --- a/src/modules/colorPicker/ColorPickerUI/ViewModelContracts/IMainViewModel.cs +++ b/src/modules/colorPicker/ColorPickerUI/ViewModelContracts/IMainViewModel.cs @@ -27,5 +27,7 @@ namespace ColorPicker.ViewModelContracts /// Gets a value indicating whether gets the show color name /// bool ShowColorName { get; } + + void RegisterWindowHandle(System.Windows.Interop.HwndSource hwndSource); } } diff --git a/src/modules/colorPicker/ColorPickerUI/ViewModels/MainViewModel.cs b/src/modules/colorPicker/ColorPickerUI/ViewModels/MainViewModel.cs index baf35baa17..ceeab364e6 100644 --- a/src/modules/colorPicker/ColorPickerUI/ViewModels/MainViewModel.cs +++ b/src/modules/colorPicker/ColorPickerUI/ViewModels/MainViewModel.cs @@ -65,7 +65,15 @@ namespace ColorPicker.ViewModels } _userSettings.ShowColorName.PropertyChanged += (s, e) => { OnPropertyChanged(nameof(ShowColorName)); }; - keyboardMonitor?.Start(); + + // Only start a local keyboard low level hook if running as a standalone. + // Otherwise, the global keyboard hook from runner will be used to activate Color Picker through ShowColorPickerSharedEvent + // and the Escape key will be registered as a shortcut by appStateHandler when ColorPicker is being used. + // This is much lighter than using a local low level keyboard hook. + if ((System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys()) + { + keyboardMonitor?.Start(); + } } /// @@ -163,5 +171,10 @@ namespace ColorPicker.ViewModels /// The new values for the zoom private void MouseInfoProvider_OnMouseWheel(object sender, Tuple e) => _zoomWindowHelper.Zoom(e.Item1, e.Item2); + + public void RegisterWindowHandle(System.Windows.Interop.HwndSource hwndSource) + { + _appStateHandler.RegisterWindowHandle(hwndSource); + } } }