mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-01-19 15:03:36 +08:00
[ColorPicker][Accessibility] adding feature control ColorPicker with keyboard (#32371)
* [ColorPicker] Accessibility: adding feature move ColorPicker with arrow keys * Removing shortcut for Esc, catching Esc, Enter and Space with low level keyboard hook. Starting and disposing the hook only when ColorPicker is active. * Remove code which allows disposing the keyboard hook only once
This commit is contained in:
parent
b9da1e6abf
commit
f9d16fdde2
@ -9,8 +9,8 @@ using System.Windows.Interop;
|
|||||||
using ColorPicker.Settings;
|
using ColorPicker.Settings;
|
||||||
using ColorPicker.ViewModelContracts;
|
using ColorPicker.ViewModelContracts;
|
||||||
using Common.UI;
|
using Common.UI;
|
||||||
using ManagedCommon;
|
|
||||||
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
|
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
|
||||||
|
using static ColorPicker.Helpers.NativeMethodsHelper;
|
||||||
|
|
||||||
namespace ColorPicker.Helpers
|
namespace ColorPicker.Helpers
|
||||||
{
|
{
|
||||||
@ -43,6 +43,12 @@ namespace ColorPicker.Helpers
|
|||||||
|
|
||||||
public event EventHandler AppClosed;
|
public event EventHandler AppClosed;
|
||||||
|
|
||||||
|
public event EventHandler EnterPressed;
|
||||||
|
|
||||||
|
public event EventHandler UserSessionStarted;
|
||||||
|
|
||||||
|
public event EventHandler UserSessionEnded;
|
||||||
|
|
||||||
public void StartUserSession()
|
public void StartUserSession()
|
||||||
{
|
{
|
||||||
EndUserSession(); // Ends current user session if there's an active one.
|
EndUserSession(); // Ends current user session if there's an active one.
|
||||||
@ -62,10 +68,9 @@ namespace ColorPicker.Helpers
|
|||||||
ShowColorPicker();
|
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())
|
if (!(System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys())
|
||||||
{
|
{
|
||||||
SetupEscapeGlobalKeyShortcut();
|
UserSessionStarted?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,10 +90,9 @@ namespace ColorPicker.Helpers
|
|||||||
HideColorPicker();
|
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())
|
if (!(System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys())
|
||||||
{
|
{
|
||||||
ClearEscapeGlobalKeyShortcut();
|
UserSessionEnded?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionEventHelper.End();
|
SessionEventHelper.End();
|
||||||
@ -213,62 +217,36 @@ namespace ColorPicker.Helpers
|
|||||||
_hwndSource = hwndSource;
|
_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:
|
return false;
|
||||||
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 IntPtr.Zero;
|
EnterPressed?.Invoke(this, EventArgs.Empty);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetupEscapeGlobalKeyShortcut()
|
public bool HandleEscPressed()
|
||||||
{
|
{
|
||||||
if (_hwndSource == null)
|
if (!BlockEscapeKeyClosingColorPickerEditor)
|
||||||
{
|
{
|
||||||
return;
|
return EndUserSession();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
_hwndSource.AddHook(ProcessWindowMessages);
|
|
||||||
if (!NativeMethods.RegisterHotKey(_hwndSource.Handle, _globalHotKeyId, NativeMethods.MOD_NOREPEAT, NativeMethods.VK_ESCAPE))
|
|
||||||
{
|
{
|
||||||
Logger.LogWarning("Couldn't register the hotkey for Esc.");
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearEscapeGlobalKeyShortcut()
|
internal void MoveCursor(int xOffset, int yOffset)
|
||||||
{
|
{
|
||||||
if (_hwndSource == null)
|
POINT lpPoint;
|
||||||
{
|
GetCursorPos(out lpPoint);
|
||||||
return;
|
lpPoint.X += xOffset;
|
||||||
}
|
lpPoint.Y += yOffset;
|
||||||
|
SetCursorPos(lpPoint.X, lpPoint.Y);
|
||||||
if (!NativeMethods.UnregisterHotKey(_hwndSource.Handle, _globalHotKeyId))
|
|
||||||
{
|
|
||||||
Logger.LogWarning("Couldn't unregister the hotkey for Esc.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_hwndSource.RemoveHook(ProcessWindowMessages);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Struct representing a point.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
@ -22,8 +22,9 @@ namespace ColorPicker.Keyboard
|
|||||||
|
|
||||||
private List<string> _activationKeys = new List<string>();
|
private List<string> _activationKeys = new List<string>();
|
||||||
private GlobalKeyboardHook _keyboardHook;
|
private GlobalKeyboardHook _keyboardHook;
|
||||||
private bool disposedValue;
|
|
||||||
private bool _activationShortcutPressed;
|
private bool _activationShortcutPressed;
|
||||||
|
private int keyboardMoveSpeed;
|
||||||
|
private Key lastArrowKeyPressed = Key.None;
|
||||||
|
|
||||||
[ImportingConstructor]
|
[ImportingConstructor]
|
||||||
public KeyboardMonitor(AppStateHandler appStateHandler, IUserSettings userSettings)
|
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);
|
var name = Helper.GetKeyName((uint)virtualCode);
|
||||||
|
|
||||||
// If the last key pressed is a modifier key, then currentlyPressedKeys cannot possibly match with _activationKeys
|
// 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<string> first, List<string> second)
|
private static bool ArraysAreSame(List<string> first, List<string> second)
|
||||||
{
|
{
|
||||||
if (first.Count != second.Count || (first.Count == 0 && second.Count == 0))
|
if (first.Count != second.Count || (first.Count == 0 && second.Count == 0))
|
||||||
@ -158,15 +221,7 @@ namespace ColorPicker.Keyboard
|
|||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!disposedValue)
|
_keyboardHook?.Dispose();
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_keyboardHook?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
@ -25,6 +25,7 @@ namespace ColorPicker.ViewModels
|
|||||||
private readonly ZoomWindowHelper _zoomWindowHelper;
|
private readonly ZoomWindowHelper _zoomWindowHelper;
|
||||||
private readonly AppStateHandler _appStateHandler;
|
private readonly AppStateHandler _appStateHandler;
|
||||||
private readonly IUserSettings _userSettings;
|
private readonly IUserSettings _userSettings;
|
||||||
|
private KeyboardMonitor _keyboardMonitor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Backing field for <see cref="OtherColor"/>
|
/// Backing field for <see cref="OtherColor"/>
|
||||||
@ -53,6 +54,7 @@ namespace ColorPicker.ViewModels
|
|||||||
_zoomWindowHelper = zoomWindowHelper;
|
_zoomWindowHelper = zoomWindowHelper;
|
||||||
_appStateHandler = appStateHandler;
|
_appStateHandler = appStateHandler;
|
||||||
_userSettings = userSettings;
|
_userSettings = userSettings;
|
||||||
|
_keyboardMonitor = keyboardMonitor;
|
||||||
|
|
||||||
NativeEventWaiter.WaitForEventLoop(
|
NativeEventWaiter.WaitForEventLoop(
|
||||||
Constants.ShowColorPickerSharedEvent(),
|
Constants.ShowColorPickerSharedEvent(),
|
||||||
@ -77,16 +79,36 @@ namespace ColorPicker.ViewModels
|
|||||||
|
|
||||||
_userSettings.ShowColorName.PropertyChanged += (s, e) => { OnPropertyChanged(nameof(ShowColorName)); };
|
_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.
|
// 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
|
// 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.
|
// The appStateHandler starts and disposes a low level hook when ColorPicker is being used.
|
||||||
// This is much lighter than using a local low level keyboard hook.
|
// 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())
|
if ((System.Windows.Application.Current as ColorPickerUI.App).IsRunningDetachedFromPowerToys())
|
||||||
{
|
{
|
||||||
keyboardMonitor?.Start();
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current selected color as a <see cref="Brush"/>
|
/// Gets the current selected color as a <see cref="Brush"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
Loading…
Reference in New Issue
Block a user