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.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())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnterPressed?.Invoke(this, EventArgs.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool HandleEscPressed()
|
||||
{
|
||||
case NativeMethods.WM_HOTKEY:
|
||||
if (!BlockEscapeKeyClosingColorPickerEditor)
|
||||
{
|
||||
handled = EndUserSession();
|
||||
return 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));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
public void SetupEscapeGlobalKeyShortcut()
|
||||
internal void MoveCursor(int xOffset, int yOffset)
|
||||
{
|
||||
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);
|
||||
POINT lpPoint;
|
||||
GetCursorPos(out lpPoint);
|
||||
lpPoint.X += xOffset;
|
||||
lpPoint.Y += yOffset;
|
||||
SetCursorPos(lpPoint.X, lpPoint.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 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<string> first, List<string> second)
|
||||
{
|
||||
if (first.Count != second.Count || (first.Count == 0 && second.Count == 0))
|
||||
@ -157,18 +220,10 @@ namespace ColorPicker.Keyboard
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_keyboardHook?.Dispose();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
|
@ -25,6 +25,7 @@ namespace ColorPicker.ViewModels
|
||||
private readonly ZoomWindowHelper _zoomWindowHelper;
|
||||
private readonly AppStateHandler _appStateHandler;
|
||||
private readonly IUserSettings _userSettings;
|
||||
private KeyboardMonitor _keyboardMonitor;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="OtherColor"/>
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current selected color as a <see cref="Brush"/>
|
||||
/// </summary>
|
||||
|
Loading…
Reference in New Issue
Block a user