Add 'src/modules/launcher/' from commit '28acd466b352a807910cebbc74477d3173b31511'

git-subtree-dir: src/modules/launcher
git-subtree-mainline: 852689b3df
git-subtree-split: 28acd466b3
This commit is contained in:
Alekhya Reddy Kommuru
2020-03-10 14:15:29 -07:00
487 changed files with 28191 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers>
<PropertyChanged />
</Weavers>

View File

@@ -0,0 +1,54 @@
using System;
using System.Diagnostics;
using Wox.Plugin;
namespace Wox.Core.Plugin
{
internal class ExecutablePlugin : JsonRPCPlugin
{
private readonly ProcessStartInfo _startInfo;
public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable;
public ExecutablePlugin(string filename)
{
_startInfo = new ProcessStartInfo
{
FileName = filename,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
}
protected override string ExecuteQuery(Query query)
{
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
{
Method = "query",
Parameters = new object[] { query.Search },
};
_startInfo.Arguments = $"\"{request}\"";
return Execute(_startInfo);
}
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
{
_startInfo.Arguments = $"\"{rpcRequest}\"";
return Execute(_startInfo);
}
protected override string ExecuteContextMenu(Result selectedResult) {
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel {
Method = "contextmenu",
Parameters = new object[] { selectedResult.ContextData },
};
_startInfo.Arguments = $"\"{request}\"";
return Execute(_startInfo);
}
}
}

View File

@@ -0,0 +1,135 @@

/* We basically follow the Json-RPC 2.0 spec (http://www.jsonrpc.org/specification) to invoke methods between Wox 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.
*
* When execute a query:
* Wox -------JsonRPCServerRequestModel--------> client
* Wox <------JsonRPCQueryResponseModel--------- client
*
* When execute a action (which mean user select an item in reulst item):
* Wox -------JsonRPCServerRequestModel--------> client
* Wox <------JsonRPCResponseModel-------------- client
*
*/
using System.Collections.Generic;
using System.Linq;
using Wox.Plugin;
namespace Wox.Core.Plugin
{
public class JsonRPCErrorModel
{
public int Code { get; set; }
public string Message { get; set; }
public string Data { get; set; }
}
public class JsonRPCModelBase
{
public int Id { get; set; }
}
public class JsonRPCResponseModel : JsonRPCModelBase
{
public string Result { get; set; }
public JsonRPCErrorModel Error { get; set; }
}
public class JsonRPCQueryResponseModel : JsonRPCResponseModel
{
public new List<JsonRPCResult> Result { get; set; }
}
public class JsonRPCRequestModel : JsonRPCModelBase
{
public string Method { get; set; }
public object[] Parameters { get; set; }
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(@"""", @"\\""""");
}
}
/// <summary>
/// Json RPC Request that Wox sent to client
/// </summary>
public class JsonRPCServerRequestModel : JsonRPCRequestModel
{
public override string ToString()
{
string rpc = base.ToString();
return rpc + "}";
}
}
/// <summary>
/// Json RPC Request(in query response) that client sent to Wox
/// </summary>
public class JsonRPCClientRequestModel : JsonRPCRequestModel
{
public bool DontHideAfterAction { get; set; }
public override string ToString()
{
string rpc = base.ToString();
return rpc + "}";
}
}
/// <summary>
/// Represent the json-rpc result item that client send to Wox
/// Typically, we will send back this request model to client after user select the result item
/// But if the request method starts with "Wox.", we will invoke the public APIs we expose.
/// </summary>
public class JsonRPCResult : Result
{
public JsonRPCClientRequestModel JsonRPCAction { get; set; }
}
}

View File

