diff --git a/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs b/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs index f52c717a10..db04f4c18d 100644 --- a/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs +++ b/src/modules/colorPicker/ColorPickerUI/Helpers/AppStateHandler.cs @@ -9,8 +9,8 @@ using System.Windows.Interop; using ColorPicker.Settings; using ColorPicker.ViewModelContracts; using Common.UI; -using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library.Enumerations; +using static ColorPicker.Helpers.NativeMethodsHelper; namespace ColorPicker.Helpers { @@ -43,6 +43,12 @@ namespace ColorPicker.Helpers public event EventHandler AppClosed; + public event EventHandler EnterPressed; + + public event EventHandler UserSessionStarted; + + public event EventHandler UserSessionEnded; + public void StartUserSession() { EndUserSession(); // Ends current user session if there's an active one. @@ -62,10 +68,9 @@ 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(); + UserSessionStarted?.Invoke(this, EventArgs.Empty); } } } @@ -85,10 +90,9 @@ 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(); + UserSessionEnded?.Invoke(this, EventArgs.Empty); } SessionEventHelper.End(); @@ -213,62 +217,36 @@ namespace ColorPicker.Helpers _hwndSource = hwndSource; } - public IntPtr ProcessWindowMessages(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) + public bool HandleEnterPressed() { - switch (msg) + if (!IsColorPickerVisible()) { - 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://learn.microsoft.com/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); - - // Need to make the value unchecked as a workaround for changes introduced in .NET 7 - // https://github.com/dotnet/roslyn/blob/main/docs/compilers/CSharp/Compiler%20Breaking%20Changes%20-%20DotNet%207.md#checked-operators-on-systemintptr-and-systemuintptr - handled &= NativeMethods.PostMessage(_hwndSource.Handle, NativeMethods.WM_KEYUP, (IntPtr)NativeMethods.VK_ESCAPE, unchecked((IntPtr)0xC0010001)); - } - - break; + return false; } - return IntPtr.Zero; + EnterPressed?.Invoke(this, EventArgs.Empty); + return true; } - public void SetupEscapeGlobalKeyShortcut() + public bool HandleEscPressed() { - if (_hwndSource == null) + if (!BlockEscapeKeyClosingColorPickerEditor) { - return; + return EndUserSession(); } - - _hwndSource.AddHook(ProcessWindowMessages); - if (!NativeMethods.RegisterHotKey(_hwndSource.Handle, _globalHotKeyId, NativeMethods.MOD_NOREPEAT, NativeMethods.VK_ESCAPE)) + else { - Logger.LogWarning("Couldn't register the hotkey for Esc."); + return false; } } - public void ClearEscapeGlobalKeyShortcut() + internal void MoveCursor(int xOffset, int yOffset) { - if (_hwndSource == null) - { - return; - } - - if (!NativeMethods.UnregisterHotKey(_hwndSource.Handle, _globalHotKeyId)) - { - Logger.LogWarning("Couldn't unregister the hotkey for Esc."); - } - - _hwndSource.RemoveHook(ProcessWindowMessages); + POINT lpPoint; + GetCursorPos(out lpPoint); + lpPoint.X += xOffset; + lpPoint.Y += yOffset; + SetCursorPos(lpPoint.X, lpPoint.Y); } } } diff --git a/src/modules/colorPicker/ColorPickerUI/Helpers/NativeMethodsHelper.cs b/src/modules/colorPicker/ColorPickerUI/Helpers/NativeMethodsHelper.cs new file mode 100644 index 0000000000..7f4f4dfd3f --- /dev/null +++ b/src/modules/colorPicker/ColorPickerUI/Helpers/NativeMethodsHelper.cs @@ -0,0 +1,34 @@ +// 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.Runtime.InteropServices; +using System.Windows; + +namespace ColorPicker.Helpers +{ + internal class NativeMethodsHelper + { + /// + /// Struct representing a point. + /// + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + + public static implicit operator Point(POINT point) + { + return new Point(point.X, point.Y); + } + } + + [DllImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SetCursorPos(int x, int y); + + [DllImport("user32.dll")] + internal static extern bool GetCursorPos(out POINT lpPoint); + } +} diff --git a/src/modules/colorPicker/ColorPickerUI/Keyboard/KeyboardMonitor.cs b/src/modules/colorPicker/ColorPickerUI/Keyboard/KeyboardMonitor.cs index e42a957cd0..c04898cb72 100644 --- a/src/modules/colorPicker/ColorPickerUI/Keyboard/KeyboardMonitor.cs +++ b/src/modules/colorPicker/ColorPickerUI/Keyboard/KeyboardMonitor.cs @@ -22,8 +22,9 @@ namespace ColorPicker.Keyboard private List _activationKeys = new List(); private GlobalKeyboardHook _keyboardHook; - private bool disposedValue; private bool _activationShortcutPressed; + private int keyboardMoveSpeed; + private Key lastArrowKeyPressed = Key.None; [ImportingConstructor] public KeyboardMonitor(AppStateHandler appStateHandler, IUserSettings userSettings) @@ -80,6 +81,39 @@ namespace ColorPicker.Keyboard } } + if ((virtualCode == KeyInterop.VirtualKeyFromKey(Key.Space) || virtualCode == KeyInterop.VirtualKeyFromKey(Key.Enter)) && (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown)) + { + e.Handled = _appStateHandler.HandleEnterPressed(); + return; + } + + if (virtualCode == KeyInterop.VirtualKeyFromKey(Key.Back) && e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown) + { + e.Handled = _appStateHandler.HandleEscPressed(); + return; + } + + if (CheckMoveNeeded(virtualCode, Key.Up, e, 0, -1)) + { + e.Handled = true; + return; + } + else if (CheckMoveNeeded(virtualCode, Key.Down, e, 0, 1)) + { + e.Handled = true; + return; + } + else if (CheckMoveNeeded(virtualCode, Key.Left, e, -1, 0)) + { + e.Handled = true; + return; + } + else if (CheckMoveNeeded(virtualCode, Key.Right, e, 1, 0)) + { + e.Handled = true; + return; + } + var name = Helper.GetKeyName((uint)virtualCode); // If the last key pressed is a modifier key, then currentlyPressedKeys cannot possibly match with _activationKeys @@ -115,6 +149,35 @@ namespace ColorPicker.Keyboard } } + private bool CheckMoveNeeded(int virtualCode, Key key, GlobalKeyboardHookEventArgs e, int xMove, int yMove) + { + if (virtualCode == KeyInterop.VirtualKeyFromKey(key)) + { + if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown && _appStateHandler.IsColorPickerVisible()) + { + if (lastArrowKeyPressed == key) + { + keyboardMoveSpeed++; + } + else + { + keyboardMoveSpeed = 1; + } + + lastArrowKeyPressed = key; + _appStateHandler.MoveCursor(keyboardMoveSpeed * xMove, keyboardMoveSpeed * yMove); + return true; + } + else if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyUp) + { + lastArrowKeyPressed = Key.None; + keyboardMoveSpeed = 0; + } + } + + return false; + } + private static bool ArraysAreSame(List first, List second) { if (first.Count != second.Count || (first.Count == 0 && second.Count == 0)) @@ -158,15 +221,7 @@ namespace ColorPicker.Keyboard protected virtual void Dispose(bool disposing) { - if (!disposedValue) - { - if (disposing) - { - _keyboardHook?.Dispose(); - } - - disposedValue = true; - } + _keyboardHook?.Dispose(); } public void Dispose() diff --git a/src/modules/colorPicker/ColorPickerUI/ViewModels/MainViewModel.cs b/src/modules/colorPicker/ColorPickerUI/ViewModels/MainViewModel.cs index 87d69741ce..957f813aae 100644 --- a/src/modules/colorPicker/ColorPickerUI/ViewModels/MainViewModel.cs +++ b/src/modules/colorPicker/ColorPickerUI/ViewModels/MainViewModel.cs @@ -25,6 +25,7 @@ namespace ColorPicker.ViewModels private readonly ZoomWindowHelper _zoomWindowHelper; private readonly AppStateHandler _appStateHandler; private readonly IUserSettings _userSettings; + private KeyboardMonitor _keyboardMonitor; /// /// Backing field for @@ -53,6 +54,7 @@ namespace ColorPicker.ViewModels _zoomWindowHelper = zoomWindowHelper; _appStateHandler = appStateHandler; _userSettings = userSettings; + _keyboardMonitor = keyboardMonitor; NativeEventWaiter.WaitForEventLoop( Constants.ShowColorPickerSharedEvent(), @@ -77,16 +79,36 @@ namespace ColorPicker.ViewModels _userSettings.ShowColorName.PropertyChanged += (s, e) => { OnPropertyChanged(nameof(ShowColorName)); }; + _appStateHandler.EnterPressed += AppStateHandler_EnterPressed; + _appStateHandler.UserSessionStarted += AppStateHandler_UserSessionStarted; + _appStateHandler.UserSessionEnded += AppStateHandler_UserSessionEnded; + // 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. + // The appStateHandler starts and disposes a low level hook when ColorPicker is being used. + // The hook catches the Esc, Space, Enter and Arrow key presses. + // This is much lighter than using a permanent local low level keyboard hook. if ((System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys()) { keyboardMonitor?.Start(); } } + private void AppStateHandler_UserSessionEnded(object sender, EventArgs e) + { + _keyboardMonitor.Dispose(); + } + + private void AppStateHandler_UserSessionStarted(object sender, EventArgs e) + { + _keyboardMonitor?.Start(); + } + + private void AppStateHandler_EnterPressed(object sender, EventArgs e) + { + MouseInfoProvider_OnMouseDown(null, default(System.Drawing.Point)); + } + /// /// Gets the current selected color as a ///