diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 444a44a0a81..6f8a55dc281 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -6,9 +6,11 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Windows.Input; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -28,6 +30,8 @@ public static class PluginManager public static readonly HashSet GlobalPlugins = new(); public static readonly Dictionary NonGlobalPlugins = new(); + public static Action PluginHotkeyChanged { get; set; } + // We should not initialize API in static constructor because it will create another API instance private static IPublicAPI api = null; private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); @@ -39,6 +43,10 @@ public static class PluginManager private static IEnumerable _homePlugins; private static IEnumerable _resultUpdatePlugin; private static IEnumerable _translationPlugins; + private static IEnumerable _hotkeyPlugins; + + private static readonly Dictionary> _pluginHotkeyInfo = new(); + private static readonly Dictionary> _windowPluginHotkeys = new(); /// /// Directories that will hold Flow Launcher plugin directory @@ -186,6 +194,7 @@ public static void LoadPlugins(PluginsSettings settings) _homePlugins = GetPluginsForInterface(); _resultUpdatePlugin = GetPluginsForInterface(); _translationPlugins = GetPluginsForInterface(); + _hotkeyPlugins = GetPluginsForInterface(); } private static void UpdatePluginDirectory(List metadatas) @@ -255,6 +264,10 @@ public static async Task InitializePluginsAsync() await Task.WhenAll(InitTasks); + InitializePluginHotkeyInfo(); + Settings.UpdatePluginHotkeyInfo(GetPluginHotkeyInfo()); + InitializeWindowPluginHotkeys(); + foreach (var plugin in AllPlugins) { // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin @@ -429,6 +442,11 @@ public static IList GetTranslationPlugins() return _translationPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); } + public static IList GetHotkeyPlugins() + { + return _hotkeyPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + } + public static List GetContextMenusForPlugin(Result result) { var results = new List(); @@ -463,6 +481,131 @@ public static bool IsHomePlugin(string id) return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id); } + public static IDictionary> GetPluginHotkeyInfo() + { + return _pluginHotkeyInfo.Where(p => !PluginModified(p.Key.Metadata.ID)) + .ToDictionary(p => p.Key, p => p.Value); + } + + public static IDictionary> 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 ActionKeywordRegistered(string actionKeyword) { // this method is only checking for action keywords (defined as not '*') registration @@ -732,5 +875,28 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool remo } #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 } } diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index 7b7d6eef661..0b173b69bf6 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -293,6 +293,9 @@ private void UpdatePluginMetadataTranslations() API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); } } + + // Update plugin hotkey name & description + PluginManager.UpdatePluginHotkeyInfoTranslations(); } private static string LanguageFile(string folder, string language) diff --git a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs index 25bc75a56c1..6d2abff4c15 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/HotkeyModel.cs @@ -49,6 +49,10 @@ public ModifierKeys ModifierKeys } } + public readonly bool IsEmpty => CharKey == Key.None && !Alt && !Shift && !Win && !Ctrl; + + public static HotkeyModel Empty => new(); + public HotkeyModel(string hotkeyString) { Parse(hotkeyString); @@ -117,12 +121,22 @@ private void Parse(string hotkeyString) } } - public override string ToString() + public bool Equals(HotkeyModel other) + { + return CharKey == other.CharKey && ModifierKeys == other.ModifierKeys; + } + + public KeyGesture ToKeyGesture() + { + return new KeyGesture(CharKey, ModifierKeys); + } + + public override readonly string ToString() { return string.Join(" + ", EnumerateDisplayKeys()); } - public IEnumerable EnumerateDisplayKeys() + public readonly IEnumerable EnumerateDisplayKeys() { if (Ctrl && CharKey is not (Key.LeftCtrl or Key.RightCtrl)) { diff --git a/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs b/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs index 448a70d191c..c20641d68e2 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/IHotkeySettings.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.ObjectModel; namespace Flow.Launcher.Infrastructure.Hotkey; @@ -13,5 +13,5 @@ public interface IHotkeySettings /// A list of hotkeys that have already been registered. The dialog will display these hotkeys and provide a way to /// unregister them. /// - public List RegisteredHotkeys { get; } + public ObservableCollection RegisteredHotkeys { get; } } diff --git a/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs b/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs index b6a10fc3075..befd49318b2 100644 --- a/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs +++ b/Flow.Launcher.Infrastructure/Hotkey/RegisteredHotkeyData.cs @@ -1,4 +1,6 @@ using System; +using System.Windows.Input; +using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.Hotkey; @@ -11,10 +13,20 @@ namespace Flow.Launcher.Infrastructure.Hotkey; /// public record RegisteredHotkeyData { + /// + /// Type of this hotkey in the context of the application. + /// + public RegisteredHotkeyType RegisteredType { get; } + + /// + /// Type of this hotkey. + /// + public HotkeyType Type { get; } + /// /// representation of this hotkey. /// - public HotkeyModel Hotkey { get; } + public HotkeyModel Hotkey { get; private set; } /// /// String key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, @@ -28,6 +40,16 @@ public record RegisteredHotkeyData /// public object?[] DescriptionFormatVariables { get; } = Array.Empty(); + /// + /// Command of this hotkey. If it's null, the hotkey is assumed to be registered by system. + /// + public ICommand? Command { get; } + + /// + /// Command parameter of this hotkey. + /// + public object? CommandParameter { get; } + /// /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that /// this hotkey can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. @@ -39,6 +61,12 @@ public record RegisteredHotkeyData /// descriptionResourceKey doesn't need any arguments for string.Format. If it does, /// use one of the other constructors. /// + /// + /// The type of this hotkey in the context of the application. + /// + /// + /// Whether this hotkey is global or search window specific. + /// /// /// The hotkey this class will represent. /// Example values: F1, Ctrl+Shift+Enter @@ -47,14 +75,68 @@ public record RegisteredHotkeyData /// The key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, /// which represents the string "Reload Plugins Data" in en.xaml /// + /// + /// The command that will be executed when this hotkey is triggered. If it's null, the hotkey is assumed to be registered by system. + /// + /// + /// The command parameter that will be passed to the command when this hotkey is triggered. If it's null, no parameter will be passed. + /// /// /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. /// - public RegisteredHotkeyData(string hotkey, string descriptionResourceKey, Action? removeHotkey = null) + public RegisteredHotkeyData( + RegisteredHotkeyType registeredType, HotkeyType type, string hotkey, string descriptionResourceKey, + ICommand? command, object? parameter = null, Action? removeHotkey = null) { + RegisteredType = registeredType; + Type = type; Hotkey = new HotkeyModel(hotkey); DescriptionResourceKey = descriptionResourceKey; + Command = command; + CommandParameter = parameter; + RemoveHotkey = removeHotkey; + } + + /// + /// Creates an instance of RegisteredHotkeyData. Assumes that the key specified in + /// descriptionResourceKey doesn't need any arguments for string.Format. If it does, + /// use one of the other constructors. + /// + /// + /// The type of this hotkey in the context of the application. + /// + /// + /// Whether this hotkey is global or search window specific. + /// + /// + /// The hotkey this class will represent. + /// Example values: F1, Ctrl+Shift+Enter + /// + /// + /// The key in the localization dictionary that represents this hotkey. For example, ReloadPluginHotkey, + /// which represents the string "Reload Plugins Data" in en.xaml + /// + /// + /// The command that will be executed when this hotkey is triggered. If it's null, the hotkey is assumed to be registered by system. + /// + /// + /// The command parameter that will be passed to the command when this hotkey is triggered. If it's null, no parameter will be passed. + /// + /// + /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey + /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. + /// + public RegisteredHotkeyData( + RegisteredHotkeyType registeredType, HotkeyType type, HotkeyModel hotkey, string descriptionResourceKey, + ICommand? command, object? parameter = null, Action? removeHotkey = null) + { + RegisteredType = registeredType; + Type = type; + Hotkey = hotkey; + DescriptionResourceKey = descriptionResourceKey; + Command = command; + CommandParameter = parameter; RemoveHotkey = removeHotkey; } @@ -62,6 +144,12 @@ public RegisteredHotkeyData(string hotkey, string descriptionResourceKey, Action /// Creates an instance of RegisteredHotkeyData. Assumes that the key specified in /// descriptionResourceKey needs exactly one argument for string.Format. /// + /// + /// The type of this hotkey in the context of the application. + /// + /// + /// Whether this hotkey is global or search window specific. + /// /// /// The hotkey this class will represent. /// Example values: F1, Ctrl+Shift+Enter @@ -73,17 +161,28 @@ public RegisteredHotkeyData(string hotkey, string descriptionResourceKey, Action /// /// The value that will replace {0} in the localized string found via description. /// + /// + /// The command that will be executed when this hotkey is triggered. If it's null, the hotkey is assumed to be registered by system. + /// + /// + /// The command parameter that will be passed to the command when this hotkey is triggered. If it's null, no parameter will be passed. + /// /// /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. /// public RegisteredHotkeyData( - string hotkey, string descriptionResourceKey, object? descriptionFormatVariable, Action? removeHotkey = null + RegisteredHotkeyType registeredType, HotkeyType type, string hotkey, string descriptionResourceKey, object? descriptionFormatVariable, + ICommand? command, object? parameter = null, Action? removeHotkey = null ) { + RegisteredType = registeredType; + Type = type; Hotkey = new HotkeyModel(hotkey); DescriptionResourceKey = descriptionResourceKey; DescriptionFormatVariables = new[] { descriptionFormatVariable }; + Command = command; + CommandParameter = parameter; RemoveHotkey = removeHotkey; } @@ -91,6 +190,12 @@ public RegisteredHotkeyData( /// Creates an instance of RegisteredHotkeyData. Assumes that the key specified in /// needs multiple arguments for string.Format. /// + /// + /// The type of this hotkey in the context of the application. + /// + /// + /// Whether this hotkey is global or search window specific. + /// /// /// The hotkey this class will represent. /// Example values: F1, Ctrl+Shift+Enter @@ -103,17 +208,102 @@ public RegisteredHotkeyData( /// Array of values that will replace {0}, {1}, {2}, etc. /// in the localized string found via description. /// + /// + /// The command that will be executed when this hotkey is triggered. If it's null, the hotkey is assumed to be registered by system. + /// + /// + /// The command parameter that will be passed to the command when this hotkey is triggered. If it's null, no parameter will be passed. + /// /// /// An action that, when called, will unregister this hotkey. If it's null, it's assumed that this hotkey /// can't be unregistered, and the "Overwrite" option will not appear in the hotkey dialog. /// public RegisteredHotkeyData( - string hotkey, string descriptionResourceKey, object?[] descriptionFormatVariables, Action? removeHotkey = null + RegisteredHotkeyType registeredType, HotkeyType type, string hotkey, string descriptionResourceKey, object?[] descriptionFormatVariables, + ICommand? command, object? parameter = null, Action? removeHotkey = null ) { + RegisteredType = registeredType; + Type = type; Hotkey = new HotkeyModel(hotkey); DescriptionResourceKey = descriptionResourceKey; DescriptionFormatVariables = descriptionFormatVariables; + Command = command; + CommandParameter = parameter; RemoveHotkey = removeHotkey; } + + /// + /// Sets the hotkey for this registered hotkey data. + /// + /// + public void SetHotkey(HotkeyModel hotkey) + { + Hotkey = hotkey; + } + + /// + public override string ToString() + { + return Hotkey.IsEmpty ? $"{RegisteredType} - {Hotkey}" : $"{RegisteredType} - None"; + } +} + +public enum RegisteredHotkeyType +{ + CtrlShiftEnter, + CtrlEnter, + AltEnter, + + Up, + Down, + Left, + Right, + + Esc, + Reload, + SelectFirstResult, + SelectLastResult, + ReQuery, + IncreaseWidth, + DecreaseWidth, + IncreaseMaxResult, + DecreaseMaxResult, + ShiftEnter, + Enter, + ToggleGameMode, + CopyFilePath, + OpenResultN1, + OpenResultN2, + OpenResultN3, + OpenResultN4, + OpenResultN5, + OpenResultN6, + OpenResultN7, + OpenResultN8, + OpenResultN9, + OpenResultN10, + + Toggle, + + Preview, + AutoComplete, + AutoComplete2, + SelectNextItem, + SelectNextItem2, + SelectPrevItem, + SelectPrevItem2, + SettingWindow, + OpenHistory, + OpenContextMenu, + SelectNextPage, + SelectPrevPage, + CycleHistoryUp, + CycleHistoryDown, + + CustomQuery, + + PluginGlobalHotkey, + + PluginWindowHotkey, } diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginHotkey.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginHotkey.cs index 9dc395acaea..0c5c3802879 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginHotkey.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginHotkey.cs @@ -1,4 +1,5 @@ -using Flow.Launcher.Plugin; +using System; +using Flow.Launcher.Plugin; namespace Flow.Launcher.Infrastructure.UserSettings { @@ -6,5 +7,26 @@ public class CustomPluginHotkey : BaseModel { public string Hotkey { get; set; } public string ActionKeyword { get; set; } + + public CustomPluginHotkey(string hotkey, string actionKeyword) + { + Hotkey = hotkey; + ActionKeyword = actionKeyword; + } + + public override bool Equals(object other) + { + if (other is CustomPluginHotkey otherHotkey) + { + return Hotkey == otherHotkey.Hotkey && ActionKeyword == otherHotkey.ActionKeyword; + } + + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(Hotkey, ActionKeyword); + } } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index 920abc28426..0e881005a9e 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -89,6 +89,110 @@ public void UpdatePluginSettings(List metadatas) } } + /// + /// Update plugin hotkey information in metadata and plugin setting. + /// + /// + public void UpdatePluginHotkeyInfo(IDictionary> hotkeyPluginInfo) + { + foreach (var info in hotkeyPluginInfo) + { + var pluginPair = info.Key; + var hotkeyInfo = info.Value; + var metadata = pluginPair.Metadata; + if (Plugins.TryGetValue(pluginPair.Metadata.ID, out var plugin)) + { + if (plugin.pluginHotkeys == null || plugin.pluginHotkeys.Count == 0) + { + // If plugin hotkeys does not exist, create a new one and initialize with default values + plugin.pluginHotkeys = new List(); + foreach (var hotkey in hotkeyInfo) + { + plugin.pluginHotkeys.Add(new PluginHotkey + { + Id = hotkey.Id, + DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values + Hotkey = hotkey.DefaultHotkey // use default value + }); + metadata.PluginHotkeys.Add(new PluginHotkey + { + Id = hotkey.Id, + DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values + Hotkey = hotkey.DefaultHotkey // use default value + }); + } + } + else + { + // If plugin hotkeys exist, update the existing hotkeys with the new values + foreach (var hotkey in hotkeyInfo) + { + var existingHotkey = plugin.pluginHotkeys.Find(h => h.Id == hotkey.Id); + if (existingHotkey != null) + { + // Update existing hotkey + existingHotkey.DefaultHotkey = hotkey.DefaultHotkey; // hotkey info provides default values + metadata.PluginHotkeys.Add(new PluginHotkey + { + Id = hotkey.Id, + DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values + Hotkey = existingHotkey.Hotkey // use settings value + }); + } + else + { + // Add new hotkey if it does not exist + plugin.pluginHotkeys.Add(new PluginHotkey + { + Id = hotkey.Id, + DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values + Hotkey = hotkey.DefaultHotkey // use default value + }); + metadata.PluginHotkeys.Add(new PluginHotkey + { + Id = hotkey.Id, + DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values + Hotkey = hotkey.DefaultHotkey // use default value + }); + } + } + } + } + else + { + // If settings does not exist, create a new one + Plugins[metadata.ID] = new Plugin + { + ID = metadata.ID, + Name = metadata.Name, + Version = metadata.Version, + DefaultActionKeywords = metadata.ActionKeywords, // metadata provides default values + ActionKeywords = metadata.ActionKeywords, // use default value + Disabled = metadata.Disabled, + HomeDisabled = metadata.HomeDisabled, + Priority = metadata.Priority, + DefaultSearchDelayTime = metadata.SearchDelayTime, // metadata provides default values + SearchDelayTime = metadata.SearchDelayTime, // use default value + }; + foreach (var hotkey in hotkeyInfo) + { + Plugins[metadata.ID].pluginHotkeys.Add(new PluginHotkey + { + Id = hotkey.Id, + DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values + Hotkey = hotkey.DefaultHotkey // use default value + }); + metadata.PluginHotkeys.Add(new PluginHotkey + { + Id = hotkey.Id, + DefaultHotkey = hotkey.DefaultHotkey, // hotkey info provides default values + Hotkey = hotkey.DefaultHotkey // use default value + }); + } + } + } + } + public Plugin GetPluginSettings(string id) { if (Plugins.TryGetValue(id, out var plugin)) @@ -126,6 +230,8 @@ public class Plugin public int? SearchDelayTime { get; set; } + public List pluginHotkeys { get; set; } = new List(); + /// /// Used only to save the state of the plugin in settings /// diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 2dbdf0bf8a9..88c4cfb8194 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -39,25 +39,220 @@ public void Save() _storage.Save(); } - public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}"; public string OpenResultModifiers { get; set; } = KeyConstant.Alt; public string ColorScheme { get; set; } = "System"; public bool ShowOpenResultHotkey { get; set; } = true; public double WindowSize { get; set; } = 580; - public string PreviewHotkey { get; set; } = $"F1"; - public string AutoCompleteHotkey { get; set; } = $"{KeyConstant.Ctrl} + Tab"; - public string AutoCompleteHotkey2 { get; set; } = $""; - public string SelectNextItemHotkey { get; set; } = $"Tab"; - public string SelectNextItemHotkey2 { get; set; } = $""; - public string SelectPrevItemHotkey { get; set; } = $"Shift + Tab"; - public string SelectPrevItemHotkey2 { get; set; } = $""; - public string SelectNextPageHotkey { get; set; } = $"PageUp"; - public string SelectPrevPageHotkey { get; set; } = $"PageDown"; - public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O"; - public string SettingWindowHotkey { get; set; } = $"Ctrl+I"; - public string OpenHistoryHotkey { get; set; } = $"Ctrl+H"; - public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; - public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; + + private string _hotkey = $"{KeyConstant.Alt} + {KeyConstant.Space}"; + public string Hotkey + { + get => _hotkey; + set + { + if (_hotkey != value) + { + _hotkey = value; + OnPropertyChanged(); + } + } + } + + private string _previewHotkey = "F1"; + public string PreviewHotkey + { + get => _previewHotkey; + set + { + if (_previewHotkey != value) + { + _previewHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _autoCompleteHotkey = $"{KeyConstant.Ctrl} + Tab"; + public string AutoCompleteHotkey + { + get => _autoCompleteHotkey; + set + { + if (_autoCompleteHotkey != value) + { + _autoCompleteHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _autoCompleteHotkey2 = ""; + public string AutoCompleteHotkey2 + { + get => _autoCompleteHotkey2; + set + { + if (_autoCompleteHotkey2 != value) + { + _autoCompleteHotkey2 = value; + OnPropertyChanged(); + } + } + } + + private string _selectNextItemHotkey = "Tab"; + public string SelectNextItemHotkey + { + get => _selectNextItemHotkey; + set + { + if (_selectNextItemHotkey != value) + { + _selectNextItemHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _selectNextItemHotkey2 = ""; + public string SelectNextItemHotkey2 + { + get => _selectNextItemHotkey2; + set + { + if (_selectNextItemHotkey2 != value) + { + _selectNextItemHotkey2 = value; + OnPropertyChanged(); + } + } + } + + private string _selectPrevItemHotkey = "Shift + Tab"; + public string SelectPrevItemHotkey + { + get => _selectPrevItemHotkey; + set + { + if (_selectPrevItemHotkey != value) + { + _selectPrevItemHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _selectPrevItemHotkey2 = ""; + public string SelectPrevItemHotkey2 + { + get => _selectPrevItemHotkey2; + set + { + if (_selectPrevItemHotkey2 != value) + { + _selectPrevItemHotkey2 = value; + OnPropertyChanged(); + } + } + } + + private string _selectNextPageHotkey = "PageUp"; + public string SelectNextPageHotkey + { + get => _selectNextPageHotkey; + set + { + if (_selectNextPageHotkey != value) + { + _selectNextPageHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _selectPrevPageHotkey = "PageDown"; + public string SelectPrevPageHotkey + { + get => _selectPrevPageHotkey; + set + { + if (_selectPrevPageHotkey != value) + { + _selectPrevPageHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _openContextMenuHotkey = "Ctrl+O"; + public string OpenContextMenuHotkey + { + get => _openContextMenuHotkey; + set + { + if (_openContextMenuHotkey != value) + { + _openContextMenuHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _settingWindowHotkey = "Ctrl+I"; + public string SettingWindowHotkey + { + get => _settingWindowHotkey; + set + { + if (_settingWindowHotkey != value) + { + _settingWindowHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _openHistoryHotkey = "Ctrl+H"; + public string OpenHistoryHotkey + { + get => _openHistoryHotkey; + set + { + if (_openHistoryHotkey != value) + { + _openHistoryHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _cycleHistoryUpHotkey = $"{KeyConstant.Alt} + Up"; + public string CycleHistoryUpHotkey + { + get => _cycleHistoryUpHotkey; + set + { + if (_cycleHistoryUpHotkey != value) + { + _cycleHistoryUpHotkey = value; + OnPropertyChanged(); + } + } + } + + private string _cycleHistoryDownHotkey = $"{KeyConstant.Alt} + Down"; + public string CycleHistoryDownHotkey + { + get => _cycleHistoryDownHotkey; + set + { + if (_cycleHistoryDownHotkey != value) + { + _cycleHistoryDownHotkey = value; + OnPropertyChanged(); + } + } + } private string _language = Constant.SystemLanguageCode; public string Language @@ -359,9 +554,9 @@ public bool KeepMaxResults public int ActivateTimes { get; set; } - public ObservableCollection CustomPluginHotkeys { get; set; } = new ObservableCollection(); + public ObservableCollection CustomPluginHotkeys { get; set; } = new(); - public ObservableCollection CustomShortcuts { get; set; } = new ObservableCollection(); + public ObservableCollection CustomShortcuts { get; set; } = new(); [JsonIgnore] public ObservableCollection BuiltinShortcuts { get; set; } = new() @@ -432,95 +627,10 @@ public bool ShowAtTopmost public bool WMPInstalled { get; set; } = true; // This needs to be loaded last by staying at the bottom - public PluginsSettings PluginSettings { get; set; } = new PluginsSettings(); + public PluginsSettings PluginSettings { get; set; } = new(); [JsonIgnore] - public List RegisteredHotkeys - { - get - { - var list = FixedHotkeys(); - - // Customizeable hotkeys - if (!string.IsNullOrEmpty(Hotkey)) - list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = "")); - if (!string.IsNullOrEmpty(PreviewHotkey)) - list.Add(new(PreviewHotkey, "previewHotkey", () => PreviewHotkey = "")); - if (!string.IsNullOrEmpty(AutoCompleteHotkey)) - list.Add(new(AutoCompleteHotkey, "autoCompleteHotkey", () => AutoCompleteHotkey = "")); - if (!string.IsNullOrEmpty(AutoCompleteHotkey2)) - list.Add(new(AutoCompleteHotkey2, "autoCompleteHotkey", () => AutoCompleteHotkey2 = "")); - if (!string.IsNullOrEmpty(SelectNextItemHotkey)) - list.Add(new(SelectNextItemHotkey, "SelectNextItemHotkey", () => SelectNextItemHotkey = "")); - if (!string.IsNullOrEmpty(SelectNextItemHotkey2)) - list.Add(new(SelectNextItemHotkey2, "SelectNextItemHotkey", () => SelectNextItemHotkey2 = "")); - if (!string.IsNullOrEmpty(SelectPrevItemHotkey)) - list.Add(new(SelectPrevItemHotkey, "SelectPrevItemHotkey", () => SelectPrevItemHotkey = "")); - if (!string.IsNullOrEmpty(SelectPrevItemHotkey2)) - list.Add(new(SelectPrevItemHotkey2, "SelectPrevItemHotkey", () => SelectPrevItemHotkey2 = "")); - if (!string.IsNullOrEmpty(SettingWindowHotkey)) - list.Add(new(SettingWindowHotkey, "SettingWindowHotkey", () => SettingWindowHotkey = "")); - if (!string.IsNullOrEmpty(OpenHistoryHotkey)) - list.Add(new(OpenHistoryHotkey, "OpenHistoryHotkey", () => OpenHistoryHotkey = "")); - if (!string.IsNullOrEmpty(OpenContextMenuHotkey)) - list.Add(new(OpenContextMenuHotkey, "OpenContextMenuHotkey", () => OpenContextMenuHotkey = "")); - if (!string.IsNullOrEmpty(SelectNextPageHotkey)) - list.Add(new(SelectNextPageHotkey, "SelectNextPageHotkey", () => SelectNextPageHotkey = "")); - if (!string.IsNullOrEmpty(SelectPrevPageHotkey)) - list.Add(new(SelectPrevPageHotkey, "SelectPrevPageHotkey", () => SelectPrevPageHotkey = "")); - if (!string.IsNullOrEmpty(CycleHistoryUpHotkey)) - list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = "")); - if (!string.IsNullOrEmpty(CycleHistoryDownHotkey)) - list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = "")); - - // Custom Query Hotkeys - foreach (var customPluginHotkey in CustomPluginHotkeys) - { - if (!string.IsNullOrEmpty(customPluginHotkey.Hotkey)) - list.Add(new(customPluginHotkey.Hotkey, "customQueryHotkey", () => customPluginHotkey.Hotkey = "")); - } - - return list; - } - } - - private List FixedHotkeys() - { - return new List - { - new("Up", "HotkeyLeftRightDesc"), - new("Down", "HotkeyLeftRightDesc"), - new("Left", "HotkeyUpDownDesc"), - new("Right", "HotkeyUpDownDesc"), - new("Escape", "HotkeyESCDesc"), - new("F5", "ReloadPluginHotkey"), - new("Alt+Home", "HotkeySelectFirstResult"), - new("Alt+End", "HotkeySelectLastResult"), - new("Ctrl+R", "HotkeyRequery"), - new("Ctrl+OemCloseBrackets", "QuickWidthHotkey"), - new("Ctrl+OemOpenBrackets", "QuickWidthHotkey"), - new("Ctrl+OemPlus", "QuickHeightHotkey"), - new("Ctrl+OemMinus", "QuickHeightHotkey"), - new("Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc"), - new("Shift+Enter", "OpenContextMenuHotkey"), - new("Enter", "HotkeyRunDesc"), - new("Ctrl+Enter", "OpenContainFolderHotkey"), - new("Alt+Enter", "HotkeyOpenResult"), - new("Ctrl+F12", "ToggleGameModeHotkey"), - new("Ctrl+Shift+C", "CopyFilePathHotkey"), - - new($"{OpenResultModifiers}+D1", "HotkeyOpenResultN", 1), - new($"{OpenResultModifiers}+D2", "HotkeyOpenResultN", 2), - new($"{OpenResultModifiers}+D3", "HotkeyOpenResultN", 3), - new($"{OpenResultModifiers}+D4", "HotkeyOpenResultN", 4), - new($"{OpenResultModifiers}+D5", "HotkeyOpenResultN", 5), - new($"{OpenResultModifiers}+D6", "HotkeyOpenResultN", 6), - new($"{OpenResultModifiers}+D7", "HotkeyOpenResultN", 7), - new($"{OpenResultModifiers}+D8", "HotkeyOpenResultN", 8), - new($"{OpenResultModifiers}+D9", "HotkeyOpenResultN", 9), - new($"{OpenResultModifiers}+D0", "HotkeyOpenResultN", 10) - }; - } + public ObservableCollection RegisteredHotkeys { get; } = new(); } public enum LastQueryMode diff --git a/Flow.Launcher.Plugin/ActionContext.cs b/Flow.Launcher.Plugin/ActionContext.cs index 9e05bbd0617..052b2063975 100644 --- a/Flow.Launcher.Plugin/ActionContext.cs +++ b/Flow.Launcher.Plugin/ActionContext.cs @@ -1,4 +1,5 @@ -using System.Windows.Input; +using System; +using System.Windows.Input; namespace Flow.Launcher.Plugin { @@ -6,6 +7,7 @@ namespace Flow.Launcher.Plugin /// Context provided as a parameter when invoking a /// or /// + [Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")] public class ActionContext { /// diff --git a/Flow.Launcher.Plugin/Interfaces/IPluginHotkey.cs b/Flow.Launcher.Plugin/Interfaces/IPluginHotkey.cs new file mode 100644 index 00000000000..34ee2f1bc2a --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IPluginHotkey.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Flow.Launcher.Plugin +{ + /// + /// Represent plugins that support global hotkey or search window hotkey. + /// + public interface IPluginHotkey : IFeatures + { + /// + /// Get the list of plugin hotkeys which will be registered in the settings page. + /// + /// + List GetPluginHotkeys(); + } +} diff --git a/Flow.Launcher.Plugin/PluginHotkey.cs b/Flow.Launcher.Plugin/PluginHotkey.cs new file mode 100644 index 00000000000..18f650e88b9 --- /dev/null +++ b/Flow.Launcher.Plugin/PluginHotkey.cs @@ -0,0 +1,134 @@ +using System; + +namespace Flow.Launcher.Plugin; + +/// +/// Represents a base plugin hotkey model. +/// +/// +/// Do not use this class directly. Use or instead. +/// +public class BasePluginHotkey +{ + /// + /// Initializes a new instance of the class with the specified hotkey type. + /// + /// + protected BasePluginHotkey(HotkeyType type) + { + HotkeyType = type; + } + + /// + /// The unique identifier for the hotkey, which is used to identify and rank the hotkey in the settings page. + /// + public int Id { get; set; } = 0; + + /// + /// The name of the hotkey, which will be displayed in the settings page. + /// + public string Name { get; set; } = string.Empty; + + /// + /// The description of the hotkey, which will be displayed in the settings page. + /// + public string Description { get; set; } = string.Empty; + + /// + /// The glyph information for the hotkey, which will be displayed in the settings page. + /// + public GlyphInfo Glyph { get; set; } + + /// + /// The default hotkey that will be used if the user does not set a custom hotkey. + /// + public string DefaultHotkey { get; set; } = string.Empty; + + /// + /// The type of the hotkey, which can be either global or search window specific. + /// + public HotkeyType HotkeyType { get; } = HotkeyType.Global; + + /// + /// Indicates whether the hotkey is editable by the user in the settings page. + /// + public bool Editable { get; set; } = false; + + /// + /// Whether to show the hotkey in the settings page. + /// + public bool Visible { get; set; } = true; +} + +/// +/// Represent a global plugin hotkey model. +/// +public class GlobalPluginHotkey : BasePluginHotkey +{ + /// + /// Initializes a new instance of the class. + /// + public GlobalPluginHotkey() : base(HotkeyType.Global) + { + } + + /// + /// An action that will be executed when the hotkey is triggered. + /// + public Action Action { get; set; } = null; +} + +/// +/// Represents a plugin hotkey that is specific to the search window. +/// +public class SearchWindowPluginHotkey : BasePluginHotkey +{ + /// + /// Initializes a new instance of the class. + /// + public SearchWindowPluginHotkey() : base(HotkeyType.SearchWindow) + { + } + + /// + /// An action that will be executed when the hotkey is triggered and a result is selected. + /// + public Func Action { get; set; } = null; +} + +/// +/// Represents the type of hotkey for a plugin. +/// +public enum HotkeyType +{ + /// + /// A hotkey that will be trigged globally, regardless of the active window. + /// + Global, + + /// + /// A hotkey that will be triggered only when the search window is active. + /// + SearchWindow +} + +/// +/// Represents a plugin hotkey model which is used to store the hotkey information for a plugin. +/// +public class PluginHotkey +{ + /// + /// The unique identifier for the hotkey. + /// + public int Id { get; set; } = 0; + + /// + /// The default hotkey that will be used if the user does not set a custom hotkey. + /// + public string DefaultHotkey { get; set; } = string.Empty; + + /// + /// The current hotkey that the user has set for the plugin. + /// + public string Hotkey { get; set; } = string.Empty; +} diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index 09803cbd7cc..77edb5c7bec 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -152,6 +152,11 @@ internal set /// public string PluginCacheDirectoryPath { get; internal set; } + /// + /// List of registered plugin hotkeys. + /// + public List PluginHotkeys { get; set; } = new List(); + /// /// Convert to string. /// diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index f0fcd48ffc0..2e73d7b3b88 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -257,6 +257,12 @@ public string PluginDirectory /// public bool ShowBadge { get; set; } = false; + /// + /// List of hotkey IDs that are supported for this result. + /// Those hotkeys should be registed by IPluginHotkey interface. + /// + public IList HotkeyIds { get; set; } = new List(); + /// /// Run this result, asynchronously /// @@ -308,6 +314,7 @@ public Result Clone() AddSelectedCount = AddSelectedCount, RecordKey = RecordKey, ShowBadge = ShowBadge, + HotkeyIds = HotkeyIds, }; } diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 6c506cfc06c..3f74c256574 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -364,5 +364,44 @@ public static void ValidateDataDirectory(string bundledDataDirectory, string dat } } } + + /// + /// Return true is the given name is a valid file name + /// + public static bool IsValidFileName(string name) + { + if (IsReservedName(name)) return false; + if (name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || string.IsNullOrWhiteSpace(name)) + { + return false; + } + return true; + } + + /// + /// Returns true is the given name is a valid name for a directory, not a path + /// + public static bool IsValidDirectoryName(string name) + { + if (IsReservedName(name)) return false; + var invalidChars = Path.GetInvalidPathChars().Append('/').ToArray().Append('\\').ToArray(); + if (name.IndexOfAny(invalidChars) >= 0) + { + return false; + } + return true; + } + + private static readonly string[] ReservedNames = new[] { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; + + private static bool IsReservedName(string name) + { + var nameWithoutExtension = Path.GetFileNameWithoutExtension(name).ToUpperInvariant(); + if (ReservedNames.Contains(nameWithoutExtension)) + { + return true; + } + return false; + } } } diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml b/Flow.Launcher/CustomQueryHotkeySetting.xaml index 0171e6d79c3..9575f812181 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml @@ -119,7 +119,8 @@ Grid.Column="1" Margin="10" HorizontalAlignment="Stretch" - VerticalAlignment="Center" /> + VerticalAlignment="Center" + Text="{Binding ActionKeyword}" /> diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs index 77febde9d5b..685fdf00ab0 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs @@ -1,73 +1,52 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Windows; -using System.Windows.Input; +using System.Windows; using System.Windows.Controls; -using Flow.Launcher.Helper; +using System.Windows.Input; using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher { public partial class CustomQueryHotkeySetting : Window { - private readonly Settings _settings; + public string Hotkey { get; set; } = string.Empty; + public string ActionKeyword { get; set; } = string.Empty; - private bool update; - private CustomPluginHotkey updateCustomHotkey; + private readonly bool update; + private readonly CustomPluginHotkey originalCustomHotkey; - public CustomQueryHotkeySetting(Settings settings) + public CustomQueryHotkeySetting() { - _settings = settings; InitializeComponent(); + lblAdd.Visibility = Visibility.Visible; } - private void BtnCancel_OnClick(object sender, RoutedEventArgs e) + public CustomQueryHotkeySetting(CustomPluginHotkey hotkey) { - Close(); + originalCustomHotkey = hotkey; + update = true; + ActionKeyword = originalCustomHotkey.ActionKeyword; + InitializeComponent(); + lblUpdate.Visibility = Visibility.Visible; + HotkeyControl.SetHotkey(originalCustomHotkey.Hotkey, false); } - private void btnAdd_OnClick(object sender, RoutedEventArgs e) + private void BtnCancel_OnClick(object sender, RoutedEventArgs e) { - if (!update) - { - _settings.CustomPluginHotkeys ??= new ObservableCollection(); - - var pluginHotkey = new CustomPluginHotkey - { - Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text - }; - _settings.CustomPluginHotkeys.Add(pluginHotkey); - - HotKeyMapper.SetCustomQueryHotkey(pluginHotkey); - } - else - { - var oldHotkey = updateCustomHotkey.Hotkey; - updateCustomHotkey.ActionKeyword = tbAction.Text; - updateCustomHotkey.Hotkey = HotkeyControl.CurrentHotkey.ToString(); - //remove origin hotkey - HotKeyMapper.RemoveHotkey(oldHotkey); - HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey); - } - + DialogResult = false; Close(); } - public void UpdateItem(CustomPluginHotkey item) + private void btnAdd_OnClick(object sender, RoutedEventArgs e) { - updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o => - o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey); - if (updateCustomHotkey == null) + Hotkey = HotkeyControl.CurrentHotkey.ToString(); + + if (string.IsNullOrEmpty(Hotkey) && string.IsNullOrEmpty(ActionKeyword)) { - App.API.ShowMsgBox(App.API.GetTranslation("invalidPluginHotkey")); - Close(); + App.API.ShowMsgBox(App.API.GetTranslation("emptyPluginHotkey")); return; } - tbAction.Text = updateCustomHotkey.ActionKeyword; - HotkeyControl.SetHotkey(updateCustomHotkey.Hotkey, false); - update = true; - lblAdd.Text = App.API.GetTranslation("update"); + DialogResult = !update || originalCustomHotkey.Hotkey != Hotkey || originalCustomHotkey.ActionKeyword != ActionKeyword; + Close(); } private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e) @@ -79,6 +58,7 @@ private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e) private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e) { + DialogResult = false; Close(); } diff --git a/Flow.Launcher/CustomShortcutSetting.xaml.cs b/Flow.Launcher/CustomShortcutSetting.xaml.cs index e180f657024..f4644a267e9 100644 --- a/Flow.Launcher/CustomShortcutSetting.xaml.cs +++ b/Flow.Launcher/CustomShortcutSetting.xaml.cs @@ -43,12 +43,14 @@ private void BtnAdd_OnClick(object sender, RoutedEventArgs e) App.API.ShowMsgBox(App.API.GetTranslation("emptyShortcut")); return; } + // Check if key is modified or adding a new one if (((update && originalKey != Key) || !update) && _hotkeyVm.DoesShortcutExist(Key)) { App.API.ShowMsgBox(App.API.GetTranslation("duplicateShortcut")); return; } + DialogResult = !update || originalKey != Key || originalValue != Value; Close(); } diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index e5fabb3a89f..93be0f42a0e 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -1,14 +1,25 @@ -using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Infrastructure.UserSettings; -using System; -using NHotkey; -using NHotkey.Wpf; -using Flow.Launcher.ViewModel; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Windows; +using System.Windows.Input; using ChefKeys; using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; +using Flow.Launcher.Core.Plugin; +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using Flow.Launcher.ViewModel; +using NHotkey.Wpf; namespace Flow.Launcher.Helper; +/// +/// Set Flow Launcher global hotkeys & window hotkeys +/// internal static class HotKeyMapper { private static readonly string ClassName = nameof(HotKeyMapper); @@ -16,44 +27,417 @@ internal static class HotKeyMapper private static Settings _settings; private static MainViewModel _mainViewModel; + #region Initialization + internal static void Initialize() { _mainViewModel = Ioc.Default.GetRequiredService(); _settings = Ioc.Default.GetService(); - SetHotkey(_settings.Hotkey, OnToggleHotkey); - LoadCustomPluginHotkey(); + InitializeActionContextHotkeys(); + InitializeRegisteredHotkeys(); + + _settings.PropertyChanged += Settings_PropertyChanged; + _settings.CustomPluginHotkeys.CollectionChanged += CustomPluginHotkeys_CollectionChanged; + PluginManager.PluginHotkeyChanged += PluginManager_PluginHotkeyChanged; } - internal static void OnToggleHotkey(object sender, HotkeyEventArgs args) + private static void InitializeRegisteredHotkeys() { - if (!_mainViewModel.ShouldIgnoreHotkeys()) - _mainViewModel.ToggleFlowLauncher(); + // Fixed hotkeys & Editable hotkeys + var list = new List + { + // System default window hotkeys + new(RegisteredHotkeyType.Up, HotkeyType.SearchWindow, "Up", "HotkeyLeftRightDesc", null), + new(RegisteredHotkeyType.Down, HotkeyType.SearchWindow, "Down", "HotkeyLeftRightDesc", null), + new(RegisteredHotkeyType.Left, HotkeyType.SearchWindow, "Left", "HotkeyUpDownDesc", null), + new(RegisteredHotkeyType.Right, HotkeyType.SearchWindow, "Right", "HotkeyUpDownDesc", null), + + // Flow Launcher window hotkeys + new(RegisteredHotkeyType.Esc, HotkeyType.SearchWindow, "Escape", "HotkeyESCDesc", _mainViewModel.EscCommand), + new(RegisteredHotkeyType.Reload, HotkeyType.SearchWindow, "F5", "ReloadPluginHotkey", _mainViewModel.ReloadPluginDataCommand), + new(RegisteredHotkeyType.SelectFirstResult, HotkeyType.SearchWindow, "Alt+Home", "HotkeySelectFirstResult", _mainViewModel.SelectFirstResultCommand), + new(RegisteredHotkeyType.SelectLastResult, HotkeyType.SearchWindow, "Alt+End", "HotkeySelectLastResult", _mainViewModel.SelectLastResultCommand), + new(RegisteredHotkeyType.ReQuery, HotkeyType.SearchWindow, "Ctrl+R", "HotkeyRequery", _mainViewModel.ReQueryCommand), + new(RegisteredHotkeyType.IncreaseWidth, HotkeyType.SearchWindow, "Ctrl+OemCloseBrackets", "QuickWidthHotkey", _mainViewModel.IncreaseWidthCommand), + new(RegisteredHotkeyType.DecreaseWidth, HotkeyType.SearchWindow, "Ctrl+OemOpenBrackets", "QuickWidthHotkey", _mainViewModel.DecreaseWidthCommand), + new(RegisteredHotkeyType.IncreaseMaxResult, HotkeyType.SearchWindow, "Ctrl+OemPlus", "QuickHeightHotkey", _mainViewModel.IncreaseMaxResultCommand), + new(RegisteredHotkeyType.DecreaseMaxResult, HotkeyType.SearchWindow, "Ctrl+OemMinus", "QuickHeightHotkey", _mainViewModel.DecreaseMaxResultCommand), + new(RegisteredHotkeyType.ShiftEnter, HotkeyType.SearchWindow, "Shift+Enter", "OpenContextMenuHotkey", _mainViewModel.LoadContextMenuCommand), + new(RegisteredHotkeyType.Enter, HotkeyType.SearchWindow, "Enter", "HotkeyRunDesc", _mainViewModel.OpenResultCommand), + new(RegisteredHotkeyType.ToggleGameMode, HotkeyType.SearchWindow, "Ctrl+F12", "ToggleGameModeHotkey", _mainViewModel.ToggleGameModeCommand), + new(RegisteredHotkeyType.CopyFilePath, HotkeyType.SearchWindow, "Ctrl+Shift+C", "CopyFilePathHotkey", _mainViewModel.CopyAlternativeCommand), + new(RegisteredHotkeyType.OpenResultN1, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D1", "HotkeyOpenResultN", 1, _mainViewModel.OpenResultCommand, 0), + new(RegisteredHotkeyType.OpenResultN2, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D2", "HotkeyOpenResultN", 2, _mainViewModel.OpenResultCommand, 1), + new(RegisteredHotkeyType.OpenResultN3, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D3", "HotkeyOpenResultN", 3, _mainViewModel.OpenResultCommand, 2), + new(RegisteredHotkeyType.OpenResultN4, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D4", "HotkeyOpenResultN", 4, _mainViewModel.OpenResultCommand, 3), + new(RegisteredHotkeyType.OpenResultN5, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D5", "HotkeyOpenResultN", 5, _mainViewModel.OpenResultCommand, 4), + new(RegisteredHotkeyType.OpenResultN6, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D6", "HotkeyOpenResultN", 6, _mainViewModel.OpenResultCommand, 5), + new(RegisteredHotkeyType.OpenResultN7, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D7", "HotkeyOpenResultN", 7, _mainViewModel.OpenResultCommand, 6), + new(RegisteredHotkeyType.OpenResultN8, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D8", "HotkeyOpenResultN", 8, _mainViewModel.OpenResultCommand, 7), + new(RegisteredHotkeyType.OpenResultN9, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D9", "HotkeyOpenResultN", 9, _mainViewModel.OpenResultCommand, 8), + new(RegisteredHotkeyType.OpenResultN10, HotkeyType.SearchWindow, $"{_settings.OpenResultModifiers}+D0", "HotkeyOpenResultN", 10, _mainViewModel.OpenResultCommand, 9), + + // Flow Launcher global hotkeys + new(RegisteredHotkeyType.Toggle, HotkeyType.Global, _settings.Hotkey, "flowlauncherHotkey", _mainViewModel.CheckAndToggleFlowLauncherCommand, null, () => _settings.Hotkey = ""), + + // Flow Launcher window hotkeys + new(RegisteredHotkeyType.Preview, HotkeyType.SearchWindow, _settings.PreviewHotkey, "previewHotkey", _mainViewModel.TogglePreviewCommand, null, () => _settings.PreviewHotkey = ""), + new(RegisteredHotkeyType.AutoComplete, HotkeyType.SearchWindow, _settings.AutoCompleteHotkey, "autoCompleteHotkey", _mainViewModel.AutocompleteQueryCommand, null, () => _settings.AutoCompleteHotkey = ""), + new(RegisteredHotkeyType.AutoComplete2, HotkeyType.SearchWindow, _settings.AutoCompleteHotkey2, "autoCompleteHotkey", _mainViewModel.AutocompleteQueryCommand, null, () => _settings.AutoCompleteHotkey2 = ""), + new(RegisteredHotkeyType.SelectNextItem, HotkeyType.SearchWindow, _settings.SelectNextItemHotkey, "SelectNextItemHotkey", _mainViewModel.SelectNextItemCommand, null, () => _settings.SelectNextItemHotkey = ""), + new(RegisteredHotkeyType.SelectNextItem2, HotkeyType.SearchWindow, _settings.SelectNextItemHotkey2, "SelectNextItemHotkey", _mainViewModel.SelectNextItemCommand, null, () => _settings.SelectNextItemHotkey2 = ""), + new(RegisteredHotkeyType.SelectPrevItem, HotkeyType.SearchWindow, _settings.SelectPrevItemHotkey, "SelectPrevItemHotkey", _mainViewModel.SelectPrevItemCommand, null, () => _settings.SelectPrevItemHotkey = ""), + new(RegisteredHotkeyType.SelectPrevItem2, HotkeyType.SearchWindow, _settings.SelectPrevItemHotkey2, "SelectPrevItemHotkey", _mainViewModel.SelectPrevItemCommand, null, () => _settings.SelectPrevItemHotkey2 = ""), + new(RegisteredHotkeyType.SettingWindow, HotkeyType.SearchWindow, _settings.SettingWindowHotkey, "SettingWindowHotkey", _mainViewModel.OpenSettingCommand, null, () => _settings.SettingWindowHotkey = ""), + new(RegisteredHotkeyType.OpenHistory, HotkeyType.SearchWindow, _settings.OpenHistoryHotkey, "OpenHistoryHotkey", _mainViewModel.LoadHistoryCommand, null, () => _settings.OpenHistoryHotkey = ""), + new(RegisteredHotkeyType.OpenContextMenu, HotkeyType.SearchWindow, _settings.OpenContextMenuHotkey, "OpenContextMenuHotkey", _mainViewModel.LoadContextMenuCommand, null, () => _settings.OpenContextMenuHotkey = ""), + new(RegisteredHotkeyType.SelectNextPage, HotkeyType.SearchWindow, _settings.SelectNextPageHotkey, "SelectNextPageHotkey", _mainViewModel.SelectNextPageCommand, null, () => _settings.SelectNextPageHotkey = ""), + new(RegisteredHotkeyType.SelectPrevPage, HotkeyType.SearchWindow, _settings.SelectPrevPageHotkey, "SelectPrevPageHotkey", _mainViewModel.SelectPrevPageCommand, null, () => _settings.SelectPrevPageHotkey = ""), + new(RegisteredHotkeyType.CycleHistoryUp, HotkeyType.SearchWindow, _settings.CycleHistoryUpHotkey, "CycleHistoryUpHotkey", _mainViewModel.ReverseHistoryCommand, null, () => _settings.CycleHistoryUpHotkey = ""), + new(RegisteredHotkeyType.CycleHistoryDown, HotkeyType.SearchWindow, _settings.CycleHistoryDownHotkey, "CycleHistoryDownHotkey", _mainViewModel.ForwardHistoryCommand, null, () => _settings.CycleHistoryDownHotkey = "") + }; + + // Custom query global hotkeys + foreach (var customPluginHotkey in _settings.CustomPluginHotkeys) + { + list.Add(GetRegisteredHotkeyData(customPluginHotkey)); + } + + // Plugin hotkeys + // Global plugin hotkeys + var pluginHotkeyInfos = PluginManager.GetPluginHotkeyInfo(); + foreach (var info in pluginHotkeyInfos) + { + var pluginPair = info.Key; + var hotkeyInfo = info.Value; + var metadata = pluginPair.Metadata; + foreach (var hotkey in hotkeyInfo) + { + if (hotkey.HotkeyType == HotkeyType.Global && hotkey is GlobalPluginHotkey globalHotkey) + { + var hotkeyStr = metadata.PluginHotkeys.Find(h => h.Id == hotkey.Id)?.Hotkey ?? hotkey.DefaultHotkey; + list.Add(GetRegisteredHotkeyData(new(hotkeyStr), metadata, globalHotkey)); + } + } + } + + // Window plugin hotkeys + var windowPluginHotkeys = PluginManager.GetWindowPluginHotkeys(); + foreach (var hotkey in windowPluginHotkeys) + { + var hotkeyModel = hotkey.Key; + var windowHotkeys = hotkey.Value; + list.Add(GetRegisteredHotkeyData(hotkeyModel, windowHotkeys)); + } + + // Add registered hotkeys & Set them + foreach (var hotkey in list) + { + _settings.RegisteredHotkeys.Add(hotkey); + SetHotkey(hotkey); + } + + App.API.LogDebug(ClassName, $"Initialize {_settings.RegisteredHotkeys.Count} hotkeys:\n[\n\t{string.Join(",\n\t", _settings.RegisteredHotkeys)}\n]"); } - internal static void OnToggleHotkeyWithChefKeys() + #endregion + + #region Hotkey Change Events + + private static void Settings_PropertyChanged(object sender, PropertyChangedEventArgs e) { - if (!_mainViewModel.ShouldIgnoreHotkeys()) - _mainViewModel.ToggleFlowLauncher(); + switch (e.PropertyName) + { + // Flow Launcher global hotkeys + case nameof(_settings.Hotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.Toggle, _settings.Hotkey); + break; + + // Flow Launcher window hotkeys + case nameof(_settings.PreviewHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.Preview, _settings.PreviewHotkey); + break; + case nameof(_settings.AutoCompleteHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.AutoComplete, _settings.AutoCompleteHotkey); + break; + case nameof(_settings.AutoCompleteHotkey2): + ChangeRegisteredHotkey(RegisteredHotkeyType.AutoComplete2, _settings.AutoCompleteHotkey2); + break; + case nameof(_settings.SelectNextItemHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.SelectNextItem, _settings.SelectNextItemHotkey); + break; + case nameof(_settings.SelectNextItemHotkey2): + ChangeRegisteredHotkey(RegisteredHotkeyType.SelectNextItem2, _settings.SelectNextItemHotkey2); + break; + case nameof(_settings.SelectPrevItemHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.SelectPrevItem, _settings.SelectPrevItemHotkey); + break; + case nameof(_settings.SelectPrevItemHotkey2): + ChangeRegisteredHotkey(RegisteredHotkeyType.SelectPrevItem2, _settings.SelectPrevItemHotkey2); + break; + case nameof(_settings.SettingWindowHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.SettingWindow, _settings.SettingWindowHotkey); + break; + case nameof(_settings.OpenHistoryHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.OpenHistory, _settings.OpenHistoryHotkey); + break; + case nameof(_settings.OpenContextMenuHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.OpenContextMenu, _settings.OpenContextMenuHotkey); + break; + case nameof(_settings.SelectNextPageHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.SelectNextPage, _settings.SelectNextPageHotkey); + break; + case nameof(_settings.SelectPrevPageHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.SelectPrevPage, _settings.SelectPrevPageHotkey); + break; + case nameof(_settings.CycleHistoryUpHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.CycleHistoryUp, _settings.CycleHistoryUpHotkey); + break; + case nameof(_settings.CycleHistoryDownHotkey): + ChangeRegisteredHotkey(RegisteredHotkeyType.CycleHistoryDown, _settings.CycleHistoryDownHotkey); + break; + } } - private static void SetHotkey(string hotkeyStr, EventHandler action) + private static void CustomPluginHotkeys_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - var hotkey = new HotkeyModel(hotkeyStr); - SetHotkey(hotkey, action); + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (var item in e.NewItems) + { + if (item is CustomPluginHotkey customPluginHotkey) + { + var hotkeyData = GetRegisteredHotkeyData(customPluginHotkey); + _settings.RegisteredHotkeys.Add(hotkeyData); + SetHotkey(hotkeyData); + } + } + break; + case NotifyCollectionChangedAction.Remove: + foreach (var item in e.OldItems) + { + if (item is CustomPluginHotkey customPluginHotkey) + { + var hotkeyData = SearchRegisteredHotkeyData(customPluginHotkey); + _settings.RegisteredHotkeys.Remove(hotkeyData); + RemoveHotkey(hotkeyData); + } + } + break; + case NotifyCollectionChangedAction.Replace: + foreach (var item in e.OldItems) + { + if (item is CustomPluginHotkey customPluginHotkey) + { + var hotkeyData = SearchRegisteredHotkeyData(customPluginHotkey); + _settings.RegisteredHotkeys.Remove(hotkeyData); + RemoveHotkey(hotkeyData); + } + } + foreach (var item in e.NewItems) + { + if (item is CustomPluginHotkey customPluginHotkey) + { + var hotkeyData = GetRegisteredHotkeyData(customPluginHotkey); + _settings.RegisteredHotkeys.Add(hotkeyData); + SetHotkey(hotkeyData); + } + } + break; + } } - private static void SetWithChefKeys(string hotkeyStr) + private static void PluginManager_PluginHotkeyChanged(PluginManager.PluginHotkeyChangedEvent e) { + var oldHotkey = e.OldHotkey; + var newHotkey = e.NewHotkey; + var metadata = e.Metadata; + var pluginHotkey = e.PluginHotkey; + + if (pluginHotkey is GlobalPluginHotkey globalPluginHotkey) + { + var hotkeyData = SearchRegisteredHotkeyData(metadata, globalPluginHotkey); + RemoveHotkey(hotkeyData); + hotkeyData.SetHotkey(newHotkey); + SetHotkey(hotkeyData); + } + else if (pluginHotkey is SearchWindowPluginHotkey) + { + // Search hotkey & Remove registered hotkey data & Unregister hotkeys + var oldHotkeyData = SearchRegisteredHotkeyData(RegisteredHotkeyType.PluginWindowHotkey, oldHotkey); + _settings.RegisteredHotkeys.Remove(oldHotkeyData); + RemoveHotkey(oldHotkeyData); + var newHotkeyData = SearchRegisteredHotkeyData(RegisteredHotkeyType.PluginWindowHotkey, newHotkey); + _settings.RegisteredHotkeys.Remove(newHotkeyData); + RemoveHotkey(newHotkeyData); + + // Get hotkey data & Add new registered hotkeys & Register hotkeys + var windowPluginHotkeys = PluginManager.GetWindowPluginHotkeys(); + if (windowPluginHotkeys.TryGetValue(oldHotkey, out var oldHotkeyModels)) + { + oldHotkeyData = GetRegisteredHotkeyData(oldHotkey, oldHotkeyModels); + _settings.RegisteredHotkeys.Add(oldHotkeyData); + SetHotkey(oldHotkeyData); + } + if (windowPluginHotkeys.TryGetValue(newHotkey, out var newHotkeyModels)) + { + newHotkeyData = GetRegisteredHotkeyData(newHotkey, newHotkeyModels); + _settings.RegisteredHotkeys.Add(newHotkeyData); + SetHotkey(newHotkeyData); + } + } + } + + #endregion + + #region Custom Query Hotkey + + private static RegisteredHotkeyData GetRegisteredHotkeyData(CustomPluginHotkey customPluginHotkey) + { + return new(RegisteredHotkeyType.CustomQuery, HotkeyType.Global, customPluginHotkey.Hotkey, "customQueryHotkey", CustomQueryHotkeyCommand, customPluginHotkey, () => ClearHotkeyForCustomQueryHotkey(customPluginHotkey)); + } + + private static RegisteredHotkeyData SearchRegisteredHotkeyData(CustomPluginHotkey customPluginHotkey) + { + return _settings.RegisteredHotkeys.FirstOrDefault(h => + h.RegisteredType == RegisteredHotkeyType.CustomQuery && + customPluginHotkey.Equals(h.CommandParameter)); + } + + private static void ClearHotkeyForCustomQueryHotkey(CustomPluginHotkey customPluginHotkey) + { + // Clear hotkey for custom query hotkey + customPluginHotkey.Hotkey = string.Empty; + + // Remove hotkey events + var hotkeyData = SearchRegisteredHotkeyData(customPluginHotkey); + _settings.RegisteredHotkeys.Remove(hotkeyData); + RemoveHotkey(hotkeyData); + } + + #endregion + + #region Plugin Hotkey + + private static RegisteredHotkeyData GetRegisteredHotkeyData(HotkeyModel hotkey, PluginMetadata metadata, GlobalPluginHotkey pluginHotkey) + { + Action removeHotkeyAction = pluginHotkey.Editable ? + () => PluginManager.ChangePluginHotkey(metadata, pluginHotkey, HotkeyModel.Empty) : null; + 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) + { + Action removeHotkeysAction = windowHotkeys.All(h => h.PluginHotkey.Editable) ? + () => + { + foreach (var (metadata, pluginHotkey) in windowHotkeys) + { + PluginManager.ChangePluginHotkey(metadata, pluginHotkey, HotkeyModel.Empty); + } + } : null; + return new(RegisteredHotkeyType.PluginWindowHotkey, HotkeyType.SearchWindow, hotkey, "pluginHotkey", WindowPluginHotkeyCommand, new WindowPluginHotkeyPair(hotkey, windowHotkeys), removeHotkeysAction); + } + + private static RegisteredHotkeyData SearchRegisteredHotkeyData(PluginMetadata metadata, GlobalPluginHotkey globalPluginHotkey) + { + return _settings.RegisteredHotkeys.FirstOrDefault(h => + h.RegisteredType == RegisteredHotkeyType.PluginGlobalHotkey && + h.CommandParameter is GlobalPluginHotkeyPair pair && + pair.Metadata.ID == metadata.ID && + pair.GlobalPluginHotkey.Id == globalPluginHotkey.Id); + } + + #endregion + + #region Hotkey Setting + + private static void SetHotkey(RegisteredHotkeyData hotkeyData) + { + if (hotkeyData is null || // Hotkey data is invalid + hotkeyData.Hotkey.IsEmpty || // Hotkey is none + hotkeyData.Command is null) // No need to set - it is a system command + { + return; + } + + if (hotkeyData.Type == HotkeyType.Global) + { + SetGlobalHotkey(hotkeyData); + } + else if (hotkeyData.Type == HotkeyType.SearchWindow) + { + SetWindowHotkey(hotkeyData); + } + } + + private static void RemoveHotkey(RegisteredHotkeyData hotkeyData) + { + if (hotkeyData is null || // Hotkey data is invalid + hotkeyData.Hotkey.IsEmpty) // Hotkey is none + { + return; + } + + if (hotkeyData.Type == HotkeyType.Global) + { + RemoveGlobalHotkey(hotkeyData); + } + else if (hotkeyData.Type == HotkeyType.SearchWindow) + { + RemoveWindowHotkey(hotkeyData); + } + } + + private static void SetGlobalHotkey(RegisteredHotkeyData hotkeyData) + { + var hotkey = hotkeyData.Hotkey; + var hotkeyStr = hotkey.ToString(); + var hotkeyCommand = hotkeyData.Command; + var hotkeyCommandParameter = hotkeyData.CommandParameter; try { - ChefKeysManager.RegisterHotkey(hotkeyStr, hotkeyStr, OnToggleHotkeyWithChefKeys); + if (hotkeyStr == "LWin" || hotkeyStr == "RWin") + { + SetGlobalHotkeyWithChefKeys(hotkeyData); + return; + } + + HotkeyManager.Current.AddOrReplace( + hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, + (s, e) => hotkeyCommand.Execute(hotkeyCommandParameter)); + } + catch (Exception e) + { + App.API.LogError(ClassName, $"Error registering hotkey {hotkeyStr}: {e.Message} \nStackTrace:{e.StackTrace}"); + var errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr); + var errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + App.API.ShowMsgBox(errorMsg, errorMsgTitle); + } + } + + private static void SetGlobalHotkeyWithChefKeys(RegisteredHotkeyData hotkeyData) + { + var hotkey = hotkeyData.Hotkey; + if (hotkey.IsEmpty) + { + return; + } + + var hotkeyStr = hotkey.ToString(); + var hotkeyCommand = hotkeyData.Command; + var hotkeyCommandParameter = hotkeyData.CommandParameter; + try + { + ChefKeysManager.RegisterHotkey(hotkeyStr, hotkeyStr, () => hotkeyCommand.Execute(hotkeyCommandParameter)); ChefKeysManager.Start(); } catch (Exception e) { App.API.LogError(ClassName, - string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}", + string.Format("Error registering hotkey: {0} \nStackTrace:{1}", e.Message, e.StackTrace)); string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr); @@ -62,39 +446,60 @@ private static void SetWithChefKeys(string hotkeyStr) } } - internal static void SetHotkey(HotkeyModel hotkey, EventHandler action) + private static void SetWindowHotkey(RegisteredHotkeyData hotkeyData) { - string hotkeyStr = hotkey.ToString(); + var hotkey = hotkeyData.Hotkey; + var hotkeyCommand = hotkeyData.Command; + var hotkeyCommandParameter = hotkeyData.CommandParameter; try { - if (hotkeyStr == "LWin" || hotkeyStr == "RWin") + if (Application.Current?.MainWindow is MainWindow window) { - SetWithChefKeys(hotkeyStr); - return; - } + // Check if the hotkey already exists + var keyGesture = hotkey.ToKeyGesture(); + var existingBinding = window.InputBindings + .OfType() + .FirstOrDefault(kb => + kb.Gesture is KeyGesture keyGesture1 && + keyGesture.Key == keyGesture1.Key && + keyGesture.Modifiers == keyGesture1.Modifiers); + if (existingBinding != null) + { + // If the hotkey is not a hotkey for ActionContext events, throw an exception to avoid duplicates + if (!IsActionContextEvent(window, existingBinding, hotkey)) + { + throw new InvalidOperationException($"Windows key {hotkey} already exists"); + } + } - HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action); + // Add the new hotkey binding + var keyBinding = new KeyBinding() + { + Gesture = keyGesture, + Command = hotkeyCommand, + CommandParameter = hotkeyCommandParameter + }; + window.InputBindings.Add(keyBinding); + } } catch (Exception e) { - App.API.LogError(ClassName, - string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}", - e.Message, - e.StackTrace, - hotkeyStr)); - string errorMsg = string.Format(App.API.GetTranslation("registerHotkeyFailed"), hotkeyStr); - string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + App.API.LogError(ClassName, $"Error registering window hotkey {hotkey}: {e.Message} \nStackTrace:{e.StackTrace}"); + var errorMsg = string.Format(App.API.GetTranslation("registerWindowHotkeyFailed"), hotkey); + var errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); App.API.ShowMsgBox(errorMsg, errorMsgTitle); } } - internal static void RemoveHotkey(string hotkeyStr) + private static void RemoveGlobalHotkey(RegisteredHotkeyData hotkeyData) { + var hotkey = hotkeyData.Hotkey; + var hotkeyStr = hotkey.ToString(); try { if (hotkeyStr == "LWin" || hotkeyStr == "RWin") { - RemoveWithChefKeys(hotkeyStr); + RemoveGlobalHotkeyWithChefKeys(hotkeyData); return; } @@ -103,45 +508,181 @@ internal static void RemoveHotkey(string hotkeyStr) } catch (Exception e) { - App.API.LogError(ClassName, - string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}", - e.Message, - e.StackTrace)); - string errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr); - string errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + App.API.LogError(ClassName, $"Error removing hotkey: {e.Message} \nStackTrace:{e.StackTrace}"); + var errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr); + var errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); App.API.ShowMsgBox(errorMsg, errorMsgTitle); } } - private static void RemoveWithChefKeys(string hotkeyStr) + private static void RemoveGlobalHotkeyWithChefKeys(RegisteredHotkeyData hotkeyData) { - ChefKeysManager.UnregisterHotkey(hotkeyStr); - ChefKeysManager.Stop(); + var hotkey = hotkeyData.Hotkey; + var hotkeyStr = hotkey.ToString(); + try + { + ChefKeysManager.UnregisterHotkey(hotkeyStr); + ChefKeysManager.Stop(); + } + catch (Exception e) + { + App.API.LogError(ClassName, $"Error removing hotkey: {e.Message} \nStackTrace:{e.StackTrace}"); + var errorMsg = string.Format(App.API.GetTranslation("unregisterHotkeyFailed"), hotkeyStr); + var errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + App.API.ShowMsgBox(errorMsg, errorMsgTitle); + } } - internal static void LoadCustomPluginHotkey() + private static void RemoveWindowHotkey(RegisteredHotkeyData hotkeyData) { - if (_settings.CustomPluginHotkeys == null) - return; + var hotkey = hotkeyData.Hotkey; + try + { + if (Application.Current?.MainWindow is MainWindow window) + { + // Remove the key binding + var keyGesture = hotkey.ToKeyGesture(); + var existingBinding = window.InputBindings + .OfType() + .FirstOrDefault(kb => + kb.Gesture is KeyGesture keyGesture1 && + keyGesture.Key == keyGesture1.Key && + keyGesture.Modifiers == keyGesture1.Modifiers); + if (existingBinding != null) + { + window.InputBindings.Remove(existingBinding); + } + + // Restore the key binding for ActionContext events + RestoreActionContextEvent(hotkey, keyGesture); + } + } + catch (Exception e) + { + App.API.LogError(ClassName, $"Error removing window hotkey: {e.Message} \nStackTrace:{e.StackTrace}"); + var errorMsg = string.Format(App.API.GetTranslation("unregisterWindowHotkeyFailed"), hotkey); + var errorMsgTitle = App.API.GetTranslation("MessageBoxTitle"); + App.API.ShowMsgBox(errorMsg, errorMsgTitle); + } + } + + #endregion + + #region Hotkey Changing - foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys) + private static void ChangeRegisteredHotkey(RegisteredHotkeyType registeredType, string newHotkeyStr) + { + var newHotkey = new HotkeyModel(newHotkeyStr); + ChangeRegisteredHotkey(registeredType, newHotkey); + } + + private static void ChangeRegisteredHotkey(RegisteredHotkeyType registeredType, HotkeyModel newHotkey) + { + // Find the old registered hotkey data item + var registeredHotkeyData = _settings.RegisteredHotkeys.FirstOrDefault(h => h.RegisteredType == registeredType); + + // If it is not found, return + if (registeredHotkeyData == null) { - SetCustomQueryHotkey(hotkey); + return; } + + // Remove the old hotkey + RemoveHotkey(registeredHotkeyData); + + // Update the hotkey string + registeredHotkeyData.SetHotkey(newHotkey); + + // Set the new hotkey + SetHotkey(registeredHotkeyData); + } + + #endregion + + #region Hotkey Searching + + private static RegisteredHotkeyData SearchRegisteredHotkeyData(RegisteredHotkeyType registeredHotkeyType, HotkeyModel hotkeyModel) + { + return _settings.RegisteredHotkeys.FirstOrDefault(h => + h.RegisteredType == registeredHotkeyType && + h.Hotkey.Equals(hotkeyModel)); + } + + #endregion + + #region Commands + + private static RelayCommand _customQueryHotkeyCommand; + private static IRelayCommand CustomQueryHotkeyCommand => _customQueryHotkeyCommand ??= new RelayCommand(CustomQueryHotkey); + + private static RelayCommand _globalPluginHotkeyCommand; + private static IRelayCommand GlobalPluginHotkeyCommand => _globalPluginHotkeyCommand ??= new RelayCommand(GlobalPluginHotkey); + + private static RelayCommand _windowPluginHotkeyCommand; + private static IRelayCommand WindowPluginHotkeyCommand => _windowPluginHotkeyCommand ??= new RelayCommand(WindowPluginHotkey); + + private static void CustomQueryHotkey(CustomPluginHotkey customPluginHotkey) + { + if (_mainViewModel.ShouldIgnoreHotkeys()) + return; + + App.API.ShowMainWindow(); + App.API.ChangeQuery(customPluginHotkey.ActionKeyword, true); + } + + private static void GlobalPluginHotkey(GlobalPluginHotkeyPair pair) + { + var metadata = pair.Metadata; + var pluginHotkey = pair.GlobalPluginHotkey; + + if (metadata.Disabled || // Check plugin enabled state + App.API.PluginModified(metadata.ID)) // Check plugin modified state + return; + + if (_mainViewModel.ShouldIgnoreHotkeys()) + return; + + pluginHotkey.Action?.Invoke(); } - internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey) + private static void WindowPluginHotkey(WindowPluginHotkeyPair pair) { - SetHotkey(hotkey.Hotkey, (s, e) => + // Get selected result + var selectedResult = _mainViewModel.GetSelectedResults().SelectedItem?.Result; + + // Check result nullability + if (selectedResult != null) { - if (_mainViewModel.ShouldIgnoreHotkeys()) + var pluginId = selectedResult.PluginID; + foreach (var hotkeyModel in pair.HotkeyModels) + { + var metadata = hotkeyModel.Metadata; + var pluginHotkey = hotkeyModel.PluginHotkey; + + if (metadata.ID != pluginId || // Check plugin ID match + metadata.Disabled || // Check plugin enabled state + App.API.PluginModified(metadata.ID) || // Check plugin modified state + !selectedResult.HotkeyIds.Contains(pluginHotkey.Id) || // Check hotkey supported state + pluginHotkey.Action == null) // Check action nullability + continue; + + // TODO: Remove return to skip other commands + if (pluginHotkey.Action.Invoke(selectedResult)) + App.API.HideMainWindow(); + + // Return after invoking the first matching hotkey action so that we will not invoke action context event return; + } - App.API.ShowMainWindow(); - App.API.ChangeQuery(hotkey.ActionKeyword, true); - }); + // When no plugin hotkey action is invoked, invoke the action context event + InvokeActionContextEvent(pair.Hotkey); + } } + #endregion + + #region Check Hotkey + internal static bool CheckAvailability(HotkeyModel currentHotkey) { try @@ -160,4 +701,109 @@ internal static bool CheckAvailability(HotkeyModel currentHotkey) return false; } + + #endregion + + #region Action Context Hotkey (Obsolete) + + [Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")] + private static List _actionContextRegisteredHotkeys; + + [Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")] + private static readonly Dictionary _actionContextHotkeyEvents = new(); + + [Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")] + private static void InitializeActionContextHotkeys() + { + // Fixed hotkeys for ActionContext + _actionContextRegisteredHotkeys = new List + { + new(RegisteredHotkeyType.CtrlShiftEnter, HotkeyType.SearchWindow, "Ctrl+Shift+Enter", "HotkeyCtrlShiftEnterDesc", _mainViewModel.OpenResultCommand), + new(RegisteredHotkeyType.CtrlEnter, HotkeyType.SearchWindow, "Ctrl+Enter", "OpenContainFolderHotkey", _mainViewModel.OpenResultCommand), + new(RegisteredHotkeyType.AltEnter, HotkeyType.SearchWindow, "Alt+Enter", "HotkeyOpenResult", _mainViewModel.OpenResultCommand), + }; + + // Register ActionContext hotkeys and they will be cached and restored in _actionContextHotkeyEvents + foreach (var hotkey in _actionContextRegisteredHotkeys) + { + _actionContextHotkeyEvents[hotkey.Hotkey] = (hotkey.Command, hotkey.CommandParameter); + SetWindowHotkey(hotkey); + } + } + + [Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")] + private static bool IsActionContextEvent(MainWindow window, KeyBinding existingBinding, HotkeyModel hotkey) + { + // Check if this hotkey is a hotkey for ActionContext events + if (!_actionContextHotkeyEvents.ContainsKey(hotkey) && + _actionContextHotkeyEvents[hotkey].Command == existingBinding.Command || + _actionContextHotkeyEvents[hotkey].Parameter == existingBinding.CommandParameter) + { + // If the hotkey is not for ActionContext events, return false + return true; + } + + return false; + } + + [Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")] + private static void RestoreActionContextEvent(HotkeyModel hotkey, KeyGesture keyGesture) + { + // Restore the ActionContext event by adding the key binding back + if (_actionContextHotkeyEvents.TryGetValue(hotkey, out var actionContextItem)) + { + if (Application.Current?.MainWindow is MainWindow window) + { + var keyBinding = new KeyBinding + { + Gesture = keyGesture, + Command = actionContextItem.Command, + CommandParameter = actionContextItem.Parameter + }; + window.InputBindings.Add(keyBinding); + } + } + } + + [Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")] + private static void InvokeActionContextEvent(HotkeyModel hotkey) + { + if (_actionContextHotkeyEvents.TryGetValue(hotkey, out var actionContextItem)) + { + actionContextItem.Command.Execute(actionContextItem.Parameter); + } + } + + #endregion + + #region Private Classes + + private class GlobalPluginHotkeyPair + { + public PluginMetadata Metadata { get; } + + public GlobalPluginHotkey GlobalPluginHotkey { get; } + + public GlobalPluginHotkeyPair(PluginMetadata metadata, GlobalPluginHotkey globalPluginHotkey) + { + Metadata = metadata; + GlobalPluginHotkey = globalPluginHotkey; + } + } + + private class WindowPluginHotkeyPair + { + [Obsolete("ActionContext support is deprecated and will be removed in a future release. Please use IPluginHotkey instead.")] + public HotkeyModel Hotkey { get; } + + public List<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> HotkeyModels { get; } + + public WindowPluginHotkeyPair(HotkeyModel hotkey, List<(PluginMetadata Metadata, SearchWindowPluginHotkey PluginHotkey)> hotkeys) + { + Hotkey = hotkey; + HotkeyModels = hotkeys; + } + } + + #endregion } diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index e8961058cdf..9e3b28f0a1c 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -110,7 +110,10 @@ public enum HotkeyType SelectPrevItemHotkey, SelectPrevItemHotkey2, SelectNextItemHotkey, - SelectNextItemHotkey2 + SelectNextItemHotkey2, + // Plugin hotkeys + GlobalPluginHotkey, + WindowPluginHotkey, } // We can initialize settings in static field because it has been constructed in App constuctor @@ -142,6 +145,9 @@ public string Hotkey HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2, HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey, HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2, + // Plugin hotkeys + HotkeyType.GlobalPluginHotkey => hotkey, + HotkeyType.WindowPluginHotkey => hotkey, _ => throw new System.NotImplementedException("Hotkey type not set") }; } @@ -201,6 +207,17 @@ public string Hotkey case HotkeyType.SelectNextItemHotkey2: _settings.SelectNextItemHotkey2 = value; break; + // Plugin hotkeys + case HotkeyType.GlobalPluginHotkey: + // We should not save it to settings here because it is a custom plugin hotkey + // and it will be saved in the plugin settings + hotkey = value; + break; + case HotkeyType.WindowPluginHotkey: + // We should not save it to settings here because it is a custom plugin hotkey + // and it will be saved in the plugin settings + hotkey = value; + break; default: throw new System.NotImplementedException("Hotkey type not set"); } @@ -242,11 +259,6 @@ public void GetNewHotkey(object sender, RoutedEventArgs e) private async Task OpenHotkeyDialogAsync() { - if (!string.IsNullOrEmpty(Hotkey)) - { - HotKeyMapper.RemoveHotkey(Hotkey); - } - var dialog = new HotkeyControlDialog(Hotkey, DefaultHotkey, WindowTitle) { Owner = Window.GetWindow(this) @@ -300,8 +312,6 @@ private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true) public void Delete() { - if (!string.IsNullOrEmpty(Hotkey)) - HotKeyMapper.RemoveHotkey(Hotkey); Hotkey = ""; SetKeysToDisplay(new HotkeyModel(false, false, false, false, Key.None)); } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index bd4cbd28247..96a31005278 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -21,6 +21,8 @@ Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. Failed to unregister hotkey "{0}". Please try again or see log for details + Failed to register window hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Failed to unregister window hotkey "{0}". Please try again or see log for details Flow Launcher Could not start {0} Invalid Flow Launcher plugin file format @@ -313,6 +315,7 @@ Show Result Badges For supported plugins, badges are displayed to help distinguish them more easily. Show Result Badges for Global Query Only + Plugin hotkey HTTP Proxy @@ -429,13 +432,14 @@ Press a custom hotkey to open Flow Launcher and input the specified query automatically. Preview Hotkey is unavailable, please select a new hotkey - Invalid plugin hotkey + Hotkey is invalid Update Binding Hotkey Current hotkey is unavailable. This hotkey is reserved for "{0}" and can't be used. Please choose another hotkey. This hotkey is already in use by "{0}". If you press "Overwrite", it will be removed from "{0}". Press the keys you want to use for this function. + Hotkey and action keyword are empty Custom Query Shortcut @@ -444,6 +448,7 @@ Shortcut already exists, please enter a new Shortcut or edit the existing one. Shortcut and/or its expansion is empty. + Shortcut is invalid Save diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 9ff38a56442..70b6c1bf49e 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -49,169 +49,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml index cf0dff9ab37..8ea4974f19e 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage2.xaml +++ b/Flow.Launcher/Resources/Pages/WelcomePage2.xaml @@ -38,7 +38,7 @@ - + @@ -90,11 +90,12 @@ - + + Text="{DynamicResource Welcome_Page2_Title}" + TextWrapping="WrapWithOverflow" /> WallpaperPathRetrieval.GetWallpaperBrush(); diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs index 7a7c19dd358..2cd3b66c440 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneHotkeyViewModel.cs @@ -1,9 +1,7 @@ using System.Linq; using System.Windows; using CommunityToolkit.Mvvm.Input; -using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -28,12 +26,6 @@ public SettingsPaneHotkeyViewModel(Settings settings) Settings = settings; } - [RelayCommand] - private void SetTogglingHotkey(HotkeyModel hotkey) - { - HotKeyMapper.SetHotkey(hotkey, HotKeyMapper.OnToggleHotkey); - } - [RelayCommand] private void CustomHotkeyDelete() { @@ -55,7 +47,6 @@ private void CustomHotkeyDelete() if (result is MessageBoxResult.Yes) { Settings.CustomPluginHotkeys.Remove(item); - HotKeyMapper.RemoveHotkey(item.Hotkey); } } @@ -69,15 +60,30 @@ private void CustomHotkeyEdit() return; } - var window = new CustomQueryHotkeySetting(Settings); - window.UpdateItem(item); - window.ShowDialog(); + var settingItem = Settings.CustomPluginHotkeys.FirstOrDefault(o => + o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey); + if (settingItem == null) + { + App.API.ShowMsgBox(App.API.GetTranslation("invalidPluginHotkey")); + return; + } + + var window = new CustomQueryHotkeySetting(settingItem); + if (window.ShowDialog() is not true) return; + + var index = Settings.CustomPluginHotkeys.IndexOf(settingItem); + Settings.CustomPluginHotkeys[index] = new CustomPluginHotkey(window.Hotkey, window.ActionKeyword); } [RelayCommand] private void CustomHotkeyAdd() { - new CustomQueryHotkeySetting(Settings).ShowDialog(); + var window = new CustomQueryHotkeySetting(); + if (window.ShowDialog() is true) + { + var customHotkey = new CustomPluginHotkey(window.Hotkey, window.ActionKeyword); + Settings.CustomPluginHotkeys.Add(customHotkey); + } } [RelayCommand] @@ -114,10 +120,18 @@ private void CustomShortcutEdit() return; } - var window = new CustomShortcutSetting(item.Key, item.Value, this); + var settingItem = Settings.CustomShortcuts.FirstOrDefault(o => + o.Key == item.Key && o.Value == item.Value); + if (settingItem == null) + { + App.API.ShowMsgBox(App.API.GetTranslation("invalidShortcut")); + return; + } + + var window = new CustomShortcutSetting(settingItem.Key, settingItem.Value, this); if (window.ShowDialog() is not true) return; - var index = Settings.CustomShortcuts.IndexOf(item); + var index = Settings.CustomShortcuts.IndexOf(settingItem); Settings.CustomShortcuts[index] = new CustomShortcutModel(window.Key, window.Value); } diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml index d7f5772bbf7..a211d67101b 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneHotkey.xaml @@ -32,7 +32,6 @@ Icon="" Sub="{DynamicResource flowlauncherHotkeyToolTip}"> - + + !h.Visible); + if (allHotkeyInvisible) continue; + + var excard = new ExCard() + { + Title = metadata.Name, + Margin = new Thickness(0, 4, 0, 0), + }; + var hotkeyStackPanel = new StackPanel + { + Orientation = Orientation.Vertical + }; + + var sortedHotkeyInfo = hotkeyInfo.OrderBy(h => h.Id).ToList(); + foreach (var hotkey in hotkeyInfo) + { + // Skip invisible hotkeys + if (!hotkey.Visible) continue; + + var card = new Card() + { + Title = hotkey.Name, + Sub = hotkey.Description, + Icon = hotkey.Glyph.Glyph, + Type = Card.CardType.Inside + }; + var hotkeySetting = metadata.PluginHotkeys.Find(h => h.Id == hotkey.Id)?.Hotkey ?? hotkey.DefaultHotkey; + if (hotkey.Editable) + { + var hotkeyControl = new HotkeyControl + { + Type = hotkey.HotkeyType == HotkeyType.Global ? + HotkeyControl.HotkeyType.GlobalPluginHotkey : + HotkeyControl.HotkeyType.WindowPluginHotkey, + DefaultHotkey = hotkey.DefaultHotkey, + ValidateKeyGesture = true + }; + hotkeyControl.SetHotkey(hotkeySetting, true); + hotkeyControl.ChangeHotkey = new RelayCommand((h) => ChangePluginHotkey(metadata, hotkey, h)); + card.Content = hotkeyControl; + } + else + { + var hotkeyDisplay = new HotkeyDisplay + { + Keys = hotkeySetting + }; + card.Content = hotkeyDisplay; + } + hotkeyStackPanel.Children.Add(card); + } + excard.Content = hotkeyStackPanel; + PluginHotkeySettings.Children.Add(excard); + } + } + + private static void ChangePluginHotkey(PluginMetadata metadata, BasePluginHotkey pluginHotkey, HotkeyModel newHotkey) + { + if (pluginHotkey is GlobalPluginHotkey globalPluginHotkey) + { + PluginManager.ChangePluginHotkey(metadata, globalPluginHotkey, newHotkey); + } + else if (pluginHotkey is SearchWindowPluginHotkey windowPluginHotkey) + { + PluginManager.ChangePluginHotkey(metadata, windowPluginHotkey, newHotkey); + } + } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index dbea62a6bb0..e7a3171e3a8 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -509,6 +509,13 @@ private static IReadOnlyList DeepCloneResults(IReadOnlyList resu #region BasicCommands + [RelayCommand] + private void CheckAndToggleFlowLauncher() + { + if (!ShouldIgnoreHotkeys()) + ToggleFlowLauncher(); + } + [RelayCommand] private void OpenSetting() { @@ -1725,6 +1732,11 @@ internal bool ResultsSelected(ResultsViewModel results) return selected; } + internal ResultsViewModel GetSelectedResults() + { + return SelectedResults; + } + #endregion #region Hotkey diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs index c331c498568..f01852b43dc 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs @@ -10,6 +10,8 @@ using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; using Flow.Launcher.Plugin.Explorer.Helper; using Flow.Launcher.Plugin.Explorer.ViewModels; +using Flow.Launcher.Plugin.Explorer.Views; +using System.Windows.Controls; namespace Flow.Launcher.Plugin.Explorer { @@ -188,6 +190,35 @@ public List LoadContextMenus(Result selectedResult) IcoPath = icoPath, Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uf12b") }); + contextMenus.Add(new Result + { + Title = Context.API.GetTranslation("plugin_explorer_rename_a_file"), + SubTitle = Context.API.GetTranslation("plugin_explorer_rename_subtitle"), + Action = _ => + { + RenameFile window; + switch (record.Type) + { + case ResultType.Folder: + window = new RenameFile(Context.API, new DirectoryInfo(record.FullPath)); + break; + case ResultType.File: + window = new RenameFile(Context.API, new FileInfo(record.FullPath)); + break; + default: + Context.API.ShowMsgError(Context.API.GetTranslation("plugin_explorer_cannot_rename")); + return false; + } + window.ShowDialog(); + + return false; + + }, + // placeholder until real image is found + IcoPath = Constants.RenameImagePath, + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8ac") + + }); if (record.Type is ResultType.File or ResultType.Folder) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Helper/RenameThing.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/RenameThing.cs new file mode 100644 index 00000000000..7743bc5275c --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/RenameThing.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using System.Runtime.Serialization; + +namespace Flow.Launcher.Plugin.Explorer.Helper; + +public static class RenameThing +{ + private static void Rename(this FileSystemInfo info, string newName) + { + if (info is FileInfo file) + { + if (!SharedCommands.FilesFolders.IsValidFileName(newName)) + { + throw new InvalidNameException(); + } + DirectoryInfo directory; + var rootPath = Path.GetPathRoot(file.FullName); + if (string.IsNullOrEmpty(rootPath)) return; + directory = file.Directory ?? new DirectoryInfo(rootPath); + string newPath = Path.Join(directory.FullName, newName); + if (info.FullName == newPath) + { + throw new NotANewNameException("New name was the same as the old name"); + } + if (File.Exists(newPath)) throw new ElementAlreadyExistsException(); + File.Move(info.FullName, newPath); + return; + } + else if (info is DirectoryInfo directory) + { + if (!SharedCommands.FilesFolders.IsValidDirectoryName(newName)) + { + throw new InvalidNameException(); + } + DirectoryInfo parent; + var rootPath = Path.GetPathRoot(directory.FullName); + if (string.IsNullOrEmpty(rootPath)) return; + parent = directory.Parent ?? new DirectoryInfo(rootPath); + string newPath = Path.Join(parent.FullName, newName); + if (info.FullName == newPath) + { + throw new NotANewNameException("New name was the same as the old name"); + } + if (Directory.Exists(newPath)) throw new ElementAlreadyExistsException(); + + Directory.Move(info.FullName, newPath); + + } + else + { + throw new ArgumentException($"{nameof(info)} must be either, {nameof(FileInfo)} or {nameof(DirectoryInfo)}"); + } + } + + /// + /// Renames a file system element (directory or file) + /// + /// The requested new name + /// The or representing the old file + /// An instance of so this can create msgboxes + public static void Rename(string NewFileName, FileSystemInfo oldInfo, IPublicAPI api) + { + // if it's just whitespace and nothing else + if (NewFileName.Trim() == "" || NewFileName == "") + { + api.ShowMsgError(string.Format(api.GetTranslation("plugin_explorer_field_may_not_be_empty"), "New file name")); + return; + } + + try + { + oldInfo.Rename(NewFileName); + } + catch (Exception exception) + { + switch (exception) + { + case FileNotFoundException: + api.ShowMsgError(string.Format(api.GetTranslation("plugin_explorer_file_not_found"), oldInfo.FullName)); + return; + case NotANewNameException: + api.ShowMsgError(string.Format(api.GetTranslation("plugin_explorer_not_a_new_name"), NewFileName)); + return; + case InvalidNameException: + api.ShowMsgError(string.Format(api.GetTranslation("plugin_explorer_invalid_name"), NewFileName)); + return; + case ElementAlreadyExistsException: + api.ShowMsgError(string.Format(api.GetTranslation("plugin_explorer_element_already_exists"), NewFileName)); + break; + default: + string msg = exception.Message; + if (!string.IsNullOrEmpty(msg)) + { + api.ShowMsgError(string.Format(api.GetTranslation("plugin_explorer_exception"), exception.Message)); + return; + } + else + { + api.ShowMsgError(api.GetTranslation("plugin_explorer_no_reason_given_exception")); + } + + return; + } + } + api.ShowMsg(string.Format(api.GetTranslation("plugin_explorer_successful_rename"), NewFileName)); + } +} + +internal class NotANewNameException : IOException +{ + public NotANewNameException() { } + public NotANewNameException(string message) : base(message) { } + public NotANewNameException(string message, Exception inner) : base(message, inner) { } + protected NotANewNameException( + SerializationInfo info, + StreamingContext context) : base(info, context) { } +} + internal class ElementAlreadyExistsException : IOException { + public ElementAlreadyExistsException() { } + public ElementAlreadyExistsException(string message) : base(message) { } + public ElementAlreadyExistsException(string message, Exception inner) : base(message, inner) { } + protected ElementAlreadyExistsException( + SerializationInfo info, + StreamingContext context) : base(info, context) { } +} + +internal class InvalidNameException : Exception +{ + public InvalidNameException() { } + public InvalidNameException(string message) : base(message) { } + public InvalidNameException(string message, Exception inner) : base(message, inner) { } + protected InvalidNameException( + SerializationInfo info, + StreamingContext context) : base(info, context) { } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/rename.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/rename.png new file mode 100644 index 00000000000..ad70eb8e7b8 Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/rename.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index 2e0f6a67db3..324a14c6155 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -130,6 +130,7 @@ Show Windows Context Menu Open With Select a program to open with + Run As Administrator {0} free of {1} @@ -189,4 +190,20 @@ {0} months ago 1 year ago {0} years ago + + + New name + Rename + Rename + The given name: {0} was not new. + {0} may not be empty. + {0} is an invalid name. + The specified item: {0} was not found + Open a dialog to rename file or folder + This cannot be renamed. + Successfully renamed it to: {0} + There is already a file with the name: {0} in this location + Failed to open rename dialog. + An error occurred: {0}. + An error occurred and no reason was given. diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 0d1d99f8a1b..136de3c5efd 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -1,24 +1,26 @@ -using Flow.Launcher.Plugin.Explorer.Helper; -using Flow.Launcher.Plugin.Explorer.Search; -using Flow.Launcher.Plugin.Explorer.Search.Everything; -using Flow.Launcher.Plugin.Explorer.ViewModels; -using Flow.Launcher.Plugin.Explorer.Views; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using Flow.Launcher.Plugin.Explorer.Exceptions; +using Flow.Launcher.Plugin.Explorer.Helper; +using Flow.Launcher.Plugin.Explorer.Search; +using Flow.Launcher.Plugin.Explorer.Search.Everything; +using Flow.Launcher.Plugin.Explorer.ViewModels; +using Flow.Launcher.Plugin.Explorer.Views; namespace Flow.Launcher.Plugin.Explorer { - public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n + public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IPluginHotkey { internal static PluginInitContext Context { get; set; } internal static Settings Settings { get; set; } + private static readonly string ClassName = nameof(Main); + private SettingsViewModel viewModel; private IContextMenu contextMenu; @@ -42,11 +44,12 @@ public Task InitAsync(PluginInitContext context) contextMenu = new ContextMenu(Context, Settings, viewModel); searchManager = new SearchManager(Settings, Context); ResultManager.Init(Context, Settings); - + SortOptionTranslationHelper.API = context.API; EverythingApiDllImport.Load(Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "EverythingSDK", Environment.Is64BitProcess ? "x64" : "x86")); + return Task.CompletedTask; } @@ -108,5 +111,148 @@ private void FillQuickAccessLinkNames() } } } + + public List GetPluginHotkeys() + { + return new List + { + new SearchWindowPluginHotkey() + { + Id = 0, + Name = Context.API.GetTranslation("plugin_explorer_opencontainingfolder"), + Description = Context.API.GetTranslation("plugin_explorer_opencontainingfolder_subtitle"), + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue838"), + DefaultHotkey = "Ctrl+Enter", + Editable = false, + Visible = true, + Action = (r) => + { + if (r.ContextData is SearchResult record) + { + if (record.Type is ResultType.File) + { + ResultManager.OpenFolder(record.FullPath, record.FullPath); + } + else + { + try + { + Context.API.OpenDirectory(Path.GetDirectoryName(record.FullPath), record.FullPath); + } + catch (Exception e) + { + var message = $"Fail to open file at {record.FullPath}"; + Context.API.LogException(ClassName, message, e); + Context.API.ShowMsgBox(e.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); + return false; + } + + return true; + } + } + + return false; + } + }, + new SearchWindowPluginHotkey() + { + Id = 1, + Name = Context.API.GetTranslation("plugin_explorer_show_contextmenu_title"), + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue700"), + DefaultHotkey = "Alt+Enter", + Editable = false, + Visible = true, + Action = (r) => + { + if (r.ContextData is SearchResult record && record.Type is not ResultType.Volume) + { + try + { + ResultManager.ShowNativeContextMenu(record.FullPath, record.Type); + } + catch (Exception e) + { + var message = $"Fail to show context menu for {record.FullPath}"; + Context.API.LogException(ClassName, message, e); + } + } + + return false; + } + }, + new SearchWindowPluginHotkey() + { + Id = 2, + Name = Context.API.GetTranslation("plugin_explorer_run_as_administrator"), + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE7EF"), + DefaultHotkey = "Ctrl+Shift+Enter", + Editable = false, + Visible = true, + Action = (r) => + { + if (r.ContextData is SearchResult record) + { + if (record.Type is ResultType.File) + { + var filePath = record.FullPath; + ResultManager.OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty, true); + } + else + { + try + { + ResultManager.OpenFolder(record.FullPath); + return true; + } + catch (Exception ex) + { + var message = $"Fail to open file at {record.FullPath}"; + Context.API.LogException(ClassName, message, ex); + Context.API.ShowMsgBox(ex.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); + return false; + } + } + return true; + } + + return false; + } + }, + new SearchWindowPluginHotkey() + { + Id = 3, + Name = Context.API.GetTranslation("plugin_explorer_rename_a_file"), + Description = Context.API.GetTranslation("plugin_explorer_rename_subtitle"), + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8ac"), + DefaultHotkey = "F2", + Editable = true, + Visible = true, + Action = (r) => + { + if (r.ContextData is SearchResult record) + { + RenameFile window; + switch (record.Type) + { + case ResultType.Folder: + window = new RenameFile(Context.API, new DirectoryInfo(record.FullPath)); + break; + case ResultType.File: + window = new RenameFile(Context.API, new FileInfo(record.FullPath)); + break; + default: + Context.API.ShowMsgError(Context.API.GetTranslation("plugin_explorer_cannot_rename")); + return false; + } + window.ShowDialog(); + + return false; + } + + return false; + } + } + }; + } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs index 4bddfd9b27d..16a557af750 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs @@ -17,6 +17,7 @@ internal static class Constants internal const string QuickAccessImagePath = "Images\\quickaccess.png"; internal const string RemoveQuickAccessImagePath = "Images\\removequickaccess.png"; internal const string ShowContextMenuImagePath = "Images\\context_menu.png"; + internal const string RenameImagePath = "Images\\rename.png"; internal const string EverythingErrorImagePath = "Images\\everything_error.png"; internal const string IndexSearchWarningImagePath = "Images\\index_error.png"; internal const string WindowsIndexErrorImagePath = "Images\\index_error2.png"; diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index 7791a98817c..68c1f0ccc38 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -1,9 +1,9 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows.Controls; -using System.Windows.Input; using Flow.Launcher.Plugin.Explorer.Search.Everything; using Flow.Launcher.Plugin.Explorer.Views; using Flow.Launcher.Plugin.SharedCommands; @@ -108,40 +108,6 @@ internal static Result CreateFolderResult(string title, string subtitle, string PreviewPanel = new Lazy(() => new PreviewPanel(Settings, path, ResultType.Folder)), Action = c => { - if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) - { - ShowNativeContextMenu(path, ResultType.Folder); - return false; - } - // open folder - if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift)) - { - try - { - OpenFolder(path); - return true; - } - catch (Exception ex) - { - Context.API.ShowMsgBox(ex.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); - return false; - } - } - // Open containing folder - if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control) - { - try - { - Context.API.OpenDirectory(Path.GetDirectoryName(path), path); - return true; - } - catch (Exception ex) - { - Context.API.ShowMsgBox(ex.Message, Context.API.GetTranslation("plugin_explorer_opendir_error")); - return false; - } - } - // If path search is disabled just open it in file manager if (Settings.DefaultOpenFolderInFileManager || (!Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled)) { @@ -167,7 +133,11 @@ internal static Result CreateFolderResult(string title, string subtitle, string Score = score, TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenDirectory"), SubTitleToolTip = Settings.DisplayMoreInformationInToolTip ? GetFolderMoreInfoTooltip(path) : path, - ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed } + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed }, + HotkeyIds = new List + { + 0, 1, 2, 3 + }, }; } @@ -269,15 +239,14 @@ internal static Result CreateOpenCurrentFolderResult(string path, string actionK CopyText = folderPath, Action = c => { - if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) - { - ShowNativeContextMenu(folderPath, ResultType.Folder); - return false; - } OpenFolder(folderPath); return true; }, - ContextData = new SearchResult { Type = ResultType.Folder, FullPath = folderPath, WindowsIndexed = windowsIndexed } + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = folderPath, WindowsIndexed = windowsIndexed }, + HotkeyIds = new List + { + 1 + }, }; } @@ -306,25 +275,9 @@ internal static Result CreateFileResult(string filePath, Query query, int score PreviewPanel = new Lazy(() => new PreviewPanel(Settings, filePath, ResultType.File)), Action = c => { - if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Alt) - { - ShowNativeContextMenu(filePath, ResultType.File); - return false; - } try { - if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift)) - { - OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty, true); - } - else if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control) - { - OpenFolder(filePath, filePath); - } - else - { - OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty); - } + OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty); } catch (Exception ex) { @@ -335,7 +288,11 @@ internal static Result CreateFileResult(string filePath, Query query, int score }, TitleToolTip = Main.Context.API.GetTranslation("plugin_explorer_plugin_ToolTipOpenContainingFolder"), SubTitleToolTip = Settings.DisplayMoreInformationInToolTip ? GetFileMoreInfoTooltip(filePath) : filePath, - ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed } + ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed }, + HotkeyIds = new List + { + 0, 1, 2, 3 + }, }; return result; } @@ -347,13 +304,13 @@ private static bool IsMedia(string extension) return MediaExtensions.Contains(extension.ToLowerInvariant()); } - private static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false) + public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false) { IncrementEverythingRunCounterIfNeeded(filePath); FilesFolders.OpenFile(filePath, workingDir, asAdmin, (string str) => Context.API.ShowMsgBox(str)); } - private static void OpenFolder(string folderPath, string fileNameOrFilePath = null) + public static void OpenFolder(string folderPath, string fileNameOrFilePath = null) { IncrementEverythingRunCounterIfNeeded(folderPath); Context.API.OpenDirectory(folderPath, fileNameOrFilePath); diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/RenameFile.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/RenameFile.xaml new file mode 100644 index 00000000000..ded2d92844b --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/RenameFile.xaml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/RenameFile.xaml.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Views/RenameFile.xaml.cs new file mode 100644 index 00000000000..323bb912fc9 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/RenameFile.xaml.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using Flow.Launcher.Plugin.Explorer.Helper; + +namespace Flow.Launcher.Plugin.Explorer.Views +{ + [INotifyPropertyChanged] + public partial class RenameFile : Window + { + public string NewFileName + { + get => _newFileName; + set + { + _ = SetProperty(ref _newFileName, value); + } + } + + private string _newFileName; + + private readonly IPublicAPI _api; + private readonly string _oldFilePath; + + private readonly FileSystemInfo _info; + + public RenameFile(IPublicAPI api, FileSystemInfo info) + { + _api = api; + _info = info; + _oldFilePath = _info.FullName; + NewFileName = _info.Name; + + InitializeComponent(); + + ShowInTaskbar = false; + RenameTb.Focus(); + var window = Window.GetWindow(this); + window.KeyDown += (s, e) => + { + if (e.Key == Key.Escape) + { + Close(); + } + }; + } + + /// + /// https://stackoverflow.com/a/59560352/24045055 + /// + private async void SelectAll_OnTextBoxGotFocus(object sender, RoutedEventArgs e) + { + if (sender is not TextBox textBox) return; + if (_info is DirectoryInfo) + { + await Application.Current.Dispatcher.InvokeAsync(textBox.SelectAll, DispatcherPriority.Background); + } + // select everything but the extension + else if (_info is FileInfo info) + { + string properName = Path.GetFileNameWithoutExtension(info.Name); + Application.Current.Dispatcher.Invoke(textBox.Select, DispatcherPriority.Background, textBox.Text.IndexOf(properName), properName.Length); + } + } + + + private void OnDoneButtonClick(object sender, RoutedEventArgs e) + { + RenameThing.Rename(NewFileName, _info, _api); + Close(); + } + + private void BtnCancel(object sender, RoutedEventArgs e) + { + Close(); + } + + private void RenameTb_OnKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + btnDone.Focus(); + OnDoneButtonClick(sender, e); + e.Handled = true; + } + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs index 742d85fc1d4..4a53c4e85f7 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs @@ -1,14 +1,14 @@ using System.Collections.Generic; using System.Linq; -using System.Windows.Controls; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; +using System.Windows.Controls; using Flow.Launcher.Plugin.PluginsManager.ViewModels; using Flow.Launcher.Plugin.PluginsManager.Views; namespace Flow.Launcher.Plugin.PluginsManager { - public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n + public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IPluginHotkey { internal static PluginInitContext Context { get; set; } @@ -69,5 +69,36 @@ public string GetTranslatedPluginDescription() { return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description"); } + + public List GetPluginHotkeys() + { + return new List + { + new SearchWindowPluginHotkey + { + Id = 0, + Name = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_openwebsite_title"), + Description = Context.API.GetTranslation("plugin_pluginsmanager_plugin_contextmenu_openwebsite_subtitle"), + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uEB41"), + DefaultHotkey = "Ctrl+Enter", + Editable = false, + Visible = true, + Action = (r) => + { + if (r.ContextData is UserPlugin plugin) + { + if (!string.IsNullOrWhiteSpace(plugin.Website)) + { + Context.API.OpenUrl(plugin.Website); + + return true; + } + } + + return false; + } + } + }; + } } } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 25182f6d3d2..a0990f5e386 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -388,12 +388,15 @@ await Context.API.UpdatePluginAsync(x.PluginExistingMetadata, x.PluginNewUserPlu return true; }, - ContextData = - new UserPlugin - { - Website = x.PluginNewUserPlugin.Website, - UrlSourceCode = x.PluginNewUserPlugin.UrlSourceCode - } + ContextData = new UserPlugin + { + Website = x.PluginNewUserPlugin.Website, + UrlSourceCode = x.PluginNewUserPlugin.UrlSourceCode + }, + HotkeyIds = new List + { + 0 + }, }); // Update all result @@ -527,12 +530,6 @@ internal List InstallFromWeb(string url) IcoPath = icoPath, Action = e => { - if (e.SpecialKeyState.CtrlPressed) - { - SearchWeb.OpenInBrowserTab(plugin.UrlDownload); - return ShouldHideWindow; - } - if (Settings.WarnFromUnknownSource) { if (!InstallSourceKnown(plugin.UrlDownload) @@ -633,17 +630,15 @@ internal async ValueTask> RequestInstallOrUpdateAsync(string search IcoPath = x.IcoPath, Action = e => { - if (e.SpecialKeyState.CtrlPressed) - { - SearchWeb.OpenInBrowserTab(x.Website); - return ShouldHideWindow; - } - Context.API.HideMainWindow(); _ = InstallOrUpdateAsync(x); // No need to wait return ShouldHideWindow; }, - ContextData = x + ContextData = x, + HotkeyIds = new List + { + 0 + }, }); return Search(results, search); @@ -736,7 +731,15 @@ internal List RequestUninstall(string search) } return false; - } + }, + ContextData = new UserPlugin + { + Website = x.Metadata.Website + }, + HotkeyIds = new List + { + 0 + }, }); return Search(results, search); diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index fd687bfaeda..bf4d833e060 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -16,7 +16,7 @@ namespace Flow.Launcher.Plugin.Program { - public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable + public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable, IPluginHotkey { private static readonly string ClassName = nameof(Main); @@ -459,5 +459,59 @@ public void Dispose() { Win32.Dispose(); } + + public List GetPluginHotkeys() + { + return new List + { + new SearchWindowPluginHotkey() + { + Id = 0, + Name = Context.API.GetTranslation("flowlauncher_plugin_program_open_containing_folder"), + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue838"), + DefaultHotkey = "Ctrl+Enter", + Editable = false, + Visible = true, + Action = (r) => + { + if (r.ContextData is UWPApp uwp) + { + Context.API.OpenDirectory(uwp.Location); + return true; + } + else if (r.ContextData is Win32 win32) + { + Context.API.OpenDirectory(win32.ParentDirectory, win32.FullPath); + return true; + } + + return false; + } + }, + // TODO: Do it after administrator mode PR + /*new SearchWindowPluginHotkey() + { + Id = 1, + Name = Context.API.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), + Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE7EF"), + DefaultHotkey = "Ctrl+Shift+Enter", + Editable = false, + Visible = true, + Action = (r) => + { + if (r.ContextData is UWPApp uwp) + { + return true; + } + else if (r.ContextData is Win32 win32) + { + return true; + } + + return false; + } + },*/ + }; + } } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs index f67111b4ec2..49116e6e3ab 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs @@ -442,14 +442,6 @@ public Result Result(string query, IPublicAPI api) ContextData = this, Action = e => { - // Ctrl + Enter to open containing folder - bool openFolder = e.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control; - if (openFolder) - { - Main.Context.API.OpenDirectory(Location); - return true; - } - // Ctrl + Shift + Enter to run elevated bool elevated = e.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift); @@ -465,7 +457,11 @@ public Result Result(string query, IPublicAPI api) } return true; - } + }, + HotkeyIds = new List + { + 0 + }, }; return result; diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index 7aca8f3b6a7..36fd6feb30b 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -185,14 +185,6 @@ public Result Result(string query, IPublicAPI api) TitleToolTip = $"{title}\n{ExecutablePath}", Action = c => { - // Ctrl + Enter to open containing folder - bool openFolder = c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control; - if (openFolder) - { - Main.Context.API.OpenDirectory(ParentDirectory, FullPath); - return true; - } - // Ctrl + Shift + Enter to run as admin bool runAsAdmin = c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift); @@ -207,7 +199,11 @@ public Result Result(string query, IPublicAPI api) _ = Task.Run(() => Main.StartProcess(Process.Start, info)); return true; - } + }, + HotkeyIds = new List + { + 0 + }, }; return result;