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;