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
///