Merge pull request #363 from Flow-Launcher/dev

Release 1.8.0 | Plugin 2.0.0
This commit is contained in:
Jeremy Wu 2021-07-19 14:08:51 +10:00 committed by GitHub
commit aeba056df0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
225 changed files with 3538 additions and 1937 deletions

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View file

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View file

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View file

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View file

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View file

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View file

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View file

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View file

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 409 B

After

Width:  |  Height:  |  Size: 409 B

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 663 B

After

Width:  |  Height:  |  Size: 663 B

View file

Before

Width:  |  Height:  |  Size: 1,019 B

After

Width:  |  Height:  |  Size: 1,019 B

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View file

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View file

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWpf>true</UseWpf>
<UseWindowsForms>true</UseWindowsForms>
<OutputType>Library</OutputType>
@ -16,7 +16,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\Output\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
@ -53,14 +53,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Droplex" Version="1.3.1" />
<PackageReference Include="FSharp.Core" Version="4.7.1" />
<PackageReference Include="squirrel.windows" Version="1.5.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />

View file

@ -1,5 +1,8 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin
@ -21,17 +24,17 @@ namespace Flow.Launcher.Core.Plugin
};
}
protected override string ExecuteQuery(Query query)
protected override Task<Stream> ExecuteQueryAsync(Query query, CancellationToken token)
{
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
{
Method = "query",
Parameters = new object[] { query.Search },
Parameters = new object[] {query.Search},
};
_startInfo.Arguments = $"\"{request}\"";
return Execute(_startInfo);
return ExecuteAsync(_startInfo, token);
}
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
@ -40,10 +43,12 @@ namespace Flow.Launcher.Core.Plugin
return Execute(_startInfo);
}
protected override string ExecuteContextMenu(Result selectedResult) {
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel {
protected override string ExecuteContextMenu(Result selectedResult)
{
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
{
Method = "contextmenu",
Parameters = new object[] { selectedResult.ContextData },
Parameters = new object[] {selectedResult.ContextData},
};
_startInfo.Arguments = $"\"{request}\"";

View file

@ -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; }
@ -45,57 +42,23 @@ namespace Flow.Launcher.Core.Plugin
{
[JsonPropertyName("result")]
public new List<JsonRPCResult> Result { get; set; }
}
public class JsonRPCRequestModel : JsonRPCModelBase
public string DebugMessage { get; set; }
}
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 = Parameters.Aggregate("[", (current, o) => current + (GetParameterByType(o) + ","));
parameters = parameters.Substring(0, parameters.Length - 1) + "]";
rpc = string.Format(@"{{\""method\"":\""{0}\"",\""parameters\"":{1}", Method, parameters);
}
else
{
rpc = string.Format(@"{{\""method\"":\""{0}\"",\""parameters\"":[]", Method);
}
return rpc;
}
private string GetParameterByType(object parameter)
{
if (parameter == null) {
return "null";
}
if (parameter is string)
{
return string.Format(@"\""{0}\""", ReplaceEscapes(parameter.ToString()));
}
if (parameter is int || parameter is float || parameter is double)
{
return string.Format(@"{0}", parameter);
}
if (parameter is bool)
{
return string.Format(@"{0}", parameter.ToString().ToLower());
}
return 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);
}
}
@ -104,11 +67,7 @@ namespace Flow.Launcher.Core.Plugin
/// </summary>
public class JsonRPCServerRequestModel : JsonRPCRequestModel
{
public override string ToString()
{
string rpc = base.ToString();
return rpc + "}";
}
}
/// <summary>
@ -117,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 + "}";
}
}
/// <summary>
@ -134,4 +87,4 @@ namespace Flow.Launcher.Core.Plugin
{
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
}
}
}

View file

@ -1,15 +1,17 @@
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;
using System.Threading.Tasks;
using System.Windows.Forms;
using Flow.Launcher.Infrastructure.Exception;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin;
using JetBrains.Annotations;
namespace Flow.Launcher.Core.Plugin
{
@ -17,7 +19,7 @@ namespace Flow.Launcher.Core.Plugin
/// Represent the plugin that using JsonPRC
/// every JsonRPC plugin should has its own plugin instance
/// </summary>
internal abstract class JsonRPCPlugin : IPlugin, IContextMenu
internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu
{
protected PluginInitContext context;
public const string JsonRPC = "JsonRPC";
@ -27,24 +29,10 @@ namespace Flow.Launcher.Core.Plugin
/// </summary>
public abstract string SupportedLanguage { get; set; }
protected abstract string ExecuteQuery(Query query);
protected abstract Task<Stream> ExecuteQueryAsync(Query query, CancellationToken token);
protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest);
protected abstract string ExecuteContextMenu(Result selectedResult);
public List<Result> Query(Query query)
{
string output = ExecuteQuery(query);
try
{
return DeserializedResult(output);
}
catch (Exception e)
{
Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e);
return null;
}
}
public List<Result> LoadContextMenus(Result selectedResult)
{
string output = ExecuteContextMenu(selectedResult);
@ -59,55 +47,94 @@ namespace Flow.Launcher.Core.Plugin
}
}
private static readonly JsonSerializerOptions options = new()
{
PropertyNameCaseInsensitive = true,
IgnoreNullValues = true,
Converters =
{
new JsonObjectConverter()
}
};
private async Task<List<Result>> DeserializedResultAsync(Stream output)
{
if (output == Stream.Null) return null;
var queryResponseModel = await
JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
await output.DisposeAsync();
return ParseResults(queryResponseModel);
}
private List<Result> DeserializedResult(string output)
{
if (!String.IsNullOrEmpty(output))
if (string.IsNullOrEmpty(output)) return null;
var queryResponseModel =
JsonSerializer.Deserialize<JsonRPCQueryResponseModel>(output, options);
return ParseResults(queryResponseModel);
}
private List<Result> ParseResults(JsonRPCQueryResponseModel queryResponseModel)
{
var results = new List<Result>();
if (queryResponseModel.Result == null) return null;
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
{
List<Result> results = new List<Result>();
context.API.ShowMsg(queryResponseModel.DebugMessage);
}
JsonRPCQueryResponseModel queryResponseModel = JsonSerializer.Deserialize<JsonRPCQueryResponseModel>(output);
if (queryResponseModel.Result == null) return null;
foreach (JsonRPCResult result in queryResponseModel.Result)
foreach (JsonRPCResult result in queryResponseModel.Result)
{
result.Action = c =>
{
JsonRPCResult result1 = result;
result.Action = c =>
{
if (result1.JsonRPCAction == null) return false;
if (result.JsonRPCAction == null) return false;
if (!String.IsNullOrEmpty(result1.JsonRPCAction.Method))
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
{
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))
{
if (result1.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
{
ExecuteFlowLauncherAPI(result1.JsonRPCAction.Method.Substring(4), result1.JsonRPCAction.Parameters);
}
else
{
string actionReponse = ExecuteCallback(result1.JsonRPCAction);
JsonRPCRequestModel jsonRpcRequestModel = JsonSerializer.Deserialize<JsonRPCRequestModel>(actionReponse);
if (jsonRpcRequestModel != null
&& !String.IsNullOrEmpty(jsonRpcRequestModel.Method)
&& jsonRpcRequestModel.Method.StartsWith("Flow.Launcher."))
{
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4), jsonRpcRequestModel.Parameters);
}
}
return !result.JsonRPCAction.DontHideAfterAction;
}
return !result1.JsonRPCAction.DontHideAfterAction;
};
results.Add(result);
}
return results;
}
else
{
return null;
var jsonRpcRequestModel = JsonSerializer.Deserialize<JsonRPCRequestModel>(actionResponse, options);
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
{
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
jsonRpcRequestModel.Parameters);
}
}
return !result.JsonRPCAction.DontHideAfterAction;
};
results.Add(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
@ -117,9 +144,7 @@ namespace Flow.Launcher.Core.Plugin
catch (Exception)
{
#if (DEBUG)
{
throw;
}
throw;
#endif
}
}
@ -130,17 +155,20 @@ namespace Flow.Launcher.Core.Plugin
/// </summary>
/// <param name="fileName"></param>
/// <param name="arguments"></param>
/// <param name="token">Cancellation Token</param>
/// <returns></returns>
protected string Execute(string fileName, string arguments)
protected Task<Stream> ExecuteAsync(string fileName, string arguments, CancellationToken token = default)
{
ProcessStartInfo start = new ProcessStartInfo();
start.FileName = fileName;
start.Arguments = arguments;
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
return Execute(start);
ProcessStartInfo start = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
return ExecuteAsync(start, token);
}
protected string Execute(ProcessStartInfo startInfo)
@ -148,52 +176,104 @@ namespace Flow.Launcher.Core.Plugin
try
{
using var process = Process.Start(startInfo);
if (process == null)
{
Log.Error("|JsonRPCPlugin.Execute|Can't start new process");
return string.Empty;
}
if (process == null) return string.Empty;
using var standardOutput = process.StandardOutput;
var result = standardOutput.ReadToEnd();
if (string.IsNullOrEmpty(result))
{
using (var standardError = process.StandardError)
using var standardError = process.StandardError;
var error = standardError.ReadToEnd();
if (!string.IsNullOrEmpty(error))
{
var error = standardError.ReadToEnd();
if (!string.IsNullOrEmpty(error))
{
Log.Error($"|JsonRPCPlugin.Execute|{error}");
return string.Empty;
}
else
{
Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error.");
return string.Empty;
}
Log.Error($"|JsonRPCPlugin.Execute|{error}");
return string.Empty;
}
}
else if (result.StartsWith("DEBUG:"))
{
MessageBox.Show(new Form { TopMost = true }, result.Substring(6));
Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error.");
return string.Empty;
}
else
if (result.StartsWith("DEBUG:"))
{
return result;
MessageBox.Show(new Form
{
TopMost = true
}, result.Substring(6));
return string.Empty;
}
return result;
}
catch (Exception e)
{
Log.Exception($"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", e);
Log.Exception(
$"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>",
e);
return string.Empty;
}
}
public void Init(PluginInitContext ctx)
protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default)
{
context = ctx;
try
{
using var process = Process.Start(startInfo);
if (process == null)
{
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
return Stream.Null;
}
var result = process.StandardOutput.BaseStream;
token.ThrowIfCancellationRequested();
if (!process.StandardError.EndOfStream)
{
using var standardError = process.StandardError;
var error = await standardError.ReadToEndAsync();
if (!string.IsNullOrEmpty(error))
{
Log.Error($"|JsonRPCPlugin.ExecuteAsync|{error}");
return Stream.Null;
}
Log.Error("|JsonRPCPlugin.ExecuteAsync|Empty standard output and standard error.");
return Stream.Null;
}
return result;
}
catch (Exception e)
{
Log.Exception(
$"|JsonRPCPlugin.ExecuteAsync|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>",
e);
return Stream.Null;
}
}
public async Task<List<Result>> 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;
}
}
public virtual Task InitAsync(PluginInitContext context)
{
this.context = context;
return Task.CompletedTask;
}
}
}

View file

@ -1,5 +1,7 @@
using Flow.Launcher.Infrastructure;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
@ -11,17 +13,26 @@ namespace Flow.Launcher.Core.Plugin
{
private readonly AssemblyDependencyResolver dependencyResolver;
private readonly AssemblyDependencyResolver referencedPluginPackageDependencyResolver;
private readonly AssemblyName assemblyName;
private static readonly ConcurrentDictionary<string, byte> loadedAssembly;
static PluginAssemblyLoader()
{
var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
loadedAssembly = new ConcurrentDictionary<string, byte>(
currentAssemblies.Select(x => new KeyValuePair<string, byte>(x.FullName, default)));
AppDomain.CurrentDomain.AssemblyLoad += (sender, args) =>
{
loadedAssembly[args.LoadedAssembly.FullName] = default;
};
}
internal PluginAssemblyLoader(string assemblyFilePath)
{
dependencyResolver = new AssemblyDependencyResolver(assemblyFilePath);
assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyFilePath));
referencedPluginPackageDependencyResolver =
new AssemblyDependencyResolver(Path.Combine(Constant.ProgramDirectory, "Flow.Launcher.Plugin.dll"));
}
internal Assembly LoadAssemblyAndDependencies()
@ -33,25 +44,24 @@ namespace Flow.Launcher.Core.Plugin
{
string assemblyPath = dependencyResolver.ResolveAssemblyToPath(assemblyName);
// When resolving dependencies, ignore assembly depenedencies that already exits with Flow.Launcher.Plugin
// Otherwise will get unexpected behaviour with plugins, e.g. JsonIgnore attribute not honored in WebSearch or other plugins
// that use Newtonsoft.Json
if (assemblyPath == null || ExistsInReferencedPluginPackage(assemblyName))
// When resolving dependencies, ignore assembly depenedencies that already exits with Flow.Launcher
// Otherwise duplicate assembly will be loaded and some weird behavior will occur, such as WinRT.Runtime.dll
// will fail due to loading multiple versions in process, each with their own static instance of registration state
if (assemblyPath == null || ExistsInReferencedPackage(assemblyName))
return null;
return LoadFromAssemblyPath(assemblyPath);
}
internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, params Type[] types)
internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type)
{
var allTypes = assembly.ExportedTypes;
return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Intersect(types).Any());
return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Any(t => t == type));
}
internal bool ExistsInReferencedPluginPackage(AssemblyName assemblyName)
internal bool ExistsInReferencedPackage(AssemblyName assemblyName)
{
return referencedPluginPackageDependencyResolver.ResolveAssemblyToPath(assemblyName) != null;
return loadedAssembly.ContainsKey(assemblyName.FullName);
}
}
}
}

View file

@ -20,7 +20,7 @@ namespace Flow.Launcher.Core.Plugin
public static List<PluginMetadata> Parse(string[] pluginDirectories)
{
var allPluginMetadata = new List<PluginMetadata>();
var directories = pluginDirectories.SelectMany(Directory.GetDirectories);
var directories = pluginDirectories.SelectMany(Directory.EnumerateDirectories);
// todo use linq when diable plugin is implmented since parallel.foreach + list is not thread saft
foreach (var directory in directories)

View file

@ -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<PluginPair> _contextMenuPlugins;
public static List<PluginPair> AllPlugins { get; private set; }
public static readonly List<PluginPair> GlobalPlugins = new List<PluginPair>();
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new Dictionary<string, PluginPair>();
public static readonly HashSet<PluginPair> GlobalPlugins = new();
public static readonly Dictionary<string, PluginPair> NonGlobalPlugins = new();
public static IPublicAPI API { private set; get; }
@ -33,7 +33,10 @@ namespace Flow.Launcher.Core.Plugin
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
/// </summary>
private static readonly string[] Directories = { Constant.PreinstalledDirectory, DataLocation.PluginsDirectory };
private static readonly string[] Directories =
{
Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
};
private static void DeletePythonBinding()
{
@ -51,6 +54,24 @@ 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()
@ -97,16 +118,9 @@ namespace Flow.Launcher.Core.Plugin
{
try
{
var milliseconds = pair.Plugin switch
{
IAsyncPlugin plugin
=> await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>",
() => plugin.InitAsync(new PluginInitContext(pair.Metadata, API))),
IPlugin plugin
=> Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>",
() => plugin.Init(new PluginInitContext(pair.Metadata, API))),
_ => throw new ArgumentException(),
};
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>",
() => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, API)));
pair.Metadata.InitTime += milliseconds;
Log.Info(
$"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
@ -124,7 +138,9 @@ namespace Flow.Launcher.Core.Plugin
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
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)
{
@ -147,12 +163,15 @@ namespace Flow.Launcher.Core.Plugin
}
}
public static List<PluginPair> ValidPluginsForQuery(Query query)
public static ICollection<PluginPair> ValidPluginsForQuery(Query query)
{
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
{
var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair> { plugin };
return new List<PluginPair>
{
plugin
};
}
else
{
@ -160,7 +179,7 @@ namespace Flow.Launcher.Core.Plugin
}
}
public static async Task<List<Result>> QueryForPlugin(PluginPair pair, Query query, CancellationToken token)
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
{
var results = new List<Result>();
try
@ -169,22 +188,12 @@ namespace Flow.Launcher.Core.Plugin
long milliseconds = -1L;
switch (pair.Plugin)
{
case IAsyncPlugin plugin:
milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}",
async () => results = await plugin.QueryAsync(query, token).ConfigureAwait(false));
break;
case IPlugin plugin:
await Task.Run(() => milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}",
() => results = plugin.Query(query)), token).ConfigureAwait(false);
break;
default:
throw new ArgumentOutOfRangeException();
}
milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}",
async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
if (results == null)
return results;
return null;
UpdatePluginMetadata(results, metadata, query);
metadata.QueryCount += 1;
@ -195,11 +204,7 @@ namespace Flow.Launcher.Core.Plugin
catch (OperationCanceledException)
{
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
return results = null;
}
catch (Exception e)
{
Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e);
return null;
}
return results;
@ -245,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;
@ -266,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);
}
@ -290,7 +297,7 @@ namespace Flow.Launcher.Core.Plugin
}
/// <summary>
/// used to add action keyword for multiple action keyword plugin
/// used to remove action keyword for multiple action keyword plugin
/// e.g. web search
/// </summary>
public static void RemoveActionKeyword(string id, string oldActionkeyword)
@ -299,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);
}
@ -322,4 +327,4 @@ namespace Flow.Launcher.Core.Plugin
}
}
}
}
}

View file

@ -3,26 +3,27 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading.Tasks;
using System.Windows.Forms;
using Droplex;
using Flow.Launcher.Infrastructure;
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<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
{
var dotnetPlugins = DotNetPlugins(metadatas);
var pythonPlugins = PythonPlugins(metadatas, settings.PythonDirectory);
var pythonPlugins = PythonPlugins(metadatas, settings);
var executablePlugins = ExecutablePlugins(metadatas);
var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
return plugins;
@ -43,23 +44,23 @@ namespace Flow.Launcher.Core.Plugin
#if DEBUG
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
var assembly = assemblyLoader.LoadAssemblyAndDependencies();
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin),
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
typeof(IAsyncPlugin));
var plugin = Activator.CreateInstance(type);
var plugin = Activator.CreateInstance(type) as IAsyncPlugin;
#else
Assembly assembly = null;
object plugin = null;
IAsyncPlugin plugin = null;
try
{
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
assembly = assemblyLoader.LoadAssemblyAndDependencies();
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly, typeof(IPlugin),
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
typeof(IAsyncPlugin));
plugin = Activator.CreateInstance(type);
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
}
catch (Exception e) when (assembly == null)
{
@ -77,18 +78,14 @@ namespace Flow.Launcher.Core.Plugin
{
Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
#endif
if (plugin == null)
{
erroredPlugins.Add(metadata.Name);
return;
}
#endif
plugins.Add(new PluginPair
{
Plugin = plugin,
Metadata = metadata
});
plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
});
metadata.InitTime += milliseconds;
}
@ -113,73 +110,100 @@ namespace Flow.Launcher.Core.Plugin
return plugins;
}
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, string pythonDirectory)
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, PluginsSettings settings)
{
// try to set Constant.PythonPath, either from
// PATH or from the given pythonDirectory
if (string.IsNullOrEmpty(pythonDirectory))
{
var paths = Environment.GetEnvironmentVariable(PATH);
if (paths != null)
{
var pythonInPath = paths
.Split(';')
.Where(p => p.ToLower().Contains(Python))
.Any();
if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
return new List<PluginPair>();
if (pythonInPath)
{
Constant.PythonPath = PythonExecutable;
}
else
{
Log.Error("|PluginsLoader.PythonPlugins|Python can't be found in PATH.");
}
}
else
if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory))
return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable));
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
{
Log.Error("|PluginsLoader.PythonPlugins|PATH environment variable is not set.");
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var result = dlg.ShowDialog();
if (result == DialogResult.OK)
{
string pythonDirectory = dlg.SelectedPath;
if (!string.IsNullOrEmpty(pythonDirectory))
{
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(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.PythonPlugins|Can't find python executable in {path}");
Log.Error("PluginsLoader",
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
"PythonPlugins");
}
}
// if we have a path to the python executable,
// load every python plugin pair.
if (String.IsNullOrEmpty(Constant.PythonPath))
if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(pythonPath))
{
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");
return new List<PluginPair>();
}
else
{
return source
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
.Select(metadata => new PluginPair
{
Plugin = new PythonPlugin(Constant.PythonPath),
Metadata = metadata
});
}
return SetPythonPathForPluginPairs(source, pythonPath);
}
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
private static IEnumerable<PluginPair> SetPythonPathForPluginPairs(List<PluginMetadata> source, string pythonPath)
=> source
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
.Select(metadata => new PluginPair
{
Plugin = new PythonPlugin(pythonPath),
Metadata = metadata
})
.ToList();
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> 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
});
}
}

View file

@ -1,6 +1,8 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin;
@ -26,39 +28,52 @@ 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 string ExecuteQuery(Query query)
protected override Task<Stream> 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;
return Execute(_startInfo);
return ExecuteAsync(_startInfo, token);
}
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;
}
}
}

View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]

View file

@ -0,0 +1,42 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Core.Resource
{
public class JsonObjectConverter : JsonConverter<object>
{
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.");
}
}
}

View file

@ -17,6 +17,8 @@ namespace Flow.Launcher.Core.Resource
{
public class Theme
{
private const int ShadowExtraMargin = 12;
private readonly List<string> _themeDirectories = new List<string>();
private ResourceDictionary _oldResource;
private string _oldTheme;
@ -26,6 +28,10 @@ namespace Flow.Launcher.Core.Resource
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder);
public bool BlurEnabled { get; set; }
private double mainWindowWidth;
public Theme()
{
_themeDirectories.Add(DirectoryPath);
@ -86,8 +92,12 @@ namespace Flow.Launcher.Core.Resource
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
if (Settings.UseDropShadowEffect)
BlurEnabled = IsBlurTheme();
if (Settings.UseDropShadowEffect && !BlurEnabled)
AddDropShadowEffectToCurrentTheme();
SetBlurForWindow();
}
catch (DirectoryNotFoundException e)
{
@ -179,6 +189,21 @@ namespace Flow.Launcher.Core.Resource
Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p)));
}
var windowStyle = dict["WindowStyle"] as Style;
var width = windowStyle?.Setters.OfType<Setter>().Where(x => x.Property.Name == "Width")
.Select(x => x.Value).FirstOrDefault();
if (width == null)
{
windowStyle = dict["BaseWindowStyle"] as Style;
width = windowStyle?.Setters.OfType<Setter>().Where(x => x.Property.Name == "Width")
.Select(x => x.Value).FirstOrDefault();
}
mainWindowWidth = (double)width;
return dict;
}
@ -224,17 +249,54 @@ 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);
}
public void RemoveDropShadowEffectToCurrentTheme()
public void RemoveDropShadowEffectFromCurrentTheme()
{
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);
}
@ -281,35 +343,39 @@ namespace Flow.Launcher.Core.Resource
/// </summary>
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);

View file

@ -87,7 +87,7 @@ namespace Flow.Launcher.Core
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
}
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException || e is TaskCanceledException)
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException || e.InnerException is TimeoutException)
{
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),

View file

@ -21,6 +21,7 @@ namespace Flow.Launcher.Infrastructure
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new";
public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion;
public const string Documentation = "https://flow-launcher.github.io/docs/#/usage-tips";
public static readonly int ThumbnailSize = 64;
private static readonly string ImagesDirectory = Path.Combine(ProgramDirectory, "Images");

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<ProjectGuid>{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}</ProjectGuid>
<OutputType>Library</OutputType>
<UseWpf>true</UseWpf>
@ -15,7 +15,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\Output\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
@ -49,8 +49,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NLog.Schema" Version="4.7.0-rc1" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.9.0" />
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="16.10.56" />
<PackageReference Include="NLog" Version="4.7.10" />
<PackageReference Include="NLog.Schema" Version="4.7.10" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.12.0" />
<PackageReference Include="System.Drawing.Common" Version="4.7.0" />
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
</ItemGroup>

View file

@ -90,7 +90,7 @@ namespace Flow.Launcher.Infrastructure.Http
{
try
{
using var response = await client.GetAsync(url, token);
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
if (response.StatusCode == HttpStatusCode.OK)
{
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
@ -150,7 +150,7 @@ namespace Flow.Launcher.Infrastructure.Http
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
var response = await client.GetAsync(url, token);
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
return await response.Content.ReadAsStreamAsync();
}
}

View file

@ -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
}
/// <param name="message">example: "|prefix|unprefixed" </param>
[MethodImpl(MethodImplOptions.Synchronized)]
/// <param name="e">Exception</param>
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);
}

View file

@ -8,7 +8,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher.Infrastructure.Storage
{
public class FlowLauncherJsonStorage<T> : JsonStrorage<T> where T : new()
public class FlowLauncherJsonStorage<T> : JsonStorage<T> where T : new()
{
public FlowLauncherJsonStorage()
{

View file

@ -1,11 +1,8 @@
namespace Flow.Launcher.Infrastructure.Storage
using System;
namespace Flow.Launcher.Infrastructure.Storage
{
/// <summary>
/// Save plugin settings/cache,
/// todo should be merged into a abstract class intead of seperate interface
/// </summary>
public interface ISavable
{
void Save();
}
}
[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 { }
}

View file

@ -9,10 +9,9 @@ namespace Flow.Launcher.Infrastructure.Storage
/// <summary>
/// Serialize object using json format.
/// </summary>
public class JsonStrorage<T> where T : new()
public class JsonStorage<T> where T : new()
{
private readonly JsonSerializerOptions _serializerSettings;
private T _data;
protected T _data;
// need a new directory name
public const string DirectoryName = "Settings";
public const string FileSuffix = ".json";
@ -20,24 +19,14 @@ namespace Flow.Launcher.Infrastructure.Storage
public string DirectoryPath { get; set; }
internal JsonStrorage()
{
// use property initialization instead of DefaultValueAttribute
// easier and flexible for default value of object
_serializerSettings = new JsonSerializerOptions
{
IgnoreNullValues = false
};
}
public T Load()
{
if (File.Exists(FilePath))
{
var searlized = File.ReadAllText(FilePath);
if (!string.IsNullOrWhiteSpace(searlized))
var serialized = File.ReadAllText(FilePath);
if (!string.IsNullOrWhiteSpace(serialized))
{
Deserialize(searlized);
Deserialize(serialized);
}
else
{
@ -51,16 +40,16 @@ namespace Flow.Launcher.Infrastructure.Storage
return _data.NonNull();
}
private void Deserialize(string searlized)
private void Deserialize(string serialized)
{
try
{
_data = JsonSerializer.Deserialize<T>(searlized, _serializerSettings);
_data = JsonSerializer.Deserialize<T>(serialized);
}
catch (JsonException e)
{
LoadDefault();
Log.Exception($"|JsonStrorage.Deserialize|Deserialize error for json <{FilePath}>", e);
Log.Exception($"|JsonStorage.Deserialize|Deserialize error for json <{FilePath}>", e);
}
if (_data == null)
@ -98,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<T> : JsonStorage<T> where T : new() { }
}

View file

@ -3,17 +3,23 @@ using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher.Infrastructure.Storage
{
public class PluginJsonStorage<T> :JsonStrorage<T> where T : new()
public class PluginJsonStorage<T> :JsonStorage<T> where T : new()
{
public PluginJsonStorage()
{
// C# releated, add python releated below
// C# related, add python related below
var dataType = typeof(T);
var assemblyName = typeof(T).Assembly.GetName().Name;
var assemblyName = dataType.Assembly.GetName().Name;
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName);
Helper.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}");
}
public PluginJsonStorage(T data) : this()
{
_data = data;
}
}
}

View file

@ -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<string>();
metadata.ActionKeyword = string.Empty;
}
metadata.Disabled = settings.Disabled;
metadata.Priority = settings.Priority;
}

View file

@ -97,6 +97,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))]

View file

@ -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<Result> LoadContextMenus(Result selectedResult);
}
/// <summary>
/// Represent plugins that support internationalization
/// </summary>
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<Result> Results;
public Query Query;
}
}

View file

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Base Interface for Flow's special plugin feature interface
/// </summary>
public interface IFeatures
{
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<ProjectGuid>{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}</ProjectGuid>
<UseWPF>true</UseWPF>
<OutputType>Library</OutputType>
@ -14,10 +14,10 @@
</PropertyGroup>
<PropertyGroup>
<Version>1.4.0</Version>
<PackageVersion>1.4.0</PackageVersion>
<AssemblyVersion>1.4.0</AssemblyVersion>
<FileVersion>1.4.0</FileVersion>
<Version>2.0.0</Version>
<PackageVersion>2.0.0</PackageVersion>
<AssemblyVersion>2.0.0</AssemblyVersion>
<FileVersion>2.0.0</FileVersion>
<PackageId>Flow.Launcher.Plugin</PackageId>
<Authors>Flow-Launcher</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -34,7 +34,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\Output\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>

View file

@ -1,104 +0,0 @@
using Flow.Launcher.Plugin.SharedModels;
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Public APIs that plugin can use
/// </summary>
public interface IPublicAPI
{
/// <summary>
/// Change Flow.Launcher query
/// </summary>
/// <param name="query">query text</param>
/// <param name="requery">
/// force requery By default, Flow Launcher will not fire query if your query is same with existing one.
/// Set this to true to force Flow Launcher requerying
/// </param>
void ChangeQuery(string query, bool requery = false);
/// <summary>
/// Restart Flow Launcher
/// </summary>
void RestartApp();
/// <summary>
/// Save all Flow Launcher settings
/// </summary>
void SaveAppAllSettings();
/// <summary>
/// Reloads any Plugins that have the
/// IReloadable implemented. It refeshes
/// Plugin's in memory data with new content
/// added by user.
/// </summary>
Task ReloadAllPluginData();
/// <summary>
/// Check for new Flow Launcher update
/// </summary>
void CheckForNewUpdate();
/// <summary>
/// Show message box
/// </summary>
/// <param name="title">Message title</param>
/// <param name="subTitle">Message subtitle</param>
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
void ShowMsg(string title, string subTitle = "", string iconPath = "");
/// <summary>
/// Show message box
/// </summary>
/// <param name="title">Message title</param>
/// <param name="subTitle">Message subtitle</param>
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
/// <param name="useMainWindowAsOwner">when true will use main windows as the owner</param>
void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true);
/// <summary>
/// Open setting dialog
/// </summary>
void OpenSettingDialog();
/// <summary>
/// Get translation of current language
/// You need to implement IPluginI18n if you want to support multiple languages for your plugin
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
string GetTranslation(string key);
/// <summary>
/// Get all loaded plugins
/// </summary>
/// <returns></returns>
List<PluginPair> GetAllPlugins();
/// <summary>
/// Fired after global keyboard events
/// if you want to hook something like Ctrl+R, you should use this event
/// </summary>
event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
MatchResult FuzzySearch(string query, string stringToCompare);
Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default);
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath);
void AddActionKeyword(string pluginId, string newActionKeyword);
void RemoveActionKeyword(string pluginId, string oldActionKeyword);
}
}

View file

@ -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.
/// </summary>
public interface IAsyncReloadable
public interface IAsyncReloadable : IFeatures
{
Task ReloadDataAsync();
}
}
}

View file

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Flow.Launcher.Plugin
{
public interface IContextMenu : IFeatures
{
List<Result> LoadContextMenus(Result selectedResult);
}
}

View file

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
@ -9,7 +11,7 @@ namespace Flow.Launcher.Plugin
/// or performaing CPU intense jobs (performing better with cancellation), please try the IAsyncPlugin interface
/// </para>
/// </summary>
public interface IPlugin
public interface IPlugin : IAsyncPlugin
{
/// <summary>
/// Querying when user's search changes
@ -27,5 +29,9 @@ namespace Flow.Launcher.Plugin
/// </summary>
/// <param name="context"></param>
void Init(PluginInitContext context);
Task IAsyncPlugin.InitAsync(PluginInitContext context) => Task.Run(() => Init(context));
Task<List<Result>> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query));
}
}

View file

@ -0,0 +1,12 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Represent plugins that support internationalization
/// </summary>
public interface IPluginI18n : IFeatures
{
string GetTranslatedPluginTitle();
string GetTranslatedPluginDescription();
}
}

View file

@ -0,0 +1,189 @@
using Flow.Launcher.Plugin.SharedModels;
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Public APIs that plugin can use
/// </summary>
public interface IPublicAPI
{
/// <summary>
/// Change Flow.Launcher query
/// </summary>
/// <param name="query">query text</param>
/// <param name="requery">
/// force requery By default, Flow Launcher will not fire query if your query is same with existing one.
/// Set this to true to force Flow Launcher requerying
/// </param>
void ChangeQuery(string query, bool requery = false);
/// <summary>
/// Restart Flow Launcher
/// </summary>
void RestartApp();
/// <summary>
/// Save everything, all of Flow Launcher and plugins' data and settings
/// </summary>
void SaveAppAllSettings();
/// <summary>
/// Save all Flow's plugins settings
/// </summary>
void SavePluginSettings();
/// <summary>
/// Reloads any Plugins that have the
/// IReloadable implemented. It refeshes
/// Plugin's in memory data with new content
/// added by user.
/// </summary>
Task ReloadAllPluginData();
/// <summary>
/// Check for new Flow Launcher update
/// </summary>
void CheckForNewUpdate();
/// <summary>
/// Show the error message using Flow's standard error icon.
/// </summary>
/// <param name="title">Message title</param>
/// <param name="subTitle">Optional message subtitle</param>
void ShowMsgError(string title, string subTitle = "");
/// <summary>
/// Show message box
/// </summary>
/// <param name="title">Message title</param>
/// <param name="subTitle">Message subtitle</param>
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
void ShowMsg(string title, string subTitle = "", string iconPath = "");
/// <summary>
/// Show message box
/// </summary>
/// <param name="title">Message title</param>
/// <param name="subTitle">Message subtitle</param>
/// <param name="iconPath">Message icon path (relative path to your plugin folder)</param>
/// <param name="useMainWindowAsOwner">when true will use main windows as the owner</param>
void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true);
/// <summary>
/// Open setting dialog
/// </summary>
void OpenSettingDialog();
/// <summary>
/// Get translation of current language
/// You need to implement IPluginI18n if you want to support multiple languages for your plugin
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
string GetTranslation(string key);
/// <summary>
/// Get all loaded plugins
/// </summary>
/// <returns></returns>
List<PluginPair> GetAllPlugins();
/// <summary>
/// Fired after global keyboard events
/// if you want to hook something like Ctrl+R, you should use this event
/// </summary>
event FlowLauncherGlobalKeyboardEventHandler GlobalKeyboardEvent;
/// <summary>
/// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses
/// </summary>
/// <param name="query">Query string</param>
/// <param name="stringToCompare">The string that will be compared against the query</param>
/// <returns>Match results</returns>
MatchResult FuzzySearch(string query, string stringToCompare);
/// <summary>
/// Http download the spefic url and return as string
/// </summary>
/// <param name="url">URL to call Http Get</param>
/// <param name="token">Cancellation Token</param>
/// <returns>Task to get string result</returns>
Task<string> HttpGetStringAsync(string url, CancellationToken token = default);
/// <summary>
/// Http download the spefic url and return as stream
/// </summary>
/// <param name="url">URL to call Http Get</param>
/// <param name="token">Cancellation Token</param>
/// <returns>Task to get stream result</returns>
Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default);
/// <summary>
/// Download the specific url to a cretain file path
/// </summary>
/// <param name="url">URL to download file</param>
/// <param name="token">place to store file</param>
/// <returns>Task showing the progress</returns>
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
/// <summary>
/// Add ActionKeyword for specific plugin
/// </summary>
/// <param name="pluginId">ID for plugin that needs to add action keyword</param>
/// <param name="newActionKeyword">The actionkeyword that is supposed to be added</param>
void AddActionKeyword(string pluginId, string newActionKeyword);
/// <summary>
/// Remove ActionKeyword for specific plugin
/// </summary>
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
/// <param name="newActionKeyword">The actionkeyword that is supposed to be removed</param>
void RemoveActionKeyword(string pluginId, string oldActionKeyword);
/// <summary>
/// Log debug message
/// Message will only be logged in Debug mode
/// </summary>
void LogDebug(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log info message
/// </summary>
void LogInfo(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log warning message
/// </summary>
void LogWarn(string className, string message, [CallerMemberName] string methodName = "");
/// <summary>
/// Log an Exception. Will throw if in debug mode so developer will be aware,
/// otherwise logs the eror message. This is the primary logging method used for Flow
/// </summary>
void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "");
/// <summary>
/// Load JsonStorage for current plugin's setting. This is the method used to load settings from json in Flow.
/// When the file is not exist, it will create a new instance for the specific type.
/// </summary>
/// <typeparam name="T">Type for deserialization</typeparam>
/// <returns></returns>
T LoadSettingJsonStorage<T>() where T : new();
/// <summary>
/// Save JsonStorage for current plugin's setting. This is the method used to save settings to json in Flow.Launcher
/// This method will save the original instance loaded with LoadJsonStorage.
/// This API call is for manually Save. Flow will automatically save all setting type that has called LoadSettingJsonStorage or SaveSettingJsonStorage previously.
/// </summary>
/// <typeparam name="T">Type for Serialization</typeparam>
/// <returns></returns>
void SaveSettingJsonStorage<T>() where T : new();
}
}

View file

@ -15,8 +15,8 @@
/// If requiring reloading data asynchronously, please use the IAsyncReloadable interface
/// </para>
/// </summary>
public interface IReloadable
public interface IReloadable : IFeatures
{
void ReloadData();
}
}
}

View file

@ -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<Result> Results;
public Query Query;
public CancellationToken Token { get; init; }
}
}

View file

@ -0,0 +1,12 @@
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Save addtional plugin data. Inherit this interface if additional data e.g. cache needs to be saved,
/// Otherwise if LoadSettingJsonStorage or SaveSettingJsonStorage has been callded,
/// plugin settings will be automatically saved (see Flow.Launcher/PublicAPIInstance.SavePluginSettings) by Flow
/// </summary>
public interface ISavable : IFeatures
{
void Save();
}
}

View file

@ -2,7 +2,7 @@
{
public class PluginPair
{
public object Plugin { get; internal set; }
public IAsyncPlugin Plugin { get; internal set; }
public PluginMetadata Metadata { get; internal set; }

View file

@ -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

View file

@ -10,9 +10,18 @@ namespace Flow.Launcher.Plugin
{
private string _pluginDirectory;
private string _icoPath;
/// <summary>
/// Provides the title of the result. This is always required.
/// </summary>
public string Title { get; set; }
public string SubTitle { get; set; }
/// <summary>
/// Provides additional details for the result. This is optional
/// </summary>
public string SubTitle { get; set; } = string.Empty;
/// <summary>
/// This holds the action keyword that triggered the result.

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
<ProjectGuid>{FF742965-9A80-41A5-B042-D6C7D3A21708}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
@ -15,7 +15,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
@ -49,12 +49,12 @@
<ItemGroup>
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
</ItemGroup>
</Project>

View file

@ -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);

View file

@ -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<Stream> 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<JsonRPCQueryResponseModel> 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);
}
}
}
}

View file

@ -61,7 +61,6 @@ namespace Flow.Launcher
_settingsVM = new SettingWindowViewModel(_updater, _portable);
_settings = _settingsVM.Settings;
_alphabet.Initialize(_settings);
_stringMatcher = new StringMatcher(_alphabet);
StringMatcher.Instance = _stringMatcher;
@ -69,6 +68,9 @@ namespace Flow.Launcher
PluginManager.LoadPlugins(_settings.PluginSettings);
_mainVM = new MainViewModel(_settings);
HotKeyMapper.Initialize(_mainVM);
API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet);
Http.API = API;

View file

@ -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<HotkeyEventArgs> 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)

View file

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<StartupObject>Flow.Launcher.App</StartupObject>
@ -17,7 +17,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<DebugType>portable</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\Output\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
@ -81,7 +81,7 @@
<ItemGroup>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="ModernWpfUI" Version="0.8.3" />
<PackageReference Include="ModernWpfUI" Version="0.9.3" />
<PackageReference Include="NHotkey.Wpf" Version="1.2.1" />
<PackageReference Include="NuGet.CommandLine" Version="5.4.0">
<PrivateAssets>all</PrivateAssets>

View file

@ -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;
}
}
}
}

View file

@ -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<HotkeyEventArgs> action)
{
var hotkey = new HotkeyModel(hotkeyStr);
SetHotkey(hotkey, action);
}
internal static void SetHotkey(HotkeyModel hotkey, EventHandler<HotkeyEventArgs> 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;
}
}
/// <summary>
/// Checks if Flow Launcher should ignore any hotkeys
/// </summary>
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;
}
}
}

View file

@ -11,7 +11,16 @@ namespace Flow.Launcher.Helper
var window = Application.Current.Windows.OfType<Window>().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;

View file

@ -10,7 +10,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="125" />
</Grid.ColumnDefinitions>
<TextBox x:Name="tbHotkey" TabIndex="100" VerticalContentAlignment="Center" Grid.Column="0"
PreviewKeyDown="TbHotkey_OnPreviewKeyDown" input:InputMethod.IsInputMethodEnabled="False"/>

View file

@ -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
{

View file

@ -31,11 +31,15 @@
<system:String x:Key="ignoreHotkeysOnFullscreen">Ignore hotkeys in fullscreen mode</system:String>
<system:String x:Key="pythonDirectory">Python Directory</system:String>
<system:String x:Key="autoUpdates">Auto Update</system:String>
<system:String x:Key="autoHideScrollBar">Auto Hide Scroll Bar</system:String>
<system:String x:Key="autoHideScrollBarToolTip">Automatically hides the Settings window scroll bar and show when hover the mouse over it</system:String>
<system:String x:Key="selectPythonDirectory">Select</system:String>
<system:String x:Key="hideOnStartup">Hide Flow Launcher on startup</system:String>
<system:String x:Key="hideNotifyIcon">Hide tray icon</system:String>
<system:String x:Key="querySearchPrecision">Query Search Precision</system:String>
<system:String x:Key="ShouldUsePinyin">Should Use Pinyin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for transliterating Chinese</system:String>
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
<!--Setting Plugin-->
<system:String x:Key="plugin">Plugin</system:String>
@ -47,6 +51,7 @@
<system:String x:Key="newActionKeyword">New action keyword:</system:String>
<system:String x:Key="currentPriority">Current Priority:</system:String>
<system:String x:Key="newPriority">New Priority:</system:String>
<system:String x:Key="priority">Priority:</system:String>
<system:String x:Key="pluginDirectory">Plugin Directory</system:String>
<system:String x:Key="author">Author</system:String>
<system:String x:Key="plugin_init_time">Init time:</system:String>
@ -69,6 +74,7 @@
<system:String x:Key="openResultModifiers">Open Result Modifiers</system:String>
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
<system:String x:Key="customQueryHotkey">Custom Query Hotkey</system:String>
<system:String x:Key="customQuery">Query</system:String>
<system:String x:Key="delete">Delete</system:String>
<system:String x:Key="edit">Edit</system:String>
<system:String x:Key="add">Add</system:String>
@ -107,6 +113,7 @@
or go to https://github.com/Flow-Launcher/Flow.Launcher/releases to download updates manually.
</system:String>
<system:String x:Key="releaseNotes">Release Notes</system:String>
<system:String x:Key="documentation">Usage Tips:</system:String>
<!--Priority Setting Dialog-->
<system:String x:Key="priority_tips">Greater the number, the higher the result will be ranked. Try setting it as 5. If you want the results to be lower than any other plugin's, provide a negative number</system:String>
@ -121,6 +128,7 @@
<system:String x:Key="newActionKeywordsCannotBeEmpty">New Action Keyword can't be empty</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, please choose a different one</system:String>
<system:String x:Key="success">Success</system:String>
<system:String x:Key="completedSuccessfully">Completed successfully</system:String>
<system:String x:Key="actionkeyword_tips">Use * if you don't want to specify an action keyword</system:String>
<!--Custom Query Hotkey Dialog-->
@ -131,7 +139,7 @@
<system:String x:Key="update">Update</system:String>
<!--Hotkey Control-->
<system:String x:Key="hotkeyUnavailable">Hotkey unavailable</system:String>
<system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>
<!--Crash Reporter-->
<system:String x:Key="reportWindow_version">Version</system:String>

View file

@ -36,6 +36,7 @@
<system:String x:Key="hideNotifyIcon">Schovať ikonu z oblasti oznámení</system:String>
<system:String x:Key="querySearchPrecision">Presnosť vyhľadávania</system:String>
<system:String x:Key="ShouldUsePinyin">Použiť Pinyin</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">Umožňuje vyhľadávanie pomocou Pinyin. Pinyin je štandardný systém romanizovaného pravopisu pre transliteráciu čínštiny</system:String>
<!--Setting Plugin-->
<system:String x:Key="plugin">Plugin</system:String>
@ -107,6 +108,7 @@
alebo prejdite na https://github.com/Flow-Launcher/Flow.Launcher/releases pre manuálne stiahnutie aktualizácie.
</system:String>
<system:String x:Key="releaseNotes">Poznámky k vydaniu</system:String>
<system:String x:Key="documentation">Tipy na používanie:</system:String>
<!--Priority Setting Dialog-->
<system:String x:Key="priority_tips">Vyššie číslo znamená, že výsledok bude vyššie. Skúste nastaviť napr. 5. Ak chcete, aby boli výsledky nižšie ako ktorékoľvek iné doplnky, zadajte záporné číslo</system:String>
@ -121,6 +123,7 @@
<system:String x:Key="newActionKeywordsCannotBeEmpty">Nová skratka pre akciu nemôže byť prázdna</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">Nová skratka pre akciu bola priradená pre iný plugin, prosím, zvoľte inú skratku</system:String>
<system:String x:Key="success">Úspešné</system:String>
<system:String x:Key="completedSuccessfully">Úspešne dokončené</system:String>
<system:String x:Key="actionkeyword_tips">Použite * ak nechcete určiť skratku pre akciu</system:String>
<!--Custom Query Hotkey Dialog-->
@ -158,7 +161,7 @@
<system:String x:Key="update_flowlauncher_update_found">Bola nájdená aktualizácia</system:String>
<system:String x:Key="update_flowlauncher_updating">Aktualizuje sa…</system:String>
<system:String x:Key="update_flowlauncher_fail_moving_portable_user_profile_data">Flow Launcher nedokázal presunúť používateľské údaje do aktualizovanej verzie.
Prosím, presuňte profilový priečinok data z {0} do {1}</system:String>
Prosím, presuňte profilový priečinok data z {0} do {1}</system:String>
<system:String x:Key="update_flowlauncher_new_update">Nová aktualizácia</system:String>
<system:String x:Key="update_flowlauncher_update_new_version_available">Je dostupná nová verzia Flow Launchera {0}</system:String>
<system:String x:Key="update_flowlauncher_update_error">Počas inštalácie aktualizácií došlo k chybe</system:String>

View file

@ -4,7 +4,7 @@
<!--主窗体-->
<system:String x:Key="registerHotkeyFailed">注册热键:{0} 失败</system:String>
<system:String x:Key="couldnotStartCmd">启动命令 {0} 失败</system:String>
<system:String x:Key="invalidFlowLauncherPluginFileFormat">不是合法的Flow Launcher插件格式</system:String>
<system:String x:Key="invalidFlowLauncherPluginFileFormat">Flow Launcher插件格式错误</system:String>
<system:String x:Key="setAsTopMostInThisQuery">在当前查询中置顶</system:String>
<system:String x:Key="cancelTopMostInThisQuery">取消置顶</system:String>
<system:String x:Key="executeQuery">执行查询:{0}</system:String>
@ -17,28 +17,37 @@
<!--设置,通用-->
<system:String x:Key="flowlauncher_settings">Flow Launcher设置</system:String>
<system:String x:Key="general">通用</system:String>
<system:String x:Key="startFlowLauncherOnSystemStartup">开机启动</system:String>
<system:String x:Key="portableMode">便携模式</system:String>
<system:String x:Key="startFlowLauncherOnSystemStartup">开机自动启动</system:String>
<system:String x:Key="hideFlowLauncherWhenLoseFocus">失去焦点时自动隐藏Flow Launcher</system:String>
<system:String x:Key="dontPromptUpdateMsg">不显示新版本提示</system:String>
<system:String x:Key="rememberLastLocation">记住上次启动位置</system:String>
<system:String x:Key="language">语言</system:String>
<system:String x:Key="lastQueryMode">上次搜索关键字模式</system:String>
<system:String x:Key="LastQueryPreserved">保留上次搜索关键字</system:String>
<system:String x:Key="LastQuerySelected">选上次搜索关键字</system:String>
<system:String x:Key="LastQuerySelected">选上次搜索关键字</system:String>
<system:String x:Key="LastQueryEmpty">清空上次搜索关键字</system:String>
<system:String x:Key="maxShowResults">最大结果显示个数</system:String>
<system:String x:Key="ignoreHotkeysOnFullscreen">全屏模式下忽略热键</system:String>
<system:String x:Key="pythonDirectory">Python 路径</system:String>
<system:String x:Key="autoUpdates">自动更新</system:String>
<system:String x:Key="selectPythonDirectory">Select</system:String>
<system:String x:Key="selectPythonDirectory">选择</system:String>
<system:String x:Key="hideOnStartup">启动时不显示主窗口</system:String>
<system:String x:Key="hideNotifyIcon">隐藏任务栏图标</system:String>
<system:String x:Key="querySearchPrecision">查询搜索精度</system:String>
<system:String x:Key="ShouldUsePinyin">启动拼音搜索</system:String>
<system:String x:Key="ShouldUsePinyinToolTip">允许使用拼音进行搜索。</system:String>
<!--设置,插件-->
<system:String x:Key="plugin">插件</system:String>
<system:String x:Key="browserMorePlugins">浏览更多插件</system:String>
<system:String x:Key="enable">启用</system:String>
<system:String x:Key="disable">禁用</system:String>
<system:String x:Key="actionKeywords">触发关键字</system:String>
<system:String x:Key="currentActionKeywords">当前操作关键字:</system:String>
<system:String x:Key="newActionKeyword">新动作关键字:</system:String>
<system:String x:Key="currentPriority">当前优先级:</system:String>
<system:String x:Key="newPriority">新优先级:</system:String>
<system:String x:Key="pluginDirectory">插件目录</system:String>
<system:String x:Key="author">作者</system:String>
<system:String x:Key="plugin_init_time">加载耗时</system:String>
@ -47,6 +56,7 @@
<!--设置,主题-->
<system:String x:Key="theme">主题</system:String>
<system:String x:Key="browserMoreThemes">浏览更多主题</system:String>
<system:String x:Key="hiThere">在这里输入</system:String>
<system:String x:Key="queryBoxFont">查询框字体</system:String>
<system:String x:Key="resultItemFont">结果项字体</system:String>
<system:String x:Key="windowMode">窗口模式</system:String>
@ -65,6 +75,9 @@
<system:String x:Key="add">增加</system:String>
<system:String x:Key="pleaseSelectAnItem">请选择一项</system:String>
<system:String x:Key="deleteCustomHotkeyWarning">你确定要删除插件 {0} 的热键吗?</system:String>
<system:String x:Key="queryWindowShadowEffect">查询窗口阴影效果</system:String>
<system:String x:Key="shadowEffectCPUUsage">阴影效果将占用大量的GPU资源。</system:String>
<system:String x:Key="shadowEffectPerformance">如果您的计算机性能有限,则不建议使用。</system:String>
<!--设置,代理-->
<system:String x:Key="proxy">HTTP 代理</system:String>
@ -88,8 +101,18 @@
<system:String x:Key="version">版本</system:String>
<system:String x:Key="about_activate_times">你已经激活了Flow Launcher {0} 次</system:String>
<system:String x:Key="checkUpdates">检查更新</system:String>
<system:String x:Key="newVersionTips">发现新版本 {0} , 请重启 flowlauncher。</system:String>
<system:String x:Key="newVersionTips">发现新版本 {0} , 请重启 Flow Launcher。</system:String>
<system:String x:Key="checkUpdatesFailed">下载更新失败,请检查您与 api.github.com 的连接状态或检查代理设置。</system:String>
<system:String x:Key="downloadUpdatesFailed">
下载更新失败,请检查您与 github-cloud.s3.amazonaws.com 的连接状态或检查代理设置,
或访问 https://github.com/Flow-Launcher/Flow.Launcher/releases 手动下载更新。
</system:String>
<system:String x:Key="releaseNotes">更新说明:</system:String>
<system:String x:Key="documentation">使用技巧:</system:String>
<!--优先级设置对话框-->
<system:String x:Key="priority_tips">数字越大,结果排名越高。如果你想要结果比任何其他插件的低,请使用负数</system:String>
<system:String x:Key="invalidPriority">请提供有效的整数作为优先级设置值!</system:String>
<!--Action Keyword 设置对话框-->
<system:String x:Key="oldActionKeywords">旧触发关键字</system:String>
@ -98,11 +121,13 @@
<system:String x:Key="done">确定</system:String>
<system:String x:Key="cannotFindSpecifiedPlugin">找不到指定的插件</system:String>
<system:String x:Key="newActionKeywordsCannotBeEmpty">新触发关键字不能为空</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">新触发关键字已经被指派给其他插件了,请重新选择一个关键字</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">新触发关键字已经被指派给其他插件了,请一个关键字</system:String>
<system:String x:Key="success">成功</system:String>
<system:String x:Key="completedSuccessfully">成功完成</system:String>
<system:String x:Key="actionkeyword_tips">如果你不想设置触发关键字,可以使用*代替</system:String>
<!--Custom Query Hotkey 对话框-->
<system:String x:Key="customeQueryHotkeyTitle">自定义插件热键</system:String>
<system:String x:Key="preview">预览</system:String>
<system:String x:Key="hotkeyIsNotUnavailable">热键不可用,请选择一个新的热键</system:String>
<system:String x:Key="invalidPluginHotkey">插件热键不合法</system:String>
@ -127,12 +152,24 @@
<system:String x:Key="reportWindow_report_failed">发送失败</system:String>
<system:String x:Key="reportWindow_flowlauncher_got_an_error">Flow Launcher出错啦</system:String>
<!--General Notice-->
<system:String x:Key="pleaseWait">请稍等...</system:String>
<!--更新-->
<system:String x:Key="update_flowlauncher_update_check">检查新的更新</system:String>
<system:String x:Key="update_flowlauncher_already_on_latest">您已经拥有最新的Flow Launcher版本</system:String>
<system:String x:Key="update_flowlauncher_update_found">检查到更新</system:String>
<system:String x:Key="update_flowlauncher_updating">更新中...</system:String>
<system:String x:Key="update_flowlauncher_fail_moving_portable_user_profile_data">Flow Launcher无法将您的用户配置文件数据移动到新的更新版本中。
请手动将您的用户配置文件数据文件夹从 {0} 到 {1}</system:String>
<system:String x:Key="update_flowlauncher_new_update">新的更新</system:String>
<system:String x:Key="update_flowlauncher_update_new_version_available">发现Flow Launcher新版本 V{0}</system:String>
<system:String x:Key="update_flowlauncher_update_error">更新Flow Launcher出错</system:String>
<system:String x:Key="update_flowlauncher_update_error">尝试安装软件更新时发生错误</system:String>
<system:String x:Key="update_flowlauncher_update">更新</system:String>
<system:String x:Key="update_flowlauncher_update_cancel">取消</system:String>
<system:String x:Key="update_flowlauncher_update_restart_flowlauncher_tip">此更新需要重启Flow Launcher</system:String>
<system:String x:Key="update_flowlauncher_fail">更新错误</system:String>
<system:String x:Key="update_flowlauncher_check_connection">检查网络是否可以连接至github-cloud.s3.amazonaws.com.</system:String>
<system:String x:Key="update_flowlauncher_update_restart_flowlauncher_tip">此次更新需要重启Flow Launcher</system:String>
<system:String x:Key="update_flowlauncher_update_upadte_files">下列文件会被更新</system:String>
<system:String x:Key="update_flowlauncher_update_files">更新文件</system:String>
<system:String x:Key="update_flowlauncher_update_upadte_description">更新日志</system:String>

View file

@ -34,6 +34,7 @@
<Window.InputBindings>
<KeyBinding Key="Escape" Command="{Binding EscCommand}"></KeyBinding>
<KeyBinding Key="F1" Command="{Binding StartHelpCommand}"></KeyBinding>
<KeyBinding Key="F5" Command="{Binding ReloadPluginDataCommand}"></KeyBinding>
<KeyBinding Key="Tab" Command="{Binding SelectNextItemCommand}"></KeyBinding>
<KeyBinding Key="Tab" Modifiers="Shift" Command="{Binding SelectPrevItemCommand}"></KeyBinding>
<KeyBinding Key="N" Modifiers="Ctrl" Command="{Binding SelectNextItemCommand}"></KeyBinding>
@ -59,7 +60,7 @@
<KeyBinding Key="D8" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="7"></KeyBinding>
<KeyBinding Key="D9" Modifiers="{Binding OpenResultCommandModifiers}" Command="{Binding OpenResultCommand}" CommandParameter="8"></KeyBinding>
</Window.InputBindings>
<Grid Width="750">
<Grid>
<Border Style="{DynamicResource WindowBorderStyle}" MouseDown="OnMouseDown" CornerRadius="5" >
<StackPanel Orientation="Vertical">
<Grid>
@ -78,7 +79,6 @@
Style="{DynamicResource QueryBoxStyle}"
Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PreviewDragOver="OnPreviewDragOver"
TextChanged="OnTextChanged"
AllowDrop="True"
Visibility="Visible"
Background="Transparent"

View file

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Animation;
@ -10,6 +11,7 @@ using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.ViewModel;
using Application = System.Windows.Application;
using Screen = System.Windows.Forms.Screen;
using ContextMenuStrip = System.Windows.Forms.ContextMenuStrip;
using DataFormats = System.Windows.DataFormats;
@ -22,7 +24,6 @@ namespace Flow.Launcher
{
public partial class MainWindow
{
#region Private Fields
private readonly Storyboard _progressBarStoryboard = new Storyboard();
@ -40,20 +41,23 @@ namespace Flow.Launcher
_settings = settings;
InitializeComponent();
}
public MainWindow()
{
InitializeComponent();
}
private void OnClosing(object sender, CancelEventArgs e)
private async void OnClosing(object sender, CancelEventArgs e)
{
_notifyIcon.Visible = false;
_viewModel.Save();
e.Cancel = true;
await PluginManager.DisposePluginsAsync();
Application.Current.Shutdown();
}
private void OnInitialized(object sender, EventArgs e)
{
}
private void OnLoaded(object sender, RoutedEventArgs _)
@ -61,8 +65,6 @@ namespace Flow.Launcher
// show notify icon when flowlauncher is hidden
InitializeNotifyIcon();
// todo is there a way to set blur only once?
ThemeManager.Instance.SetBlurForWindow();
WindowsInteropHelper.DisableControlBox(this);
InitProgressbarAnimation();
InitializePosition();
@ -72,47 +74,64 @@ namespace Flow.Launcher
_viewModel.PropertyChanged += (o, e) =>
{
if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility))
switch (e.PropertyName)
{
if (_viewModel.MainWindowVisibility == Visibility.Visible)
case nameof(MainViewModel.MainWindowVisibility):
{
Activate();
QueryTextBox.Focus();
UpdatePosition();
_settings.ActivateTimes++;
if (!_viewModel.LastQuerySelected)
if (_viewModel.MainWindowVisibility == Visibility.Visible)
{
QueryTextBox.SelectAll();
_viewModel.LastQuerySelected = true;
Activate();
QueryTextBox.Focus();
UpdatePosition();
_settings.ActivateTimes++;
if (!_viewModel.LastQuerySelected)
{
QueryTextBox.SelectAll();
_viewModel.LastQuerySelected = true;
}
if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
{
_progressBarStoryboard.Begin(ProgressBar, true);
isProgressBarStoryboardPaused = false;
}
}
if (_viewModel.ProgressBarVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
if (!isProgressBarStoryboardPaused)
{
_progressBarStoryboard.Resume();
isProgressBarStoryboardPaused = false;
}
}
else if (!isProgressBarStoryboardPaused)
{
_progressBarStoryboard.Pause();
isProgressBarStoryboardPaused = true;
}
}
else if (e.PropertyName == nameof(MainViewModel.ProgressBarVisibility))
{
Dispatcher.Invoke(() =>
{
if (_viewModel.ProgressBarVisibility == Visibility.Hidden && !isProgressBarStoryboardPaused)
{
_progressBarStoryboard.Pause();
_progressBarStoryboard.Stop(ProgressBar);
isProgressBarStoryboardPaused = true;
}
else if (_viewModel.MainWindowVisibility == Visibility.Visible && isProgressBarStoryboardPaused)
break;
}
case nameof(MainViewModel.ProgressBarVisibility):
{
Dispatcher.Invoke(async () =>
{
_progressBarStoryboard.Resume();
isProgressBarStoryboardPaused = false;
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;
}
case nameof(MainViewModel.QueryTextCursorMovedToEnd):
if (_viewModel.QueryTextCursorMovedToEnd)
{
MoveQueryTextToEnd();
_viewModel.QueryTextCursorMovedToEnd = false;
}
}, System.Windows.Threading.DispatcherPriority.Render);
break;
}
};
_settings.PropertyChanged += (o, e) =>
@ -189,14 +208,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;
}
@ -210,10 +230,10 @@ namespace Flow.Launcher
{
if (sender != null && e.OriginalSource != null)
{
var r = (ResultListBox)sender;
var d = (DependencyObject)e.OriginalSource;
var r = (ResultListBox) sender;
var d = (DependencyObject) e.OriginalSource;
var item = ItemsControl.ContainerFromElement(r, d) as ListBoxItem;
var result = (ResultViewModel)item?.DataContext;
var result = (ResultViewModel) item?.DataContext;
if (result != null)
{
if (e.ChangedButton == MouseButton.Left)
@ -316,13 +336,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;
}
}
}

Some files were not shown because too many files have changed in this diff Show more