diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 576bf6f2f13..4f1c17149c7 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -181,7 +181,7 @@ - + diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index 0a28264849a..bf29562c1d1 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; +using System.Windows.Input; using ChefKeys; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; using NHotkey; using NHotkey.Wpf; @@ -16,6 +19,7 @@ internal static class HotKeyMapper private static Settings _settings; private static MainViewModel _mainViewModel; + private static readonly Dictionary> _winComboCallbacks = new(); internal static void Initialize() { @@ -82,6 +86,14 @@ internal static void SetHotkey(HotkeyModel hotkey, EventHandler } catch (Exception e) { + if (hotkey.Win && hotkey.CharKey != Key.None) + { + App.API.LogDebug(ClassName, + $"|HotkeyMapper.SetHotkey|RegisterHotKey failed for {hotkeyStr} ({e.Message}); falling back to global keyboard callback."); + SetWithGlobalCallback(hotkey, action); + return; + } + App.API.LogError(ClassName, string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}", e.Message, @@ -93,6 +105,47 @@ internal static void SetHotkey(HotkeyModel hotkey, EventHandler } } + private static void SetWithGlobalCallback(HotkeyModel hotkey, EventHandler action) + { + string hotkeyStr = hotkey.ToString(); + if (_winComboCallbacks.TryGetValue(hotkeyStr, out var existing)) + { + App.API.RemoveGlobalKeyboardCallback(existing); + _winComboCallbacks.Remove(hotkeyStr); + } + + int expectedVkCode = KeyInterop.VirtualKeyFromKey(hotkey.CharKey); + bool needCtrl = hotkey.Ctrl; + bool needAlt = hotkey.Alt; + bool needShift = hotkey.Shift; + bool keyCurrentlyDown = false; + + Func callback = (keyEvent, vkCode, state) => + { + bool isMatch = vkCode == expectedVkCode + && state.WinPressed + && state.CtrlPressed == needCtrl + && state.AltPressed == needAlt + && state.ShiftPressed == needShift; + + if (isMatch && (keyEvent == (int)KeyEvent.WM_KEYDOWN || keyEvent == (int)KeyEvent.WM_SYSKEYDOWN) && !keyCurrentlyDown) + { + keyCurrentlyDown = true; + action?.Invoke(null, null); + return false; + } + if (isMatch && (keyEvent == (int)KeyEvent.WM_KEYUP || keyEvent == (int)KeyEvent.WM_SYSKEYUP)) + { + keyCurrentlyDown = false; + return false; + } + return true; + }; + + _winComboCallbacks[hotkeyStr] = callback; + App.API.RegisterGlobalKeyboardCallback(callback); + } + internal static void RemoveHotkey(string hotkeyStr) { try @@ -103,6 +156,13 @@ internal static void RemoveHotkey(string hotkeyStr) return; } + if (_winComboCallbacks.TryGetValue(hotkeyStr, out var callback)) + { + App.API.RemoveGlobalKeyboardCallback(callback); + _winComboCallbacks.Remove(hotkeyStr); + return; + } + if (!string.IsNullOrEmpty(hotkeyStr)) HotkeyManager.Current.Remove(hotkeyStr); } diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index b920b53a740..7b07674ea65 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -278,9 +278,10 @@ private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true) { bool hotkeyAvailable; // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157 - if (keyModel.ToString() == "LWin" || keyModel.ToString() == "RWin") + if (keyModel.ToString() == "LWin" || keyModel.ToString() == "RWin" + || (Type == HotkeyType.Hotkey && keyModel.Win && keyModel.CharKey != Key.None)) { - hotkeyAvailable = true; + hotkeyAvailable = keyModel.Validate(ValidateKeyGesture); } else { diff --git a/Flow.Launcher/HotkeyControlDialog.xaml.cs b/Flow.Launcher/HotkeyControlDialog.xaml.cs index e1fc86f9541..60c06d38eea 100644 --- a/Flow.Launcher/HotkeyControlDialog.xaml.cs +++ b/Flow.Launcher/HotkeyControlDialog.xaml.cs @@ -35,7 +35,8 @@ public enum EResultType public string ResultValue { get; private set; } = string.Empty; public static string EmptyHotkey => Localize.none(); - private static bool isOpenFlowHotkey; + private bool isOpenFlowHotkey; + private Func? _winComboInterceptor; public HotkeyControlDialog(string hotkey, string defaultHotkey, string windowTitle = "") { @@ -52,11 +53,45 @@ public HotkeyControlDialog(string hotkey, string defaultHotkey, string windowTit // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157 isOpenFlowHotkey = _hotkeySettings.RegisteredHotkeys - .Any(x => x.DescriptionResourceKey == "flowlauncherHotkey" + .Any(x => x.DescriptionResourceKey == "flowlauncherHotkey" && x.Hotkey.ToString() == hotkey); ChefKeysManager.StartMenuEnableBlocking = true; ChefKeysManager.Start(); + + if (isOpenFlowHotkey) + { + _winComboInterceptor = (keyEvent, vkCode, state) => + { + const int VK_LWIN = 0x5B; + const int VK_RWIN = 0x5C; + if ((keyEvent == (int)KeyEvent.WM_KEYDOWN || keyEvent == (int)KeyEvent.WM_SYSKEYDOWN) + && state.WinPressed + && vkCode != VK_LWIN && vkCode != VK_RWIN) + { + var key = KeyInterop.KeyFromVirtualKey(vkCode); + if (key is Key.None + or Key.LeftCtrl or Key.RightCtrl + or Key.LeftAlt or Key.RightAlt + or Key.LeftShift or Key.RightShift + or Key.LWin or Key.RWin) + { + return false; + } + _ = App.Current.Dispatcher.InvokeAsync(() => + { + if (!IsLoaded) return; + var hotkeyModel = new HotkeyModel(state.AltPressed, state.ShiftPressed, state.WinPressed, state.CtrlPressed, key); + CurrentHotkey = hotkeyModel; + SetKeysToDisplay(CurrentHotkey); + }); + return false; + } + return true; + }; + App.API.RegisterGlobalKeyboardCallback(_winComboInterceptor); + this.Closed += (_, _) => UnregisterWinComboInterceptor(); + } } private void Reset(object sender, RoutedEventArgs routedEventArgs) @@ -70,10 +105,20 @@ private void Delete(object sender, RoutedEventArgs routedEventArgs) KeysToDisplay.Add(EmptyHotkey); } + private void UnregisterWinComboInterceptor() + { + if (_winComboInterceptor != null) + { + App.API.RemoveGlobalKeyboardCallback(_winComboInterceptor); + _winComboInterceptor = null; + } + } + private void Cancel(object sender, RoutedEventArgs routedEventArgs) { ChefKeysManager.StartMenuEnableBlocking = false; ChefKeysManager.Stop(); + UnregisterWinComboInterceptor(); ResultType = EResultType.Cancel; Hide(); @@ -83,6 +128,7 @@ private void Save(object sender, RoutedEventArgs routedEventArgs) { ChefKeysManager.StartMenuEnableBlocking = false; ChefKeysManager.Stop(); + UnregisterWinComboInterceptor(); if (KeysToDisplay.Count == 1 && KeysToDisplay[0] == EmptyHotkey) { @@ -182,10 +228,11 @@ private void SetKeysToDisplay(HotkeyModel? hotkey) } } - private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) + private bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) { - if (isOpenFlowHotkey && (hotkey.ToString() == "LWin" || hotkey.ToString() == "RWin")) - return true; + if (isOpenFlowHotkey && (hotkey.ToString() == "LWin" || hotkey.ToString() == "RWin" + || (hotkey.Win && hotkey.CharKey != Key.None))) + return hotkey.Validate(validateKeyGesture); return hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); } diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 5cefd2298cb..e7527d07817 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -538,12 +538,19 @@ public bool IsGameModeOn() } private readonly List> _globalKeyboardHandlers = new(); + private readonly object _globalKeyboardHandlersLock = new(); - public void RegisterGlobalKeyboardCallback(Func callback) => - _globalKeyboardHandlers.Add(callback); + public void RegisterGlobalKeyboardCallback(Func callback) + { + lock (_globalKeyboardHandlersLock) + _globalKeyboardHandlers.Add(callback); + } - public void RemoveGlobalKeyboardCallback(Func callback) => - _globalKeyboardHandlers.Remove(callback); + public void RemoveGlobalKeyboardCallback(Func callback) + { + lock (_globalKeyboardHandlersLock) + _globalKeyboardHandlers.Remove(callback); + } public void ReQuery(bool reselect = true) => _mainVM.ReQuery(reselect); @@ -657,10 +664,21 @@ public event ActualApplicationThemeChangedEventHandler ActualApplicationThemeCha private bool KListener_hookedKeyboardCallback(KeyEvent keyevent, int vkcode, SpecialKeyState state) { + Func[] snapshot; + lock (_globalKeyboardHandlersLock) + snapshot = _globalKeyboardHandlers.ToArray(); + var continueHook = true; - foreach (var x in _globalKeyboardHandlers) + foreach (var x in snapshot) { - continueHook &= x((int)keyevent, vkcode, state); + try + { + continueHook &= x((int)keyevent, vkcode, state); + } + catch (Exception e) + { + LogException(ClassName, "Global keyboard callback failed", e); + } } return continueHook;