Refactor plugin hotkey and action keyword management

Refactored hotkey management to improve thread safety by replacing `Dictionary` with `ConcurrentDictionary` and introducing a lock for updates. Added new methods for hotkey initialization, validation, and translation updates. Enhanced plugin initialization by integrating hotkey checks and removing redundant calls.

Refactored action keyword management with new methods for adding and removing keywords, ensuring thread safety with `TryAdd`, `AddOrUpdate`, and `TryRemove`. Introduced the `PluginHotkeyChangedEvent` class to encapsulate hotkey change events.

Improved thread safety and performance by using thread-safe collections and LINQ. Cleaned up code by removing deprecated methods, redundant comments, and outdated TODOs. Fixed concurrency issues and ensured consistent updates to plugin metadata and hotkey information.
This commit is contained in:
Jack251970 2025-10-16 20:30:17 +08:00
parent 8e71130831
commit 79b0385336
2 changed files with 179 additions and 164 deletions

View file

@ -44,9 +44,9 @@ namespace Flow.Launcher.Core.Plugin
private static readonly ConcurrentBag<PluginPair> _hotkeyPlugins = [];
private static readonly ConcurrentBag<PluginPair> _externalPreviewPlugins = [];
// TODO
private static readonly Dictionary<PluginPair, List<BasePluginHotkey>> _pluginHotkeyInfo = new();
private static readonly Dictionary<HotkeyModel, List<(PluginMetadata, SearchWindowPluginHotkey)>> _windowPluginHotkeys = new();
private static readonly Lock _pluginHotkeyInfoUpdateLock = new();
private static readonly ConcurrentDictionary<PluginPair, List<BasePluginHotkey>> _pluginHotkeyInfo = [];
private static readonly ConcurrentDictionary<HotkeyModel, List<(PluginMetadata, SearchWindowPluginHotkey)>> _windowPluginHotkeys = [];
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
@ -311,13 +311,11 @@ namespace Flow.Launcher.Core.Plugin
// Add plugin to Dialog Jump plugin list after the plugin is initialized
DialogJump.InitializeDialogJumpPlugin(pair);
// Check and initialize plugin hotkeys after the plugin is initialized
CheckPluginHotkeys(pair);
// Add plugin to lists after the plugin is initialized
AddPluginToLists(pair);
// TODO
InitializePluginHotkeyInfo();
Settings.UpdatePluginHotkeyInfo(GetPluginHotkeyInfo());
InitializeWindowPluginHotkeys();
}));
await Task.WhenAll(initTasks);
@ -373,6 +371,20 @@ namespace Flow.Launcher.Core.Plugin
_allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
}
private static void CheckPluginHotkeys(PluginPair pair)
{
if (pair.Plugin is IPluginHotkey)
{
InitializePluginHotkeyInfo(pair);
// Since settings cannot be changed concurrently, we must use a lock here
lock (_pluginHotkeyInfoUpdateLock)
{
Settings.UpdatePluginHotkeyInfo(GetPluginHotkeyInfo(pair.Metadata.ID));
}
InitializeWindowPluginHotkey(pair);
}
}
#endregion
#region Validate & Query Plugins
@ -600,6 +612,11 @@ namespace Flow.Launcher.Core.Plugin
return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
public static List<PluginPair> GetHotkeyPlugins()
{
return [.. _hotkeyPlugins.Where(p => !PluginModified(p.Metadata.ID))];
}
#endregion
#region Update Metadata & Get Plugin
@ -636,11 +653,6 @@ namespace Flow.Launcher.Core.Plugin
#region Get Context Menus
public static IList<PluginPair> GetHotkeyPlugins()
{
return _hotkeyPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList();
}
public static List<Result> GetContextMenusForPlugin(Result result)
{
var results = new List<Result>();
@ -718,131 +730,6 @@ namespace Flow.Launcher.Core.Plugin
}
}
public static IDictionary<PluginPair, List<BasePluginHotkey>> GetPluginHotkeyInfo()
{
return _pluginHotkeyInfo.Where(p => !PluginModified(p.Key.Metadata.ID))
.ToDictionary(p => p.Key, p => p.Value);
}
public static IDictionary<HotkeyModel, List<(PluginMetadata Metadata, SearchWindowPluginHotkey SearchWindowPluginHotkey)>> GetWindowPluginHotkeys()
{
// 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);
}
public static void UpdatePluginHotkeyInfoTranslations()
{
foreach (var plugin in GetHotkeyPlugins())
{
var newHotkeys = ((IPluginHotkey)plugin.Plugin).GetPluginHotkeys();
if (_pluginHotkeyInfo.TryGetValue(plugin, out var oldHotkeys))
{
foreach (var newHotkey in newHotkeys)
{
if (oldHotkeys.FirstOrDefault(h => h.Id == newHotkey.Id) is BasePluginHotkey pluginHotkey)
{
pluginHotkey.Name = newHotkey.Name;
pluginHotkey.Description = newHotkey.Description;
}
else
{
oldHotkeys.Add(newHotkey);
}
}
}
else
{
_pluginHotkeyInfo.Add(plugin, newHotkeys);
}
}
}
private static void InitializePluginHotkeyInfo()
{
foreach (var plugin in GetHotkeyPlugins())
{
var hotkeys = ((IPluginHotkey)plugin.Plugin).GetPluginHotkeys();
_pluginHotkeyInfo.Add(plugin, hotkeys);
}
}
private static void InitializeWindowPluginHotkeys()
{
foreach (var info in GetPluginHotkeyInfo())
{
var pluginPair = info.Key;
var hotkeyInfo = info.Value;
var metadata = pluginPair.Metadata;
foreach (var hotkey in hotkeyInfo)
{
if (hotkey.HotkeyType == HotkeyType.SearchWindow && hotkey is SearchWindowPluginHotkey searchWindowHotkey)
{
var hotkeySetting = metadata.PluginHotkeys.Find(h => h.Id == hotkey.Id)?.Hotkey ?? hotkey.DefaultHotkey;
var hotkeyModel = new HotkeyModel(hotkeySetting);
if (!_windowPluginHotkeys.TryGetValue(hotkeyModel, out var list))
{
list = new List<(PluginMetadata, SearchWindowPluginHotkey)>();
_windowPluginHotkeys[hotkeyModel] = list;
}
list.Add((pluginPair.Metadata, searchWindowHotkey));
}
}
}
}
public static void ChangePluginHotkey(PluginMetadata plugin, GlobalPluginHotkey pluginHotkey, HotkeyModel newHotkey)
{
var oldHotkeyItem = plugin.PluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var settingHotkeyItem = Settings.GetPluginSettings(plugin.ID).pluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var oldHotkeyStr = settingHotkeyItem.Hotkey;
var oldHotkey = new HotkeyModel(oldHotkeyStr);
var newHotkeyStr = newHotkey.ToString();
// Update hotkey in plugin metadata & setting
oldHotkeyItem.Hotkey = newHotkeyStr;
settingHotkeyItem.Hotkey = newHotkeyStr;
PluginHotkeyChanged?.Invoke(new PluginHotkeyChangedEvent(oldHotkey, newHotkey, plugin, pluginHotkey));
}
public static void ChangePluginHotkey(PluginMetadata plugin, SearchWindowPluginHotkey pluginHotkey, HotkeyModel newHotkey)
{
var oldHotkeyItem = plugin.PluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var settingHotkeyItem = Settings.GetPluginSettings(plugin.ID).pluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var oldHotkeyStr = settingHotkeyItem.Hotkey;
var converter = new KeyGestureConverter();
var oldHotkey = new HotkeyModel(oldHotkeyStr);
var newHotkeyStr = newHotkey.ToString();
// Update hotkey in plugin metadata & setting
oldHotkeyItem.Hotkey = newHotkeyStr;
settingHotkeyItem.Hotkey = newHotkeyStr;
// 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.Remove(oldHotkey);
}
if (_windowPluginHotkeys.TryGetValue(newHotkey, out var newHotkeyModels))
{
var newList = newHotkeyModels.ToList();
newList.Add((plugin, pluginHotkey));
_windowPluginHotkeys[newHotkey] = newList;
}
else
{
_windowPluginHotkeys[newHotkey] = new List<(PluginMetadata, SearchWindowPluginHotkey)>()
{
(plugin, pluginHotkey)
};
}
PluginHotkeyChanged?.Invoke(new PluginHotkeyChangedEvent(oldHotkey, newHotkey, plugin, pluginHotkey));
}
public static bool IsInitializationFailed(string id)
{
// Id does not exist in loaded plugins
@ -935,6 +822,156 @@ namespace Flow.Launcher.Core.Plugin
#endregion
#region Plugin Hotkey
private static void InitializePluginHotkeyInfo(PluginPair pair)
{
var plugin = (IPluginHotkey)pair.Plugin;
var hotkeys = plugin.GetPluginHotkeys();
_pluginHotkeyInfo.TryAdd(pair, hotkeys);
}
private static void InitializeWindowPluginHotkey(PluginPair pair)
{
foreach (var info in GetPluginHotkeyInfo(pair.Metadata.ID))
{
var pluginPair = info.Key;
var hotkeyInfo = info.Value;
var metadata = pluginPair.Metadata;
foreach (var hotkey in hotkeyInfo)
{
if (hotkey.HotkeyType == HotkeyType.SearchWindow && hotkey is SearchWindowPluginHotkey searchWindowHotkey)
{
var hotkeySetting = metadata.PluginHotkeys.Find(h => h.Id == hotkey.Id)?.Hotkey ?? hotkey.DefaultHotkey;
var hotkeyModel = new HotkeyModel(hotkeySetting);
if (!_windowPluginHotkeys.TryGetValue(hotkeyModel, out var list))
{
list = [];
_windowPluginHotkeys[hotkeyModel] = list;
}
list.Add((pluginPair.Metadata, searchWindowHotkey));
}
}
}
}
public static Dictionary<PluginPair, List<BasePluginHotkey>> GetPluginHotkeyInfo(string id = null)
{
if (id == null)
{
// Return all plugin hotkey info except those from modified plugins
return _pluginHotkeyInfo.Where(p => !PluginModified(p.Key.Metadata.ID))
.ToDictionary(p => p.Key, p => p.Value);
}
else
{
// Return plugin hotkey info for specified plugin id
return _pluginHotkeyInfo.Where(p => p.Key.Metadata.ID == id)
.ToDictionary(p => p.Key, p => p.Value);
}
}
public static Dictionary<HotkeyModel, List<(PluginMetadata Metadata, SearchWindowPluginHotkey SearchWindowPluginHotkey)>> GetWindowPluginHotkeys()
{
// 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);
}
public static void UpdatePluginHotkeyInfoTranslations(PluginPair pair)
{
var newHotkeys = ((IPluginHotkey)pair.Plugin).GetPluginHotkeys();
if (_pluginHotkeyInfo.TryGetValue(pair, out var oldHotkeys))
{
foreach (var newHotkey in newHotkeys)
{
if (oldHotkeys.FirstOrDefault(h => h.Id == newHotkey.Id) is BasePluginHotkey pluginHotkey)
{
pluginHotkey.Name = newHotkey.Name;
pluginHotkey.Description = newHotkey.Description;
}
else
{
oldHotkeys.Add(newHotkey);
}
}
}
else
{
_pluginHotkeyInfo.TryAdd(pair, newHotkeys);
}
}
public static void ChangePluginHotkey(PluginMetadata plugin, GlobalPluginHotkey pluginHotkey, HotkeyModel newHotkey)
{
var oldHotkeyItem = plugin.PluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var settingHotkeyItem = Settings.GetPluginSettings(plugin.ID).pluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var oldHotkeyStr = settingHotkeyItem.Hotkey;
var oldHotkey = new HotkeyModel(oldHotkeyStr);
var newHotkeyStr = newHotkey.ToString();
// Update hotkey in plugin metadata & setting
oldHotkeyItem.Hotkey = newHotkeyStr;
settingHotkeyItem.Hotkey = newHotkeyStr;
PluginHotkeyChanged?.Invoke(new PluginHotkeyChangedEvent(oldHotkey, newHotkey, plugin, pluginHotkey));
}
public static void ChangePluginHotkey(PluginMetadata plugin, SearchWindowPluginHotkey pluginHotkey, HotkeyModel newHotkey)
{
var oldHotkeyItem = plugin.PluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var settingHotkeyItem = Settings.GetPluginSettings(plugin.ID).pluginHotkeys.First(h => h.Id == pluginHotkey.Id);
var oldHotkeyStr = settingHotkeyItem.Hotkey;
var converter = new KeyGestureConverter();
var oldHotkey = new HotkeyModel(oldHotkeyStr);
var newHotkeyStr = newHotkey.ToString();
// Update hotkey in plugin metadata & setting
oldHotkeyItem.Hotkey = newHotkeyStr;
settingHotkeyItem.Hotkey = newHotkeyStr;
// 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.TryRemove(oldHotkey, out var _);
}
if (_windowPluginHotkeys.TryGetValue(newHotkey, out var newHotkeyModels))
{
var newList = newHotkeyModels.ToList();
newList.Add((plugin, pluginHotkey));
_windowPluginHotkeys[newHotkey] = newList;
}
else
{
_windowPluginHotkeys[newHotkey] =
[
(plugin, pluginHotkey)
];
}
PluginHotkeyChanged?.Invoke(new PluginHotkeyChangedEvent(oldHotkey, newHotkey, plugin, pluginHotkey));
}
#region Class
public class PluginHotkeyChangedEvent(HotkeyModel oldHotkey, HotkeyModel newHotkey,
PluginMetadata metadata, BasePluginHotkey pluginHotkey)
{
public HotkeyModel NewHotkey { get; } = newHotkey;
public HotkeyModel OldHotkey { get; } = oldHotkey;
public PluginMetadata Metadata { get; } = metadata;
public BasePluginHotkey PluginHotkey { get; } = pluginHotkey;
}
#endregion
#endregion
#region Plugin Install & Uninstall & Update
#region Private Functions
@ -1184,27 +1221,6 @@ namespace Flow.Launcher.Core.Plugin
#endregion
#region Class
public class PluginHotkeyChangedEvent
{
public HotkeyModel NewHotkey { get; }
public HotkeyModel OldHotkey { get; }
public PluginMetadata Metadata { get; }
public BasePluginHotkey PluginHotkey { get; }
public PluginHotkeyChangedEvent(HotkeyModel oldHotkey, HotkeyModel newHotkey, PluginMetadata metadata, BasePluginHotkey pluginHotkey)
{
OldHotkey = oldHotkey;
NewHotkey = newHotkey;
Metadata = metadata;
PluginHotkey = pluginHotkey;
}
}
#endregion
}
}

View file

@ -360,7 +360,7 @@ namespace Flow.Launcher.Core.Resource
public static void UpdatePluginMetadataTranslations()
{
// Update plugin metadata name & description
// Update plugin metadata name & description & plugin hotkey name & description
foreach (var p in PluginManager.GetTranslationPlugins())
{
if (p.Plugin is not IPluginI18n pluginI18N) return;
@ -368,6 +368,7 @@ namespace Flow.Launcher.Core.Resource
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
PluginManager.UpdatePluginHotkeyInfoTranslations(p);
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
}
catch (Exception e)
@ -375,19 +376,17 @@ namespace Flow.Launcher.Core.Resource
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
}
}
// Update plugin hotkey name & description
PluginManager.UpdatePluginHotkeyInfoTranslations();
}
public static void UpdatePluginMetadataTranslation(PluginPair p)
{
// Update plugin metadata name & description
// Update plugin metadata name & description & plugin hotkey name & description
if (p.Plugin is not IPluginI18n pluginI18N) return;
try
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
PluginManager.UpdatePluginHotkeyInfoTranslations(p);
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
}
catch (Exception e)