From 811239b15815fe4a9920bae35ec1cd582e013f58 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 16 Oct 2025 22:07:13 +0800 Subject: [PATCH] Refactor hotkey handling and improve thread safety - Added `PluginHotkeyInitialized` event in `PluginManager` to notify when plugin hotkeys are initialized. - Replaced `List` with `ConcurrentBag` for `_windowPluginHotkeys` to ensure thread-safe operations. - Enhanced `GetWindowPluginHotkeys` to support filtering by plugin ID. - Updated `HotKeyMapper` to handle `PluginHotkeyInitialized` and register hotkeys dynamically. - Refactored `WindowPluginHotkeyPair` and `GetRegisteredHotkeyData` to use `ConcurrentBag`. - Improved debug logging for hotkey initialization. - Refactored initialization of ActionContext hotkeys using modern C# collection syntax. - General code cleanup for readability and maintainability. --- Flow.Launcher.Core/Plugin/PluginManager.cs | 34 +++++++++++--- Flow.Launcher/Helper/HotKeyMapper.cs | 53 +++++++++++++--------- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index d58cdb58b..360fc1986 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -34,6 +34,7 @@ namespace Flow.Launcher.Core.Plugin private static readonly ConcurrentDictionary _nonGlobalPlugins = []; public static event Action PluginHotkeyChanged; + public static event Action PluginHotkeyInitialized; private static PluginsSettings Settings; private static readonly ConcurrentBag ModifiedPlugins = []; @@ -46,7 +47,7 @@ namespace Flow.Launcher.Core.Plugin private static readonly Lock _pluginHotkeyInfoUpdateLock = new(); private static readonly ConcurrentDictionary> _pluginHotkeyInfo = []; - private static readonly ConcurrentDictionary> _windowPluginHotkeys = []; + private static readonly ConcurrentDictionary> _windowPluginHotkeys = []; /// /// Directories that will hold Flow Launcher plugin directory @@ -383,6 +384,7 @@ namespace Flow.Launcher.Core.Plugin } InitializeWindowPluginHotkey(pair); _hotkeyPlugins.Add(pair); + PluginHotkeyInitialized?.Invoke(pair); } } @@ -872,10 +874,30 @@ namespace Flow.Launcher.Core.Plugin } } - public static Dictionary> GetWindowPluginHotkeys() + public static Dictionary> GetWindowPluginHotkeys(string id = null) { // Here we do not need to check PluginModified since we will check it in hotkey events - return _windowPluginHotkeys.ToDictionary(p => p.Key, p => p.Value); + if (id == null) + { + // Return all window plugin hotkeys + return _windowPluginHotkeys.ToDictionary(p => p.Key, p => p.Value); + } + else + { + // Return window plugin hotkeys for specified plugin id + // If one HotkeyModel is already included by other plugins, it will be removed so that HotkeyMapper will not register this Window hotkey duplicately + var windowPluginHotkeys = new Dictionary>(); + foreach (var key in _windowPluginHotkeys) + { + // Check if all items in the list are from the specified plugin + if (key.Value.All(x => x.Item1.ID == id)) + { + // We must use the reference of this ConcurrentBag so that it can be updated in the next + windowPluginHotkeys[key.Key] = key.Value; + } + } + return windowPluginHotkeys; + } } public static void UpdatePluginHotkeyInfoTranslations(PluginPair pair) @@ -932,8 +954,8 @@ namespace Flow.Launcher.Core.Plugin // Update window plugin hotkey dictionary var oldHotkeyModels = _windowPluginHotkeys[oldHotkey]; - _windowPluginHotkeys[oldHotkey] = oldHotkeyModels.Where(x => x.Item1.ID != plugin.ID || x.Item2.Id != pluginHotkey.Id).ToList(); - if (_windowPluginHotkeys[oldHotkey].Count == 0) + _windowPluginHotkeys[oldHotkey] = [.. oldHotkeyModels.Where(x => x.Item1.ID != plugin.ID || x.Item2.Id != pluginHotkey.Id)]; + if (_windowPluginHotkeys[oldHotkey].IsEmpty) { _windowPluginHotkeys.TryRemove(oldHotkey, out var _); } @@ -942,7 +964,7 @@ namespace Flow.Launcher.Core.Plugin { var newList = newHotkeyModels.ToList(); newList.Add((plugin, pluginHotkey)); - _windowPluginHotkeys[newHotkey] = newList; + _windowPluginHotkeys[newHotkey] = [.. newList]; } else { diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index 28e4518ea..31ab33ceb 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; @@ -41,6 +42,7 @@ internal static class HotKeyMapper _settings.PropertyChanged += Settings_PropertyChanged; _settings.CustomPluginHotkeys.CollectionChanged += CustomPluginHotkeys_CollectionChanged; PluginManager.PluginHotkeyChanged += PluginManager_PluginHotkeyChanged; + PluginManager.PluginHotkeyInitialized += PluginManager_PluginHotkeyInitialized; } private static void InitializeRegisteredHotkeys() @@ -109,9 +111,27 @@ internal static class HotKeyMapper list.Add(GetRegisteredHotkeyData(customPluginHotkey)); } - // Plugin hotkeys + // Add registered hotkeys & Set them + foreach (var hotkey in list) + { + _settings.RegisteredHotkeys.Add(hotkey); + if (hotkey.RegisteredType == RegisteredHotkeyType.DialogJump && !_settings.EnableDialogJump) + { + // If dialog jump is disabled, do not register the hotkey + continue; + } + SetHotkey(hotkey); + } + + App.API.LogDebug(ClassName, $"Initialize {_settings.RegisteredHotkeys.Count} hotkeys:\n[\n\t{string.Join(",\n\t", _settings.RegisteredHotkeys)}\n]"); + } + + private static void PluginManager_PluginHotkeyInitialized(PluginPair pair) + { + var list = new List(); + // Global plugin hotkeys - var pluginHotkeyInfos = PluginManager.GetPluginHotkeyInfo(); + var pluginHotkeyInfos = PluginManager.GetPluginHotkeyInfo(pair.Metadata.ID); foreach (var info in pluginHotkeyInfos) { var pluginPair = info.Key; @@ -128,7 +148,7 @@ internal static class HotKeyMapper } // Window plugin hotkeys - var windowPluginHotkeys = PluginManager.GetWindowPluginHotkeys(); + var windowPluginHotkeys = PluginManager.GetWindowPluginHotkeys(pair.Metadata.ID); foreach (var hotkey in windowPluginHotkeys) { var hotkeyModel = hotkey.Key; @@ -140,15 +160,10 @@ internal static class HotKeyMapper foreach (var hotkey in list) { _settings.RegisteredHotkeys.Add(hotkey); - if (hotkey.RegisteredType == RegisteredHotkeyType.DialogJump && !_settings.EnableDialogJump) - { - // If dialog jump is disabled, do not register the hotkey - continue; - } SetHotkey(hotkey); } - App.API.LogDebug(ClassName, $"Initialize {_settings.RegisteredHotkeys.Count} hotkeys:\n[\n\t{string.Join(",\n\t", _settings.RegisteredHotkeys)}\n]"); + App.API.LogDebug(ClassName, $"Initialize {list.Count} hotkeys for {pair.Metadata.Name}:\n[\n\t{string.Join(",\n\t", list)}\n]"); } #endregion @@ -359,7 +374,7 @@ internal static class HotKeyMapper return new(RegisteredHotkeyType.PluginGlobalHotkey, HotkeyType.Global, hotkey, "pluginHotkey", GlobalPluginHotkeyCommand, new GlobalPluginHotkeyPair(metadata, pluginHotkey), removeHotkeyAction); } - private static RegisteredHotkeyData GetRegisteredHotkeyData(HotkeyModel hotkey, List<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> windowHotkeys) + private static RegisteredHotkeyData GetRegisteredHotkeyData(HotkeyModel hotkey, ConcurrentBag<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> windowHotkeys) { Action removeHotkeysAction = windowHotkeys.All(h => h.PluginHotkey.Editable) ? () => @@ -747,12 +762,12 @@ internal static class HotKeyMapper private static void InitializeActionContextHotkeys() { // Fixed hotkeys for ActionContext - _actionContextRegisteredHotkeys = new List - { + _actionContextRegisteredHotkeys = + [ new(RegisteredHotkeyType.CtrlShiftEnter, HotkeyType.SearchWindow, "Ctrl+Shift+Enter", nameof(Localize.HotkeyCtrlShiftEnterDesc), _mainViewModel.OpenResultCommand), new(RegisteredHotkeyType.CtrlEnter, HotkeyType.SearchWindow, "Ctrl+Enter", nameof(Localize.OpenContainFolderHotkey), _mainViewModel.OpenResultCommand), new(RegisteredHotkeyType.AltEnter, HotkeyType.SearchWindow, "Alt+Enter", nameof(Localize.HotkeyOpenResult), _mainViewModel.OpenResultCommand), - }; + ]; // Register ActionContext hotkeys and they will be cached and restored in _actionContextHotkeyEvents foreach (var hotkey in _actionContextRegisteredHotkeys) @@ -822,18 +837,12 @@ internal static class HotKeyMapper } } - private class WindowPluginHotkeyPair + private class WindowPluginHotkeyPair(HotkeyModel hotkey, ConcurrentBag<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> hotkeys) { [Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")] - public HotkeyModel Hotkey { get; } + public HotkeyModel Hotkey { get; } = hotkey; - public List<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> HotkeyModels { get; } - - public WindowPluginHotkeyPair(HotkeyModel hotkey, List<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> hotkeys) - { - Hotkey = hotkey; - HotkeyModels = hotkeys; - } + public ConcurrentBag<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> HotkeyModels { get; } = hotkeys; } #endregion