diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 2b2bef4cf..8d1408449 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -53,7 +53,7 @@ - + diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index b1c8ff6ea..2b3bd61b0 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -1,5 +1,4 @@ - -/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins, +/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Flow Launcher and other plugins, * like python or other self-execute program. But, we added addtional infos (proxy and so on) into rpc request. Also, we didn't use the * "id" and "jsonrpc" in the request, since it's not so useful in our request model. * @@ -13,10 +12,12 @@ * */ +using Flow.Launcher.Core.Resource; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using Flow.Launcher.Plugin; +using System.Text.Json; namespace Flow.Launcher.Core.Plugin { @@ -51,42 +52,15 @@ namespace Flow.Launcher.Core.Plugin public class JsonRPCRequestModel : JsonRPCModelBase { + [JsonPropertyName("method")] public string Method { get; set; } + [JsonPropertyName("parameters")] public object[] Parameters { get; set; } public override string ToString() { - string rpc = string.Empty; - if (Parameters != null && Parameters.Length > 0) - { - string parameters = $"[{string.Join(',', Parameters.Select(GetParameterByType))}]"; - rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":{parameters}"; - } - else - { - rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":[]"; - } - - return rpc; - - } - - private string GetParameterByType(object parameter) - => parameter switch - { - null => "null", - string _ => $@"\""{ReplaceEscapes(parameter.ToString())}\""", - bool _ => $@"{parameter.ToString().ToLower()}", - _ => parameter.ToString() - }; - - - private string ReplaceEscapes(string str) - { - return str.Replace(@"\", @"\\") //Escapes in ProcessStartInfo - .Replace(@"\", @"\\") //Escapes itself when passed to client - .Replace(@"""", @"\\"""""); + return JsonSerializer.Serialize(this); } } @@ -95,11 +69,7 @@ namespace Flow.Launcher.Core.Plugin /// public class JsonRPCServerRequestModel : JsonRPCRequestModel { - public override string ToString() - { - string rpc = base.ToString(); - return rpc + "}"; - } + } /// @@ -107,13 +77,8 @@ namespace Flow.Launcher.Core.Plugin /// public class JsonRPCClientRequestModel : JsonRPCRequestModel { + [JsonPropertyName("dontHideAfterAction")] public bool DontHideAfterAction { get; set; } - - public override string ToString() - { - string rpc = base.ToString(); - return rpc + "}"; - } } /// @@ -125,4 +90,4 @@ namespace Flow.Launcher.Core.Plugin { public JsonRPCClientRequestModel JsonRPCAction { get; set; } } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 7a088bd09..ff9c11a94 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -1,4 +1,5 @@ -using System; +using Flow.Launcher.Core.Resource; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -45,14 +46,20 @@ namespace Flow.Launcher.Core.Plugin } } - + private static readonly JsonSerializerOptions _options = new() + { + Converters = + { + new JsonObjectConverter() + } + }; private async Task> DeserializedResultAsync(Stream output) { if (output == Stream.Null) return null; - JsonRPCQueryResponseModel queryResponseModel = await - JsonSerializer.DeserializeAsync(output); + var queryResponseModel = await + JsonSerializer.DeserializeAsync(output, _options); return ParseResults(queryResponseModel); } @@ -61,17 +68,18 @@ namespace Flow.Launcher.Core.Plugin { if (string.IsNullOrEmpty(output)) return null; - JsonRPCQueryResponseModel queryResponseModel = - JsonSerializer.Deserialize(output); + var queryResponseModel = + JsonSerializer.Deserialize(output, _options); return ParseResults(queryResponseModel); } + private List ParseResults(JsonRPCQueryResponseModel queryResponseModel) { var results = new List(); if (queryResponseModel.Result == null) return null; - if(!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) + if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) { context.API.ShowMsg(queryResponseModel.DebugMessage); } @@ -82,25 +90,31 @@ namespace Flow.Launcher.Core.Plugin { if (result.JsonRPCAction == null) return false; - if (!string.IsNullOrEmpty(result.JsonRPCAction.Method)) + if (string.IsNullOrEmpty(result.JsonRPCAction.Method)) { - if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) + return !result.JsonRPCAction.DontHideAfterAction; + } + + if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) + { + ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..], + result.JsonRPCAction.Parameters); + } + else + { + var actionResponse = ExecuteCallback(result.JsonRPCAction); + + if (string.IsNullOrEmpty(actionResponse)) { - ExecuteFlowLauncherAPI(result.JsonRPCAction.Method.Substring(4), - result.JsonRPCAction.Parameters); + return !result.JsonRPCAction.DontHideAfterAction; } - else + + var jsonRpcRequestModel = JsonSerializer.Deserialize(actionResponse, _options); + + if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) { - string actionReponse = ExecuteCallback(result.JsonRPCAction); - JsonRPCRequestModel jsonRpcRequestModel = - JsonSerializer.Deserialize(actionReponse); - if (jsonRpcRequestModel != null - && !string.IsNullOrEmpty(jsonRpcRequestModel.Method) - && jsonRpcRequestModel.Method.StartsWith("Flow.Launcher.")) - { - ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4), - jsonRpcRequestModel.Parameters); - } + ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], + jsonRpcRequestModel.Parameters); } } @@ -177,12 +191,14 @@ namespace Flow.Launcher.Core.Plugin if (result.StartsWith("DEBUG:")) { - MessageBox.Show(new Form { TopMost = true }, result.Substring(6)); + MessageBox.Show(new Form + { + TopMost = true + }, result.Substring(6)); return string.Empty; } return result; - } catch (Exception e) { @@ -248,10 +264,10 @@ namespace Flow.Launcher.Core.Plugin } } - public Task InitAsync(PluginInitContext context) + public virtual Task InitAsync(PluginInitContext context) { this.context = context; return Task.CompletedTask; } } -} +} \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index e2c009ee3..8d2c6df24 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -21,8 +21,8 @@ namespace Flow.Launcher.Core.Plugin private static IEnumerable _contextMenuPlugins; public static List AllPlugins { get; private set; } - public static readonly List GlobalPlugins = new List(); - public static readonly Dictionary NonGlobalPlugins = new Dictionary(); + public static readonly HashSet GlobalPlugins = new(); + public static readonly Dictionary NonGlobalPlugins = new (); public static IPublicAPI API { private set; get; } @@ -118,7 +118,9 @@ namespace Flow.Launcher.Core.Plugin _contextMenuPlugins = GetPluginsForInterface(); foreach (var plugin in AllPlugins) { - foreach (var actionKeyword in plugin.Metadata.ActionKeywords) + // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin + // has multiple global and action keywords because we will only add them here once. + foreach (var actionKeyword in plugin.Metadata.ActionKeywords.Distinct()) { switch (actionKeyword) { @@ -141,7 +143,7 @@ namespace Flow.Launcher.Core.Plugin } } - public static List ValidPluginsForQuery(Query query) + public static ICollection ValidPluginsForQuery(Query query) { if (NonGlobalPlugins.ContainsKey(query.ActionKeyword)) { @@ -250,6 +252,8 @@ namespace Flow.Launcher.Core.Plugin public static bool ActionKeywordRegistered(string actionKeyword) { + // this method is only checking for action keywords (defined as not '*') registration + // hence the actionKeyword != Query.GlobalPluginWildcardSign logic return actionKeyword != Query.GlobalPluginWildcardSign && NonGlobalPlugins.ContainsKey(actionKeyword); } @@ -274,7 +278,7 @@ namespace Flow.Launcher.Core.Plugin } /// - /// used to add action keyword for multiple action keyword plugin + /// used to remove action keyword for multiple action keyword plugin /// e.g. web search /// public static void RemoveActionKeyword(string id, string oldActionkeyword) @@ -283,9 +287,7 @@ namespace Flow.Launcher.Core.Plugin if (oldActionkeyword == Query.GlobalPluginWildcardSign && // Plugins may have multiple ActionKeywords that are global, eg. WebSearch plugin.Metadata.ActionKeywords - .Where(x => x == Query.GlobalPluginWildcardSign) - .ToList() - .Count == 1) + .Count(x => x == Query.GlobalPluginWildcardSign) == 1) { GlobalPlugins.Remove(plugin); } diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 92aca8109..1b78c68ae 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -11,13 +11,13 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; +using System.Diagnostics; +using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher.Core.Plugin { public static class PluginsLoader { - public const string PATH = "PATH"; - public const string Python = "python"; public const string PythonExecutable = "pythonw.exe"; public static List Plugins(List metadatas, PluginsSettings settings) @@ -85,11 +85,7 @@ namespace Flow.Launcher.Core.Plugin return; } - plugins.Add(new PluginPair - { - Plugin = plugin, - Metadata = metadata - }); + plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata}); }); metadata.InitTime += milliseconds; } @@ -119,102 +115,68 @@ namespace Flow.Launcher.Core.Plugin if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python)) return new List(); - // Try setting Constant.PythonPath first, either from - // PATH or from the given pythonDirectory - if (string.IsNullOrEmpty(settings.PythonDirectory)) - { - var paths = Environment.GetEnvironmentVariable(PATH); - if (paths != null) - { - var pythonInPath = paths - .Split(';') - .Where(p => p.ToLower().Contains(Python)) - .Any(); + if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory)) + return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable)); - if (pythonInPath) + var pythonPath = string.Empty; + + if (MessageBox.Show("Flow detected you have installed Python plugins, " + + "would you like to install Python to run them? " + + Environment.NewLine + Environment.NewLine + + "Click no if it's already installed, " + + "and you will be prompted to select the folder that contains the Python executable", + string.Empty, MessageBoxButtons.YesNo) == DialogResult.No + && string.IsNullOrEmpty(settings.PythonDirectory)) + { + var dlg = new FolderBrowserDialog + { + SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + }; + + var result = dlg.ShowDialog(); + if (result == DialogResult.OK) + { + string pythonDirectory = dlg.SelectedPath; + if (!string.IsNullOrEmpty(pythonDirectory)) { - Constant.PythonPath = - Path.Combine(paths.Split(';').Where(p => p.ToLower().Contains(Python)).FirstOrDefault(), PythonExecutable); - settings.PythonDirectory = FilesFolders.GetPreviousExistingDirectory(FilesFolders.LocationExists, Constant.PythonPath); - } - else - { - Log.Error("PluginsLoader", "Failed to set Python path despite the environment variable PATH is found", "PythonPlugins"); + pythonPath = Path.Combine(pythonDirectory, PythonExecutable); + if (File.Exists(pythonPath)) + { + settings.PythonDirectory = pythonDirectory; + Constant.PythonPath = pythonPath; + } + else + { + MessageBox.Show("Can't find python in given directory"); + } } } } else { - var path = Path.Combine(settings.PythonDirectory, PythonExecutable); - if (File.Exists(path)) + var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable"); + + // Python 3.8.9 is used for Windows 7 compatibility + DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait(); + + pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable); + if (FilesFolders.FileExists(pythonPath)) { - Constant.PythonPath = path; + settings.PythonDirectory = installedPythonDirectory; + Constant.PythonPath = pythonPath; } else { - Log.Error("PluginsLoader", $"Tried to automatically set from Settings.PythonDirectory " + - $"but can't find python executable in {path}", "PythonPlugins"); + Log.Error("PluginsLoader", + $"Failed to set Python path after Droplex install, {pythonPath} does not exist", + "PythonPlugins"); } } - if (string.IsNullOrEmpty(settings.PythonDirectory)) + if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(pythonPath)) { - if (MessageBox.Show("Flow detected you have installed Python plugins, " + - "would you like to install Python to run them? " + - Environment.NewLine + Environment.NewLine + - "Click no if it's already installed, " + - "and you will be prompted to select the folder that contains the Python executable", - string.Empty, MessageBoxButtons.YesNo) == DialogResult.No - && string.IsNullOrEmpty(settings.PythonDirectory)) - { - var dlg = new FolderBrowserDialog - { - SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) - }; - - var result = dlg.ShowDialog(); - if (result == DialogResult.OK) - { - string pythonDirectory = dlg.SelectedPath; - if (!string.IsNullOrEmpty(pythonDirectory)) - { - var pythonPath = Path.Combine(pythonDirectory, PythonExecutable); - if (File.Exists(pythonPath)) - { - settings.PythonDirectory = pythonDirectory; - Constant.PythonPath = pythonPath; - } - else - { - MessageBox.Show("Can't find python in given directory"); - } - } - } - } - else - { - DroplexPackage.Drop(App.python3_9_1).Wait(); - - var installedPythonDirectory = - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39"); - var pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable); - if (FilesFolders.FileExists(pythonPath)) - { - settings.PythonDirectory = installedPythonDirectory; - Constant.PythonPath = pythonPath; - } - else - { - Log.Error("PluginsLoader", - $"Failed to set Python path after Droplex install, {pythonPath} does not exist", - "PythonPlugins"); - } - } - } - - if (string.IsNullOrEmpty(settings.PythonDirectory)) - { - MessageBox.Show("Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom)."); + MessageBox.Show( + "Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom)."); Log.Error("PluginsLoader", $"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.", "PythonPlugins"); @@ -222,24 +184,26 @@ namespace Flow.Launcher.Core.Plugin return new List(); } - return source + return SetPythonPathForPluginPairs(source, pythonPath); + } + + private static IEnumerable SetPythonPathForPluginPairs(List source, string pythonPath) + => source .Where(o => o.Language.ToUpper() == AllowedLanguage.Python) .Select(metadata => new PluginPair { - Plugin = new PythonPlugin(Constant.PythonPath), + Plugin = new PythonPlugin(pythonPath), Metadata = metadata }) .ToList(); - } - public static IEnumerable ExecutablePlugins(IEnumerable source) + public static IEnumerable ExecutablePlugins(IEnumerable source) { return source .Where(o => o.Language.ToUpper() == AllowedLanguage.Executable) .Select(metadata => new PluginPair { - Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), - Metadata = metadata + Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata }); } } diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 356a68a81..16d84136f 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -28,17 +28,19 @@ namespace Flow.Launcher.Core.Plugin var path = Path.Combine(Constant.ProgramDirectory, JsonRPC); _startInfo.EnvironmentVariables["PYTHONPATH"] = path; + //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable + _startInfo.ArgumentList.Add("-B"); } protected override Task ExecuteQueryAsync(Query query, CancellationToken token) { JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { - Method = "query", - Parameters = new object[] { query.Search }, + Method = "query", Parameters = new object[] {query.Search}, }; - //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable - _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\""; + + _startInfo.ArgumentList[2] = request.ToString(); + // todo happlebao why context can't be used in constructor _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; @@ -47,16 +49,17 @@ namespace Flow.Launcher.Core.Plugin protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) { - _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\""; + _startInfo.ArgumentList[2] = rpcRequest.ToString(); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; // TODO: Async Action return Execute(_startInfo); } - protected override string ExecuteContextMenu(Result selectedResult) { - JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { - Method = "context_menu", - Parameters = new object[] { selectedResult.ContextData }, + protected override string ExecuteContextMenu(Result selectedResult) + { + JsonRPCServerRequestModel request = new JsonRPCServerRequestModel + { + Method = "context_menu", Parameters = new object[] {selectedResult.ContextData}, }; _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\""; _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; @@ -64,5 +67,13 @@ namespace Flow.Launcher.Core.Plugin // TODO: Async Action return Execute(_startInfo); } + + public override Task InitAsync(PluginInitContext context) + { + this.context = context; + _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + _startInfo.ArgumentList.Add(""); + return Task.CompletedTask; + } } } \ No newline at end of file diff --git a/Flow.Launcher.Core/Resource/JsonObjectConverter.cs b/Flow.Launcher.Core/Resource/JsonObjectConverter.cs new file mode 100644 index 000000000..5a891ae30 --- /dev/null +++ b/Flow.Launcher.Core/Resource/JsonObjectConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Flow.Launcher.Core.Resource +{ + public class JsonObjectConverter : JsonConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.True: + return true; + case JsonTokenType.False: + return false; + case JsonTokenType.Number when reader.TryGetInt32(out var i): + return i; + case JsonTokenType.Number when reader.TryGetInt64(out var l): + return l; + case JsonTokenType.Number: + return reader.GetDouble(); + case JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime): + return datetime; + case JsonTokenType.String: + return reader.GetString(); + default: + // Use JsonElement as fallback. + // Newtonsoft uses JArray or JObject. + using (var document = JsonDocument.ParseValue(ref reader)) + { + return document.RootElement.Clone(); + } + } + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + throw new InvalidOperationException("Should not get here."); + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index a5f12bbb9..fe64e0c3e 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -17,6 +17,8 @@ namespace Flow.Launcher.Core.Resource { public class Theme { + private const int ShadowExtraMargin = 12; + private readonly List _themeDirectories = new List(); private ResourceDictionary _oldResource; private string _oldTheme; @@ -224,6 +226,27 @@ namespace Flow.Launcher.Core.Resource BlurRadius = 15 }; + var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; + if (marginSetter == null) + { + marginSetter = new Setter() + { + Property = Border.MarginProperty, + Value = new Thickness(ShadowExtraMargin), + }; + windowBorderStyle.Setters.Add(marginSetter); + } + else + { + var baseMargin = (Thickness) marginSetter.Value; + var newMargin = new Thickness( + baseMargin.Left + ShadowExtraMargin, + baseMargin.Top + ShadowExtraMargin, + baseMargin.Right + ShadowExtraMargin, + baseMargin.Bottom + ShadowExtraMargin); + marginSetter.Value = newMargin; + } + windowBorderStyle.Setters.Add(effectSetter); UpdateResourceDictionary(dict); @@ -234,7 +257,23 @@ namespace Flow.Launcher.Core.Resource var dict = CurrentThemeResourceDictionary(); var windowBorderStyle = dict["WindowBorderStyle"] as Style; - dict.Remove(Border.EffectProperty); + var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter; + var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; + + if(effectSetter != null) + { + windowBorderStyle.Setters.Remove(effectSetter); + } + if (marginSetter != null) + { + var currentMargin = (Thickness)marginSetter.Value; + var newMargin = new Thickness( + currentMargin.Left - ShadowExtraMargin, + currentMargin.Top - ShadowExtraMargin, + currentMargin.Right - ShadowExtraMargin, + currentMargin.Bottom - ShadowExtraMargin); + marginSetter.Value = newMargin; + } UpdateResourceDictionary(dict); } diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index 29bc11480..abce8b41e 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -16,11 +16,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings { var settings = Plugins[metadata.ID]; - // TODO: Remove. This is one off for 1.2.0 release. - // Introduced a new action keyword in Explorer, so need to update plugin setting in the UserData folder. - // This kind of plugin meta update should be handled by a dedicated method trigger by version bump. + // TODO: Remove. This is backwards compatibility for 1.8.0 release. + // Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder. if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count) - settings.ActionKeywords = metadata.ActionKeywords; + { + settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search + settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search + } if (string.IsNullOrEmpty(settings.Version)) settings.Version = metadata.Version; @@ -30,6 +32,11 @@ namespace Flow.Launcher.Infrastructure.UserSettings metadata.ActionKeywords = settings.ActionKeywords; metadata.ActionKeyword = settings.ActionKeywords[0]; } + else + { + metadata.ActionKeywords = new List(); + metadata.ActionKeyword = string.Empty; + } metadata.Disabled = settings.Disabled; metadata.Priority = settings.Priority; } diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index b3f7b8d4f..f3403696e 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -21,6 +21,7 @@ using JetBrains.Annotations; using System.Runtime.CompilerServices; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; +using System.Collections.Concurrent; namespace Flow.Launcher { @@ -144,7 +145,7 @@ namespace Flow.Launcher public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName); - private readonly Dictionary _pluginJsonStorages = new(); + private readonly ConcurrentDictionary _pluginJsonStorages = new(); public void SavePluginSettings() { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 56ab0ff1d..4ee15b46d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -494,21 +494,16 @@ namespace Flow.Launcher.ViewModel } }, currentCancellationToken); - Task[] tasks = new Task[plugins.Count]; + // plugins is ICollection, meaning LINQ will get the Count and preallocate Array + + Task[] tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch + { + false => QueryTask(plugin), + true => Task.CompletedTask + }).ToArray(); + try { - for (var i = 0; i < plugins.Count; i++) - { - if (!plugins[i].Metadata.Disabled) - { - tasks[i] = QueryTask(plugins[i]); - } - else - { - tasks[i] = Task.CompletedTask; // Avoid Null - } - } - // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first await Task.WhenAll(tasks); } @@ -552,26 +547,9 @@ namespace Flow.Launcher.ViewModel private void RemoveOldQueryResults(Query query) { - string lastKeyword = _lastQuery.ActionKeyword; - - string keyword = query.ActionKeyword; - if (string.IsNullOrEmpty(lastKeyword)) + if (_lastQuery.ActionKeyword != query.ActionKeyword) { - if (!string.IsNullOrEmpty(keyword)) - { - Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata); - } - } - else - { - if (string.IsNullOrEmpty(keyword)) - { - Results.KeepResultsExcept(PluginManager.NonGlobalPlugins[lastKeyword].Metadata); - } - else if (lastKeyword != keyword) - { - Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata); - } + Results.Clear(); } } diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 7c8814b41..1ac320cf0 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -1,7 +1,6 @@ using System.Windows; using System.Windows.Media; using Flow.Launcher.Plugin; -using Flow.Launcher.Core.Resource; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Core.Plugin; @@ -11,8 +10,6 @@ namespace Flow.Launcher.ViewModel { public PluginPair PluginPair { get; set; } - private readonly Internationalization _translator = InternationalizationManager.Instance; - public ImageSource Image => ImageLoader.Load(PluginPair.Metadata.IcoPath); public bool PluginState { @@ -22,7 +19,7 @@ namespace Flow.Launcher.ViewModel PluginPair.Metadata.Disabled = !value; } } - public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count > 1 ? Visibility.Collapsed : Visibility.Visible; + public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count == 1 ? Visibility.Visible : Visibility.Collapsed; public string InitilizaTime => PluginPair.Metadata.InitTime.ToString() + "ms"; public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms"; public string ActionKeywordsText => string.Join(Query.ActionKeywordSeperater, PluginPair.Metadata.ActionKeywords); diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index 9ba0da3f6..a989327be 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -19,8 +19,14 @@ Quick Access Links Index Search Excluded Paths Indexing Options - Search Activation: + Search: + Path Search: File Content Search: + Index Search: + Current Action Keyword: + Done + Enabled + When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword Explorer diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml index c4a2322b2..dac6e908b 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml @@ -20,6 +20,7 @@ 索引搜索排除的路径 索引选项 搜索激活: + 路径搜索激活: 文件内容搜索: diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index 8204e755c..92b7c95a0 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -28,7 +28,7 @@ namespace Flow.Launcher.Plugin.Explorer return new ExplorerSettings(viewModel); } - public async Task InitAsync(PluginInitContext context) + public Task InitAsync(PluginInitContext context) { Context = context; @@ -47,6 +47,8 @@ namespace Flow.Launcher.Plugin.Explorer contextMenu = new ContextMenu(Context, Settings, viewModel); searchManager = new SearchManager(Settings, Context); ResultManager.Init(Context); + + return Task.CompletedTask; } public List LoadContextMenus(Result selectedResult) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs index 1e9815cb9..595ac3610 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs @@ -10,10 +10,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search { internal static bool IsEnvironmentVariableSearch(string search) { - return LoadEnvironmentStringPaths().Count > 0 - && search.StartsWith("%") + return search.StartsWith("%") && search != "%%" - && !search.Contains("\\"); + && !search.Contains("\\") && + LoadEnvironmentStringPaths().Count > 0; } internal static Dictionary LoadEnvironmentStringPaths() diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index e9b4b0fc1..6f3996b0d 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -26,6 +26,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { private static PathEqualityComparator instance; public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator(); + public bool Equals(Result x, Result y) { return x.SubTitle == y.SubTitle; @@ -39,21 +40,61 @@ namespace Flow.Launcher.Plugin.Explorer.Search internal async Task> SearchAsync(Query query, CancellationToken token) { - var results = new HashSet(PathEqualityComparator.Instance); - var querySearch = query.Search; if (IsFileContentSearch(query.ActionKeyword)) return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false); + var result = new HashSet(PathEqualityComparator.Instance); + + if (ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword) || + ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)) + { + result.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false)); + } + + if ((ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) || + ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)) && + querySearch.Length > 0 && + !querySearch.IsLocationPathString()) + { + result.UnionWith(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token) + .ConfigureAwait(false)); + } + + return result.ToList(); + } + + private bool ActionKeywordMatch(Query query, Settings.ActionKeyword allowedActionKeyword) + { + var keyword = query.ActionKeyword.Length == 0 ? Query.GlobalPluginWildcardSign : query.ActionKeyword; + + return allowedActionKeyword switch + { + Settings.ActionKeyword.SearchActionKeyword => settings.EnableSearchActionKeyword && + keyword == settings.SearchActionKeyword, + Settings.ActionKeyword.PathSearchActionKeyword => settings.EnabledPathSearchKeyword && + keyword == settings.PathSearchActionKeyword, + Settings.ActionKeyword.FileContentSearchActionKeyword => keyword == + settings.FileContentSearchActionKeyword, + Settings.ActionKeyword.IndexSearchActionKeyword => settings.EnabledIndexOnlySearchKeyword && + keyword == settings.IndexSearchActionKeyword + }; + } + + public async Task> PathSearchAsync(Query query, CancellationToken token = default) + { + var querySearch = query.Search; + // This allows the user to type the assigned action keyword and only see the list of quick folder links if (string.IsNullOrEmpty(query.Search)) return QuickAccess.AccessLinkListAll(query, settings.QuickAccessLinks); + var results = new HashSet(PathEqualityComparator.Instance); + var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, settings.QuickAccessLinks); - if (quickaccessLinks.Count > 0) - results.UnionWith(quickaccessLinks); + results.UnionWith(quickaccessLinks); var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch); @@ -63,13 +104,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search // Query is a location path with a full environment variable, eg. %appdata%\somefolder\ var isEnvironmentVariablePath = querySearch[1..].Contains("%\\"); - if (!querySearch.IsLocationPathString() && !isEnvironmentVariablePath) - { - results.UnionWith(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token).ConfigureAwait(false)); - - return results.ToList(); - } - var locationPath = querySearch; if (isEnvironmentVariablePath) @@ -99,7 +133,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search return results.ToList(); } - private async Task> WindowsIndexFileContentSearchAsync(Query query, string querySearchString, CancellationToken token) + private async Task> WindowsIndexFileContentSearchAsync(Query query, string querySearchString, + CancellationToken token) { var queryConstructor = new QueryConstructor(settings); @@ -107,11 +142,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search return new List(); return await IndexSearch.WindowsIndexSearchAsync(querySearchString, - queryConstructor.CreateQueryHelper().ConnectionString, - queryConstructor.QueryForFileContentSearch, - settings.IndexSearchExcludedSubdirectoryPaths, - query, - token).ConfigureAwait(false); + queryConstructor.CreateQueryHelper().ConnectionString, + queryConstructor.QueryForFileContentSearch, + settings.IndexSearchExcludedSubdirectoryPaths, + query, + token).ConfigureAwait(false); } public bool IsFileContentSearch(string actionKeyword) @@ -138,28 +173,30 @@ namespace Flow.Launcher.Plugin.Explorer.Search return await windowsIndexSearch(query, querySearchString, token); } - private async Task> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString, CancellationToken token) + private async Task> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString, + CancellationToken token) { var queryConstructor = new QueryConstructor(settings); return await IndexSearch.WindowsIndexSearchAsync(querySearchString, - queryConstructor.CreateQueryHelper().ConnectionString, - queryConstructor.QueryForAllFilesAndFolders, - settings.IndexSearchExcludedSubdirectoryPaths, - query, - token).ConfigureAwait(false); + queryConstructor.CreateQueryHelper().ConnectionString, + queryConstructor.QueryForAllFilesAndFolders, + settings.IndexSearchExcludedSubdirectoryPaths, + query, + token).ConfigureAwait(false); } - private async Task> WindowsIndexTopLevelFolderSearchAsync(Query query, string path, CancellationToken token) + private async Task> WindowsIndexTopLevelFolderSearchAsync(Query query, string path, + CancellationToken token) { var queryConstructor = new QueryConstructor(settings); return await IndexSearch.WindowsIndexSearchAsync(path, - queryConstructor.CreateQueryHelper().ConnectionString, - queryConstructor.QueryForTopLevelDirectorySearch, - settings.IndexSearchExcludedSubdirectoryPaths, - query, - token).ConfigureAwait(false); + queryConstructor.CreateQueryHelper().ConnectionString, + queryConstructor.QueryForTopLevelDirectorySearch, + settings.IndexSearchExcludedSubdirectoryPaths, + query, + token).ConfigureAwait(false); } private bool UseWindowsIndexForDirectorySearch(string locationPath) @@ -170,11 +207,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search return false; if (settings.IndexSearchExcludedSubdirectoryPaths - .Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory) - .StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))) + .Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory) + .StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))) return false; return IndexSearch.PathIsIndexed(pathToDirectory); } } -} +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs index a8eac986d..bd6fe7e20 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs @@ -1,6 +1,9 @@ using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; +using System; using System.Collections.Generic; +using System.IO; namespace Flow.Launcher.Plugin.Explorer { @@ -18,7 +21,57 @@ namespace Flow.Launcher.Plugin.Explorer public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List(); public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign; + public bool EnableSearchActionKeyword { get; set; } = true; public string FileContentSearchActionKeyword { get; set; } = Constants.DefaultContentSearchActionKeyword; + + public string PathSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign; + + public bool EnabledPathSearchKeyword { get; set; } + + public string IndexSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign; + + public bool EnabledIndexOnlySearchKeyword { get; set; } + + internal enum ActionKeyword + { + SearchActionKeyword, + PathSearchActionKeyword, + FileContentSearchActionKeyword, + IndexSearchActionKeyword + } + + internal string GetActionKeyword(ActionKeyword actionKeyword) => actionKeyword switch + { + ActionKeyword.SearchActionKeyword => SearchActionKeyword, + ActionKeyword.PathSearchActionKeyword => PathSearchActionKeyword, + ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword, + ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword + }; + + internal void SetActionKeyword(ActionKeyword actionKeyword, string keyword) => _ = actionKeyword switch + { + ActionKeyword.SearchActionKeyword => SearchActionKeyword = keyword, + ActionKeyword.PathSearchActionKeyword => PathSearchActionKeyword = keyword, + ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword = keyword, + ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword = keyword, + _ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "Unexpected property") + }; + + internal bool? GetActionKeywordEnable(ActionKeyword actionKeyword) => actionKeyword switch + { + ActionKeyword.SearchActionKeyword => EnableSearchActionKeyword, + ActionKeyword.PathSearchActionKeyword => EnabledPathSearchKeyword, + ActionKeyword.IndexSearchActionKeyword => EnabledIndexOnlySearchKeyword, + _ => null + }; + + internal void SetActionKeywordEnable(ActionKeyword actionKeyword, bool enable) => _ = actionKeyword switch + { + ActionKeyword.SearchActionKeyword => EnableSearchActionKeyword = enable, + ActionKeyword.PathSearchActionKeyword => EnabledPathSearchKeyword = enable, + ActionKeyword.IndexSearchActionKeyword => EnabledIndexOnlySearchKeyword = enable, + _ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "Unexpected property") + }; } } \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 92932bf4c..77ec5457b 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -41,18 +41,15 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels Process.Start(psi); } - internal void UpdateActionKeyword(string newActionKeyword, string oldActionKeyword) + internal void UpdateActionKeyword(Settings.ActionKeyword modifiedActionKeyword, string newActionKeyword, string oldActionKeyword) { PluginManager.ReplaceActionKeyword(Context.CurrentPluginMetadata.ID, oldActionKeyword, newActionKeyword); - - if (Settings.FileContentSearchActionKeyword == oldActionKeyword) - Settings.FileContentSearchActionKeyword = newActionKeyword; - - if (Settings.SearchActionKeyword == oldActionKeyword) - Settings.SearchActionKeyword = newActionKeyword; } - internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword); + internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword) + { + return PluginManager.ActionKeywordRegistered(newActionKeyword); + } internal bool IsNewActionKeywordGlobal(string newActionKeyword) => newActionKeyword == Query.GlobalPluginWildcardSign; } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml index 0e1c7e872..19ff624b0 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ActionKeywordSetting.xaml @@ -7,6 +7,7 @@ mc:Ignorable="d" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" + DataContext="{Binding RelativeSource={RelativeSource Self}}" Title="Action Keyword Setting" Height="200" Width="500"> @@ -16,21 +17,24 @@ + - + - - -