@@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
using Wox.Infrastructure.Exception;
using Wox.Infrastructure.Logger;
using Wox.Plugin;
namespace Wox.Core.Plugin
{
/// <summary>
/// Represent the plugin that using JsonPRC
/// every JsonRPC plugin should has its own plugin instance
/// </summary>
internal abstract class JsonRPCPlugin : IPlugin, IContextMenu
{
protected PluginInitContext context;
public const string JsonRPC = "JsonRPC";
/// <summary>
/// The language this JsonRPCPlugin support
/// </summary>
public abstract string SupportedLanguage { get; set; }
protected abstract string ExecuteQuery(Query query);
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);
try
{
return DeserializedResult(output);
}
catch (Exception e)
{
Log.Exception($"|JsonRPCPlugin.LoadContextMenus|Exception on result <{selectedResult}>", e);
return null;
}
}
private List<Result> DeserializedResult(string output)
{
if (!String.IsNullOrEmpty(output))
{
List<Result> results = new List<Result>();
JsonRPCQueryResponseModel queryResponseModel = JsonConvert.DeserializeObject<JsonRPCQueryResponseModel>(output);
if (queryResponseModel.Result == null) return null;
foreach (JsonRPCResult result in queryResponseModel.Result)
{
JsonRPCResult result1 = result;
result.Action = c =>
{
if (result1.JsonRPCAction == null) return false;
if (!String.IsNullOrEmpty(result1.JsonRPCAction.Method))
{
if (result1.JsonRPCAction.Method.StartsWith("Wox."))
{
ExecuteWoxAPI(result1.JsonRPCAction.Method.Substring(4), result1.JsonRPCAction.Parameters);
}
else
{
string actionReponse = ExecuteCallback(result1.JsonRPCAction);
JsonRPCRequestModel jsonRpcRequestModel = JsonConvert.DeserializeObject<JsonRPCRequestModel>(actionReponse);
if (jsonRpcRequestModel != null
&& !String.IsNullOrEmpty(jsonRpcRequestModel.Method)
&& jsonRpcRequestModel.Method.StartsWith("Wox."))
{
ExecuteWoxAPI(jsonRpcRequestModel.Method.Substring(4), jsonRpcRequestModel.Parameters);
}
}
}
return !result1.JsonRPCAction.DontHideAfterAction;
};
results.Add(result);
}
return results;
}
else
{
return null;
}
}
private void ExecuteWoxAPI(string method, object[] parameters)
{
MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method);
if (methodInfo != null)
{
try
{
methodInfo.Invoke(PluginManager.API, parameters);
}
catch (Exception)
{
#if (DEBUG)
{
throw;
}
#endif
}
}
}
/// <summary>
/// Execute external program and return the output
/// </summary>
/// <param name="fileName"></param>
/// <param name="arguments"></param>
/// <returns></returns>
protected string Execute(string fileName, string arguments)
{
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);
}
protected string Execute(ProcessStartInfo startInfo)
{
try
{
using (var process = Process.Start(startInfo))
{
if (process != null)
{
using (var standardOutput = process.StandardOutput)
{
var result = standardOutput.ReadToEnd();
if (string.IsNullOrEmpty(result))
{
using (var standardError = process.StandardError)
{
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;
}
}
}
else if (result.StartsWith("DEBUG:"))
{
MessageBox.Show(new Form { TopMost = true }, result.Substring(6));
return string.Empty;
}
else
{
return result;
}
}
}
else
{
Log.Error("|JsonRPCPlugin.Execute|Can't start new process");
return string.Empty;
}
}
}
catch (Exception e)
{
Log.Exception($"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", e);
return string.Empty;
}
}
public void Init(PluginInitContext ctx)
{
context = ctx;
}
}
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Wox.Infrastructure.Exception;
using Wox.Infrastructure.Logger;
using Wox.Plugin;
namespace Wox.Core.Plugin
{
internal abstract class PluginConfig
{
private const string PluginConfigName = "plugin.json";
private static readonly List<PluginMetadata> PluginMetadatas = new List<PluginMetadata>();
/// <summary>
/// Parse plugin metadata in giving directories
/// </summary>
/// <param name="pluginDirectories"></param>
/// <returns></returns>
public static List<PluginMetadata> Parse(string[] pluginDirectories)
{
PluginMetadatas.Clear();
var directories = pluginDirectories.SelectMany(Directory.GetDirectories);
ParsePluginConfigs(directories);
return PluginMetadatas;
}
private static void ParsePluginConfigs(IEnumerable<string> directories)
{
// todo use linq when diable plugin is implmented since parallel.foreach + list is not thread saft
foreach (var directory in directories)
{
if (File.Exists(Path.Combine(directory, "NeedDelete.txt")))
{
try
{
Directory.Delete(directory, true);
}
catch (Exception e)
{
Log.Exception($"|PluginConfig.ParsePLuginConfigs|Can't delete <{directory}>", e);
}
}
else
{
PluginMetadata metadata = GetPluginMetadata(directory);
if (metadata != null)
{
PluginMetadatas.Add(metadata);
}
}
}
}
private static PluginMetadata GetPluginMetadata(string pluginDirectory)
{
string configPath = Path.Combine(pluginDirectory, PluginConfigName);
if (!File.Exists(configPath))
{
Log.Error($"|PluginConfig.GetPluginMetadata|Didn't find config file <{configPath}>");
return null;
}
PluginMetadata metadata;
try
{
metadata = JsonConvert.DeserializeObject<PluginMetadata>(File.ReadAllText(configPath));
metadata.PluginDirectory = pluginDirectory;
// for plugins which doesn't has ActionKeywords key
metadata.ActionKeywords = metadata.ActionKeywords ?? new List<string> { metadata.ActionKeyword };
// for plugin still use old ActionKeyword
metadata.ActionKeyword = metadata.ActionKeywords?[0];
}
catch (Exception e)
{
Log.Exception($"|PluginConfig.GetPluginMetadata|invalid json for config <{configPath}>", e);
return null;
}
if (!AllowedLanguage.IsAllowed(metadata.Language))
{
Log.Error($"|PluginConfig.GetPluginMetadata|Invalid language <{metadata.Language}> for config <{configPath}>");
return null;
}
if (!File.Exists(metadata.ExecuteFilePath))
{
Log.Error($"|PluginConfig.GetPluginMetadata|execute file path didn't exist <{metadata.ExecuteFilePath}> for conifg <{configPath}");
return null;
}
return metadata;
}
}
}

View File

