Merge remote-tracking branch 'origin/dev' into StopHidingScroller

This commit is contained in:
Jeremy 2021-06-19 20:45:08 +10:00
commit 35967328aa
39 changed files with 765 additions and 434 deletions

View file

@ -53,7 +53,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Droplex" Version="1.2.0" />
<PackageReference Include="Droplex" Version="1.3.1" />
<PackageReference Include="FSharp.Core" Version="4.7.1" />
<PackageReference Include="squirrel.windows" Version="1.5.2" />
</ItemGroup>

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
{
@ -51,42 +52,15 @@ namespace Flow.Launcher.Core.Plugin
public class JsonRPCRequestModel : JsonRPCModelBase
{
[JsonPropertyName("method")]
public string Method { get; set; }
[JsonPropertyName("parameters")]
public object[] Parameters { get; set; }
public override string ToString()
{
string rpc = string.Empty;
if (Parameters != null && Parameters.Length > 0)
{
string parameters = $"[{string.Join(',', Parameters.Select(GetParameterByType))}]";
rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":{parameters}";
}
else
{
rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":[]";
}
return rpc;
}
private string GetParameterByType(object parameter)
=> parameter switch
{
null => "null",
string _ => $@"\""{ReplaceEscapes(parameter.ToString())}\""",
bool _ => $@"{parameter.ToString().ToLower()}",
_ => parameter.ToString()
};
private string ReplaceEscapes(string str)
{
return str.Replace(@"\", @"\\") //Escapes in ProcessStartInfo
.Replace(@"\", @"\\") //Escapes itself when passed to client
.Replace(@"""", @"\\""""");
return JsonSerializer.Serialize(this);
}
}
@ -95,11 +69,7 @@ namespace Flow.Launcher.Core.Plugin
/// </summary>
public class JsonRPCServerRequestModel : JsonRPCRequestModel
{
public override string ToString()
{
string rpc = base.ToString();
return rpc + "}";
}
}
/// <summary>
@ -107,13 +77,8 @@ namespace Flow.Launcher.Core.Plugin
/// </summary>
public class JsonRPCClientRequestModel : JsonRPCRequestModel
{
[JsonPropertyName("dontHideAfterAction")]
public bool DontHideAfterAction { get; set; }
public override string ToString()
{
string rpc = base.ToString();
return rpc + "}";
}
}
/// <summary>
@ -125,4 +90,4 @@ namespace Flow.Launcher.Core.Plugin
{
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using Flow.Launcher.Core.Resource;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -45,14 +46,20 @@ namespace Flow.Launcher.Core.Plugin
}
}
private static readonly JsonSerializerOptions _options = new()
{
Converters =
{
new JsonObjectConverter()
}
};
private async Task<List<Result>> DeserializedResultAsync(Stream output)
{
if (output == Stream.Null) return null;
JsonRPCQueryResponseModel queryResponseModel = await
JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output);
var queryResponseModel = await
JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, _options);
return ParseResults(queryResponseModel);
}
@ -61,17 +68,18 @@ namespace Flow.Launcher.Core.Plugin
{
if (string.IsNullOrEmpty(output)) return null;
JsonRPCQueryResponseModel queryResponseModel =
JsonSerializer.Deserialize<JsonRPCQueryResponseModel>(output);
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))
if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage))
{
context.API.ShowMsg(queryResponseModel.DebugMessage);
}
@ -82,25 +90,31 @@ namespace Flow.Launcher.Core.Plugin
{
if (result.JsonRPCAction == null) return false;
if (!string.IsNullOrEmpty(result.JsonRPCAction.Method))
if (string.IsNullOrEmpty(result.JsonRPCAction.Method))
{
if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
return !result.JsonRPCAction.DontHideAfterAction;
}
if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher."))
{
ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..],
result.JsonRPCAction.Parameters);
}
else
{
var actionResponse = ExecuteCallback(result.JsonRPCAction);
if (string.IsNullOrEmpty(actionResponse))
{
ExecuteFlowLauncherAPI(result.JsonRPCAction.Method.Substring(4),
result.JsonRPCAction.Parameters);
return !result.JsonRPCAction.DontHideAfterAction;
}
else
var jsonRpcRequestModel = JsonSerializer.Deserialize<JsonRPCRequestModel>(actionResponse, _options);
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
{
string actionReponse = ExecuteCallback(result.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);
}
ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..],
jsonRpcRequestModel.Parameters);
}
}
@ -177,12 +191,14 @@ namespace Flow.Launcher.Core.Plugin
if (result.StartsWith("DEBUG:"))
{
MessageBox.Show(new Form { TopMost = true }, result.Substring(6));
MessageBox.Show(new Form
{
TopMost = true
}, result.Substring(6));
return string.Empty;
}
return result;
}
catch (Exception e)
{
@ -248,10 +264,10 @@ namespace Flow.Launcher.Core.Plugin
}
}
public Task InitAsync(PluginInitContext context)
public virtual Task InitAsync(PluginInitContext context)
{
this.context = context;
return Task.CompletedTask;
}
}
}
}

View file

@ -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; }
@ -118,7 +118,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)
{
@ -141,7 +143,7 @@ namespace Flow.Launcher.Core.Plugin
}
}
public static List<PluginPair> ValidPluginsForQuery(Query query)
public static ICollection<PluginPair> ValidPluginsForQuery(Query query)
{
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
{
@ -250,6 +252,8 @@ namespace Flow.Launcher.Core.Plugin
public static bool ActionKeywordRegistered(string actionKeyword)
{
// this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword);
}
@ -274,7 +278,7 @@ namespace Flow.Launcher.Core.Plugin
}
/// <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)
@ -283,9 +287,7 @@ namespace Flow.Launcher.Core.Plugin
if (oldActionkeyword == Query.GlobalPluginWildcardSign
&& // Plugins may have multiple ActionKeywords that are global, eg. WebSearch
plugin.Metadata.ActionKeywords
.Where(x => x == Query.GlobalPluginWildcardSign)
.ToList()
.Count == 1)
.Count(x => x == Query.GlobalPluginWildcardSign) == 1)
{
GlobalPlugins.Remove(plugin);
}

View file

@ -11,13 +11,13 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System.Diagnostics;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Core.Plugin
{
public static class PluginsLoader
{
public const string PATH = "PATH";
public const string Python = "python";
public const string PythonExecutable = "pythonw.exe";
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
@ -85,11 +85,7 @@ namespace Flow.Launcher.Core.Plugin
return;
}
plugins.Add(new PluginPair
{
Plugin = plugin,
Metadata = metadata
});
plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata});
});
metadata.InitTime += milliseconds;
}
@ -119,102 +115,68 @@ namespace Flow.Launcher.Core.Plugin
if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
return new List<PluginPair>();
// Try setting Constant.PythonPath first, either from
// PATH or from the given pythonDirectory
if (string.IsNullOrEmpty(settings.PythonDirectory))
{
var paths = Environment.GetEnvironmentVariable(PATH);
if (paths != null)
{
var pythonInPath = paths
.Split(';')
.Where(p => p.ToLower().Contains(Python))
.Any();
if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory))
return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable));
if (pythonInPath)
var pythonPath = string.Empty;
if (MessageBox.Show("Flow detected you have installed Python plugins, " +
"would you like to install Python to run them? " +
Environment.NewLine + Environment.NewLine +
"Click no if it's already installed, " +
"and you will be prompted to select the folder that contains the Python executable",
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No
&& string.IsNullOrEmpty(settings.PythonDirectory))
{
var dlg = new FolderBrowserDialog
{
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var result = dlg.ShowDialog();
if (result == DialogResult.OK)
{
string pythonDirectory = dlg.SelectedPath;
if (!string.IsNullOrEmpty(pythonDirectory))
{
Constant.PythonPath =
Path.Combine(paths.Split(';').Where(p => p.ToLower().Contains(Python)).FirstOrDefault(), PythonExecutable);
settings.PythonDirectory = FilesFolders.GetPreviousExistingDirectory(FilesFolders.LocationExists, Constant.PythonPath);
}
else
{
Log.Error("PluginsLoader", "Failed to set Python path despite the environment variable PATH is found", "PythonPlugins");
pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
if (File.Exists(pythonPath))
{
settings.PythonDirectory = pythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
MessageBox.Show("Can't find python in given directory");
}
}
}
}
else
{
var path = Path.Combine(settings.PythonDirectory, PythonExecutable);
if (File.Exists(path))
var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable");
// Python 3.8.9 is used for Windows 7 compatibility
DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait();
pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
if (FilesFolders.FileExists(pythonPath))
{
Constant.PythonPath = path;
settings.PythonDirectory = installedPythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
Log.Error("PluginsLoader", $"Tried to automatically set from Settings.PythonDirectory " +
$"but can't find python executable in {path}", "PythonPlugins");
Log.Error("PluginsLoader",
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
"PythonPlugins");
}
}
if (string.IsNullOrEmpty(settings.PythonDirectory))
if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(pythonPath))
{
if (MessageBox.Show("Flow detected you have installed Python plugins, " +
"would you like to install Python to run them? " +
Environment.NewLine + Environment.NewLine +
"Click no if it's already installed, " +
"and you will be prompted to select the folder that contains the Python executable",
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No
&& string.IsNullOrEmpty(settings.PythonDirectory))
{
var dlg = new FolderBrowserDialog
{
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var result = dlg.ShowDialog();
if (result == DialogResult.OK)
{
string pythonDirectory = dlg.SelectedPath;
if (!string.IsNullOrEmpty(pythonDirectory))
{
var pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
if (File.Exists(pythonPath))
{
settings.PythonDirectory = pythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
MessageBox.Show("Can't find python in given directory");
}
}
}
}
else
{
DroplexPackage.Drop(App.python3_9_1).Wait();
var installedPythonDirectory =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39");
var pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
if (FilesFolders.FileExists(pythonPath))
{
settings.PythonDirectory = installedPythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
Log.Error("PluginsLoader",
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
"PythonPlugins");
}
}
}
if (string.IsNullOrEmpty(settings.PythonDirectory))
{
MessageBox.Show("Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom).");
MessageBox.Show(
"Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom).");
Log.Error("PluginsLoader",
$"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.",
"PythonPlugins");
@ -222,24 +184,26 @@ namespace Flow.Launcher.Core.Plugin
return new List<PluginPair>();
}
return source
return SetPythonPathForPluginPairs(source, pythonPath);
}
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(Constant.PythonPath),
Plugin = new PythonPlugin(pythonPath),
Metadata = metadata
})
.ToList();
}
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
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

@ -28,17 +28,19 @@ namespace Flow.Launcher.Core.Plugin
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
_startInfo.ArgumentList.Add("-B");
}
protected override Task<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;
@ -47,16 +49,17 @@ namespace Flow.Launcher.Core.Plugin
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
{
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\"";
_startInfo.ArgumentList[2] = rpcRequest.ToString();
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
// TODO: Async Action
return Execute(_startInfo);
}
protected override string ExecuteContextMenu(Result selectedResult) {
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel {
Method = "context_menu",
Parameters = new object[] { selectedResult.ContextData },
protected override string ExecuteContextMenu(Result selectedResult)
{
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
{
Method = "context_menu", Parameters = new object[] {selectedResult.ContextData},
};
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\"";
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
@ -64,5 +67,13 @@ namespace Flow.Launcher.Core.Plugin
// TODO: Async Action
return Execute(_startInfo);
}
public override Task InitAsync(PluginInitContext context)
{
this.context = context;
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
_startInfo.ArgumentList.Add("");
return Task.CompletedTask;
}
}
}

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;
@ -224,6 +226,27 @@ namespace Flow.Launcher.Core.Resource
BlurRadius = 15
};
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
if (marginSetter == null)
{
marginSetter = new Setter()
{
Property = Border.MarginProperty,
Value = new Thickness(ShadowExtraMargin),
};
windowBorderStyle.Setters.Add(marginSetter);
}
else
{
var baseMargin = (Thickness) marginSetter.Value;
var newMargin = new Thickness(
baseMargin.Left + ShadowExtraMargin,
baseMargin.Top + ShadowExtraMargin,
baseMargin.Right + ShadowExtraMargin,
baseMargin.Bottom + ShadowExtraMargin);
marginSetter.Value = newMargin;
}
windowBorderStyle.Setters.Add(effectSetter);
UpdateResourceDictionary(dict);
@ -234,7 +257,23 @@ namespace Flow.Launcher.Core.Resource
var dict = CurrentThemeResourceDictionary();
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
dict.Remove(Border.EffectProperty);
var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter;
var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter;
if(effectSetter != null)
{
windowBorderStyle.Setters.Remove(effectSetter);
}
if (marginSetter != null)
{
var currentMargin = (Thickness)marginSetter.Value;
var newMargin = new Thickness(
currentMargin.Left - ShadowExtraMargin,
currentMargin.Top - ShadowExtraMargin,
currentMargin.Right - ShadowExtraMargin,
currentMargin.Bottom - ShadowExtraMargin);
marginSetter.Value = newMargin;
}
UpdateResourceDictionary(dict);
}

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

@ -21,6 +21,7 @@ using JetBrains.Annotations;
using System.Runtime.CompilerServices;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using System.Collections.Concurrent;
namespace Flow.Launcher
{
@ -144,7 +145,7 @@ namespace Flow.Launcher
public void LogException(string className, string message, Exception e,
[CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName);
private readonly Dictionary<Type, object> _pluginJsonStorages = new();
private readonly ConcurrentDictionary<Type, object> _pluginJsonStorages = new();
public void SavePluginSettings()
{

View file

@ -494,21 +494,16 @@ namespace Flow.Launcher.ViewModel
}
}, currentCancellationToken);
Task[] tasks = new Task[plugins.Count];
// plugins is ICollection, meaning LINQ will get the Count and preallocate Array
Task[] tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch
{
false => QueryTask(plugin),
true => Task.CompletedTask
}).ToArray();
try
{
for (var i = 0; i < plugins.Count; i++)
{
if (!plugins[i].Metadata.Disabled)
{
tasks[i] = QueryTask(plugins[i]);
}
else
{
tasks[i] = Task.CompletedTask; // Avoid Null
}
}
// Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first
await Task.WhenAll(tasks);
}
@ -552,26 +547,9 @@ namespace Flow.Launcher.ViewModel
private void RemoveOldQueryResults(Query query)
{
string lastKeyword = _lastQuery.ActionKeyword;
string keyword = query.ActionKeyword;
if (string.IsNullOrEmpty(lastKeyword))
if (_lastQuery.ActionKeyword != query.ActionKeyword)
{
if (!string.IsNullOrEmpty(keyword))
{
Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata);
}
}
else
{
if (string.IsNullOrEmpty(keyword))
{
Results.KeepResultsExcept(PluginManager.NonGlobalPlugins[lastKeyword].Metadata);
}
else if (lastKeyword != keyword)
{
Results.KeepResultsFor(PluginManager.NonGlobalPlugins[keyword].Metadata);
}
Results.Clear();
}
}

View file

@ -1,7 +1,6 @@
using System.Windows;
using System.Windows.Media;
using Flow.Launcher.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Core.Plugin;
@ -11,8 +10,6 @@ namespace Flow.Launcher.ViewModel
{
public PluginPair PluginPair { get; set; }
private readonly Internationalization _translator = InternationalizationManager.Instance;
public ImageSource Image => ImageLoader.Load(PluginPair.Metadata.IcoPath);
public bool PluginState
{
@ -22,7 +19,7 @@ namespace Flow.Launcher.ViewModel
PluginPair.Metadata.Disabled = !value;
}
}
public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count > 1 ? Visibility.Collapsed : Visibility.Visible;
public Visibility ActionKeywordsVisibility => PluginPair.Metadata.ActionKeywords.Count == 1 ? Visibility.Visible : Visibility.Collapsed;
public string InitilizaTime => PluginPair.Metadata.InitTime.ToString() + "ms";
public string QueryTime => PluginPair.Metadata.AvgQueryTime + "ms";
public string ActionKeywordsText => string.Join(Query.ActionKeywordSeperater, PluginPair.Metadata.ActionKeywords);

View file

@ -19,8 +19,14 @@
<system:String x:Key="plugin_explorer_quickaccesslinks_header">Quick Access Links</system:String>
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">Index Search Excluded Paths</system:String>
<system:String x:Key="plugin_explorer_manageindexoptions">Indexing Options</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_search">Search Activation:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_search">Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_pathsearch">Path Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">File Content Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_indexsearch">Index Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_current">Current Action Keyword:</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_done">Done</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_enabled">Enabled</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_enabled_tooltip">When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword</system:String>
<!--Plugin Infos-->
<system:String x:Key="plugin_explorer_plugin_name">Explorer</system:String>

View file

@ -20,6 +20,7 @@
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">索引搜索排除的路径</system:String>
<system:String x:Key="plugin_explorer_manageindexoptions">索引选项</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_search">搜索激活:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_pathsearch">路径搜索激活:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">文件内容搜索:</system:String>
<!--Plugin Infos-->

View file

@ -28,7 +28,7 @@ namespace Flow.Launcher.Plugin.Explorer
return new ExplorerSettings(viewModel);
}
public async Task InitAsync(PluginInitContext context)
public Task InitAsync(PluginInitContext context)
{
Context = context;
@ -47,6 +47,8 @@ namespace Flow.Launcher.Plugin.Explorer
contextMenu = new ContextMenu(Context, Settings, viewModel);
searchManager = new SearchManager(Settings, Context);
ResultManager.Init(Context);
return Task.CompletedTask;
}
public List<Result> LoadContextMenus(Result selectedResult)

View file

@ -10,10 +10,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
internal static bool IsEnvironmentVariableSearch(string search)
{
return LoadEnvironmentStringPaths().Count > 0
&& search.StartsWith("%")
return search.StartsWith("%")
&& search != "%%"
&& !search.Contains("\\");
&& !search.Contains("\\") &&
LoadEnvironmentStringPaths().Count > 0;
}
internal static Dictionary<string, string> LoadEnvironmentStringPaths()

View file

@ -26,6 +26,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
private static PathEqualityComparator instance;
public static PathEqualityComparator Instance => instance ??= new PathEqualityComparator();
public bool Equals(Result x, Result y)
{
return x.SubTitle == y.SubTitle;
@ -39,21 +40,61 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal async Task<List<Result>> SearchAsync(Query query, CancellationToken token)
{
var results = new HashSet<Result>(PathEqualityComparator.Instance);
var querySearch = query.Search;
if (IsFileContentSearch(query.ActionKeyword))
return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false);
var result = new HashSet<Result>(PathEqualityComparator.Instance);
if (ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword) ||
ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword))
{
result.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false));
}
if ((ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) ||
ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)) &&
querySearch.Length > 0 &&
!querySearch.IsLocationPathString())
{
result.UnionWith(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token)
.ConfigureAwait(false));
}
return result.ToList();
}
private bool ActionKeywordMatch(Query query, Settings.ActionKeyword allowedActionKeyword)
{
var keyword = query.ActionKeyword.Length == 0 ? Query.GlobalPluginWildcardSign : query.ActionKeyword;
return allowedActionKeyword switch
{
Settings.ActionKeyword.SearchActionKeyword => settings.EnableSearchActionKeyword &&
keyword == settings.SearchActionKeyword,
Settings.ActionKeyword.PathSearchActionKeyword => settings.EnabledPathSearchKeyword &&
keyword == settings.PathSearchActionKeyword,
Settings.ActionKeyword.FileContentSearchActionKeyword => keyword ==
settings.FileContentSearchActionKeyword,
Settings.ActionKeyword.IndexSearchActionKeyword => settings.EnabledIndexOnlySearchKeyword &&
keyword == settings.IndexSearchActionKeyword
};
}
public async Task<List<Result>> PathSearchAsync(Query query, CancellationToken token = default)
{
var querySearch = query.Search;
// This allows the user to type the assigned action keyword and only see the list of quick folder links
if (string.IsNullOrEmpty(query.Search))
return QuickAccess.AccessLinkListAll(query, settings.QuickAccessLinks);
var results = new HashSet<Result>(PathEqualityComparator.Instance);
var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, settings.QuickAccessLinks);
if (quickaccessLinks.Count > 0)
results.UnionWith(quickaccessLinks);
results.UnionWith(quickaccessLinks);
var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch);
@ -63,13 +104,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search
// Query is a location path with a full environment variable, eg. %appdata%\somefolder\
var isEnvironmentVariablePath = querySearch[1..].Contains("%\\");
if (!querySearch.IsLocationPathString() && !isEnvironmentVariablePath)
{
results.UnionWith(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token).ConfigureAwait(false));
return results.ToList();
}
var locationPath = querySearch;
if (isEnvironmentVariablePath)
@ -99,7 +133,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return results.ToList();
}
private async Task<List<Result>> WindowsIndexFileContentSearchAsync(Query query, string querySearchString, CancellationToken token)
private async Task<List<Result>> WindowsIndexFileContentSearchAsync(Query query, string querySearchString,
CancellationToken token)
{
var queryConstructor = new QueryConstructor(settings);
@ -107,11 +142,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return new List<Result>();
return await IndexSearch.WindowsIndexSearchAsync(querySearchString,
queryConstructor.CreateQueryHelper().ConnectionString,
queryConstructor.QueryForFileContentSearch,
settings.IndexSearchExcludedSubdirectoryPaths,
query,
token).ConfigureAwait(false);
queryConstructor.CreateQueryHelper().ConnectionString,
queryConstructor.QueryForFileContentSearch,
settings.IndexSearchExcludedSubdirectoryPaths,
query,
token).ConfigureAwait(false);
}
public bool IsFileContentSearch(string actionKeyword)
@ -138,28 +173,30 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return await windowsIndexSearch(query, querySearchString, token);
}
private async Task<List<Result>> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString, CancellationToken token)
private async Task<List<Result>> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString,
CancellationToken token)
{
var queryConstructor = new QueryConstructor(settings);
return await IndexSearch.WindowsIndexSearchAsync(querySearchString,
queryConstructor.CreateQueryHelper().ConnectionString,
queryConstructor.QueryForAllFilesAndFolders,
settings.IndexSearchExcludedSubdirectoryPaths,
query,
token).ConfigureAwait(false);
queryConstructor.CreateQueryHelper().ConnectionString,
queryConstructor.QueryForAllFilesAndFolders,
settings.IndexSearchExcludedSubdirectoryPaths,
query,
token).ConfigureAwait(false);
}
private async Task<List<Result>> WindowsIndexTopLevelFolderSearchAsync(Query query, string path, CancellationToken token)
private async Task<List<Result>> WindowsIndexTopLevelFolderSearchAsync(Query query, string path,
CancellationToken token)
{
var queryConstructor = new QueryConstructor(settings);
return await IndexSearch.WindowsIndexSearchAsync(path,
queryConstructor.CreateQueryHelper().ConnectionString,
queryConstructor.QueryForTopLevelDirectorySearch,
settings.IndexSearchExcludedSubdirectoryPaths,
query,
token).ConfigureAwait(false);
queryConstructor.CreateQueryHelper().ConnectionString,
queryConstructor.QueryForTopLevelDirectorySearch,
settings.IndexSearchExcludedSubdirectoryPaths,
query,
token).ConfigureAwait(false);
}
private bool UseWindowsIndexForDirectorySearch(string locationPath)
@ -170,11 +207,11 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return false;
if (settings.IndexSearchExcludedSubdirectoryPaths
.Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory)
.StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)))
.Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory)
.StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)))
return false;
return IndexSearch.PathIsIndexed(pathToDirectory);
}
}
}
}

View file

@ -1,6 +1,9 @@
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using System;
using System.Collections.Generic;
using System.IO;
namespace Flow.Launcher.Plugin.Explorer
{
@ -18,7 +21,57 @@ namespace Flow.Launcher.Plugin.Explorer
public List<AccessLink> IndexSearchExcludedSubdirectoryPaths { get; set; } = new List<AccessLink>();
public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
public bool EnableSearchActionKeyword { get; set; } = true;
public string FileContentSearchActionKeyword { get; set; } = Constants.DefaultContentSearchActionKeyword;
public string PathSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
public bool EnabledPathSearchKeyword { get; set; }
public string IndexSearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
public bool EnabledIndexOnlySearchKeyword { get; set; }
internal enum ActionKeyword
{
SearchActionKeyword,
PathSearchActionKeyword,
FileContentSearchActionKeyword,
IndexSearchActionKeyword
}
internal string GetActionKeyword(ActionKeyword actionKeyword) => actionKeyword switch
{
ActionKeyword.SearchActionKeyword => SearchActionKeyword,
ActionKeyword.PathSearchActionKeyword => PathSearchActionKeyword,
ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword,
ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword
};
internal void SetActionKeyword(ActionKeyword actionKeyword, string keyword) => _ = actionKeyword switch
{
ActionKeyword.SearchActionKeyword => SearchActionKeyword = keyword,
ActionKeyword.PathSearchActionKeyword => PathSearchActionKeyword = keyword,
ActionKeyword.FileContentSearchActionKeyword => FileContentSearchActionKeyword = keyword,
ActionKeyword.IndexSearchActionKeyword => IndexSearchActionKeyword = keyword,
_ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "Unexpected property")
};
internal bool? GetActionKeywordEnable(ActionKeyword actionKeyword) => actionKeyword switch
{
ActionKeyword.SearchActionKeyword => EnableSearchActionKeyword,
ActionKeyword.PathSearchActionKeyword => EnabledPathSearchKeyword,
ActionKeyword.IndexSearchActionKeyword => EnabledIndexOnlySearchKeyword,
_ => null
};
internal void SetActionKeywordEnable(ActionKeyword actionKeyword, bool enable) => _ = actionKeyword switch
{
ActionKeyword.SearchActionKeyword => EnableSearchActionKeyword = enable,
ActionKeyword.PathSearchActionKeyword => EnabledPathSearchKeyword = enable,
ActionKeyword.IndexSearchActionKeyword => EnabledIndexOnlySearchKeyword = enable,
_ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "Unexpected property")
};
}
}

View file

@ -41,18 +41,15 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
Process.Start(psi);
}
internal void UpdateActionKeyword(string newActionKeyword, string oldActionKeyword)
internal void UpdateActionKeyword(Settings.ActionKeyword modifiedActionKeyword, string newActionKeyword, string oldActionKeyword)
{
PluginManager.ReplaceActionKeyword(Context.CurrentPluginMetadata.ID, oldActionKeyword, newActionKeyword);
if (Settings.FileContentSearchActionKeyword == oldActionKeyword)
Settings.FileContentSearchActionKeyword = newActionKeyword;
if (Settings.SearchActionKeyword == oldActionKeyword)
Settings.SearchActionKeyword = newActionKeyword;
}
internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword);
internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword)
{
return PluginManager.ActionKeywordRegistered(newActionKeyword);
}
internal bool IsNewActionKeywordGlobal(string newActionKeyword) => newActionKeyword == Query.GlobalPluginWildcardSign;
}

View file

@ -7,6 +7,7 @@
mc:Ignorable="d"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Action Keyword Setting" Height="200" Width="500">
<Grid>
<Grid.RowDefinitions>
@ -16,21 +17,24 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Margin="20 10 10 10" FontSize="14" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
HorizontalAlignment="Left" Text="Current Action Keyword:" />
<TextBox Name="txtCurrentActionKeyword"
HorizontalAlignment="Left" Text="{DynamicResource plugin_explorer_actionkeyword_current}" />
<TextBox Name="TxtCurrentActionKeyword"
Margin="10" Grid.Row="0" Width="105" Grid.Column="1"
VerticalAlignment="Center"
HorizontalAlignment="Left" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="1" Grid.Column="1">
<Button Click="OnConfirmButtonClick"
Margin="10 0 10 0" Width="80" Height="35"
Content="OK" />
<Button Click="OnCancelButtonClick"
Margin="10 0 10 0" Width="80" Height="35"
Content="Cancel" />
</StackPanel>
HorizontalAlignment="Left"
Text="{Binding ActionKeyword}"
PreviewKeyDown="TxtCurrentActionKeyword_OnKeyDown"/>
<CheckBox Name="ChkActionKeywordEnabled" ToolTip="{DynamicResource plugin_explorer_actionkeyword_enabled_tooltip}"
Margin="10" Grid.Row="0" Grid.Column="2" Content="{DynamicResource plugin_explorer_actionkeyword_enabled}"
Width="auto"
VerticalAlignment="Center" IsChecked="{Binding Enabled}"
Visibility="{Binding Visible}"/>
<Button Name="DownButton"
Click="OnDoneButtonClick" Grid.Row="1" Grid.Column="2"
Margin="10 0 10 0" Width="80" Height="35"
Content="{DynamicResource plugin_explorer_actionkeyword_done}" />
</Grid>
</Window>
</Window>

View file

@ -1,16 +1,10 @@
using Flow.Launcher.Plugin.Explorer.ViewModels;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using ICSharpCode.SharpZipLib.Zip;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Flow.Launcher.Plugin.Explorer.Views
{
@ -21,65 +15,102 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
private SettingsViewModel settingsViewModel;
private ActionKeywordView currentActionKeyword;
public ActionKeywordView CurrentActionKeyword { get; set; }
private List<ActionKeywordView> actionKeywordListView;
public ActionKeywordSetting(SettingsViewModel settingsViewModel, List<ActionKeywordView> actionKeywordListView, ActionKeywordView selectedActionKeyword)
public string ActionKeyword
{
InitializeComponent();
this.settingsViewModel = settingsViewModel;
currentActionKeyword = selectedActionKeyword;
txtCurrentActionKeyword.Text = selectedActionKeyword.Keyword;
this.actionKeywordListView = actionKeywordListView;
get => _actionKeyword;
set
{
// Set Enable to be true if user change ActionKeyword
if (Enabled is not null)
Enabled = true;
_actionKeyword = value;
}
}
private void OnConfirmButtonClick(object sender, RoutedEventArgs e)
public bool? Enabled { get; set; }
private string _actionKeyword;
public Visibility Visible =>
CurrentActionKeyword.Enabled is not null ? Visibility.Visible : Visibility.Collapsed;
public ActionKeywordSetting(SettingsViewModel settingsViewModel,
ActionKeywordView selectedActionKeyword)
{
var newActionKeyword = txtCurrentActionKeyword.Text;
this.settingsViewModel = settingsViewModel;
if (string.IsNullOrEmpty(newActionKeyword))
return;
CurrentActionKeyword = selectedActionKeyword;
if (newActionKeyword == currentActionKeyword.Keyword)
ActionKeyword = selectedActionKeyword.Keyword;
Enabled = selectedActionKeyword.Enabled;
InitializeComponent();
TxtCurrentActionKeyword.Focus();
}
private void OnDoneButtonClick(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(ActionKeyword))
ActionKeyword = Query.GlobalPluginWildcardSign;
if (CurrentActionKeyword.Keyword == ActionKeyword && CurrentActionKeyword.Enabled == Enabled)
{
Close();
return;
}
if (CurrentActionKeyword.KeywordProperty == Settings.ActionKeyword.FileContentSearchActionKeyword
&& ActionKeyword == Query.GlobalPluginWildcardSign)
{
MessageBox.Show(
settingsViewModel.Context.API.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
return;
}
if (settingsViewModel.IsNewActionKeywordGlobal(newActionKeyword)
&& currentActionKeyword.Description
== settingsViewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch"))
{
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
var oldActionKeyword = CurrentActionKeyword.Keyword;
return;
}
if (!settingsViewModel.IsActionKeywordAlreadyAssigned(newActionKeyword))
// == because of nullable
if (Enabled == false || !settingsViewModel.IsActionKeywordAlreadyAssigned(ActionKeyword))
{
settingsViewModel.UpdateActionKeyword(newActionKeyword, currentActionKeyword.Keyword);
// Update View Data
CurrentActionKeyword.Keyword = Enabled == true ? ActionKeyword : Query.GlobalPluginWildcardSign;
CurrentActionKeyword.Enabled = Enabled;
actionKeywordListView.Where(x => x.Description == currentActionKeyword.Description).FirstOrDefault().Keyword = newActionKeyword;
switch (Enabled)
{
// reset to global so it does not take up an action keyword when disabled
// not for null Enable plugin
case false when oldActionKeyword != Query.GlobalPluginWildcardSign:
settingsViewModel.UpdateActionKeyword(CurrentActionKeyword.KeywordProperty,
Query.GlobalPluginWildcardSign, oldActionKeyword);
break;
default:
settingsViewModel.UpdateActionKeyword(CurrentActionKeyword.KeywordProperty,
CurrentActionKeyword.Keyword, oldActionKeyword);
break;
}
Close();
return;
}
// The keyword is not valid, so show message
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
}
private void OnCancelButtonClick(object sender, RoutedEventArgs e)
private void TxtCurrentActionKeyword_OnKeyDown(object sender, KeyEventArgs e)
{
Close();
return;
if (e.Key == Key.Enter)
{
DownButton.Focus();
OnDoneButtonClick(sender, e);
e.Handled = true;
}
}
}
}
}

View file

@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModels="clr-namespace:Flow.Launcher.Plugin.Explorer.ViewModels"
xmlns:views="clr-namespace:Flow.Launcher.Plugin.Explorer.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
@ -17,14 +18,17 @@
Text="{Binding Path, Mode=OneTime}"
Margin="0,5,0,5" />
</DataTemplate>
<DataTemplate x:Key="ListViewActionKeywords">
<DataTemplate x:Key="ListViewActionKeywords"
DataType="views:ActionKeywordView">
<Grid>
<TextBlock
Text="{Binding Description, Mode=OneTime}"
Foreground="{Binding Color, Mode=OneWay}"
Margin="0,5,0,0" />
<TextBlock
Text="{Binding Keyword, Mode=OneTime}"
Margin="150,5,0,0" />
Foreground="{Binding Color, Mode=OneWay}"
Margin="250,5,0,0" />
</Grid>
</DataTemplate>
</UserControl.Resources>

View file

@ -35,20 +35,20 @@ namespace Flow.Launcher.Plugin.Explorer.Views
actionKeywordsListView = new List<ActionKeywordView>
{
new ActionKeywordView()
{
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search"),
Keyword = this.viewModel.Settings.SearchActionKeyword
},
new ActionKeywordView()
{
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch"),
Keyword = this.viewModel.Settings.FileContentSearchActionKeyword
}
new(Settings.ActionKeyword.SearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search")),
new(Settings.ActionKeyword.FileContentSearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch")),
new(Settings.ActionKeyword.PathSearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_pathsearch")),
new(Settings.ActionKeyword.IndexSearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_indexsearch"))
};
lbxActionKeywords.ItemsSource = actionKeywordsListView;
ActionKeywordView.Init(viewModel.Settings);
RefreshView();
}
@ -71,9 +71,9 @@ namespace Flow.Launcher.Plugin.Explorer.Views
&& btnEdit.Visibility == Visibility.Hidden)
btnEdit.Visibility = Visibility.Visible;
if ((lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0)
&& btnDelete.Visibility == Visibility.Visible
&& btnEdit.Visibility == Visibility.Visible)
if (lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0
&& btnDelete.Visibility == Visibility.Visible
&& btnEdit.Visibility == Visibility.Visible)
{
btnDelete.Visibility = Visibility.Hidden;
btnEdit.Visibility = Visibility.Hidden;
@ -122,7 +122,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void expActionKeywords_Collapsed(object sender, RoutedEventArgs e)
{
if (!expActionKeywords.IsExpanded)
expActionKeywords.Height = Double.NaN;
expActionKeywords.Height = double.NaN;
}
private void expAccessLinks_Click(object sender, RoutedEventArgs e)
@ -135,20 +135,20 @@ namespace Flow.Launcher.Plugin.Explorer.Views
if (expActionKeywords.IsExpanded)
expActionKeywords.IsExpanded = false;
RefreshView();
}
private void expAccessLinks_Collapsed(object sender, RoutedEventArgs e)
{
if (!expAccessLinks.IsExpanded)
expAccessLinks.Height = Double.NaN;
expAccessLinks.Height = double.NaN;
}
private void expExcludedPaths_Click(object sender, RoutedEventArgs e)
{
if (expExcludedPaths.IsExpanded)
expAccessLinks.Height = Double.NaN;
expAccessLinks.Height = double.NaN;
if (expAccessLinks.IsExpanded)
expAccessLinks.IsExpanded = false;
@ -161,11 +161,12 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink?? lbxExcludedPaths.SelectedItem as AccessLink;
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink;
if (selectedRow != null)
{
string msg = string.Format(viewModel.Context.API.GetTranslation("plugin_explorer_delete_folder_link"), selectedRow.Path);
string msg = string.Format(viewModel.Context.API.GetTranslation("plugin_explorer_delete_folder_link"),
selectedRow.Path);
if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
@ -191,7 +192,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
var selectedActionKeyword = lbxActionKeywords.SelectedItem as ActionKeywordView;
var actionKeywordWindow = new ActionKeywordSetting(viewModel, actionKeywordsListView, selectedActionKeyword);
var actionKeywordWindow = new ActionKeywordSetting(viewModel,
selectedActionKeyword);
actionKeywordWindow.ShowDialog();
@ -199,7 +201,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
}
else
{
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink;
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ??
lbxExcludedPaths.SelectedItem as AccessLink;
if (selectedRow != null)
{
@ -215,7 +218,8 @@ namespace Flow.Launcher.Plugin.Explorer.Views
if (expExcludedPaths.IsExpanded)
{
var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x => x.Path == selectedRow.Path);
var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x =>
x.Path == selectedRow.Path);
link.Path = folderBrowserDialog.SelectedPath;
}
}
@ -235,10 +239,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
var folderBrowserDialog = new FolderBrowserDialog();
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
var newAccessLink = new AccessLink
{
Path = folderBrowserDialog.SelectedPath
};
var newAccessLink = new AccessLink {Path = folderBrowserDialog.SelectedPath};
AddAccessLink(newAccessLink);
}
@ -259,10 +260,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
if (Directory.Exists(s))
{
var newFolderLink = new AccessLink
{
Path = s
};
var newFolderLink = new AccessLink {Path = s};
AddAccessLink(newFolderLink);
}
@ -275,7 +273,7 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void AddAccessLink(AccessLink newAccessLink)
{
if (expAccessLinks.IsExpanded
&& !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path))
&& !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path))
{
if (viewModel.Settings.QuickAccessLinks == null)
viewModel.Settings.QuickAccessLinks = new List<AccessLink>();
@ -313,8 +311,35 @@ namespace Flow.Launcher.Plugin.Explorer.Views
public class ActionKeywordView
{
public string Description { get; set; }
private static Settings _settings;
public string Keyword { get; set; }
public static void Init(Settings settings)
{
_settings = settings;
}
internal ActionKeywordView(Settings.ActionKeyword actionKeyword, string description)
{
KeywordProperty = actionKeyword;
Description = description;
}
public string Description { get; private init; }
public string Color => Enabled ?? true ? "Black" : "Gray";
internal Settings.ActionKeyword KeywordProperty { get; }
public string Keyword
{
get => _settings.GetActionKeyword(KeywordProperty);
set => _settings.SetActionKeyword(KeywordProperty, value);
}
public bool? Enabled
{
get => _settings.GetActionKeywordEnable(KeywordProperty);
set => _settings.SetActionKeywordEnable(KeywordProperty,
value ?? throw new ArgumentException("Unexpected null value"));
}
}
}
}

View file

@ -1,13 +1,15 @@
{
"ID": "572be03c74c642baae319fc283e561a8",
"ActionKeywords": [
"*",
"doc:"
"*",
"doc:",
"*",
"*"
],
"Name": "Explorer",
"Description": "Search and manage files and folders. Explorer utilises Windows Index Search",
"Author": "Jeremy Wu",
"Version": "1.7.7",
"Version": "1.8.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",

View file

@ -13,7 +13,11 @@
<system:String x:Key="flowlauncher_plugin_program_reindex">Reindex</system:String>
<system:String x:Key="flowlauncher_plugin_program_indexing">Indexing</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">Index Start Menu</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start_tooltip">When enabled, Flow will load programs from the start menu</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry">Index Registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry_tooltip">When enabled, Flow will load programs from the registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description">Enable Program Description</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description_tooltip">Disabling this will also stop Flow from searching via the program desciption</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_header">Suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max Depth</system:String>

View file

@ -12,8 +12,12 @@
<system:String x:Key="flowlauncher_plugin_program_suffixes">Prípony súborov</system:String>
<system:String x:Key="flowlauncher_plugin_program_reindex">Reindexovať</system:String>
<system:String x:Key="flowlauncher_plugin_program_indexing">Indexovanie</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">Indexovať Ponuku Štart</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry">Indexovať Registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">Indexovať ponuku Štart</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start_tooltip">Ak je povolené, Flow načíta programy z ponuky Štart</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry">Indexovať databázu Registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry_tooltip">Ak je povolené, Flow načíta programy z databázy Registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description">Povoliť popis programu</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description_tooltip">Zakázaním tejto funkcie sa tiež zastaví vyhľadávanie popisu programu cez Flow</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_header">Prípony</system:String>
<system:String x:Key="flowlauncher_plugin_program_max_depth_header">Max. hĺbka</system:String>

View file

@ -14,6 +14,7 @@
<system:String x:Key="flowlauncher_plugin_program_indexing">索引中</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">索引开始菜单</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry">索引注册表</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description">启用程序描述</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_header">后缀</system:String>
<system:String x:Key="flowlauncher_plugin_program_max_depth_header">最大深度</system:String>

View file

@ -17,6 +17,7 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger;
using Rect = System.Windows.Rect;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher.Plugin.Program.Programs
{
@ -63,14 +64,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (hResult == Hresult.Ok)
{
var apps = new List<Application>();
List<AppxPackageHelper.IAppxManifestApplication> _apps = _helper.getAppsFromManifest(stream);
foreach(var _app in _apps)
foreach (var _app in _apps)
{
var app = new Application(_app, this);
apps.Add(app);
}
Apps = apps.Where(a => a.AppListEntry != "none").ToArray();
}
else
@ -81,6 +82,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
Apps = new List<Application>().ToArray();
}
if (Marshal.ReleaseComObject(stream) > 0)
{
Log.Error("Flow.Launcher.Plugin.Program.Programs.UWP", "AppxManifest.xml was leaked");
}
}
@ -275,7 +281,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
MatchResult matchResult;
// We suppose Name won't be null
if (Description == null || Name.StartsWith(Description))
if (!Main._settings.EnableDescription || Description == null || Name.StartsWith(Description))
{
title = Name;
matchResult = StringMatcher.FuzzySearch(query, title);

View file

@ -54,51 +54,43 @@ namespace Flow.Launcher.Plugin.Program.Programs
public Result Result(string query, IPublicAPI api)
{
string title;
var nameMatchResult = StringMatcher.FuzzySearch(query, Name);
var descriptionMatchResult = StringMatcher.FuzzySearch(query, Description);
var pathMatchResult = new MatchResult(false, 0, new List<int>(), 0);
if (ExecutableName != null) // only lnk program will need this one
pathMatchResult = StringMatcher.FuzzySearch(query, ExecutableName);
MatchResult matchResult = nameMatchResult;
if (nameMatchResult.Score < descriptionMatchResult.Score)
matchResult = descriptionMatchResult;
if (!matchResult.IsSearchPrecisionScoreMet())
{
if (pathMatchResult.IsSearchPrecisionScoreMet())
matchResult = pathMatchResult;
else return null;
}
MatchResult matchResult;
// We suppose Name won't be null
if (Description == null || Name.StartsWith(Description))
if (!Main._settings.EnableDescription || Description == null || Name.StartsWith(Description))
{
title = Name;
matchResult = StringMatcher.FuzzySearch(query, title);
}
else if (Description.StartsWith(Name))
{
title = Description;
matchResult = StringMatcher.FuzzySearch(query, Description);
}
else
{
title = $"{Name}: {Description}";
if (matchResult == descriptionMatchResult)
var nameMatch = StringMatcher.FuzzySearch(query, Name);
var desciptionMatch = StringMatcher.FuzzySearch(query, Description);
if (desciptionMatch.Score > nameMatch.Score)
{
for (int i = 0; i < descriptionMatchResult.MatchData.Count; i++)
for (int i = 0; i < desciptionMatch.MatchData.Count; i++)
{
matchResult.MatchData[i] += Name.Length + 2; // 2 is ": "
desciptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": "
}
matchResult = desciptionMatch;
}
else matchResult = nameMatch;
}
if (matchResult == pathMatchResult)
if (!matchResult.IsSearchPrecisionScoreMet())
{
// path Match won't have valid highlight data
if (ExecutableName != null) // only lnk program will need this one
matchResult = StringMatcher.FuzzySearch(query, ExecutableName);
if (!matchResult.IsSearchPrecisionScoreMet())
return null;
matchResult.MatchData = new List<int>();
}

View file

@ -13,6 +13,7 @@ namespace Flow.Launcher.Plugin.Program
public bool EnableStartMenuSource { get; set; } = true;
public bool EnableDescription { get; set; } = true;
public bool EnableRegistrySource { get; set; } = true;
public string CustomizedExplorer { get; set; } = Explorer;
public string CustomizedArgs { get; set; } = ExplorerArgs;

View file

@ -4,19 +4,21 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:program="clr-namespace:Flow.Launcher.Plugin.Program"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d"
d:DesignHeight="404.508" d:DesignWidth="600">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="75"/>
<RowDefinition Height="120"/>
<RowDefinition Height="*"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Stretch">
<StackPanel Orientation="Vertical" Width="205">
<CheckBox Name="StartMenuEnabled" Click="StartMenuEnabled_Click" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_start}" />
<CheckBox Name="RegistryEnabled" Click="RegistryEnabled_Click" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_registry}" />
<StackPanel Orientation="Vertical" Width="250">
<CheckBox Name="StartMenuEnabled" IsChecked="{Binding EnableStartMenuSource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_start}" ToolTip="{DynamicResource flowlauncher_plugin_program_index_start_tooltip}" />
<CheckBox Name="RegistryEnabled" IsChecked="{Binding EnableRegistrySource}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_index_registry}" ToolTip="{DynamicResource flowlauncher_plugin_program_index_registry_tooltip}" />
<CheckBox Name="DescriptionEnabled" IsChecked="{Binding EnableDescription}" Margin="5" Content="{DynamicResource flowlauncher_plugin_program_enable_description}" ToolTip="{DynamicResource flowlauncher_plugin_program_enable_description_tooltip}" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button Height="31" HorizontalAlignment="Right" Margin="10" Width="100" x:Name="btnLoadAllProgramSource" Click="btnLoadAllProgramSource_OnClick" Content="{DynamicResource flowlauncher_plugin_program_all_programs}" />
@ -79,10 +81,10 @@
<TextBlock Text="{DynamicResource flowlauncher_plugin_program_customizedexplorer}" VerticalAlignment="Center" FontSize="15"
ToolTip= "{DynamicResource flowlauncher_plugin_program_tooltip_customizedexplorer}"/>
<TextBox Margin="10,0,10,0" TextWrapping="NoWrap" VerticalAlignment="Center" Width="200" Height="30" FontSize="15"
TextChanged="CustomizeExplorer" x:Name="CustomizeExplorerBox"/>
Text="{Binding CustomizedExplorerPath}" x:Name="CustomizeExplorerBox"/>
<TextBlock Text="{DynamicResource flowlauncher_plugin_program_args}" VerticalAlignment="Center" FontSize="15"
ToolTip="{DynamicResource flowlauncher_plugin_program_tooltip_args}" />
<TextBox Margin="10,0,0,0" Width="135" Height="30" FontSize="15" x:Name="CustomizeArgsBox" TextChanged="CustomizeExplorerArgs"></TextBox>
<TextBox Margin="10,0,0,0" Width="135" Height="30" FontSize="15" x:Name="CustomizeArgsBox" Text="{Binding CustomizedExplorerArg}"></TextBox>
</StackPanel>
</Grid>
</UserControl>

View file

@ -27,12 +27,50 @@ namespace Flow.Launcher.Plugin.Program.Views
// this as temporary holder for displaying all loaded programs sources.
internal static List<ProgramSource> ProgramSettingDisplayList { get; set; }
public bool EnableDescription
{
get => _settings.EnableDescription;
set => _settings.EnableDescription = value;
}
public bool EnableRegistrySource
{
get => _settings.EnableRegistrySource;
set
{
_settings.EnableRegistrySource = value;
ReIndexing();
}
}
public bool EnableStartMenuSource
{
get => _settings.EnableStartMenuSource;
set
{
_settings.EnableStartMenuSource = value;
ReIndexing();
}
}
public string CustomizedExplorerPath
{
get => _settings.CustomizedExplorer;
set => _settings.CustomizedExplorer = value;
}
public string CustomizedExplorerArg
{
get => _settings.CustomizedArgs;
set => _settings.CustomizedArgs = value;
}
public ProgramSetting(PluginInitContext context, Settings settings, Win32[] win32s, UWP.Application[] uwps)
{
this.context = context;
InitializeComponent();
Loaded += Setting_Loaded;
_settings = settings;
Loaded += Setting_Loaded;
InitializeComponent();
}
private void Setting_Loaded(object sender, RoutedEventArgs e)
@ -40,12 +78,6 @@ namespace Flow.Launcher.Plugin.Program.Views
ProgramSettingDisplayList = _settings.ProgramSources.LoadProgramSources();
programSourceView.ItemsSource = ProgramSettingDisplayList;
StartMenuEnabled.IsChecked = _settings.EnableStartMenuSource;
RegistryEnabled.IsChecked = _settings.EnableRegistrySource;
CustomizeExplorerBox.Text = _settings.CustomizedExplorer;
CustomizeArgsBox.Text = _settings.CustomizedArgs;
ViewRefresh();
}
@ -178,18 +210,6 @@ namespace Flow.Launcher.Plugin.Program.Views
}
}
private void StartMenuEnabled_Click(object sender, RoutedEventArgs e)
{
_settings.EnableStartMenuSource = StartMenuEnabled.IsChecked ?? false;
ReIndexing();
}
private void RegistryEnabled_Click(object sender, RoutedEventArgs e)
{
_settings.EnableRegistrySource = RegistryEnabled.IsChecked ?? false;
ReIndexing();
}
private void btnLoadAllProgramSource_OnClick(object sender, RoutedEventArgs e)
{
ProgramSettingDisplayList.LoadAllApplications();

View file

@ -4,7 +4,7 @@
"Name": "Program",
"Description": "Search programs in Flow.Launcher",
"Author": "qianlifeng",
"Version": "1.4.6",
"Version": "1.5.3",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Program.dll",

View file

@ -271,8 +271,8 @@ namespace Flow.Launcher.Plugin.Shell
public void Init(PluginInitContext context)
{
this.context = context;
context.API.GlobalKeyboardEvent += API_GlobalKeyboardEvent;
_settings = context.API.LoadSettingJsonStorage<Settings>();
context.API.GlobalKeyboardEvent += API_GlobalKeyboardEvent;
}
bool API_GlobalKeyboardEvent(int keyevent, int vkcode, SpecialKeyState state)

View file

@ -4,7 +4,7 @@
"Name": "Shell",
"Description": "Provide executing commands from Flow Launcher",
"Author": "qianlifeng",
"Version": "1.4.1",
"Version": "1.4.2",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Shell.dll",

View file

@ -13,6 +13,24 @@
<Setter Property="Height" Value="28"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<DataTemplate x:Key="HeaderTemplateArrowUp">
<DockPanel>
<TextBlock HorizontalAlignment="Center" Text="{Binding}"/>
<Path x:Name="arrow"
StrokeThickness = "1"
Fill = "gray"
Data = "M 5,10 L 15,10 L 10,5 L 5,10"/>
</DockPanel>
</DataTemplate>
<DataTemplate x:Key="HeaderTemplateArrowDown">
<DockPanel>
<TextBlock HorizontalAlignment="Center" Text="{Binding }"/>
<Path x:Name="arrow"
StrokeThickness = "1"
Fill = "gray"
Data = "M 5,5 L 10,10 L 15,5 L 5,5"/>
</DockPanel>
</DataTemplate>
</UserControl.Resources>
<Grid Margin="15 0 0 0">
<Grid.RowDefinitions>
@ -47,23 +65,40 @@
</StackPanel>
<ListView ItemsSource="{Binding Settings.SearchSources}"
SelectedItem="{Binding Settings.SelectedSearchSource}"
x:Name="SearchSourcesListView"
Grid.Row="3"
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}"
BorderBrush="DarkGray"
BorderThickness="1">
BorderThickness="1"
GridViewColumnHeader.Click="SortByColumn"
MouseDoubleClick="MouseDoubleClickItem">
<ListView.View>
<GridView>
<GridViewColumn Header="{DynamicResource flowlauncher_plugin_websearch_action_keyword}">
<GridViewColumn Header="{DynamicResource flowlauncher_plugin_websearch_title}"
DisplayMemberBinding="{Binding Title}"
Width="130">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding ActionKeyword}" />
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="{DynamicResource flowlauncher_plugin_websearch_url}" Width="418">
<GridViewColumn Header="{DynamicResource flowlauncher_plugin_websearch_action_keyword}"
DisplayMemberBinding="{Binding ActionKeyword}"
Width="137">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Url}" />
<TextBlock Text="{Binding ActionKeyword}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="{DynamicResource flowlauncher_plugin_websearch_url}"
DisplayMemberBinding="{Binding Url}"
Width="auto">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Url}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>

View file

@ -2,6 +2,8 @@ using Microsoft.Win32;
using System.Windows;
using System.Windows.Controls;
using Flow.Launcher.Core.Plugin;
using System.ComponentModel;
using System.Windows.Data;
namespace Flow.Launcher.Plugin.WebSearch
{
@ -88,5 +90,85 @@ namespace Flow.Launcher.Plugin.WebSearch
{
_settings.BrowserPath = browserPathBox.Text;
}
GridViewColumnHeader _lastHeaderClicked = null;
ListSortDirection _lastDirection = ListSortDirection.Ascending;
private void SortByColumn(object sender, RoutedEventArgs e)
{
ListSortDirection direction;
if (e.OriginalSource is not GridViewColumnHeader headerClicked)
{
return;
}
if (headerClicked.Role == GridViewColumnHeaderRole.Padding)
{
return;
}
if (headerClicked != _lastHeaderClicked)
{
direction = ListSortDirection.Ascending;
}
else
{
if (_lastDirection == ListSortDirection.Ascending)
{
direction = ListSortDirection.Descending;
}
else
{
direction = ListSortDirection.Ascending;
}
}
var columnBinding = headerClicked.Column.DisplayMemberBinding as Binding;
var sortBy = columnBinding?.Path.Path ?? headerClicked.Column.Header as string;
Sort(sortBy, direction);
if (direction == ListSortDirection.Ascending)
{
headerClicked.Column.HeaderTemplate =
Resources["HeaderTemplateArrowUp"] as DataTemplate;
}
else
{
headerClicked.Column.HeaderTemplate =
Resources["HeaderTemplateArrowDown"] as DataTemplate;
}
// Remove arrow from previously sorted header
if (_lastHeaderClicked != null && _lastHeaderClicked != headerClicked)
{
_lastHeaderClicked.Column.HeaderTemplate = null;
}
_lastHeaderClicked = headerClicked;
_lastDirection = direction;
}
private void Sort(string sortBy, ListSortDirection direction)
{
ICollectionView dataView = CollectionViewSource.GetDefaultView(SearchSourcesListView.ItemsSource);
dataView.SortDescriptions.Clear();
SortDescription sd = new(sortBy, direction);
dataView.SortDescriptions.Add(sd);
dataView.Refresh();
}
private void MouseDoubleClickItem(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (((FrameworkElement)e.OriginalSource).DataContext is SearchSource && _settings.SelectedSearchSource != null)
{
var webSearch = new SearchSourceSettingWindow
(
_settings.SearchSources, _context, _settings.SelectedSearchSource
);
webSearch.ShowDialog();
}
}
}
}

View file

@ -26,7 +26,7 @@
"Name": "Web Searches",
"Description": "Provide the web search ability",
"Author": "qianlifeng",
"Version": "1.3.9",
"Version": "1.4.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.WebSearch.dll",

View file

@ -35,8 +35,8 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
## Running Flow Launcher
| [Windows 7 and up](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest) |
| ---------------------------------------------------------------------------------- |
| [Windows 7 and up installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest) | `WinGet install "Flow Launcher"` |
| -------------------------------------------------------------------------------------------- | -------------------------------- |
Windows may complain about security due to code not being signed, this will be completed at a later stage. If you downloaded from this repo, you are good to continue the set up.
@ -63,7 +63,7 @@ Windows may complain about security due to code not being signed, this will be c
## Plugins
There is a [list of actively maintained plugins](https://github.com/Flow-Launcher/docs/blob/main/Plugins.md) in the documentation section, some of which are integrated from other launchers.
There is a [list of actively maintained plugins](https://github.com/Flow-Launcher/docs/blob/main/plugins.md) in the documentation section, some of which are integrated from other launchers.
## Status