diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md.not_used
similarity index 100%
rename from .github/ISSUE_TEMPLATE/discussion.md
rename to .github/ISSUE_TEMPLATE/discussion.md.not_used
diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
index 2b2bef4cf..9e932a508 100644
--- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj
+++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
@@ -53,15 +53,12 @@
-
+
+
-
-
-
-
diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs
index b1c8ff6ea..5232e46da 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
{
@@ -29,12 +30,8 @@ namespace Flow.Launcher.Core.Plugin
public string Data { get; set; }
}
- public class JsonRPCModelBase
- {
- public int Id { get; set; }
- }
- public class JsonRPCResponseModel : JsonRPCModelBase
+ public class JsonRPCResponseModel
{
public string Result { get; set; }
@@ -48,45 +45,20 @@ namespace Flow.Launcher.Core.Plugin
public string DebugMessage { get; set; }
}
-
- public class JsonRPCRequestModel : JsonRPCModelBase
+
+ public class JsonRPCRequestModel
{
public string Method { get; set; }
public object[] Parameters { get; set; }
+ private static readonly JsonSerializerOptions options = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
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, options);
}
}
@@ -95,11 +67,7 @@ namespace Flow.Launcher.Core.Plugin
///
public class JsonRPCServerRequestModel : JsonRPCRequestModel
{
- public override string ToString()
- {
- string rpc = base.ToString();
- return rpc + "}";
- }
+
}
///
@@ -108,12 +76,6 @@ namespace Flow.Launcher.Core.Plugin
public class JsonRPCClientRequestModel : JsonRPCRequestModel
{
public bool DontHideAfterAction { get; set; }
-
- public override string ToString()
- {
- string rpc = base.ToString();
- return rpc + "}";
- }
}
///
@@ -125,4 +87,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..0df853a5d 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
@@ -1,7 +1,9 @@
-using System;
+using Flow.Launcher.Core.Resource;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading;
@@ -9,7 +11,9 @@ using System.Threading.Tasks;
using System.Windows.Forms;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
+using ICSharpCode.SharpZipLib.Zip;
using JetBrains.Annotations;
+using Microsoft.IO;
namespace Flow.Launcher.Core.Plugin
{
@@ -31,28 +35,30 @@ namespace Flow.Launcher.Core.Plugin
protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest);
protected abstract string ExecuteContextMenu(Result selectedResult);
+ private static readonly RecyclableMemoryStreamManager BufferManager = new();
+
public List LoadContextMenus(Result selectedResult)
{
- string output = ExecuteContextMenu(selectedResult);
- try
- {
- return DeserializedResult(output);
- }
- catch (Exception e)
- {
- Log.Exception($"|JsonRPCPlugin.LoadContextMenus|Exception on result <{selectedResult}>", e);
- return null;
- }
+ var output = ExecuteContextMenu(selectedResult);
+ return DeserializedResult(output);
}
-
+ private static readonly JsonSerializerOptions options = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ IgnoreNullValues = true,
+ 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,60 +67,71 @@ 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);
}
- foreach (JsonRPCResult result in queryResponseModel.Result)
+ foreach (var result in queryResponseModel.Result)
{
result.Action = c =>
{
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);
}
}
return !result.JsonRPCAction.DontHideAfterAction;
};
- results.Add(result);
}
+ var results = new List();
+
+ results.AddRange(queryResponseModel.Result);
+
return results;
}
private void ExecuteFlowLauncherAPI(string method, object[] parameters)
{
- MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method);
+ var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
+ MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method, parametersTypeArray);
if (methodInfo != null)
{
try
@@ -177,12 +194,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)
{
@@ -195,19 +214,53 @@ namespace Flow.Launcher.Core.Plugin
protected async Task ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default)
{
+ Process process = null;
+ bool disposed = false;
try
{
- using var process = Process.Start(startInfo);
+ process = Process.Start(startInfo);
if (process == null)
{
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
return Stream.Null;
}
- var result = process.StandardOutput.BaseStream;
+ await using var source = process.StandardOutput.BaseStream;
+
+ var buffer = BufferManager.GetStream();
+
+ token.Register(() =>
+ {
+ // ReSharper disable once AccessToModifiedClosure
+ // Manually Check whether disposed
+ if (!disposed && !process.HasExited)
+ process.Kill();
+ });
+
+ try
+ {
+ // token expire won't instantly trigger the exception,
+ // manually kill process at before
+ await source.CopyToAsync(buffer, token);
+ }
+ catch (OperationCanceledException)
+ {
+ await buffer.DisposeAsync();
+ return Stream.Null;
+ }
+
+ buffer.Seek(0, SeekOrigin.Begin);
token.ThrowIfCancellationRequested();
+ if (buffer.Length == 0)
+ {
+ var errorMessage = process.StandardError.EndOfStream ?
+ "Empty JSONRPC Response" :
+ await process.StandardError.ReadToEndAsync();
+ throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}");
+ }
+
if (!process.StandardError.EndOfStream)
{
using var standardError = process.StandardError;
@@ -215,40 +268,26 @@ namespace Flow.Launcher.Core.Plugin
if (!string.IsNullOrEmpty(error))
{
- Log.Error($"|JsonRPCPlugin.ExecuteAsync|{error}");
- return Stream.Null;
+ Log.Error($"|{context.CurrentPluginMetadata.Name}.{nameof(ExecuteAsync)}|{error}");
}
-
- Log.Error("|JsonRPCPlugin.ExecuteAsync|Empty standard output and standard error.");
- return Stream.Null;
}
- return result;
+ return buffer;
}
- catch (Exception e)
+ finally
{
- Log.Exception(
- $"|JsonRPCPlugin.ExecuteAsync|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>",
- e);
- return Stream.Null;
+ process?.Dispose();
+ disposed = true;
}
}
public async Task> QueryAsync(Query query, CancellationToken token)
{
var output = await ExecuteQueryAsync(query, token);
- try
- {
- return await DeserializedResultAsync(output);
- }
- catch (Exception e)
- {
- Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e);
- return null;
- }
+ return await DeserializedResultAsync(output);
}
- public Task InitAsync(PluginInitContext context)
+ public virtual Task InitAsync(PluginInitContext context)
{
this.context = context;
return Task.CompletedTask;
diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs
index e2c009ee3..134c3c002 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -7,9 +7,9 @@ using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
-using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
+using ISavable = Flow.Launcher.Plugin.ISavable;
namespace Flow.Launcher.Core.Plugin
{
@@ -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; }
@@ -33,7 +33,10 @@ namespace Flow.Launcher.Core.Plugin
///
/// Directories that will hold Flow Launcher plugin directory
///
- private static readonly string[] Directories = { Constant.PreinstalledDirectory, DataLocation.PluginsDirectory };
+ private static readonly string[] Directories =
+ {
+ Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
+ };
private static void DeletePythonBinding()
{
@@ -51,9 +54,26 @@ namespace Flow.Launcher.Core.Plugin
var savable = plugin.Plugin as ISavable;
savable?.Save();
}
+
API.SavePluginSettings();
}
+ public static async ValueTask DisposePluginsAsync()
+ {
+ foreach (var pluginPair in AllPlugins)
+ {
+ switch (pluginPair.Plugin)
+ {
+ case IDisposable disposable:
+ disposable.Dispose();
+ break;
+ case IAsyncDisposable asyncDisposable:
+ await asyncDisposable.DisposeAsync();
+ break;
+ }
+ }
+ }
+
public static async Task ReloadData()
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
@@ -99,7 +119,7 @@ namespace Flow.Launcher.Core.Plugin
try
{
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>",
- () => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
+ () => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
pair.Metadata.InitTime += milliseconds;
Log.Info(
@@ -118,7 +138,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,12 +163,15 @@ namespace Flow.Launcher.Core.Plugin
}
}
- public static List ValidPluginsForQuery(Query query)
+ public static ICollection ValidPluginsForQuery(Query query)
{
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
{
var plugin = NonGlobalPlugins[query.ActionKeyword];
- return new List { plugin };
+ return new List
+ {
+ plugin
+ };
}
else
{
@@ -154,7 +179,7 @@ namespace Flow.Launcher.Core.Plugin
}
}
- public static async Task> QueryForPlugin(PluginPair pair, Query query, CancellationToken token)
+ public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
{
var results = new List();
try
@@ -168,7 +193,7 @@ namespace Flow.Launcher.Core.Plugin
token.ThrowIfCancellationRequested();
if (results == null)
- return results;
+ return null;
UpdatePluginMetadata(results, metadata, query);
metadata.QueryCount += 1;
@@ -181,10 +206,6 @@ namespace Flow.Launcher.Core.Plugin
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
return null;
}
- catch (Exception e)
- {
- Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e);
- }
return results;
}
@@ -229,7 +250,7 @@ namespace Flow.Launcher.Core.Plugin
try
{
- results = plugin.LoadContextMenus(result);
+ results = plugin.LoadContextMenus(result) ?? results;
foreach (var r in results)
{
r.PluginDirectory = pluginPair.Metadata.PluginDirectory;
@@ -250,6 +271,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 +297,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 +306,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);
}
@@ -306,4 +327,4 @@ namespace Flow.Launcher.Core.Plugin
}
}
}
-}
+}
\ No newline at end of file
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..5d2d8d51d 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,22 +49,31 @@ 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.ArgumentList[2] = request.ToString();
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
// 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/Properties/AssemblyInfo.cs b/Flow.Launcher.Core/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..40017c46c
--- /dev/null
+++ b/Flow.Launcher.Core/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
\ 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 void SetBlurForWindow()
{
+ if (BlurEnabled)
+ {
+ SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
+ }
+ else
+ {
+ SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
+ }
+ }
- // Exception of FindResource can't be cathed if global exception handle is set
+ private bool IsBlurTheme()
+ {
if (Environment.OSVersion.Version >= new Version(6, 2))
{
var resource = Application.Current.TryFindResource("ThemeBlurEnabled");
- bool blur;
- if (resource is bool)
- {
- blur = (bool)resource;
- }
- else
- {
- blur = false;
- }
- if (blur)
- {
- SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_ENABLE_BLURBEHIND);
- }
- else
- {
- SetWindowAccent(Application.Current.MainWindow, AccentState.ACCENT_DISABLED);
- }
+ if (resource is bool)
+ return (bool)resource;
+
+ return false;
}
+
+ return false;
}
private void SetWindowAccent(Window w, AccentState state)
{
var windowHelper = new WindowInteropHelper(w);
+
+ // this determines the width of the main query window
+ w.Width = mainWindowWidth;
+ windowHelper.EnsureHandle();
+
var accent = new AccentPolicy { AccentState = state };
var accentStructSize = Marshal.SizeOf(accent);
diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
index 2f919b5c9..aea43506e 100644
--- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
+++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
@@ -49,8 +49,11 @@
-
-
+
+
+
+
+
diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs
index bb7ec6817..13277c7d9 100644
--- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs
+++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
@@ -26,7 +27,8 @@ namespace Flow.Launcher.Infrastructure.Image
private const int MaxCached = 50;
public ConcurrentDictionary Data { get; private set; } = new ConcurrentDictionary();
private const int permissibleFactor = 2;
-
+ private SemaphoreSlim semaphore = new(1, 1);
+
public void Initialization(Dictionary usage)
{
foreach (var key in usage.Keys)
@@ -60,20 +62,29 @@ namespace Flow.Launcher.Infrastructure.Image
}
);
- // To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size
- // This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
- if (Data.Count > permissibleFactor * MaxCached)
+ SliceExtra();
+
+ async void SliceExtra()
{
- // To delete the images from the data dictionary based on the resizing of the Usage Dictionary.
- foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key))
- Data.TryRemove(key, out _);
+ // To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size
+ // This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time
+ if (Data.Count > permissibleFactor * MaxCached)
+ {
+ await semaphore.WaitAsync().ConfigureAwait(false);
+ // To delete the images from the data dictionary based on the resizing of the Usage Dictionary
+ // Double Check to avoid concurrent remove
+ if (Data.Count > permissibleFactor * MaxCached)
+ foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key).ToArray())
+ Data.TryRemove(key, out _);
+ semaphore.Release();
+ }
}
}
}
public bool ContainsKey(string key)
{
- return Data.ContainsKey(key) && Data[key].imageSource != null;
+ return key is not null && Data.ContainsKey(key) && Data[key].imageSource != null;
}
public int CacheSize()
diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs
index 94132b27f..26e305ace 100644
--- a/Flow.Launcher.Infrastructure/Logger/Log.cs
+++ b/Flow.Launcher.Infrastructure/Logger/Log.cs
@@ -5,6 +5,11 @@ using NLog;
using NLog.Config;
using NLog.Targets;
using Flow.Launcher.Infrastructure.UserSettings;
+using JetBrains.Annotations;
+using NLog.Fluent;
+using NLog.Targets.Wrappers;
+using System.Runtime.ExceptionServices;
+using System.Text;
namespace Flow.Launcher.Infrastructure.Logger
{
@@ -23,15 +28,37 @@ namespace Flow.Launcher.Infrastructure.Logger
}
var configuration = new LoggingConfiguration();
- var target = new FileTarget();
- configuration.AddTarget("file", target);
- target.FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt";
+
+ const string layout =
+ @"${date:format=HH\:mm\:ss.ffffK} - " +
+ @"${level:uppercase=true:padding=-5} - ${logger} - ${message:l}" +
+ @"${onexception:${newline}" +
+ @"EXCEPTION OCCURS\: ${exception:format=tostring}${newline}}";
+
+ var fileTarget = new FileTarget
+ {
+ FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt",
+ Layout = layout
+ };
+
+ var fileTargetASyncWrapper = new AsyncTargetWrapper(fileTarget);
+
+ var debugTarget = new OutputDebugStringTarget
+ {
+ Layout = layout
+ };
+
+ configuration.AddTarget("file", fileTargetASyncWrapper);
+ configuration.AddTarget("debug", debugTarget);
+
#if DEBUG
- var rule = new LoggingRule("*", LogLevel.Debug, target);
+ var fileRule = new LoggingRule("*", LogLevel.Debug, fileTargetASyncWrapper);
+ var debugRule = new LoggingRule("*", LogLevel.Debug, debugTarget);
+ configuration.LoggingRules.Add(debugRule);
#else
- var rule = new LoggingRule("*", LogLevel.Info, target);
+ var fileRule = new LoggingRule("*", LogLevel.Info, fileTargetASyncWrapper);
#endif
- configuration.LoggingRules.Add(rule);
+ configuration.LoggingRules.Add(fileRule);
LogManager.Configuration = configuration;
}
@@ -39,7 +66,6 @@ namespace Flow.Launcher.Infrastructure.Logger
{
var logger = LogManager.GetLogger("FaultyLogger");
message = $"Wrong logger message format <{message}>";
- System.Diagnostics.Debug.WriteLine($"FATAL|{message}");
logger.Fatal(message);
}
@@ -51,12 +77,11 @@ namespace Flow.Launcher.Infrastructure.Logger
}
-
- [MethodImpl(MethodImplOptions.Synchronized)]
public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "")
{
+ exception = exception.Demystify();
#if DEBUG
- throw exception;
+ ExceptionDispatchInfo.Capture(exception).Throw();
#else
var classNameWithMethod = CheckClassAndMessageAndReturnFullClassWithMethod(className, message, methodName);
@@ -90,23 +115,9 @@ namespace Flow.Launcher.Infrastructure.Logger
{
var logger = LogManager.GetLogger(classAndMethod);
- System.Diagnostics.Debug.WriteLine($"ERROR|{message}");
+ var messageBuilder = new StringBuilder();
- logger.Error("-------------------------- Begin exception --------------------------");
- logger.Error(message);
-
- do
- {
- logger.Error($"Exception full name:\n <{e.GetType().FullName}>");
- logger.Error($"Exception message:\n <{e.Message}>");
- logger.Error($"Exception stack trace:\n <{e.StackTrace}>");
- logger.Error($"Exception source:\n <{e.Source}>");
- logger.Error($"Exception target site:\n <{e.TargetSite}>");
- logger.Error($"Exception HResult:\n <{e.HResult}>");
- e = e.InnerException;
- } while (e != null);
-
- logger.Error("-------------------------- End exception --------------------------");
+ logger.Error(e, message);
}
private static void LogInternal(string message, LogLevel level)
@@ -117,8 +128,6 @@ namespace Flow.Launcher.Infrastructure.Logger
var prefix = parts[1];
var unprefixed = parts[2];
var logger = LogManager.GetLogger(prefix);
-
- System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}");
logger.Log(level, unprefixed);
}
else
@@ -128,11 +137,12 @@ namespace Flow.Launcher.Infrastructure.Logger
}
/// example: "|prefix|unprefixed"
- [MethodImpl(MethodImplOptions.Synchronized)]
+ /// Exception
public static void Exception(string message, System.Exception e)
{
+ e = e.Demystify();
#if DEBUG
- throw e;
+ ExceptionDispatchInfo.Capture(e).Throw();
#else
if (FormatValid(message))
{
@@ -165,7 +175,6 @@ namespace Flow.Launcher.Infrastructure.Logger
var logger = LogManager.GetLogger(classNameWithMethod);
- System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}");
logger.Log(level, message);
}
diff --git a/Flow.Launcher.Infrastructure/Storage/ISavable.cs b/Flow.Launcher.Infrastructure/Storage/ISavable.cs
new file mode 100644
index 000000000..ba2b58c6a
--- /dev/null
+++ b/Flow.Launcher.Infrastructure/Storage/ISavable.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace Flow.Launcher.Infrastructure.Storage
+{
+ [Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " +
+ "This is used only for Everything plugin v1.4.9 or below backwards compatibility")]
+ public interface ISavable : Plugin.ISavable { }
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
index 43d19bea8..0083ccb87 100644
--- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
@@ -87,4 +87,8 @@ namespace Flow.Launcher.Infrastructure.Storage
File.WriteAllText(FilePath, serialized);
}
}
+
+ [Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " +
+ "This is used only for Everything plugin v1.4.9 or below backwards compatibility")]
+ public class JsonStrorage : JsonStorage where T : new() { }
}
diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
index c26aef95d..923a1a6b5 100644
--- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs
@@ -22,3 +22,4 @@ namespace Flow.Launcher.Infrastructure.Storage
}
}
}
+
diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs
index 29bc11480..fd2464f2e 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.
- if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count)
- settings.ActionKeywords = metadata.ActionKeywords;
+ // 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.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.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 76a370978..ebef2c631 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -39,7 +39,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
///
public bool ShouldUsePinyin { get; set; } = false;
- internal SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
+ [JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
+ public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
[JsonIgnore]
public string QuerySearchPrecisionString
@@ -97,6 +98,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool RememberLastLaunchLocation { get; set; }
public bool IgnoreHotkeysOnFullscreen { get; set; }
+ public bool AutoHideScrollBar { get; set; }
+
public HttpProxy Proxy { get; set; } = new HttpProxy();
[JsonConverter(typeof(JsonStringEnumConverter))]
diff --git a/Flow.Launcher.Plugin/Feature.cs b/Flow.Launcher.Plugin/Feature.cs
deleted file mode 100644
index 81839d816..000000000
--- a/Flow.Launcher.Plugin/Feature.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-
-namespace Flow.Launcher.Plugin
-{
- public interface IFeatures { }
-
- public interface IContextMenu : IFeatures
- {
- List LoadContextMenus(Result selectedResult);
- }
-
- ///
- /// Represent plugins that support internationalization
- ///
- public interface IPluginI18n : IFeatures
- {
- string GetTranslatedPluginTitle();
-
- string GetTranslatedPluginDescription();
- }
-
- public interface IResultUpdated : IFeatures
- {
- event ResultUpdatedEventHandler ResultsUpdated;
- }
-
- public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
-
- public class ResultUpdatedEventArgs : EventArgs
- {
- public List Results;
- public Query Query;
- }
-}
diff --git a/Flow.Launcher.Plugin/Features.cs b/Flow.Launcher.Plugin/Features.cs
new file mode 100644
index 000000000..5b9c6a7b9
--- /dev/null
+++ b/Flow.Launcher.Plugin/Features.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Threading;
+
+namespace Flow.Launcher.Plugin
+{
+ ///
+ /// Base Interface for Flow's special plugin feature interface
+ ///
+ public interface IFeatures
+ {
+ }
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
index e3d26f5d3..664373434 100644
--- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
+++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj
@@ -14,10 +14,10 @@
- 1.4.0
- 1.4.0
- 1.4.0
- 1.4.0
+ 2.0.0
+ 2.0.0
+ 2.0.0
+ 2.0.0
Flow.Launcher.Plugin
Flow-Launcher
MIT
diff --git a/Flow.Launcher.Plugin/IAsyncPlugin.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncPlugin.cs
similarity index 100%
rename from Flow.Launcher.Plugin/IAsyncPlugin.cs
rename to Flow.Launcher.Plugin/Interfaces/IAsyncPlugin.cs
diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs
index fc4ac4715..bd4500a7e 100644
--- a/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IAsyncReloadable.cs
@@ -13,8 +13,8 @@ namespace Flow.Launcher.Plugin
/// The command that allows user to manual reload is exposed via Plugin.Sys, and
/// it will call the plugins that have implemented this interface.
///
- public interface IAsyncReloadable
+ public interface IAsyncReloadable : IFeatures
{
Task ReloadDataAsync();
}
-}
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs b/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs
new file mode 100644
index 000000000..5befbf5c9
--- /dev/null
+++ b/Flow.Launcher.Plugin/Interfaces/IContextMenu.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Flow.Launcher.Plugin
+{
+ public interface IContextMenu : IFeatures
+ {
+ List LoadContextMenus(Result selectedResult);
+ }
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Plugin/IPlugin.cs b/Flow.Launcher.Plugin/Interfaces/IPlugin.cs
similarity index 100%
rename from Flow.Launcher.Plugin/IPlugin.cs
rename to Flow.Launcher.Plugin/Interfaces/IPlugin.cs
diff --git a/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs b/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs
new file mode 100644
index 000000000..e332d450e
--- /dev/null
+++ b/Flow.Launcher.Plugin/Interfaces/IPluginI18n.cs
@@ -0,0 +1,12 @@
+namespace Flow.Launcher.Plugin
+{
+ ///
+ /// Represent plugins that support internationalization
+ ///
+ public interface IPluginI18n : IFeatures
+ {
+ string GetTranslatedPluginTitle();
+
+ string GetTranslatedPluginDescription();
+ }
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Plugin/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
similarity index 100%
rename from Flow.Launcher.Plugin/IPublicAPI.cs
rename to Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
diff --git a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs
index 31611519c..bd1ad406e 100644
--- a/Flow.Launcher.Plugin/Interfaces/IReloadable.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IReloadable.cs
@@ -15,8 +15,8 @@
/// If requiring reloading data asynchronously, please use the IAsyncReloadable interface
///
///
- public interface IReloadable
+ public interface IReloadable : IFeatures
{
void ReloadData();
}
-}
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs
new file mode 100644
index 000000000..fd21460ac
--- /dev/null
+++ b/Flow.Launcher.Plugin/Interfaces/IResultUpdated.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Flow.Launcher.Plugin
+{
+ public interface IResultUpdated : IFeatures
+ {
+ event ResultUpdatedEventHandler ResultsUpdated;
+ }
+
+ public delegate void ResultUpdatedEventHandler(IResultUpdated sender, ResultUpdatedEventArgs e);
+
+ public class ResultUpdatedEventArgs : EventArgs
+ {
+ public List Results;
+ public Query Query;
+ public CancellationToken Token { get; init; }
+ }
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Plugin/Interfaces/ISavable.cs b/Flow.Launcher.Plugin/Interfaces/ISavable.cs
index 6f408dc2e..2d13eaa6e 100644
--- a/Flow.Launcher.Plugin/Interfaces/ISavable.cs
+++ b/Flow.Launcher.Plugin/Interfaces/ISavable.cs
@@ -5,8 +5,8 @@
/// Otherwise if LoadSettingJsonStorage or SaveSettingJsonStorage has been callded,
/// plugin settings will be automatically saved (see Flow.Launcher/PublicAPIInstance.SavePluginSettings) by Flow
///
- public interface ISavable
+ public interface ISavable : IFeatures
{
void Save();
}
-}
+}
\ No newline at end of file
diff --git a/Flow.Launcher.Plugin/ISettingProvider.cs b/Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs
similarity index 100%
rename from Flow.Launcher.Plugin/ISettingProvider.cs
rename to Flow.Launcher.Plugin/Interfaces/ISettingProvider.cs
diff --git a/Flow.Launcher.Plugin/README.md b/Flow.Launcher.Plugin/README.md
index 3b4d1598a..5c5b7c3ed 100644
--- a/Flow.Launcher.Plugin/README.md
+++ b/Flow.Launcher.Plugin/README.md
@@ -3,4 +3,4 @@
* Defines base objects and interfaces for plugins
* Plugin authors making C# plugins should reference this DLL via nuget
-* Contains base commands used by all plugins
+* Contains commands and models that can be used by plugins
diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj
index 1d0dce5e7..84c9137b7 100644
--- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj
+++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj
@@ -49,12 +49,12 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
\ No newline at end of file
diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
index 3d0a9a64f..9d7fccad9 100644
--- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs
+++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
@@ -133,6 +133,13 @@ namespace Flow.Launcher.Test.Plugins
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
+ var baseQuery = queryConstructor.CreateBaseQuery();
+
+ // system running this test could have different locale than the hard-coded 1033 LCID en-US.
+ var queryKeywordLocale = baseQuery.QueryKeywordLocale;
+ expectedString = expectedString.Replace("1033", queryKeywordLocale.ToString());
+
+
//When
var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);
diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
new file mode 100644
index 000000000..2b9512692
--- /dev/null
+++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs
@@ -0,0 +1,105 @@
+using NUnit;
+using NUnit.Framework;
+using Flow.Launcher.Core.Plugin;
+using Flow.Launcher.Plugin;
+using System.Threading.Tasks;
+using System.IO;
+using System.Threading;
+using System.Text;
+using System.Text.Json;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace Flow.Launcher.Test.Plugins
+{
+ [TestFixture]
+ // ReSharper disable once InconsistentNaming
+ internal class JsonRPCPluginTest : JsonRPCPlugin
+ {
+ public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable;
+
+ protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ protected override string ExecuteContextMenu(Result selectedResult)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ protected override Task ExecuteQueryAsync(Query query, CancellationToken token)
+ {
+ var byteInfo = Encoding.UTF8.GetBytes(query.RawQuery);
+
+ var resultStream = new MemoryStream(byteInfo);
+ return Task.FromResult((Stream)resultStream);
+ }
+
+ [TestCase("{\"result\":[],\"DebugMessage\":null}", Description = "Empty Result")]
+ [TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"something\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "One Result with Pascal Case")]
+ [TestCase("{\"result\":[{\"jsonRPCAction\":null,\"title\":\"something\",\"subTitle\":\"\",\"actionKeywordAssigned\":null,\"icoPath\":null}],\"debugMessage\":null}", Description = "One Result with camel Case")]
+ [TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null},{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Pascal Case")]
+ [TestCase("{\"result\":[{\"jsonrpcAction\":null,\"TItLE\":\"iii\",\"Subtitle\":\"\",\"Actionkeywordassigned\":null,\"icoPath\":null},{\"jsonRPCAction\":null,\"tiTle\":\"iii\",\"subTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Weird Case")]
+ public async Task GivenVariousJsonText_WhenVariousNamingCase_ThenExpectNotNullResults_Async(string resultText)
+ {
+ var results = await QueryAsync(new Query
+ {
+ RawQuery = resultText
+ }, default);
+
+ Assert.IsNotNull(results);
+
+ foreach (var result in results)
+ {
+ Assert.IsNotNull(result);
+ Assert.IsNotNull(result.Action);
+ Assert.IsNotNull(result.Title);
+ }
+
+ }
+
+ public static List ResponseModelsSource = new()
+ {
+ new()
+ {
+ Result = new()
+ },
+ new()
+ {
+ Result = new()
+ {
+ new JsonRPCResult
+ {
+ Title = "Test1",
+ SubTitle = "Test2"
+ }
+ }
+ }
+ };
+
+ [TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
+ public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference)
+ {
+ var camelText = JsonSerializer.Serialize(reference, new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
+
+ var pascalText = JsonSerializer.Serialize(reference);
+
+ var results1 = await QueryAsync(new Query { RawQuery = camelText }, default);
+ var results2 = await QueryAsync(new Query { RawQuery = pascalText }, default);
+
+ Assert.IsNotNull(results1);
+ Assert.IsNotNull(results2);
+
+ foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result))
+ {
+ Assert.AreEqual(result1, result2);
+ Assert.AreEqual(result1, referenceResult);
+
+ Assert.IsNotNull(result1);
+ Assert.IsNotNull(result1.Action);
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index 7b808dd54..c2a32100d 100644
--- a/Flow.Launcher/App.xaml.cs
+++ b/Flow.Launcher/App.xaml.cs
@@ -68,11 +68,14 @@ namespace Flow.Launcher
PluginManager.LoadPlugins(_settings.PluginSettings);
_mainVM = new MainViewModel(_settings);
+
+ HotKeyMapper.Initialize(_mainVM);
+
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet);
Http.API = API;
Http.Proxy = _settings.Proxy;
-
+
await PluginManager.InitializePlugins(API);
var window = new MainWindow(_settings, _mainVM);
@@ -96,6 +99,8 @@ namespace Flow.Launcher
AutoStartup();
AutoUpdates();
+ API.SaveAppAllSettings();
+
_mainVM.MainWindowVisibility = _settings.HideOnStartup ? Visibility.Hidden : Visibility.Visible;
Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
});
diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs
index d036c008c..b18bbcfad 100644
--- a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs
+++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs
@@ -1,8 +1,7 @@
using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
-using NHotkey;
-using NHotkey.Wpf;
using System;
using System.Collections.ObjectModel;
using System.Linq;
@@ -52,11 +51,7 @@ namespace Flow.Launcher
};
_settings.CustomPluginHotkeys.Add(pluginHotkey);
- SetHotkey(ctlHotkey.CurrentHotkey, delegate
- {
- App.API.ChangeQuery(pluginHotkey.ActionKeyword);
- Application.Current.MainWindow.Visibility = Visibility.Visible;
- });
+ HotKeyMapper.SetCustomQueryHotkey(pluginHotkey);
}
else
{
@@ -69,12 +64,8 @@ namespace Flow.Launcher
updateCustomHotkey.ActionKeyword = tbAction.Text;
updateCustomHotkey.Hotkey = ctlHotkey.CurrentHotkey.ToString();
//remove origin hotkey
- RemoveHotkey(oldHotkey);
- SetHotkey(new HotkeyModel(updateCustomHotkey.Hotkey), delegate
- {
- App.API.ChangeQuery(updateCustomHotkey.ActionKeyword);
- Application.Current.MainWindow.Visibility = Visibility.Visible;
- });
+ HotKeyMapper.RemoveHotkey(oldHotkey);
+ HotKeyMapper.SetCustomQueryHotkey(updateCustomHotkey);
}
Close();
@@ -100,28 +91,8 @@ namespace Flow.Launcher
{
App.API.ChangeQuery(tbAction.Text);
Application.Current.MainWindow.Visibility = Visibility.Visible;
- }
+ Application.Current.MainWindow.Focus();
- private void RemoveHotkey(string hotkeyStr)
- {
- if (!string.IsNullOrEmpty(hotkeyStr))
- {
- HotkeyManager.Current.Remove(hotkeyStr);
- }
- }
-
- private void SetHotkey(HotkeyModel hotkey, EventHandler action)
- {
- string hotkeyStr = hotkey.ToString();
- try
- {
- HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
- }
- catch (Exception)
- {
- string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
- MessageBox.Show(errorMsg);
- }
}
private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
diff --git a/Flow.Launcher/Helper/ErrorReporting.cs b/Flow.Launcher/Helper/ErrorReporting.cs
index 94e2ed2bc..f3f590167 100644
--- a/Flow.Launcher/Helper/ErrorReporting.cs
+++ b/Flow.Launcher/Helper/ErrorReporting.cs
@@ -3,6 +3,8 @@ using System.Windows.Threading;
using NLog;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Exception;
+using NLog.Fluent;
+using Log = Flow.Launcher.Infrastructure.Logger.Log;
namespace Flow.Launcher.Helper
{
@@ -45,4 +47,4 @@ namespace Flow.Launcher.Helper
return info;
}
}
-}
+}
\ No newline at end of file
diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs
new file mode 100644
index 000000000..d5fcabace
--- /dev/null
+++ b/Flow.Launcher/Helper/HotKeyMapper.cs
@@ -0,0 +1,136 @@
+using Flow.Launcher.Infrastructure.Hotkey;
+using Flow.Launcher.Infrastructure.UserSettings;
+using System;
+using NHotkey;
+using NHotkey.Wpf;
+using Flow.Launcher.Core.Resource;
+using System.Windows;
+using Flow.Launcher.ViewModel;
+
+namespace Flow.Launcher.Helper
+{
+ internal static class HotKeyMapper
+ {
+ private static Settings settings;
+ private static MainViewModel mainViewModel;
+
+ internal static void Initialize(MainViewModel mainVM)
+ {
+ mainViewModel = mainVM;
+ settings = mainViewModel._settings;
+
+ SetHotkey(settings.Hotkey, OnHotkey);
+ LoadCustomPluginHotkey();
+ }
+
+ private static void SetHotkey(string hotkeyStr, EventHandler action)
+ {
+ var hotkey = new HotkeyModel(hotkeyStr);
+ SetHotkey(hotkey, action);
+ }
+
+ internal static void SetHotkey(HotkeyModel hotkey, EventHandler action)
+ {
+ string hotkeyStr = hotkey.ToString();
+ try
+ {
+ HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
+ }
+ catch (Exception)
+ {
+ string errorMsg =
+ string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"),
+ hotkeyStr);
+ MessageBox.Show(errorMsg);
+ }
+ }
+
+ internal static void RemoveHotkey(string hotkeyStr)
+ {
+ if (!string.IsNullOrEmpty(hotkeyStr))
+ {
+ HotkeyManager.Current.Remove(hotkeyStr);
+ }
+ }
+
+ internal static void OnHotkey(object sender, HotkeyEventArgs e)
+ {
+ if (!ShouldIgnoreHotkeys())
+ {
+ UpdateLastQUeryMode();
+
+ mainViewModel.ToggleFlowLauncher();
+ e.Handled = true;
+ }
+ }
+
+ ///
+ /// Checks if Flow Launcher should ignore any hotkeys
+ ///
+ private static bool ShouldIgnoreHotkeys()
+ {
+ return settings.IgnoreHotkeysOnFullscreen && WindowsInteropHelper.IsWindowFullscreen();
+ }
+
+ private static void UpdateLastQUeryMode()
+ {
+ switch(settings.LastQueryMode)
+ {
+ case LastQueryMode.Empty:
+ mainViewModel.ChangeQueryText(string.Empty);
+ break;
+ case LastQueryMode.Preserved:
+ mainViewModel.LastQuerySelected = true;
+ break;
+ case LastQueryMode.Selected:
+ mainViewModel.LastQuerySelected = false;
+ break;
+ default:
+ throw new ArgumentException($"wrong LastQueryMode: <{settings.LastQueryMode}>");
+
+ }
+ }
+
+ internal static void LoadCustomPluginHotkey()
+ {
+ if (settings.CustomPluginHotkeys == null)
+ return;
+
+ foreach (CustomPluginHotkey hotkey in settings.CustomPluginHotkeys)
+ {
+ SetCustomQueryHotkey(hotkey);
+ }
+ }
+
+ internal static void SetCustomQueryHotkey(CustomPluginHotkey hotkey)
+ {
+ SetHotkey(hotkey.Hotkey, (s, e) =>
+ {
+ if (ShouldIgnoreHotkeys())
+ return;
+
+ mainViewModel.MainWindowVisibility = Visibility.Visible;
+ mainViewModel.ChangeQueryText(hotkey.ActionKeyword);
+ });
+ }
+
+ internal static bool CheckAvailability(HotkeyModel currentHotkey)
+ {
+ try
+ {
+ HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", currentHotkey.CharKey, currentHotkey.ModifierKeys, (sender, e) => { });
+
+ return true;
+ }
+ catch
+ {
+ }
+ finally
+ {
+ HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Flow.Launcher/Helper/SingletonWindowOpener.cs b/Flow.Launcher/Helper/SingletonWindowOpener.cs
index 3671b9fd3..fdfaaa4fc 100644
--- a/Flow.Launcher/Helper/SingletonWindowOpener.cs
+++ b/Flow.Launcher/Helper/SingletonWindowOpener.cs
@@ -11,7 +11,16 @@ namespace Flow.Launcher.Helper
var window = Application.Current.Windows.OfType().FirstOrDefault(x => x.GetType() == typeof(T))
?? (T)Activator.CreateInstance(typeof(T), args);
Application.Current.MainWindow.Hide();
+
+ // Fix UI bug
+ // Add `window.WindowState = WindowState.Normal`
+ // If only use `window.Show()`, Settings-window doesn't show when minimized in taskbar
+ // Not sure why this works tho
+ // Probably because, when `.Show()` fails, `window.WindowState == Minimized` (not `Normal`)
+ // https://stackoverflow.com/a/59719760/4230390
+ window.WindowState = WindowState.Normal;
window.Show();
+
window.Focus();
return (T)window;
diff --git a/Flow.Launcher/HotkeyControl.xaml b/Flow.Launcher/HotkeyControl.xaml
index 0164d7dd7..e732cbe97 100644
--- a/Flow.Launcher/HotkeyControl.xaml
+++ b/Flow.Launcher/HotkeyControl.xaml
@@ -10,7 +10,7 @@
-
+
diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs
index 5c7141d1c..2b6e275df 100644
--- a/Flow.Launcher/HotkeyControl.xaml.cs
+++ b/Flow.Launcher/HotkeyControl.xaml.cs
@@ -4,8 +4,8 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
-using NHotkey.Wpf;
using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Plugin;
@@ -18,11 +18,7 @@ namespace Flow.Launcher
public event EventHandler HotkeyChanged;
- protected virtual void OnHotkeyChanged()
- {
- EventHandler handler = HotkeyChanged;
- if (handler != null) handler(this, EventArgs.Empty);
- }
+ protected virtual void OnHotkeyChanged() => HotkeyChanged?.Invoke(this, EventArgs.Empty);
public HotkeyControl()
{
@@ -90,24 +86,7 @@ namespace Flow.Launcher
SetHotkey(new HotkeyModel(keyStr), triggerValidate);
}
- private bool CheckHotkeyAvailability()
- {
- try
- {
- HotkeyManager.Current.AddOrReplace("HotkeyAvailabilityTest", CurrentHotkey.CharKey, CurrentHotkey.ModifierKeys, (sender, e) => { });
-
- return true;
- }
- catch
- {
- }
- finally
- {
- HotkeyManager.Current.Remove("HotkeyAvailabilityTest");
- }
-
- return false;
- }
+ private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey);
public new bool IsFocused
{
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index 6232492ba..90a920e3e 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -31,12 +31,15 @@
Ignore hotkeys in fullscreen mode
Python Directory
Auto Update
+ Auto Hide Scroll Bar
+ Automatically hides the Settings window scroll bar and show when hover the mouse over it
Select
Hide Flow Launcher on startup
Hide tray icon
Query Search Precision
Should Use Pinyin
Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for transliterating Chinese
+ Shadow effect is not allowed while current theme has blur effect enabled
Plugin
@@ -48,6 +51,7 @@
New action keyword:
Current Priority:
New Priority:
+ Priority:
Plugin Directory
Author
Init time:
@@ -70,6 +74,7 @@
Open Result Modifiers
Show Hotkey
Custom Query Hotkey
+ Query
Delete
Edit
Add
@@ -134,7 +139,7 @@
Update
- Hotkey unavailable
+ Hotkey Unavailable
Version
diff --git a/Flow.Launcher/Languages/sk.xaml b/Flow.Launcher/Languages/sk.xaml
index ec1b015da..346c70837 100644
--- a/Flow.Launcher/Languages/sk.xaml
+++ b/Flow.Launcher/Languages/sk.xaml
@@ -31,12 +31,15 @@
Ignorovať klávesové skratky v režime na celú obrazovku
Priečinok s Pythonom
Automatická aktualizácia
+ Automaticky skryť posuvník
+ Automaticky skrývať posuvník v okne nastavení a zobraziť ho, keď naň prejdete myšou
Vybrať
Schovať Flow Launcher po spustení
Schovať ikonu z oblasti oznámení
Presnosť vyhľadávania
Použiť Pinyin
Umožňuje vyhľadávanie pomocou Pinyin. Pinyin je štandardný systém romanizovaného pravopisu pre transliteráciu čínštiny
+ Efekt tieňa nie je povolený, kým má aktuálny motív povolený efekt rozostrenia
Plugin
@@ -48,6 +51,7 @@
Nová akcia skratky:
Aktuálna priorita:
Nová priorita:
+ Priorita:
Priečinok s pluginmi
Autor
Príprava:
@@ -70,6 +74,7 @@
Modifikáčné klávesy na otvorenie výsledkov
Zobraziť klávesovú skratku
Vlastná klávesová skratka na vyhľadávanie
+ Dopyt
Odstrániť
Upraviť
Pridať
diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml
index aa0240dd4..fcc3af5c5 100644
--- a/Flow.Launcher/MainWindow.xaml
+++ b/Flow.Launcher/MainWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -79,7 +79,6 @@
Style="{DynamicResource QueryBoxStyle}"
Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PreviewDragOver="OnPreviewDragOver"
- TextChanged="OnTextChanged"
AllowDrop="True"
Visibility="Visible"
Background="Transparent"
@@ -94,7 +93,7 @@
-
{
- if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility))
+ switch (e.PropertyName)
{
- if (_viewModel.MainWindowVisibility == Visibility.Visible)
- {
- Activate();
- QueryTextBox.Focus();
- UpdatePosition();
- _settings.ActivateTimes++;
- if (!_viewModel.LastQuerySelected)
+ case nameof(MainViewModel.MainWindowVisibility):
{
- QueryTextBox.SelectAll();
- _viewModel.LastQuerySelected = true;
- }
+ if (_viewModel.MainWindowVisibility == Visibility.Visible)
+ {
+ Activate();
+ QueryTextBox.Focus();
+ UpdatePosition();
+ _settings.ActivateTimes++;
+ if (!_viewModel.LastQuerySelected)
+ {
+ QueryTextBox.SelectAll();
+ _viewModel.LastQuerySelected = true;
+ }
- if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
- {
- _progressBarStoryboard.Resume();
- isProgressBarStoryboardPaused = false;
+ if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
+ {
+ _progressBarStoryboard.Begin(ProgressBar, true);
+ isProgressBarStoryboardPaused = false;
+ }
+ }
+ else if (!isProgressBarStoryboardPaused)
+ {
+ _progressBarStoryboard.Stop(ProgressBar);
+ isProgressBarStoryboardPaused = true;
+ }
+
+ break;
}
- }
- else if (!isProgressBarStoryboardPaused)
- {
- _progressBarStoryboard.Pause();
- isProgressBarStoryboardPaused = true;
- }
- }
- else if (e.PropertyName == nameof(MainViewModel.ProgressBarVisibility))
- {
- Dispatcher.Invoke(() =>
- {
- if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused)
+ case nameof(MainViewModel.ProgressBarVisibility):
{
- _progressBarStoryboard.Pause();
- isProgressBarStoryboardPaused = true;
+ Dispatcher.Invoke(async () =>
+ {
+ if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused)
+ {
+ await Task.Delay(50);
+ _progressBarStoryboard.Stop(ProgressBar);
+ isProgressBarStoryboardPaused = true;
+ }
+ else if (_viewModel.MainWindowVisibility == Visibility.Visible &&
+ isProgressBarStoryboardPaused)
+ {
+ _progressBarStoryboard.Begin(ProgressBar, true);
+ isProgressBarStoryboardPaused = false;
+ }
+ }, System.Windows.Threading.DispatcherPriority.Render);
+
+ break;
}
- else if (_viewModel.MainWindowVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
+ case nameof(MainViewModel.QueryTextCursorMovedToEnd):
+ if (_viewModel.QueryTextCursorMovedToEnd)
{
- _progressBarStoryboard.Resume();
- isProgressBarStoryboardPaused = false;
+ MoveQueryTextToEnd();
+ _viewModel.QueryTextCursorMovedToEnd = false;
}
- }, System.Windows.Threading.DispatcherPriority.Render);
+ break;
}
};
_settings.PropertyChanged += (o, e) =>
@@ -189,14 +207,15 @@ namespace Flow.Launcher
private void InitProgressbarAnimation()
{
- var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100, new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
+ var da = new DoubleAnimation(ProgressBar.X2, ActualWidth + 100,
+ new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
var da1 = new DoubleAnimation(ProgressBar.X1, ActualWidth, new Duration(new TimeSpan(0, 0, 0, 0, 1600)));
Storyboard.SetTargetProperty(da, new PropertyPath("(Line.X2)"));
Storyboard.SetTargetProperty(da1, new PropertyPath("(Line.X1)"));
_progressBarStoryboard.Children.Add(da);
_progressBarStoryboard.Children.Add(da1);
_progressBarStoryboard.RepeatBehavior = RepeatBehavior.Forever;
- ProgressBar.BeginStoryboard(_progressBarStoryboard);
+
_viewModel.ProgressBarVisibility = Visibility.Hidden;
isProgressBarStoryboardPaused = true;
}
@@ -316,13 +335,9 @@ namespace Flow.Launcher
}
}
- private void OnTextChanged(object sender, TextChangedEventArgs e)
+ private void MoveQueryTextToEnd()
{
- if (_viewModel.QueryTextCursorMovedToEnd)
- {
- QueryTextBox.CaretIndex = QueryTextBox.Text.Length;
- _viewModel.QueryTextCursorMovedToEnd = false;
- }
+ QueryTextBox.CaretIndex = QueryTextBox.Text.Length;
}
}
}
\ No newline at end of file
diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml
index 2f9d06d81..0f70dce41 100644
--- a/Flow.Launcher/ResultListBox.xaml
+++ b/Flow.Launcher/ResultListBox.xaml
@@ -42,7 +42,7 @@
+ Source="{Binding Image}" />
diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml
index 651d7db09..38168a66c 100644
--- a/Flow.Launcher/SettingWindow.xaml
+++ b/Flow.Launcher/SettingWindow.xaml
@@ -18,6 +18,7 @@
Height="600" Width="900"
MinWidth="850"
MinHeight="500"
+ Loaded="OnLoaded"
Closed="OnClosed"
d:DataContext="{d:DesignInstance vm:SettingWindowViewModel}">
@@ -36,8 +37,8 @@
-
-
+
+
@@ -63,9 +64,14 @@
-
+
+
+
+
+ Margin="10, 0, 10, 10" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ui:ScrollViewerHelper.AutoHideScrollBars="{Binding AutoHideScrollBar, Mode=OneWay}">
@@ -174,7 +180,7 @@
Text="{Binding PluginPair.Metadata.Description}"
Grid.Row="1" Opacity="0.5" Grid.Column="2" />
-
+
-
- Open Theme Folder
+
@@ -235,7 +243,7 @@
-
+
@@ -357,7 +365,7 @@
-
+
diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs
index a922b4d67..aa341f431 100644
--- a/Flow.Launcher/SettingWindow.xaml.cs
+++ b/Flow.Launcher/SettingWindow.xaml.cs
@@ -2,18 +2,17 @@ using System;
using System.IO;
using System.Windows;
using System.Windows.Input;
+using System.Windows.Interop;
using System.Windows.Navigation;
using Microsoft.Win32;
-using NHotkey;
-using NHotkey.Wpf;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
-using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel;
+using Flow.Launcher.Helper;
namespace Flow.Launcher
{
@@ -35,6 +34,14 @@ namespace Flow.Launcher
}
#region General
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ // Fix (workaround) for the window freezes after lock screen (Win+L)
+ // https://stackoverflow.com/questions/4951058/software-rendering-mode-wpf
+ HwndSource hwndSource = PresentationSource.FromVisual(this) as HwndSource;
+ HwndTarget hwndTarget = hwndSource.CompositionTarget;
+ hwndTarget.RenderMode = RenderMode.SoftwareOnly;
+ }
private void OnAutoStartupChecked(object sender, RoutedEventArgs e)
{
@@ -118,45 +125,13 @@ namespace Flow.Launcher
{
if (HotkeyControl.CurrentHotkeyAvailable)
{
- SetHotkey(HotkeyControl.CurrentHotkey, (o, args) =>
- {
- if (!Application.Current.MainWindow.IsVisible)
- {
- Application.Current.MainWindow.Visibility = Visibility.Visible;
- }
- else
- {
- Application.Current.MainWindow.Visibility = Visibility.Hidden;
- }
- });
- RemoveHotkey(settings.Hotkey);
+
+ HotKeyMapper.SetHotkey(HotkeyControl.CurrentHotkey, HotKeyMapper.OnHotkey);
+ HotKeyMapper.RemoveHotkey(settings.Hotkey);
settings.Hotkey = HotkeyControl.CurrentHotkey.ToString();
}
}
- void SetHotkey(HotkeyModel hotkey, EventHandler action)
- {
- string hotkeyStr = hotkey.ToString();
- try
- {
- HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action);
- }
- catch (Exception)
- {
- string errorMsg =
- string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
- MessageBox.Show(errorMsg);
- }
- }
-
- void RemoveHotkey(string hotkeyStr)
- {
- if (!string.IsNullOrEmpty(hotkeyStr))
- {
- HotkeyManager.Current.Remove(hotkeyStr);
- }
- }
-
private void OnDeleteCustomHotkeyClick(object sender, RoutedEventArgs e)
{
var item = viewModel.SelectedCustomPluginHotkey;
@@ -174,7 +149,7 @@ namespace Flow.Launcher
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
settings.CustomPluginHotkeys.Remove(item);
- RemoveHotkey(item.Hotkey);
+ HotKeyMapper.RemoveHotkey(item.Hotkey);
}
}
diff --git a/Flow.Launcher/Storage/UserSelectedRecord.cs b/Flow.Launcher/Storage/UserSelectedRecord.cs
index 8d8956cfa..23d8c4faf 100644
--- a/Flow.Launcher/Storage/UserSelectedRecord.cs
+++ b/Flow.Launcher/Storage/UserSelectedRecord.cs
@@ -28,6 +28,11 @@ namespace Flow.Launcher.Storage
private static int GenerateStaticHashCode(string s, int start = HASH_INITIAL)
{
+ if (s == null)
+ {
+ return start;
+ }
+
unchecked
{
// skip the empty space
@@ -101,4 +106,4 @@ namespace Flow.Launcher.Storage
return selectedCount;
}
}
-}
+}
\ No newline at end of file
diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml
index eb2af490a..df0304bc2 100644
--- a/Flow.Launcher/Themes/Base.xaml
+++ b/Flow.Launcher/Themes/Base.xaml
@@ -46,6 +46,8 @@