@@ -0,0 +1,202 @@
using System;
using System.IO;
using System.Windows;
using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using Wox.Plugin;
namespace Wox.Core.Plugin
{
internal class PluginInstaller
{
internal static void Install(string path)
{
if (File.Exists(path))
{
string tempFoler = Path.Combine(Path.GetTempPath(), "wox\\plugins");
if (Directory.Exists(tempFoler))
{
Directory.Delete(tempFoler, true);
}
UnZip(path, tempFoler, true);
string iniPath = Path.Combine(tempFoler, "plugin.json");
if (!File.Exists(iniPath))
{
MessageBox.Show("Install failed: plugin config is missing");
return;
}
PluginMetadata plugin = GetMetadataFromJson(tempFoler);
if (plugin == null || plugin.Name == null)
{
MessageBox.Show("Install failed: plugin config is invalid");
return;
}
string pluginFolerPath = Infrastructure.Constant.PluginsDirectory;
string newPluginName = plugin.Name
.Replace("/", "_")
.Replace("\\", "_")
.Replace(":", "_")
.Replace("<", "_")
.Replace(">", "_")
.Replace("?", "_")
.Replace("*", "_")
.Replace("|", "_")
+ "-" + Guid.NewGuid();
string newPluginPath = Path.Combine(pluginFolerPath, newPluginName);
string content = $"Do you want to install following plugin?{Environment.NewLine}{Environment.NewLine}" +
$"Name: {plugin.Name}{Environment.NewLine}" +
$"Version: {plugin.Version}{Environment.NewLine}" +
$"Author: {plugin.Author}";
PluginPair existingPlugin = PluginManager.GetPluginForId(plugin.ID);
if (existingPlugin != null)
{
content = $"Do you want to update following plugin?{Environment.NewLine}{Environment.NewLine}" +
$"Name: {plugin.Name}{Environment.NewLine}" +
$"Old Version: {existingPlugin.Metadata.Version}" +
$"{Environment.NewLine}New Version: {plugin.Version}" +
$"{Environment.NewLine}Author: {plugin.Author}";
}
var result = MessageBox.Show(content, "Install plugin", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
if (existingPlugin != null && Directory.Exists(existingPlugin.Metadata.PluginDirectory))
{
//when plugin is in use, we can't delete them. That's why we need to make plugin folder a random name
File.Create(Path.Combine(existingPlugin.Metadata.PluginDirectory, "NeedDelete.txt")).Close();
}
UnZip(path, newPluginPath, true);
Directory.Delete(tempFoler, true);
//exsiting plugins may be has loaded by application,
//if we try to delelte those kind of plugins, we will get a error that indicate the
//file is been used now.
//current solution is to restart wox. Ugly.
//if (MainWindow.Initialized)
//{
// Plugins.Initialize();
//}
if (MessageBox.Show($"You have installed plugin {plugin.Name} successfully.{Environment.NewLine}" +
"Restart Wox to take effect?",
"Install plugin", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
PluginManager.API.RestarApp();
}
}
}
}
private static PluginMetadata GetMetadataFromJson(string pluginDirectory)
{
string configPath = Path.Combine(pluginDirectory, "plugin.json");
PluginMetadata metadata;
if (!File.Exists(configPath))
{
return null;
}
try
{
metadata = JsonConvert.DeserializeObject<PluginMetadata>(File.ReadAllText(configPath));
metadata.PluginDirectory = pluginDirectory;
}
catch (Exception)
{
string error = $"Parse plugin config {configPath} failed: json format is not valid";
#if (DEBUG)
{
throw new Exception(error);
}
#endif
return null;
}
if (!AllowedLanguage.IsAllowed(metadata.Language))
{
string error = $"Parse plugin config {configPath} failed: invalid language {metadata.Language}";
#if (DEBUG)
{
throw new Exception(error);
}
#endif
return null;
}
if (!File.Exists(metadata.ExecuteFilePath))
{
string error = $"Parse plugin config {configPath} failed: ExecuteFile {metadata.ExecuteFilePath} didn't exist";
#if (DEBUG)
{
throw new Exception(error);
}
#endif
return null;
}
return metadata;
}
/// <summary>
/// unzip
/// </summary>
/// <param name="zipedFile">The ziped file.</param>
/// <param name="strDirectory">The STR directory.</param>
/// <param name="overWrite">overwirte</param>
private static void UnZip(string zipedFile, string strDirectory, bool overWrite)
{
if (strDirectory == "")
strDirectory = Directory.GetCurrentDirectory();
if (!strDirectory.EndsWith("\\"))
strDirectory = strDirectory + "\\";
using (ZipInputStream s = new ZipInputStream(File.OpenRead(zipedFile)))
{
ZipEntry theEntry;
while ((theEntry = s.GetNextEntry()) != null)
{
string directoryName = "";
string pathToZip = "";
pathToZip = theEntry.Name;
if (pathToZip != "")
directoryName = Path.GetDirectoryName(pathToZip) + "\\";
string fileName = Path.GetFileName(pathToZip);
Directory.CreateDirectory(strDirectory + directoryName);
if (fileName != "")
{
if ((File.Exists(strDirectory + directoryName + fileName) && overWrite) || (!File.Exists(strDirectory + directoryName + fileName)))
{
using (FileStream streamWriter = File.Create(strDirectory + directoryName + fileName))
{
byte[] data = new byte[2048];
while (true)
{
int size = s.Read(data, 0, data.Length);
if (size > 0)
streamWriter.Write(data, 0, size);
else
break;
}
streamWriter.Close();
}
}
}
}
s.Close();
}
}
}
}

View File

@@ -0,0 +1,313 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Wox.Infrastructure;
using Wox.Infrastructure.Logger;
using Wox.Infrastructure.Storage;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin;
namespace Wox.Core.Plugin
{
/// <summary>
/// The entry for managing Wox plugins
/// </summary>
public static class PluginManager
{
private static IEnumerable<PluginPair> _contextMenuPlugins;
/// <summary>
/// Directories that will hold Wox plugin directory
/// </summary>
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 IPublicAPI API { private set; get; }
// todo happlebao, this should not be public, the indicator function should be embeded
public static PluginsSettings Settings;
private static List<PluginMetadata> _metadatas;
private static readonly string[] Directories = { Constant.PreinstalledDirectory, Constant.PluginsDirectory };
private static void ValidateUserDirectory()
{
if (!Directory.Exists(Constant.PluginsDirectory))
{
Directory.CreateDirectory(Constant.PluginsDirectory);
}
}
private static void DeletePythonBinding()
{
const string binding = "wox.py";
var directory = Constant.PluginsDirectory;
foreach (var subDirectory in Directory.GetDirectories(directory))
{
var path = Path.Combine(subDirectory, binding);
if (File.Exists(path))
{
File.Delete(path);
}
}
}
public static void Save()
{
foreach (var plugin in AllPlugins)
{
var savable = plugin.Plugin as ISavable;
savable?.Save();
}
}
public static void ReloadData()
{
foreach(var plugin in AllPlugins)
{
var reloadablePlugin = plugin.Plugin as IReloadable;
reloadablePlugin?.ReloadData();
}
}
static PluginManager()
{
ValidateUserDirectory();
// force old plugins use new python binding
DeletePythonBinding();
}
/// <summary>
/// because InitializePlugins needs API, so LoadPlugins needs to be called first
/// todo happlebao The API should be removed
/// </summary>
/// <param name="settings"></param>
public static void LoadPlugins(PluginsSettings settings)
{
_metadatas = PluginConfig.Parse(Directories);
Settings = settings;
Settings.UpdatePluginSettings(_metadatas);
AllPlugins = PluginsLoader.Plugins(_metadatas, Settings);
}
/// <summary>
/// Call initialize for all plugins
/// </summary>
/// <returns>return the list of failed to init plugins or null for none</returns>
public static void InitializePlugins(IPublicAPI api)
{
API = api;
var failedPlugins = new ConcurrentQueue<PluginPair>();
Parallel.ForEach(AllPlugins, pair =>
{
try
{
var milliseconds = Stopwatch.Debug($"|PluginManager.InitializePlugins|Init method time cost for <{pair.Metadata.Name}>", () =>
{
pair.Plugin.Init(new PluginInitContext
{
CurrentPluginMetadata = pair.Metadata,
API = API
});
});
pair.Metadata.InitTime += milliseconds;
Log.Info($"|PluginManager.InitializePlugins|Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
}
catch (Exception e)
{
Log.Exception(nameof(PluginManager), $"Fail to Init plugin: {pair.Metadata.Name}", e);
pair.Metadata.Disabled = true;
failedPlugins.Enqueue(pair);
}
});
_contextMenuPlugins = GetPluginsForInterface<IContextMenu>();
foreach (var plugin in AllPlugins)
{
if (IsGlobalPlugin(plugin.Metadata))
GlobalPlugins.Add(plugin);
// Plugins may have multiple ActionKeywords, eg. WebSearch
plugin.Metadata.ActionKeywords.Where(x => x != Query.GlobalPluginWildcardSign)
.ToList()
.ForEach(x => NonGlobalPlugins[x] = plugin);
}
if (failedPlugins.Any())
{
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
API.ShowMsg($"Fail to Init Plugins", $"Plugins: {failed} - fail to load and would be disabled, please contact plugin creator for help", "", false);
}
}
public static void InstallPlugin(string path)
{
PluginInstaller.Install(path);
}
public static List<PluginPair> ValidPluginsForQuery(Query query)
{
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
{
var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair> { plugin };
}
else
{
return GlobalPlugins;
}
}
public static List<Result> QueryForPlugin(PluginPair pair, Query query)
{
try
{
List<Result> results = null;
var metadata = pair.Metadata;
var milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", () =>
{
results = pair.Plugin.Query(query) ?? new List<Result>();
UpdatePluginMetadata(results, metadata, query);
});
metadata.QueryCount += 1;
metadata.AvgQueryTime = metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2;
return results;
}
catch (Exception e)
{
Log.Exception($"|PluginManager.QueryForPlugin|Exception for plugin <{pair.Metadata.Name}> when query <{query}>", e);
return new List<Result>();
}
}
public static void UpdatePluginMetadata(List<Result> results, PluginMetadata metadata, Query query)
{
foreach (var r in results)
{
r.PluginDirectory = metadata.PluginDirectory;
r.PluginID = metadata.ID;
r.OriginQuery = query;
}
}
private static bool IsGlobalPlugin(PluginMetadata metadata)
{
return metadata.ActionKeywords.Contains(Query.GlobalPluginWildcardSign);
}
/// <summary>
/// get specified plugin, return null if not found
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public static PluginPair GetPluginForId(string id)
{
return AllPlugins.FirstOrDefault(o => o.Metadata.ID == id);
}
public static IEnumerable<PluginPair> GetPluginsForInterface<T>() where T : IFeatures
{
return AllPlugins.Where(p => p.Plugin is T);
}
public static List<Result> GetContextMenusForPlugin(Result result)
{
var pluginPair = _contextMenuPlugins.FirstOrDefault(o => o.Metadata.ID == result.PluginID);
if (pluginPair != null)
{
var metadata = pluginPair.Metadata;
var plugin = (IContextMenu)pluginPair.Plugin;
try
{
var results = plugin.LoadContextMenus(result);
foreach (var r in results)
{
r.PluginDirectory = metadata.PluginDirectory;
r.PluginID = metadata.ID;
r.OriginQuery = result.OriginQuery;
}
return results;
}
catch (Exception e)
{
Log.Exception($"|PluginManager.GetContextMenusForPlugin|Can't load context menus for plugin <{metadata.Name}>", e);
return new List<Result>();
}
}
else
{
return new List<Result>();
}
}
public static bool ActionKeywordRegistered(string actionKeyword)
{
if (actionKeyword != Query.GlobalPluginWildcardSign &&
NonGlobalPlugins.ContainsKey(actionKeyword))
{
return true;
}
else
{
return false;
}
}
/// <summary>
/// used to add action keyword for multiple action keyword plugin
/// e.g. web search
/// </summary>
public static void AddActionKeyword(string id, string newActionKeyword)
{
var plugin = GetPluginForId(id);
if (newActionKeyword == Query.GlobalPluginWildcardSign)
{
GlobalPlugins.Add(plugin);
}
else
{
NonGlobalPlugins[newActionKeyword] = plugin;
}
plugin.Metadata.ActionKeywords.Add(newActionKeyword);
}
/// <summary>
/// used to add action keyword for multiple action keyword plugin
/// e.g. web search
/// </summary>
public static void RemoveActionKeyword(string id, string oldActionkeyword)
{
var plugin = GetPluginForId(id);
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)
{
GlobalPlugins.Remove(plugin);
}
if(oldActionkeyword != Query.GlobalPluginWildcardSign)
NonGlobalPlugins.Remove(oldActionkeyword);
plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
}
public static void ReplaceActionKeyword(string id, string oldActionKeyword, string newActionKeyword)
{
if (oldActionKeyword != newActionKeyword)
{
AddActionKeyword(id, newActionKeyword);
RemoveActionKeyword(id, oldActionKeyword);
}
}
}
}

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Wox.Infrastructure;
using Wox.Infrastructure.Exception;
using Wox.Infrastructure.Logger;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin;
namespace Wox.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 csharpPlugins = CSharpPlugins(metadatas).ToList();
var pythonPlugins = PythonPlugins(metadatas, settings.PythonDirectory);
var executablePlugins = ExecutablePlugins(metadatas);
var plugins = csharpPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
return plugins;
}
public static IEnumerable<PluginPair> CSharpPlugins(List<PluginMetadata> source)
{
var plugins = new List<PluginPair>();
var metadatas = source.Where(o => o.Language.ToUpper() == AllowedLanguage.CSharp);
foreach (var metadata in metadatas)
{
var milliseconds = Stopwatch.Debug($"|PluginsLoader.CSharpPlugins|Constructor init cost for {metadata.Name}", () =>
{
#if DEBUG
var assembly = Assembly.Load(AssemblyName.GetAssemblyName(metadata.ExecuteFilePath));
var types = assembly.GetTypes();
var type = types.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(typeof(IPlugin)));
var plugin = (IPlugin)Activator.CreateInstance(type);
#else
Assembly assembly;
try
{
assembly = Assembly.Load(AssemblyName.GetAssemblyName(metadata.ExecuteFilePath));
}
catch (Exception e)
{
Log.Exception($"|PluginsLoader.CSharpPlugins|Couldn't load assembly for {metadata.Name}", e);
return;
}
var types = assembly.GetTypes();
Type type;
try
{
type = types.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Contains(typeof(IPlugin)));
}
catch (InvalidOperationException e)
{
Log.Exception($"|PluginsLoader.CSharpPlugins|Can't find class implement IPlugin for <{metadata.Name}>", e);
return;
}
IPlugin plugin;
try
{
plugin = (IPlugin)Activator.CreateInstance(type);
}
catch (Exception e)
{
Log.Exception($"|PluginsLoader.CSharpPlugins|Can't create instance for <{metadata.Name}>", e);
return;
}
#endif
PluginPair pair = new PluginPair
{
Plugin = plugin,
Metadata = metadata
};
plugins.Add(pair);
});
metadata.InitTime += milliseconds;
}
return plugins;
}
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, string pythonDirecotry)
{
var metadatas = source.Where(o => o.Language.ToUpper() == AllowedLanguage.Python);
string filename;
if (string.IsNullOrEmpty(pythonDirecotry))
{
var paths = Environment.GetEnvironmentVariable(PATH);
if (paths != null)
{
var pythonPaths = paths.Split(';').Where(p => p.ToLower().Contains(Python));
if (pythonPaths.Any())
{
filename = PythonExecutable;
}
else
{
Log.Error("|PluginsLoader.PythonPlugins|Python can't be found in PATH.");
return new List<PluginPair>();
}
}
else
{
Log.Error("|PluginsLoader.PythonPlugins|PATH environment variable is not set.");
return new List<PluginPair>();
}
}
else
{
var path = Path.Combine(pythonDirecotry, PythonExecutable);
if (File.Exists(path))
{
filename = path;
}
else
{
Log.Error("|PluginsLoader.PythonPlugins|Can't find python executable in <b ");
return new List<PluginPair>();
}
}
Constant.PythonPath = filename;
var plugins = metadatas.Select(metadata => new PluginPair
{
Plugin = new PythonPlugin(filename),
Metadata = metadata
});
return plugins;
}
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
{
var metadatas = source.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable);
var plugins = metadatas.Select(metadata => new PluginPair
{
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath),
Metadata = metadata
});
return plugins;
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Diagnostics;
using System.IO;
using Wox.Infrastructure;
using Wox.Plugin;
namespace Wox.Core.Plugin
{
internal class PythonPlugin : JsonRPCPlugin
{
private readonly ProcessStartInfo _startInfo;
public override string SupportedLanguage { get; set; } = AllowedLanguage.Python;
public PythonPlugin(string filename)
{
_startInfo = new ProcessStartInfo
{
FileName = filename,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
// temp fix for issue #667
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
}
protected override string ExecuteQuery(Query query)
{
JsonRPCServerRequestModel request = new JsonRPCServerRequestModel
{
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}\"";
// todo happlebao why context can't be used in constructor
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
return Execute(_startInfo);
}
protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest)
{
_startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\"";
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
return Execute(_startInfo);
}
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;
return Execute(_startInfo);
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Wox.Plugin;
namespace Wox.Core.Plugin
{
public static class QueryBuilder
{
public static Query Build(string text, Dictionary<string, PluginPair> nonGlobalPlugins)
{
// replace multiple white spaces with one white space
var terms = text.Split(new[] { Query.TermSeperater }, StringSplitOptions.RemoveEmptyEntries);
if (terms.Length == 0)
{ // nothing was typed
return null;
}
var rawQuery = string.Join(Query.TermSeperater, terms);
string actionKeyword, search;
string possibleActionKeyword = terms[0];
List<string> actionParameters;
if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled)
{ // use non global plugin for query
actionKeyword = possibleActionKeyword;
actionParameters = terms.Skip(1).ToList();
search = actionParameters.Count > 0 ? rawQuery.Substring(actionKeyword.Length + 1) : string.Empty;
}
else
{ // non action keyword
actionKeyword = string.Empty;
actionParameters = terms.ToList();
search = rawQuery;
}
var query = new Query
{
Terms = terms,
RawQuery = rawQuery,
ActionKeyword = actionKeyword,
Search = search,
// Obsolete value initialisation
ActionName = actionKeyword,
ActionParameters = actionParameters
};
return query;
}
}
}

View File

@@ -0,0 +1,5 @@
There are two kinds of plugins:
1. System plugin
Those plugins that action keyword is "*", those plugin doesn't need action keyword
2. User Plugin
Those plugins that contains customized action keyword

View File

@@ -0,0 +1,5 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Wox.Core")]
[assembly: Guid("693aa0e5-741b-4759-b740-fdbb011a3280")]

View File

@@ -0,0 +1,9 @@
What does Wox.Core do?
=====
* Handle Query
* Define Wox exceptions
* Manage Plugins (including system plugin and user plugin)
* Manage Themes
* Manage i18n
* Manage Update and version

View File

@@ -0,0 +1,52 @@
using System.Collections.Generic;
namespace Wox.Core.Resource
{
internal static class AvailableLanguages
{
public static Language English = new Language("en", "English");
public static Language Chinese = new Language("zh-cn", "中文");
public static Language Chinese_TW = new Language("zh-tw", "中文(繁体)");
public static Language Ukrainian = new Language("uk-UA", "Українська");
public static Language Russian = new Language("ru", "Русский");
public static Language French = new Language("fr", "Français");
public static Language Japanese = new Language("ja", "日本語");
public static Language Dutch = new Language("nl", "Dutch");
public static Language Polish = new Language("pl", "Polski");
public static Language Danish = new Language("da", "Dansk");
public static Language German = new Language("de", "Deutsch");
public static Language Korean = new Language("ko", "한국어");
public static Language Serbian = new Language("sr", "Srpski");
public static Language Portuguese_BR = new Language("pt-br", "Português (Brasil)");
public static Language Italian = new Language("it", "Italiano");
public static Language Norwegian_Bokmal = new Language("nb-NO", "Norsk Bokmål");
public static Language Slovak = new Language("sk", "Slovenský");
public static Language Turkish = new Language("tr", "Türkçe");
public static List<Language> GetAvailableLanguages()
{
List<Language> languages = new List<Language>
{
English,
Chinese,
Chinese_TW,
Ukrainian,
Russian,
French,
Japanese,
Dutch,
Polish,
Danish,
German,
Korean,
Serbian,
Portuguese_BR,
Italian,
Norwegian_Bokmal,
Slovak,
Turkish
};
return languages;
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Linq;
using System.Windows;
using System.Windows.Media;
namespace Wox.Core.Resource
{
public static class FontHelper
{
static FontWeightConverter fontWeightConverter = new FontWeightConverter();
public static FontWeight GetFontWeightFromInvariantStringOrNormal(string value)
{
if (value == null) return FontWeights.Normal;
try
{
return (FontWeight) fontWeightConverter.ConvertFromInvariantString(value);
}
catch {
return FontWeights.Normal;
}
}
static FontStyleConverter fontStyleConverter = new FontStyleConverter();
public static FontStyle GetFontStyleFromInvariantStringOrNormal(string value)
{
if (value == null) return FontStyles.Normal;
try
{
return (FontStyle)fontStyleConverter.ConvertFromInvariantString(value);
}
catch
{
return FontStyles.Normal;
}
}
static FontStretchConverter fontStretchConverter = new FontStretchConverter();
public static FontStretch GetFontStretchFromInvariantStringOrNormal(string value)
{
if (value == null) return FontStretches.Normal;
try
{
return (FontStretch)fontStretchConverter.ConvertFromInvariantString(value);
}
catch
{
return FontStretches.Normal;
}
}
public static FamilyTypeface ChooseRegularFamilyTypeface(this FontFamily family)
{
return family.FamilyTypefaces.OrderBy(o =>
{
return Math.Abs(o.Stretch.ToOpenTypeStretch() - FontStretches.Normal.ToOpenTypeStretch()) * 100 +
Math.Abs(o.Weight.ToOpenTypeWeight() - FontWeights.Normal.ToOpenTypeWeight()) +
(o.Style == FontStyles.Normal ? 0 : o.Style == FontStyles.Oblique ? 1 : 2) * 1000;
}).FirstOrDefault() ?? family.FamilyTypefaces.FirstOrDefault();
}
public static FamilyTypeface ConvertFromInvariantStringsOrNormal(this FontFamily family, string style, string weight, string stretch)
{
var styleObj = GetFontStyleFromInvariantStringOrNormal(style);
var weightObj = GetFontWeightFromInvariantStringOrNormal(weight);
var stretchObj = GetFontStretchFromInvariantStringOrNormal(stretch);
return family.FamilyTypefaces.FirstOrDefault(o => o.Style == styleObj && o.Weight == weightObj && o.Stretch == stretchObj)
?? family.ChooseRegularFamilyTypeface();
}
}
}

View File

@@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows;
using Wox.Core.Plugin;
using Wox.Infrastructure;
using Wox.Infrastructure.Logger;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin;
namespace Wox.Core.Resource
{
public class Internationalization
{
public Settings Settings { get; set; }
private const string Folder = "Languages";
private const string DefaultFile = "en.xaml";
private const string Extension = ".xaml";
private readonly List<string> _languageDirectories = new List<string>();
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
public Internationalization()
{
AddPluginLanguageDirectories();
LoadDefaultLanguage();
// we don't want to load /Languages/en.xaml twice
// so add wox language directory after load plugin language files
AddWoxLanguageDirectory();
}
private void AddWoxLanguageDirectory()
{
var directory = Path.Combine(Constant.ProgramDirectory, Folder);
_languageDirectories.Add(directory);
}
private void AddPluginLanguageDirectories()
{
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
{
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
var dir = Path.GetDirectoryName(location);
if (dir != null)
{
var pluginThemeDirectory = Path.Combine(dir, Folder);
_languageDirectories.Add(pluginThemeDirectory);
}
else
{
Log.Error($"|Internationalization.AddPluginLanguageDirectories|Can't find plugin path <{location}> for <{plugin.Metadata.Name}>");
}
}
}
private void LoadDefaultLanguage()
{
LoadLanguage(AvailableLanguages.English);
_oldResources.Clear();
}
public void ChangeLanguage(string languageCode)
{
languageCode = languageCode.NonNull();
Language language = GetLanguageByLanguageCode(languageCode);
ChangeLanguage(language);
}
private Language GetLanguageByLanguageCode(string languageCode)
{
var lowercase = languageCode.ToLower();
var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase);
if (language == null)
{
Log.Error($"|Internationalization.GetLanguageByLanguageCode|Language code can't be found <{languageCode}>");
return AvailableLanguages.English;
}
else
{
return language;
}
}
public void ChangeLanguage(Language language)
{
language = language.NonNull();
Settings.Language = language.LanguageCode;
RemoveOldLanguageFiles();
if (language != AvailableLanguages.English)
{
LoadLanguage(language);
}
UpdatePluginMetadataTranslations();
}
private void RemoveOldLanguageFiles()
{
var dicts = Application.Current.Resources.MergedDictionaries;
foreach (var r in _oldResources)
{
dicts.Remove(r);
}
}
private void LoadLanguage(Language language)
{
var dicts = Application.Current.Resources.MergedDictionaries;
var filename = $"{language.LanguageCode}{Extension}";
var files = _languageDirectories
.Select(d => LanguageFile(d, filename))
.Where(f => !string.IsNullOrEmpty(f))
.ToArray();
if (files.Length > 0)
{
foreach (var f in files)
{
var r = new ResourceDictionary
{
Source = new Uri(f, UriKind.Absolute)
};
dicts.Add(r);
_oldResources.Add(r);
}
}
}
public List<Language> LoadAvailableLanguages()
{
return AvailableLanguages.GetAvailableLanguages();
}
public string GetTranslation(string key)
{
var translation = Application.Current.TryFindResource(key);
if (translation is string)
{
return translation.ToString();
}
else
{
Log.Error($"|Internationalization.GetTranslation|No Translation for key {key}");
return $"No Translation for key {key}";
}
}
private void UpdatePluginMetadataTranslations()
{
foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>())
{
var pluginI18N = p.Plugin as IPluginI18n;
if (pluginI18N == null) return;
try
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
}
catch (Exception e)
{
Log.Exception($"|Internationalization.UpdatePluginMetadataTranslations|Failed for <{p.Metadata.Name}>", e);
}
}
}
public string LanguageFile(string folder, string language)
{
if (Directory.Exists(folder))
{
string path = Path.Combine(folder, language);
if (File.Exists(path))
{
return path;
}
else
{
Log.Error($"|Internationalization.LanguageFile|Language path can't be found <{path}>");
string english = Path.Combine(folder, DefaultFile);
if (File.Exists(english))
{
return english;
}
else
{
Log.Error($"|Internationalization.LanguageFile|Default English Language path can't be found <{path}>");
return string.Empty;
}
}
}
else
{
return string.Empty;
}
}
}
}

View File

@@ -0,0 +1,26 @@
namespace Wox.Core.Resource
{
public static class InternationalizationManager
{
private static Internationalization instance;
private static object syncObject = new object();
public static Internationalization Instance
{
get
{
if (instance == null)
{
lock (syncObject)
{
if (instance == null)
{
instance = new Internationalization();
}
}
}
return instance;
}
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Wox.Core.Resource
{
public class Language
{
public Language(string code, string display)
{
LanguageCode = code;
Display = display;
}
/// <summary>
/// E.g. En or Zh-CN
/// </summary>
public string LanguageCode { get; set; }
public string Display { get; set; }
}
}

View File

@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Markup;
using System.Windows.Media;
using Wox.Infrastructure;
using Wox.Infrastructure.Logger;
using Wox.Infrastructure.UserSettings;
namespace Wox.Core.Resource
{
public class Theme
{
private readonly List<string> _themeDirectories = new List<string>();
private ResourceDictionary _oldResource;
private string _oldTheme;
public Settings Settings { get; set; }
private const string Folder = "Themes";
private const string Extension = ".xaml";
private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder);
private string UserDirectoryPath => Path.Combine(Constant.DataDirectory, Folder);
public Theme()
{
_themeDirectories.Add(DirectoryPath);
_themeDirectories.Add(UserDirectoryPath);
MakesureThemeDirectoriesExist();
var dicts = Application.Current.Resources.MergedDictionaries;
_oldResource = dicts.First(d =>
{
var p = d.Source.AbsolutePath;
var dir = Path.GetDirectoryName(p).NonNull();
var info = new DirectoryInfo(dir);
var f = info.Name;
var e = Path.GetExtension(p);
var found = f == Folder && e == Extension;
return found;
});
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
private void MakesureThemeDirectoriesExist()
{
foreach (string dir in _themeDirectories)
{
if (!Directory.Exists(dir))
{
try
{
Directory.CreateDirectory(dir);
}
catch (Exception e)
{
Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e);
}
}
}
}
public bool ChangeTheme(string theme)
{
const string defaultTheme = "Dark";
string path = GetThemePath(theme);
try
{
if (string.IsNullOrEmpty(path))
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
Settings.Theme = theme;
var dicts = Application.Current.Resources.MergedDictionaries;
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
if (_oldTheme != theme || theme == defaultTheme)
{
dicts.Remove(_oldResource);
var newResource = GetResourceDictionary();
dicts.Add(newResource);
_oldResource = newResource;
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
}
catch (DirectoryNotFoundException e)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
if (theme != defaultTheme)
{
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme));
ChangeTheme(defaultTheme);
}
return false;
}
catch (XamlParseException e)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
if (theme != defaultTheme)
{
MessageBox.Show(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme));
ChangeTheme(defaultTheme);
}
return false;
}
return true;
}
public ResourceDictionary GetResourceDictionary()
{
var uri = GetThemePath(Settings.Theme);
var dict = new ResourceDictionary
{
Source = new Uri(uri, UriKind.Absolute)
};
Style queryBoxStyle = dict["QueryBoxStyle"] as Style;
if (queryBoxStyle != null)
{
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, new FontFamily(Settings.QueryBoxFont)));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.QueryBoxFontStyle)));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.QueryBoxFontWeight)));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.QueryBoxFontStretch)));
}
Style resultItemStyle = dict["ItemTitleStyle"] as Style;
Style resultSubItemStyle = dict["ItemSubTitleStyle"] as Style;
Style resultItemSelectedStyle = dict["ItemTitleSelectedStyle"] as Style;
Style resultSubItemSelectedStyle = dict["ItemSubTitleSelectedStyle"] as Style;
if (resultItemStyle != null && resultSubItemStyle != null && resultSubItemSelectedStyle != null && resultItemSelectedStyle != null)
{
Setter fontFamily = new Setter(TextBlock.FontFamilyProperty, new FontFamily(Settings.ResultFont));
Setter fontStyle = new Setter(TextBlock.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.ResultFontStyle));
Setter fontWeight = new Setter(TextBlock.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.ResultFontWeight));
Setter fontStretch = new Setter(TextBlock.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.ResultFontStretch));
Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch };
Array.ForEach(new[] { resultItemStyle, resultSubItemStyle, resultItemSelectedStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p)));
}
return dict;
}
public List<string> LoadAvailableThemes()
{
List<string> themes = new List<string>();
foreach (var themeDirectory in _themeDirectories)
{
themes.AddRange(
Directory.GetFiles(themeDirectory)
.Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml"))
.ToList());
}
return themes.OrderBy(o => o).ToList();
}
private string GetThemePath(string themeName)
{
foreach (string themeDirectory in _themeDirectories)
{
string path = Path.Combine(themeDirectory, themeName + Extension);
if (File.Exists(path))
{
return path;
}
}
return string.Empty;
}
#region Blur Handling
/*
Found on https://github.com/riverar/sample-win10-aeroglass
*/
private enum AccentState
{
ACCENT_DISABLED = 0,
ACCENT_ENABLE_GRADIENT = 1,
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
ACCENT_ENABLE_BLURBEHIND = 3,
ACCENT_INVALID_STATE = 4
}
[StructLayout(LayoutKind.Sequential)]
private struct AccentPolicy
{
public AccentState AccentState;
public int AccentFlags;
public int GradientColor;
public int AnimationId;
}
[StructLayout(LayoutKind.Sequential)]
private struct WindowCompositionAttributeData
{
public WindowCompositionAttribute Attribute;
public IntPtr Data;
public int SizeOfData;
}
private enum WindowCompositionAttribute
{
WCA_ACCENT_POLICY = 19
}
[DllImport("user32.dll")]
private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
/// <summary>
/// Sets the blur for a window via SetWindowCompositionAttribute
/// </summary>
public void SetBlurForWindow()
{
// Exception of FindResource can't be cathed if global exception handle is set
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);
}
}
}
private void SetWindowAccent(Window w, AccentState state)
{
var windowHelper = new WindowInteropHelper(w);
var accent = new AccentPolicy { AccentState = state };
var accentStructSize = Marshal.SizeOf(accent);
var accentPtr = Marshal.AllocHGlobal(accentStructSize);
Marshal.StructureToPtr(accent, accentPtr, false);
var data = new WindowCompositionAttributeData
{
Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
SizeOfData = accentStructSize,
Data = accentPtr
};
SetWindowCompositionAttribute(windowHelper.Handle, ref data);
Marshal.FreeHGlobal(accentPtr);
}
#endregion
}
}

