[ColorPicker]Use Hotkey when started from runner (#17265)

* [ColorPicker]Use Hotkey when started from runner

* fix spellchecker
This commit is contained in:
Jaime Bernardo 2022-03-25 13:40:33 +00:00 committed by GitHub
parent 80e9fc0c43
commit 1be880438a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 160 additions and 32 deletions

View File

@ -1938,6 +1938,7 @@ Subdir
subfolder
subkey
SUBLANG
submenu
subquery
substr
Sul

View File

@ -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);
}
}
}

View File

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

View File

@ -14,6 +14,7 @@
Background="Transparent"
SizeToContent="WidthAndHeight"
AllowsTransparency="True"
SourceInitialized="MainWindowSourceInitialized"
AutomationProperties.Name="Color Picker">
<e:Interaction.Behaviors>
<behaviors:ChangeWindowPositionBehavior/>

View File

@ -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));
}
}
}

View File

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

View File

@ -27,5 +27,7 @@ namespace ColorPicker.ViewModelContracts
/// Gets a value indicating whether gets the show color name
/// </summary>
bool ShowColorName { get; }
void RegisterWindowHandle(System.Windows.Interop.HwndSource hwndSource);
}
}

View File

@ -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();
}
}
/// <summary>
@ -163,5 +171,10 @@ namespace ColorPicker.ViewModels
/// <param name="e">The new values for the zoom</param>
private void MouseInfoProvider_OnMouseWheel(object sender, Tuple<Point, bool> e)
=> _zoomWindowHelper.Zoom(e.Item1, e.Item2);
public void RegisterWindowHandle(System.Windows.Interop.HwndSource hwndSource)
{
_appStateHandler.RegisterWindowHandle(hwndSource);
}
}
}