View File

@@ -0,0 +1,26 @@
namespace Wox.Core.Resource
{
public class ThemeManager
{
private static Theme instance;
private static object syncObject = new object();
public static Theme Instance
{
get
{
if (instance == null)
{
lock (syncObject)
{
if (instance == null)
{
instance = new Theme();
}
}
}
return instance;
}
}
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using JetBrains.Annotations;
using Squirrel;
using Newtonsoft.Json;
using Wox.Core.Resource;
using Wox.Plugin.SharedCommands;
using Wox.Infrastructure;
using Wox.Infrastructure.Http;
using Wox.Infrastructure.Logger;
using System.IO;
namespace Wox.Core
{
public class Updater
{
public string GitHubRepository { get; }
public Updater(string gitHubRepository)
{
GitHubRepository = gitHubRepository;
}
public async Task UpdateApp(bool silentIfLatestVersion = true)
{
UpdateManager updateManager;
UpdateInfo newUpdateInfo;
try
{
updateManager = await GitHubUpdateManager(GitHubRepository);
}
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException)
{
Log.Exception($"|Updater.UpdateApp|Please check your connection and proxy settings to api.github.com.", e);
return;
}
try
{
// UpdateApp CheckForUpdate will return value only if the app is squirrel installed
newUpdateInfo = await updateManager.CheckForUpdate().NonNull();
}
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException)
{
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to api.github.com.", e);
updateManager.Dispose();
return;
}
var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString());
var currentVersion = Version.Parse(Constant.Version);
Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>");
if (newReleaseVersion <= currentVersion)
{
if (!silentIfLatestVersion)
MessageBox.Show("You already have the latest Wox version");
updateManager.Dispose();
return;
}
try
{
await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply);
}
catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException)
{
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
updateManager.Dispose();
return;
}
await updateManager.ApplyReleases(newUpdateInfo);
if (Constant.IsPortableMode)
{
var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{Constant.PortableFolderName}";
FilesFolders.Copy(Constant.PortableDataPath, targetDestination);
if (!FilesFolders.VerifyBothFolderFilesEqual(Constant.PortableDataPath, targetDestination))
MessageBox.Show(string.Format("Wox was not able to move your user profile data to the new update version. Please manually" +
"move your profile data folder from {0} to {1}", Constant.PortableDataPath, targetDestination));
}
else
{
await updateManager.CreateUninstallerRegistryEntry();
}
var newVersionTips = NewVersinoTips(newReleaseVersion.ToString());
MessageBox.Show(newVersionTips);
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
// always dispose UpdateManager
updateManager.Dispose();
}
[UsedImplicitly]
private class GithubRelease
{
[JsonProperty("prerelease")]
public bool Prerelease { get; [UsedImplicitly] set; }
[JsonProperty("published_at")]
public DateTime PublishedAt { get; [UsedImplicitly] set; }
[JsonProperty("html_url")]
public string HtmlUrl { get; [UsedImplicitly] set; }
}
/// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
private async Task<UpdateManager> GitHubUpdateManager(string repository)
{
var uri = new Uri(repository);
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
var json = await Http.Get(api);
var releases = JsonConvert.DeserializeObject<List<GithubRelease>>(json);
var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First();
var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/");
var client = new WebClient { Proxy = Http.WebProxy() };
var downloader = new FileDownloader(client);
var manager = new UpdateManager(latestUrl, urlDownloader: downloader);
return manager;
}
public string NewVersinoTips(string version)
{
var translater = InternationalizationManager.Instance;
var tips = string.Format(translater.GetTranslation("newVersionTips"), version);
return tips;
}
}
}

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B749F0DB-8E75-47DB-9E5E-265D16D0C0D2}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Wox.Core</RootNamespace>
<AssemblyName>Wox.Core</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\Output\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\Output\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SolutionAssemblyInfo.cs">
<Link>Properties\SolutionAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Plugin\ExecutablePlugin.cs" />
<Compile Include="Plugin\PluginsLoader.cs" />
<Compile Include="Plugin\QueryBuilder.cs" />
<Compile Include="Updater.cs" />
<Compile Include="Resource\AvailableLanguages.cs" />
<Compile Include="Resource\Internationalization.cs" />
<Compile Include="Resource\InternationalizationManager.cs" />
<Compile Include="Resource\Language.cs" />
<Compile Include="Resource\ThemeManager.cs" />
<Compile Include="Plugin\PluginInstaller.cs" />
<Compile Include="Plugin\JsonRPCPlugin.cs" />
<Compile Include="Plugin\JsonPRCModel.cs" />
<Compile Include="Plugin\PluginConfig.cs" />
<Compile Include="Plugin\PluginManager.cs" />
<Compile Include="Plugin\PythonPlugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Resource\FontHelper.cs" />
<Compile Include="Resource\Theme.cs" />
</ItemGroup>
<ItemGroup>
<None Include="Plugin\README.md" />
<None Include="README.md" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wox.Infrastructure\Wox.Infrastructure.csproj">
<Project>{4fd29318-a8ab-4d8f-aa47-60bc241b8da3}</Project>
<Name>Wox.Infrastructure</Name>
</ProjectReference>
<ProjectReference Include="..\Wox.Plugin\Wox.Plugin.csproj">
<Project>{8451ecdd-2ea4-4966-bb0a-7bbc40138e80}</Project>
<Name>Wox.Plugin</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="FodyWeavers.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fody">
<Version>1.29.2</Version>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="JetBrains.Annotations">
<Version>10.3.0</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>9.0.1</Version>
</PackageReference>
<PackageReference Include="PropertyChanged.Fody">
<Version>1.51.0</Version>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="squirrel.windows">
<Version>1.5.2</Version>
</PackageReference>
<PackageReference Include="System.Runtime">
<Version>4.0.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>