diff --git a/Plugins/Wox.Plugin.ControlPanel/Main.cs b/Plugins/Wox.Plugin.ControlPanel/Main.cs index 0e0a8898d6..77b57fe865 100644 --- a/Plugins/Wox.Plugin.ControlPanel/Main.cs +++ b/Plugins/Wox.Plugin.ControlPanel/Main.cs @@ -27,7 +27,6 @@ namespace Wox.Plugin.ControlPanel Directory.CreateDirectory(iconFolder); } - foreach (ControlPanelItem item in controlPanelItems) { if (!File.Exists(iconFolder + item.GUID + fileType) && item.Icon != null) @@ -43,7 +42,10 @@ namespace Wox.Plugin.ControlPanel foreach (var item in controlPanelItems) { - item.Score = Score(item, query.Search); + var titleMatch = StringMatcher.FuzzySearch(query.Search, item.LocalizedString); + var subTitleMatch = StringMatcher.FuzzySearch(query.Search, item.InfoTip); + + item.Score = Math.Max(titleMatch.Score, subTitleMatch.Score); if (item.Score > 0) { var result = new Result @@ -66,6 +68,16 @@ namespace Wox.Plugin.ControlPanel return true; } }; + + if (item.Score == titleMatch.Score) + { + result.TitleHighlightData = titleMatch.MatchData; + } + else + { + result.SubTitleHighlightData = subTitleMatch.MatchData; + } + results.Add(result); } } @@ -74,26 +86,6 @@ namespace Wox.Plugin.ControlPanel return panelItems; } - private int Score(ControlPanelItem item, string query) - { - var scores = new List {0}; - if (!string.IsNullOrEmpty(item.LocalizedString)) - { - var score1 = StringMatcher.FuzzySearch(query, item.LocalizedString).ScoreAfterSearchPrecisionFilter(); - var score2 = StringMatcher.ScoreForPinyin(item.LocalizedString, query); - scores.Add(score1); - scores.Add(score2); - } - if (!string.IsNullOrEmpty(item.InfoTip)) - { - var score1 = StringMatcher.FuzzySearch(query, item.InfoTip).ScoreAfterSearchPrecisionFilter(); - var score2 = StringMatcher.ScoreForPinyin(item.InfoTip, query); - scores.Add(score1); - scores.Add(score2); - } - return scores.Max(); - } - public string GetTranslatedPluginTitle() { return context.API.GetTranslation("wox_plugin_controlpanel_plugin_name"); diff --git a/Plugins/Wox.Plugin.Everything/Main.cs b/Plugins/Wox.Plugin.Everything/Main.cs index 7969a079fe..4188f89dda 100644 --- a/Plugins/Wox.Plugin.Everything/Main.cs +++ b/Plugins/Wox.Plugin.Everything/Main.cs @@ -55,6 +55,7 @@ namespace Wox.Plugin.Everything r.Title = Path.GetFileName(path); r.SubTitle = path; r.IcoPath = path; + r.TitleHighlightData = StringMatcher.FuzzySearch(keyword, Path.GetFileName(path)).MatchData; r.Action = c => { bool hide; @@ -78,6 +79,7 @@ namespace Wox.Plugin.Everything return hide; }; r.ContextData = s; + r.SubTitleHighlightData = StringMatcher.FuzzySearch(keyword, path).MatchData; results.Add(r); } } diff --git a/Plugins/Wox.Plugin.Folder/ContextMenuLoader.cs b/Plugins/Wox.Plugin.Folder/ContextMenuLoader.cs new file mode 100644 index 0000000000..130fd7007e --- /dev/null +++ b/Plugins/Wox.Plugin.Folder/ContextMenuLoader.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using System.Windows; +using Wox.Infrastructure.Logger; +using Wox.Infrastructure.Image; +using Wox.Plugin.SharedCommands; + +namespace Wox.Plugin.Folder +{ + internal class ContextMenuLoader : IContextMenu + { + private readonly PluginInitContext _context; + + public ContextMenuLoader(PluginInitContext context) + { + _context = context; + } + + public List LoadContextMenus(Result selectedResult) + { + var contextMenus = new List(); + if (selectedResult.ContextData is SearchResult record) + { + if (record.Type == ResultType.File) + { + contextMenus.Add(CreateOpenWithEditorResult(record)); + contextMenus.Add(CreateOpenContainingFolderResult(record)); + } + + var icoPath = (record.Type == ResultType.File) ? Main.FileImagePath : Main.FolderImagePath; + var fileOrFolder = (record.Type == ResultType.File) ? "file" : "folder"; + contextMenus.Add(new Result + { + Title = "Copy path", + SubTitle = $"Copy the current {fileOrFolder} path to clipboard", + Action = (context) => + { + try + { + Clipboard.SetText(record.FullPath); + return true; + } + catch (Exception e) + { + var message = "Fail to set text in clipboard"; + LogException(message, e); + _context.API.ShowMsg(message); + return false; + } + }, + IcoPath = Main.CopyImagePath + }); + + contextMenus.Add(new Result + { + Title = $"Copy {fileOrFolder}", + SubTitle = $"Copy the {fileOrFolder} to clipboard", + Action = (context) => + { + try + { + Clipboard.SetFileDropList(new System.Collections.Specialized.StringCollection { record.FullPath }); + return true; + } + catch (Exception e) + { + var message = $"Fail to set {fileOrFolder} in clipboard"; + LogException(message, e); + _context.API.ShowMsg(message); + return false; + } + + }, + IcoPath = icoPath + }); + + if (record.Type == ResultType.File || record.Type == ResultType.Folder) + contextMenus.Add(new Result + { + Title = $"Delete {fileOrFolder}", + SubTitle = $"Delete the selected {fileOrFolder}", + Action = (context) => + { + try + { + if (record.Type == ResultType.File) + File.Delete(record.FullPath); + else + Directory.Delete(record.FullPath); + } + catch(Exception e) + { + var message = $"Fail to delete {fileOrFolder} at {record.FullPath}"; + LogException(message, e); + _context.API.ShowMsg(message); + return false; + } + + return true; + }, + IcoPath = Main.DeleteFileFolderImagePath + }); + + if (record.Type == ResultType.File && CanRunAsDifferentUser(record.FullPath)) + contextMenus.Add(new Result + { + Title = "Run as different user", + Action = (context) => + { + try + { + Task.Run(()=> ShellCommand.RunAsDifferentUser(record.FullPath.SetProcessStartInfo())); + } + catch (FileNotFoundException e) + { + var name = "Plugin: Folder"; + var message = $"File not found: {e.Message}"; + _context.API.ShowMsg(name, message); + } + + return true; + }, + IcoPath = "Images/user.png" + }); + } + + return contextMenus; + } + + private Result CreateOpenContainingFolderResult(SearchResult record) + { + return new Result + { + Title = "Open containing folder", + Action = _ => + { + try + { + Process.Start("explorer.exe", $" /select,\"{record.FullPath}\""); + } + catch(Exception e) + { + var message = $"Fail to open file at {record.FullPath}"; + LogException(message, e); + _context.API.ShowMsg(message); + return false; + } + + return true; + }, + IcoPath = Main.FolderImagePath + }; + } + + + private Result CreateOpenWithEditorResult(SearchResult record) + { + string editorPath = "notepad.exe"; // TODO add the ability to create a custom editor + + var name = "Open With Editor: " + Path.GetFileNameWithoutExtension(editorPath); + return new Result + { + Title = name, + Action = _ => + { + try + { + Process.Start(editorPath, record.FullPath); + return true; + } + catch (Exception e) + { + var message = $"Fail to editor for file at {record.FullPath}"; + LogException(message, e); + _context.API.ShowMsg(message); + return false; + } + }, + IcoPath = editorPath + }; + } + + public void LogException(string message, Exception e) + { + Log.Exception($"|Wox.Plugin.Folder.ContextMenu|{message}", e); + } + + private bool CanRunAsDifferentUser(string path) + { + switch(Path.GetExtension(path)) + { + case ".exe": + case ".bat": + return true; + + default: + return false; + + } + } + } + + public class SearchResult + { + public string FullPath { get; set; } + public ResultType Type { get; set; } + } + + public enum ResultType + { + Volume, + Folder, + File + } +} \ No newline at end of file diff --git a/Plugins/Wox.Plugin.Folder/Images/deletefilefolder.png b/Plugins/Wox.Plugin.Folder/Images/deletefilefolder.png new file mode 100644 index 0000000000..024cc92915 Binary files /dev/null and b/Plugins/Wox.Plugin.Folder/Images/deletefilefolder.png differ diff --git a/Plugins/Wox.Plugin.Folder/Images/file.png b/Plugins/Wox.Plugin.Folder/Images/file.png new file mode 100644 index 0000000000..36156767a6 Binary files /dev/null and b/Plugins/Wox.Plugin.Folder/Images/file.png differ diff --git a/Plugins/Wox.Plugin.Folder/Images/user.png b/Plugins/Wox.Plugin.Folder/Images/user.png new file mode 100644 index 0000000000..2d45c1ee91 Binary files /dev/null and b/Plugins/Wox.Plugin.Folder/Images/user.png differ diff --git a/Plugins/Wox.Plugin.Folder/Main.cs b/Plugins/Wox.Plugin.Folder/Main.cs index e8f33a951b..d6b4a86654 100644 --- a/Plugins/Wox.Plugin.Folder/Main.cs +++ b/Plugins/Wox.Plugin.Folder/Main.cs @@ -1,27 +1,33 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; +using Wox.Infrastructure; using Wox.Infrastructure.Storage; namespace Wox.Plugin.Folder { - public class Main : IPlugin, ISettingProvider, IPluginI18n, ISavable + public class Main : IPlugin, ISettingProvider, IPluginI18n, ISavable, IContextMenu { + public const string FolderImagePath = "Images\\folder.png"; + public const string FileImagePath = "Images\\file.png"; + public const string DeleteFileFolderImagePath = "Images\\deletefilefolder.png"; + public const string CopyImagePath = "Images\\copy.png"; + private static List _driverNames; private PluginInitContext _context; private readonly Settings _settings; private readonly PluginJsonStorage _storage; + private IContextMenu _contextMenuLoader; public Main() { _storage = new PluginJsonStorage(); _settings = _storage.Load(); - InitialDriverList(); } public void Save() @@ -37,54 +43,21 @@ namespace Wox.Plugin.Folder public void Init(PluginInitContext context) { _context = context; - + _contextMenuLoader = new ContextMenuLoader(context); + InitialDriverList(); } public List Query(Query query) { + var results = GetUserFolderResults(query); + string search = query.Search.ToLower(); - - List userFolderLinks = _settings.FolderLinks.Where( - x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase)).ToList(); - List results = - userFolderLinks.Select( - item => new Result() - { - Title = item.Nickname, - IcoPath = item.Path, - SubTitle = "Ctrl + Enter to open the directory", - Action = c => - { - if (c.SpecialKeyState.CtrlPressed) - { - try - { - Process.Start(item.Path); - return true; - } - catch (Exception ex) - { - MessageBox.Show(ex.Message, "Could not start " + item.Path); - return false; - } - } - _context.API.ChangeQuery($"{query.ActionKeyword} {item.Path}{(item.Path.EndsWith("\\") ? "" : "\\")}"); - return false; - }, - ContextData = item, - }).ToList(); - - if (_driverNames != null && !_driverNames.Any(search.StartsWith)) + if (!IsDriveOrSharedFolder(search)) return results; - //if (!input.EndsWith("\\")) - //{ - // //"c:" means "the current directory on the C drive" whereas @"c:\" means "root of the C drive" - // input = input + "\\"; - //} results.AddRange(QueryInternal_Directory_Exists(query)); - // todo temp hack for scores + // todo why was this hack here? foreach (var result in results) { result.Score += 10; @@ -92,12 +65,77 @@ namespace Wox.Plugin.Folder return results; } + + private static bool IsDriveOrSharedFolder(string search) + { + if (search.StartsWith(@"\\")) + { // share folder + return true; + } + + if (_driverNames != null && _driverNames.Any(search.StartsWith)) + { // normal drive letter + return true; + } + + if (_driverNames == null && search.Length > 2 && char.IsLetter(search[0]) && search[1] == ':') + { // when we don't have the drive letters we can try... + return true; // we don't know so let's give it the possibility + } + + return false; + } + + private Result CreateFolderResult(string title, string path, Query query) + { + return new Result + { + Title = title, + IcoPath = path, + SubTitle = "Ctrl + Enter to open the directory", + TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, + Action = c => + { + if (c.SpecialKeyState.CtrlPressed) + { + try + { + Process.Start(path); + return true; + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Could not start " + path); + return false; + } + } + + string changeTo = path.EndsWith("\\") ? path : path + "\\"; + _context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ? + changeTo : + query.ActionKeyword + " " + changeTo); + return false; + }, + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path } + }; + } + + private List GetUserFolderResults(Query query) + { + string search = query.Search.ToLower(); + var userFolderLinks = _settings.FolderLinks.Where( + x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase)); + var results = userFolderLinks.Select(item => + CreateFolderResult(item.Nickname, item.Path, query)).ToList(); + return results; + } + private void InitialDriverList() { if (_driverNames == null) { _driverNames = new List(); - DriveInfo[] allDrives = DriveInfo.GetDrives(); + var allDrives = DriveInfo.GetDrives(); foreach (DriveInfo driver in allDrives) { _driverNames.Add(driver.Name.ToLower().TrimEnd('\\')); @@ -105,117 +143,135 @@ namespace Wox.Plugin.Folder } } + private static readonly char[] _specialSearchChars = new char[] + { + '?', '*', '>' + }; + private List QueryInternal_Directory_Exists(Query query) { - var search = query.Search.ToLower(); + var search = query.Search; var results = new List(); - + var hasSpecial = search.IndexOfAny(_specialSearchChars) >= 0; string incompleteName = ""; - if (!Directory.Exists(search + "\\")) + if (hasSpecial || !Directory.Exists(search + "\\")) { - //if the last component of the path is incomplete, - //then make auto complete for it. + // if folder doesn't exist, we want to take the last part and use it afterwards to help the user + // find the right folder. int index = search.LastIndexOf('\\'); if (index > 0 && index < (search.Length - 1)) { - incompleteName = search.Substring(index + 1); - incompleteName = incompleteName.ToLower(); + incompleteName = search.Substring(index + 1).ToLower(); search = search.Substring(0, index + 1); if (!Directory.Exists(search)) + { return results; + } } else + { return results; + } } else { + // folder exist, add \ at the end of doesn't exist if (!search.EndsWith("\\")) + { search += "\\"; + } } - string firstResult = "Open current directory"; + results.Add(CreateOpenCurrentFolderResult(incompleteName, search)); + + var searchOption = SearchOption.TopDirectoryOnly; + incompleteName += "*"; + + // give the ability to search all folder when starting with > + if (incompleteName.StartsWith(">")) + { + searchOption = SearchOption.AllDirectories; + incompleteName = incompleteName.Substring(1); + } + + try + { + // search folder and add results + var directoryInfo = new DirectoryInfo(search); + var fileSystemInfos = directoryInfo.GetFileSystemInfos(incompleteName, searchOption); + + foreach (var fileSystemInfo in fileSystemInfos) + { + if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue; + + var result = + fileSystemInfo is DirectoryInfo + ? CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName, query) + : CreateFileResult(fileSystemInfo.FullName, query); + results.Add(result); + } + } + catch (Exception e) + { + if (e is UnauthorizedAccessException || e is ArgumentException) + { + results.Add(new Result { Title = e.Message, Score = 501 }); + + return results; + } + + throw; + } + + return results; + } + + private static Result CreateFileResult(string filePath, Query query) + { + var result = new Result + { + Title = Path.GetFileName(filePath), + IcoPath = filePath, + TitleHighlightData = StringMatcher.FuzzySearch(query.Search, Path.GetFileName(filePath)).MatchData, + Action = c => + { + try + { + Process.Start(filePath); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Could not start " + filePath); + } + + return true; + }, + ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath} + }; + return result; + } + + private static Result CreateOpenCurrentFolderResult(string incompleteName, string search) + { + var firstResult = "Open current directory"; if (incompleteName.Length > 0) firstResult = "Open " + search; - results.Add(new Result + + var folderName = search.TrimEnd('\\').Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None).Last(); + + return new Result { Title = firstResult, + SubTitle = $"Use > to search files and subfolders within {folderName}, " + + $"* to search for file extensions in {folderName} or both >* to combine the search", IcoPath = search, - Score = 10000, + Score = 500, Action = c => { Process.Start(search); return true; } - }); - - //Add children directories - DirectoryInfo[] dirs = new DirectoryInfo(search).GetDirectories(); - foreach (DirectoryInfo dir in dirs) - { - if ((dir.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue; - - if (incompleteName.Length != 0 && !dir.Name.ToLower().StartsWith(incompleteName)) - continue; - DirectoryInfo dirCopy = dir; - var result = new Result - { - Title = dir.Name, - IcoPath = dir.FullName, - SubTitle = "Ctrl + Enter to open the directory", - Action = c => - { - if (c.SpecialKeyState.CtrlPressed) - { - try - { - Process.Start(dirCopy.FullName); - return true; - } - catch (Exception ex) - { - MessageBox.Show(ex.Message, "Could not start " + dirCopy.FullName); - return false; - } - } - _context.API.ChangeQuery($"{query.ActionKeyword} {dirCopy.FullName}\\"); - return false; - } - }; - - results.Add(result); - } - - //Add children files - FileInfo[] files = new DirectoryInfo(search).GetFiles(); - foreach (FileInfo file in files) - { - if ((file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue; - if (incompleteName.Length != 0 && !file.Name.ToLower().StartsWith(incompleteName)) - continue; - string filePath = file.FullName; - var result = new Result - { - Title = Path.GetFileName(filePath), - IcoPath = filePath, - Action = c => - { - try - { - Process.Start(filePath); - } - catch (Exception ex) - { - MessageBox.Show(ex.Message, "Could not start " + filePath); - } - - return true; - } - }; - - results.Add(result); - } - - return results; + }; } public string GetTranslatedPluginTitle() @@ -227,5 +283,10 @@ namespace Wox.Plugin.Folder { return _context.API.GetTranslation("wox_plugin_folder_plugin_description"); } + + public List LoadContextMenus(Result selectedResult) + { + return _contextMenuLoader.LoadContextMenus(selectedResult); + } } } \ No newline at end of file diff --git a/Plugins/Wox.Plugin.Folder/Wox.Plugin.Folder.csproj b/Plugins/Wox.Plugin.Folder/Wox.Plugin.Folder.csproj index 6a05ebd26f..35573c9ca6 100644 --- a/Plugins/Wox.Plugin.Folder/Wox.Plugin.Folder.csproj +++ b/Plugins/Wox.Plugin.Folder/Wox.Plugin.Folder.csproj @@ -58,6 +58,7 @@ Properties\SolutionAssemblyInfo.cs + @@ -80,6 +81,15 @@ PreserveNewest + + Always + + + PreserveNewest + + + Always + MSBuild:Compile Designer diff --git a/Plugins/Wox.Plugin.PluginManagement/Main.cs b/Plugins/Wox.Plugin.PluginManagement/Main.cs index 785ca3b1f3..1ee0fd7ae8 100644 --- a/Plugins/Wox.Plugin.PluginManagement/Main.cs +++ b/Plugins/Wox.Plugin.PluginManagement/Main.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; using Newtonsoft.Json; +using Wox.Infrastructure; using Wox.Infrastructure.Http; using Wox.Infrastructure.Logger; @@ -142,6 +143,8 @@ namespace Wox.Plugin.PluginManagement Title = r.name, SubTitle = r.description, IcoPath = "Images\\plugin.png", + TitleHighlightData = StringMatcher.FuzzySearch(query.SecondSearch, r.name).MatchData, + SubTitleHighlightData = StringMatcher.FuzzySearch(query.SecondSearch, r.description).MatchData, Action = c => { MessageBoxResult result = MessageBox.Show("Are you sure you wish to install the \'" + r.name + "\' plugin", @@ -191,6 +194,8 @@ namespace Wox.Plugin.PluginManagement Title = plugin.Name, SubTitle = plugin.Description, IcoPath = plugin.IcoPath, + TitleHighlightData = StringMatcher.FuzzySearch(query.SecondSearch, plugin.Name).MatchData, + SubTitleHighlightData = StringMatcher.FuzzySearch(query.SecondSearch, plugin.Description).MatchData, Action = e => { UnInstallPlugin(plugin); diff --git a/Plugins/Wox.Plugin.Program/Images/user.png b/Plugins/Wox.Plugin.Program/Images/user.png new file mode 100644 index 0000000000..2d45c1ee91 Binary files /dev/null and b/Plugins/Wox.Plugin.Program/Images/user.png differ diff --git a/Plugins/Wox.Plugin.Program/Languages/en.xaml b/Plugins/Wox.Plugin.Program/Languages/en.xaml index bb218887c4..b7202d590f 100644 --- a/Plugins/Wox.Plugin.Program/Languages/en.xaml +++ b/Plugins/Wox.Plugin.Program/Languages/en.xaml @@ -31,6 +31,7 @@ Successfully updated file suffixes File suffixes can't be empty + Run As Different User Run As Administrator Open containing folder Disable this program from displaying diff --git a/Plugins/Wox.Plugin.Program/Main.cs b/Plugins/Wox.Plugin.Program/Main.cs index e165a10826..e99298c788 100644 --- a/Plugins/Wox.Plugin.Program/Main.cs +++ b/Plugins/Wox.Plugin.Program/Main.cs @@ -24,7 +24,7 @@ namespace Wox.Plugin.Program private static PluginInitContext _context; private static BinaryStorage _win32Storage; - private static BinaryStorage _uwpStorage; + private static BinaryStorage _uwpStorage; private readonly PluginJsonStorage _settingsStorage; public Main() @@ -41,7 +41,7 @@ namespace Wox.Plugin.Program }); Log.Info($"|Wox.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); Log.Info($"|Wox.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); - + var a = Task.Run(() => { if (IsStartupIndexProgramsRequired || !_win32s.Any()) @@ -68,19 +68,25 @@ namespace Wox.Plugin.Program public List Query(Query query) { + Win32[] win32; + UWP.Application[] uwps; + lock (IndexLock) - { - var results1 = _win32s.AsParallel() - .Where(p => p.Enabled) - .Select(p => p.Result(query.Search, _context.API)); - - var results2 = _uwps.AsParallel() - .Where(p => p.Enabled) - .Select(p => p.Result(query.Search, _context.API)); - - var result = results1.Concat(results2).Where(r => r.Score > 0).ToList(); - return result; + { // just take the reference inside the lock to eliminate query time issues. + win32 = _win32s; + uwps = _uwps; } + + var results1 = win32.AsParallel() + .Where(p => p.Enabled) + .Select(p => p.Result(query.Search, _context.API)); + + var results2 = uwps.AsParallel() + .Where(p => p.Enabled) + .Select(p => p.Result(query.Search, _context.API)); + + var result = results1.Concat(results2).Where(r => r != null && r.Score > 0).ToList(); + return result; } public void Init(PluginInitContext context) @@ -111,14 +117,14 @@ namespace Wox.Plugin.Program public static void IndexPrograms() { - var t1 = Task.Run(()=>IndexWin32Programs()); + var t1 = Task.Run(() => IndexWin32Programs()); - var t2 = Task.Run(()=>IndexUWPPrograms()); + var t2 = Task.Run(() => IndexUWPPrograms()); Task.WaitAll(t1, t2); _settings.LastIndexTime = DateTime.Today; - } + } public Control CreateSettingPanel() { @@ -141,7 +147,7 @@ namespace Wox.Plugin.Program var program = selectedResult.ContextData as IProgram; if (program != null) { - menuOptions = program.ContextMenus(_context.API); + menuOptions = program.ContextMenus(_context.API); } menuOptions.Add( @@ -151,7 +157,7 @@ namespace Wox.Plugin.Program Action = c => { DisableProgram(program); - _context.API.ShowMsg(_context.API.GetTranslation("wox_plugin_program_disable_dlgtitle_success"), + _context.API.ShowMsg(_context.API.GetTranslation("wox_plugin_program_disable_dlgtitle_success"), _context.API.GetTranslation("wox_plugin_program_disable_dlgtitle_success_message")); return false; }, @@ -185,22 +191,19 @@ namespace Wox.Plugin.Program ); } - public static bool StartProcess(ProcessStartInfo info) + public static void StartProcess(Func runProcess, ProcessStartInfo info) { bool hide; try { - Process.Start(info); - hide = true; + runProcess(info); } catch (Exception) { var name = "Plugin: Program"; - var message = $"Can't start: {info.FileName}"; + var message = $"Unable to start: {info.FileName}"; _context.API.ShowMsg(name, message, string.Empty); - hide = false; } - return hide; } public void ReloadData() diff --git a/Plugins/Wox.Plugin.Program/Programs/UWP.cs b/Plugins/Wox.Plugin.Program/Programs/UWP.cs index 77d33e2e2f..35917cb312 100644 --- a/Plugins/Wox.Plugin.Program/Programs/UWP.cs +++ b/Plugins/Wox.Plugin.Program/Programs/UWP.cs @@ -35,7 +35,6 @@ namespace Wox.Plugin.Program.Programs public UWP(Package package) { - Location = package.InstalledLocation.Path; Name = package.Id.Name; FullName = package.Id.FullName; @@ -266,21 +265,25 @@ namespace Wox.Plugin.Program.Programs private int Score(string query) { - var score1 = StringMatcher.FuzzySearch(query, DisplayName).ScoreAfterSearchPrecisionFilter(); - var score2 = StringMatcher.ScoreForPinyin(DisplayName, query); - var score3 = StringMatcher.FuzzySearch(query, Description).ScoreAfterSearchPrecisionFilter(); - var score4 = StringMatcher.ScoreForPinyin(Description, query); - var score = new[] { score1, score2, score3, score4 }.Max(); + var displayNameMatch = StringMatcher.FuzzySearch(query, DisplayName); + var descriptionMatch = StringMatcher.FuzzySearch(query, Description); + var score = new[] { displayNameMatch.Score, descriptionMatch.Score }.Max(); return score; } public Result Result(string query, IPublicAPI api) { + var score = Score(query); + if (score <= 0) + { // no need to create result if score is 0 + return null; + } + var result = new Result { SubTitle = Package.Location, Icon = Logo, - Score = Score(query), + Score = score, ContextData = this, Action = e => { @@ -293,14 +296,18 @@ namespace Wox.Plugin.Program.Programs Description.Substring(0, DisplayName.Length) == DisplayName) { result.Title = Description; + result.TitleHighlightData = StringMatcher.FuzzySearch(query, Description).MatchData; } else if (!string.IsNullOrEmpty(Description)) { - result.Title = $"{DisplayName}: {Description}"; + var title = $"{DisplayName}: {Description}"; + result.Title = title; + result.TitleHighlightData = StringMatcher.FuzzySearch(query, title).MatchData; } else { result.Title = DisplayName; + result.TitleHighlightData = StringMatcher.FuzzySearch(query, DisplayName).MatchData; } return result; } @@ -312,11 +319,14 @@ namespace Wox.Plugin.Program.Programs new Result { Title = api.GetTranslation("wox_plugin_program_open_containing_folder"), + Action = _ => { - var hide = Main.StartProcess(new ProcessStartInfo(Package.Location)); - return hide; + Main.StartProcess(Process.Start, new ProcessStartInfo(Package.Location)); + + return true; }, + IcoPath = "Images/folder.png" } }; diff --git a/Plugins/Wox.Plugin.Program/Programs/Win32.cs b/Plugins/Wox.Plugin.Program/Programs/Win32.cs index dc11a56654..53ac2065cd 100644 --- a/Plugins/Wox.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Wox.Plugin.Program/Programs/Win32.cs @@ -6,10 +6,12 @@ using System.Linq; using System.Runtime.InteropServices; using System.Security; using System.Text; +using System.Threading.Tasks; using Microsoft.Win32; using Shell; using Wox.Infrastructure; using Wox.Plugin.Program.Logger; +using Wox.Plugin.SharedCommands; namespace Wox.Plugin.Program.Programs { @@ -33,23 +35,27 @@ namespace Wox.Plugin.Program.Programs private int Score(string query) { - var score1 = StringMatcher.FuzzySearch(query, Name).ScoreAfterSearchPrecisionFilter(); - var score2 = StringMatcher.ScoreForPinyin(Name, query); - var score3 = StringMatcher.FuzzySearch(query, Description).ScoreAfterSearchPrecisionFilter(); - var score4 = StringMatcher.ScoreForPinyin(Description, query); - var score5 = StringMatcher.FuzzySearch(query, ExecutableName).ScoreAfterSearchPrecisionFilter(); - var score = new[] { score1, score2, score3, score4, score5 }.Max(); + var nameMatch = StringMatcher.FuzzySearch(query, Name); + var descriptionMatch = StringMatcher.FuzzySearch(query, Description); + var executableNameMatch = StringMatcher.FuzzySearch(query, ExecutableName); + var score = new[] { nameMatch.Score, descriptionMatch.Score, executableNameMatch.Score }.Max(); return score; } public Result Result(string query, IPublicAPI api) { + var score = Score(query); + if (score <= 0) + { // no need to create result if this is zero + return null; + } + var result = new Result { SubTitle = FullPath, IcoPath = IcoPath, - Score = Score(query), + Score = score, ContextData = this, Action = e => { @@ -58,8 +64,10 @@ namespace Wox.Plugin.Program.Programs FileName = FullPath, WorkingDirectory = ParentDirectory }; - var hide = Main.StartProcess(info); - return hide; + + Main.StartProcess(Process.Start, info); + + return true; } }; @@ -67,14 +75,18 @@ namespace Wox.Plugin.Program.Programs Description.Substring(0, Name.Length) == Name) { result.Title = Description; + result.TitleHighlightData = StringMatcher.FuzzySearch(query, Description).MatchData; } else if (!string.IsNullOrEmpty(Description)) { - result.Title = $"{Name}: {Description}"; + var title = $"{Name}: {Description}"; + result.Title = title; + result.TitleHighlightData = StringMatcher.FuzzySearch(query, title).MatchData; } else { result.Title = Name; + result.TitleHighlightData = StringMatcher.FuzzySearch(query, Name).MatchData; } return result; @@ -85,6 +97,19 @@ namespace Wox.Plugin.Program.Programs { var contextMenus = new List { + new Result + { + Title = api.GetTranslation("wox_plugin_program_run_as_different_user"), + Action = _ => + { + var info = FullPath.SetProcessStartInfo(ParentDirectory); + + Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info)); + + return true; + }, + IcoPath = "Images/user.png" + }, new Result { Title = api.GetTranslation("wox_plugin_program_run_as_administrator"), @@ -96,8 +121,10 @@ namespace Wox.Plugin.Program.Programs WorkingDirectory = ParentDirectory, Verb = "runas" }; - var hide = Main.StartProcess(info); - return hide; + + Task.Run(() => Main.StartProcess(Process.Start, info)); + + return true; }, IcoPath = "Images/cmd.png" }, @@ -106,8 +133,9 @@ namespace Wox.Plugin.Program.Programs Title = api.GetTranslation("wox_plugin_program_open_containing_folder"), Action = _ => { - var hide = Main.StartProcess(new ProcessStartInfo(ParentDirectory)); - return hide; + Main.StartProcess(Process.Start, new ProcessStartInfo(ParentDirectory)); + + return true; }, IcoPath = "Images/folder.png" } @@ -193,7 +221,7 @@ namespace Wox.Plugin.Program.Programs catch (COMException e) { // C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\MiracastView.lnk always cause exception - ProgramLogger.LogException($"|Win32|LnkProgram|{path}"+ + ProgramLogger.LogException($"|Win32|LnkProgram|{path}" + "|Error caused likely due to trying to get the description of the program", e); program.Valid = false; diff --git a/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj b/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj index 850f972ca4..91dada0670 100644 --- a/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj +++ b/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj @@ -112,6 +112,9 @@ PreserveNewest + + Always + MSBuild:Compile Designer diff --git a/Plugins/Wox.Plugin.Shell/Images/user.png b/Plugins/Wox.Plugin.Shell/Images/user.png new file mode 100644 index 0000000000..2d45c1ee91 Binary files /dev/null and b/Plugins/Wox.Plugin.Shell/Images/user.png differ diff --git a/Plugins/Wox.Plugin.Shell/Languages/en.xaml b/Plugins/Wox.Plugin.Shell/Languages/en.xaml index 7a953986cc..0d0a41b3f1 100644 --- a/Plugins/Wox.Plugin.Shell/Languages/en.xaml +++ b/Plugins/Wox.Plugin.Shell/Languages/en.xaml @@ -4,6 +4,8 @@ Replace Win+R Do not close Command Prompt after command execution + Always run as administrator + Run as different user Shell Allows to execute system commands from Wox. Commands should start with > this command has been executed {0} times diff --git a/Plugins/Wox.Plugin.Shell/Main.cs b/Plugins/Wox.Plugin.Shell/Main.cs index 1e60ef8c9c..1a5174c4e4 100644 --- a/Plugins/Wox.Plugin.Shell/Main.cs +++ b/Plugins/Wox.Plugin.Shell/Main.cs @@ -1,14 +1,17 @@ -using System; +using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading.Tasks; using System.Windows; using WindowsInput; using WindowsInput.Native; using Wox.Infrastructure.Hotkey; using Wox.Infrastructure.Logger; using Wox.Infrastructure.Storage; +using Wox.Plugin.SharedCommands; using Application = System.Windows.Application; using Control = System.Windows.Controls.Control; using Keys = System.Windows.Forms.Keys; @@ -83,7 +86,7 @@ namespace Wox.Plugin.Shell IcoPath = Image, Action = c => { - Execute(m); + Execute(Process.Start, PrepareProcessStartInfo(m)); return true; } })); @@ -116,7 +119,7 @@ namespace Wox.Plugin.Shell IcoPath = Image, Action = c => { - Execute(m.Key); + Execute(Process.Start, PrepareProcessStartInfo(m.Key)); return true; } }; @@ -135,7 +138,7 @@ namespace Wox.Plugin.Shell IcoPath = Image, Action = c => { - Execute(cmd); + Execute(Process.Start, PrepareProcessStartInfo(cmd)); return true; } }; @@ -153,27 +156,26 @@ namespace Wox.Plugin.Shell IcoPath = Image, Action = c => { - Execute(m.Key); + Execute(Process.Start, PrepareProcessStartInfo(m.Key)); return true; } }).Take(5); return history.ToList(); } - private void Execute(string command, bool runAsAdministrator = false) + private ProcessStartInfo PrepareProcessStartInfo(string command, bool runAsAdministrator = false) { command = command.Trim(); command = Environment.ExpandEnvironmentVariables(command); + var workingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var runAsAdministratorArg = !runAsAdministrator && !_settings.RunAsAdministrator ? "" : "runas"; ProcessStartInfo info; if (_settings.Shell == Shell.Cmd) { var arguments = _settings.LeaveShellOpen ? $"/k \"{command}\"" : $"/c \"{command}\" & pause"; - info = new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = arguments, - }; + + info = ShellCommand.SetProcessStartInfo("cmd.exe", workingDirectory, arguments, runAsAdministratorArg); } else if (_settings.Shell == Shell.Powershell) { @@ -186,11 +188,8 @@ namespace Wox.Plugin.Shell { arguments = $"\"{command} ; Read-Host -Prompt \\\"Press Enter to continue\\\"\""; } - info = new ProcessStartInfo - { - FileName = "powershell.exe", - Arguments = arguments - }; + + info = ShellCommand.SetProcessStartInfo("powershell.exe", workingDirectory, arguments, runAsAdministratorArg); } else if (_settings.Shell == Shell.RunCommand) { @@ -200,41 +199,48 @@ namespace Wox.Plugin.Shell var filename = parts[0]; if (ExistInPath(filename)) { - var arguemtns = parts[1]; - info = new ProcessStartInfo - { - FileName = filename, - Arguments = arguemtns - }; + var arguments = parts[1]; + info = ShellCommand.SetProcessStartInfo(filename, workingDirectory, arguments, runAsAdministratorArg); } else { - info = new ProcessStartInfo(command); + info = ShellCommand.SetProcessStartInfo(command, verb: runAsAdministratorArg); } } else { - info = new ProcessStartInfo(command); + info = ShellCommand.SetProcessStartInfo(command, verb: runAsAdministratorArg); } } else { - return; + throw new NotImplementedException(); } - info.UseShellExecute = true; - info.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - info.Verb = runAsAdministrator ? "runas" : ""; + _settings.AddCmdHistory(command); + + return info; + } + + private void Execute(Func startProcess,ProcessStartInfo info) + { try { - Process.Start(info); - _settings.AddCmdHistory(command); + startProcess(info); } catch (FileNotFoundException e) { - MessageBox.Show($"Command not found: {e.Message}"); + var name = "Plugin: Shell"; + var message = $"Command not found: {e.Message}"; + _context.API.ShowMsg(name, message); + } + catch(Win32Exception e) + { + var name = "Plugin: Shell"; + var message = $"Error running the command: {e.Message}"; + _context.API.ShowMsg(name, message); } } @@ -316,19 +322,31 @@ namespace Wox.Plugin.Shell public List LoadContextMenus(Result selectedResult) { - return new List + var resultlist = new List { - new Result - { - Title = _context.API.GetTranslation("wox_plugin_cmd_run_as_administrator"), - Action = c => - { - Execute(selectedResult.Title, true); - return true; - }, - IcoPath = Image - } - }; + new Result + { + Title = _context.API.GetTranslation("wox_plugin_cmd_run_as_different_user"), + Action = c => + { + Task.Run(() =>Execute(ShellCommand.RunAsDifferentUser, PrepareProcessStartInfo(selectedResult.Title))); + return true; + }, + IcoPath = "Images/user.png" + }, + new Result + { + Title = _context.API.GetTranslation("wox_plugin_cmd_run_as_administrator"), + Action = c => + { + Execute(Process.Start, PrepareProcessStartInfo(selectedResult.Title, true)); + return true; + }, + IcoPath = Image + } + }; + + return resultlist; } } } diff --git a/Plugins/Wox.Plugin.Shell/Settings.cs b/Plugins/Wox.Plugin.Shell/Settings.cs index 62eb31f4e3..af149e8290 100644 --- a/Plugins/Wox.Plugin.Shell/Settings.cs +++ b/Plugins/Wox.Plugin.Shell/Settings.cs @@ -7,6 +7,8 @@ namespace Wox.Plugin.Shell public Shell Shell { get; set; } = Shell.Cmd; public bool ReplaceWinR { get; set; } = true; public bool LeaveShellOpen { get; set; } + public bool RunAsAdministrator { get; set; } = true; + public Dictionary Count = new Dictionary(); public void AddCmdHistory(string cmdName) diff --git a/Plugins/Wox.Plugin.Shell/ShellSetting.xaml b/Plugins/Wox.Plugin.Shell/ShellSetting.xaml index f631f6e228..dc6de53bac 100644 --- a/Plugins/Wox.Plugin.Shell/ShellSetting.xaml +++ b/Plugins/Wox.Plugin.Shell/ShellSetting.xaml @@ -12,10 +12,12 @@ + - + + CMD PowerShell RunCommand diff --git a/Plugins/Wox.Plugin.Shell/ShellSetting.xaml.cs b/Plugins/Wox.Plugin.Shell/ShellSetting.xaml.cs index 639c5c3c98..ffa3b58568 100644 --- a/Plugins/Wox.Plugin.Shell/ShellSetting.xaml.cs +++ b/Plugins/Wox.Plugin.Shell/ShellSetting.xaml.cs @@ -17,6 +17,7 @@ namespace Wox.Plugin.Shell { ReplaceWinR.IsChecked = _settings.ReplaceWinR; LeaveShellOpen.IsChecked = _settings.LeaveShellOpen; + AlwaysRunAsAdministrator.IsChecked = _settings.RunAsAdministrator; LeaveShellOpen.IsEnabled = _settings.Shell != Shell.RunCommand; LeaveShellOpen.Checked += (o, e) => @@ -29,6 +30,16 @@ namespace Wox.Plugin.Shell _settings.LeaveShellOpen = false; }; + AlwaysRunAsAdministrator.Checked += (o, e) => + { + _settings.RunAsAdministrator = true; + }; + + AlwaysRunAsAdministrator.Unchecked += (o, e) => + { + _settings.RunAsAdministrator = false; + }; + ReplaceWinR.Checked += (o, e) => { _settings.ReplaceWinR = true; diff --git a/Plugins/Wox.Plugin.Shell/Wox.Plugin.Shell.csproj b/Plugins/Wox.Plugin.Shell/Wox.Plugin.Shell.csproj index 2291f78bb8..270a55c3b4 100644 --- a/Plugins/Wox.Plugin.Shell/Wox.Plugin.Shell.csproj +++ b/Plugins/Wox.Plugin.Shell/Wox.Plugin.Shell.csproj @@ -70,6 +70,9 @@ + + Always + MSBuild:Compile Designer diff --git a/Plugins/Wox.Plugin.Sys/Main.cs b/Plugins/Wox.Plugin.Sys/Main.cs index 32e888514d..e888dbb378 100644 --- a/Plugins/Wox.Plugin.Sys/Main.cs +++ b/Plugins/Wox.Plugin.Sys/Main.cs @@ -56,12 +56,21 @@ namespace Wox.Plugin.Sys var results = new List(); foreach (var c in commands) { - var titleScore = StringMatcher.FuzzySearch(query.Search, c.Title).ScoreAfterSearchPrecisionFilter(); - var subTitleScore = StringMatcher.FuzzySearch(query.Search, c.SubTitle).ScoreAfterSearchPrecisionFilter(); - var score = Math.Max(titleScore, subTitleScore); + var titleMatch = StringMatcher.FuzzySearch(query.Search, c.Title); + var subTitleMatch = StringMatcher.FuzzySearch(query.Search, c.SubTitle); + + var score = Math.Max(titleMatch.Score, subTitleMatch.Score); if (score > 0) { c.Score = score; + if (score == titleMatch.Score) + { + c.TitleHighlightData = titleMatch.MatchData; + } + else + { + c.SubTitleHighlightData = subTitleMatch.MatchData; + } results.Add(c); } } diff --git a/Wox.Core/Plugin/PluginManager.cs b/Wox.Core/Plugin/PluginManager.cs index d6806e5cb2..fc9afb7401 100644 --- a/Wox.Core/Plugin/PluginManager.cs +++ b/Wox.Core/Plugin/PluginManager.cs @@ -172,13 +172,13 @@ namespace Wox.Core.Plugin public static List QueryForPlugin(PluginPair pair, Query query) { - var results = new List(); try { + List results = null; var metadata = pair.Metadata; var milliseconds = Stopwatch.Debug($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}", () => { - results = pair.Plugin.Query(query) ?? results; + results = pair.Plugin.Query(query) ?? new List(); UpdatePluginMetadata(results, metadata, query); }); metadata.QueryCount += 1; diff --git a/Wox.Infrastructure/Alphabet.cs b/Wox.Infrastructure/Alphabet.cs index d7fae882cb..63e178ba1e 100644 --- a/Wox.Infrastructure/Alphabet.cs +++ b/Wox.Infrastructure/Alphabet.cs @@ -16,16 +16,22 @@ namespace Wox.Infrastructure private static ConcurrentDictionary PinyinCache; private static BinaryStorage> _pinyinStorage; private static Settings _settings; - + public static void Initialize(Settings settings) { _settings = settings; + InitializePinyinHelpers(); + } + + private static void InitializePinyinHelpers() + { Format.setToneType(HanyuPinyinToneType.WITHOUT_TONE); Stopwatch.Normal("|Wox.Infrastructure.Alphabet.Initialize|Preload pinyin cache", () => { _pinyinStorage = new BinaryStorage>("Pinyin"); PinyinCache = _pinyinStorage.TryLoad(new ConcurrentDictionary()); + // force pinyin library static constructor initialize PinyinHelper.toHanyuPinyinStringArray('T', Format); }); @@ -34,6 +40,10 @@ namespace Wox.Infrastructure public static void Save() { + if (!_settings.ShouldUsePinyin) + { + return; + } _pinyinStorage.Save(PinyinCache); } @@ -68,39 +78,36 @@ namespace Wox.Infrastructure /// public static string[][] PinyinComination(string characters) { - if (_settings.ShouldUsePinyin && !string.IsNullOrEmpty(characters)) + if (!_settings.ShouldUsePinyin || string.IsNullOrEmpty(characters)) { - if (!PinyinCache.ContainsKey(characters)) - { + return Empty2DStringArray; + } - var allPinyins = new List(); - foreach (var c in characters) + if (!PinyinCache.ContainsKey(characters)) + { + var allPinyins = new List(); + foreach (var c in characters) + { + var pinyins = PinyinHelper.toHanyuPinyinStringArray(c, Format); + if (pinyins != null) { - var pinyins = PinyinHelper.toHanyuPinyinStringArray(c, Format); - if (pinyins != null) - { - var r = pinyins.Distinct().ToArray(); - allPinyins.Add(r); - } - else - { - var r = new[] { c.ToString() }; - allPinyins.Add(r); - } + var r = pinyins.Distinct().ToArray(); + allPinyins.Add(r); } + else + { + var r = new[] { c.ToString() }; + allPinyins.Add(r); + } + } - var combination = allPinyins.Aggregate(Combination).Select(c => c.Split(';')).ToArray(); - PinyinCache[characters] = combination; - return combination; - } - else - { - return PinyinCache[characters]; - } + var combination = allPinyins.Aggregate(Combination).Select(c => c.Split(';')).ToArray(); + PinyinCache[characters] = combination; + return combination; } else { - return Empty2DStringArray; + return PinyinCache[characters]; } } @@ -142,7 +149,5 @@ namespace Wox.Infrastructure ).ToArray(); return combination; } - - } } diff --git a/Wox.Infrastructure/Logger/Log.cs b/Wox.Infrastructure/Logger/Log.cs index dfa05c7154..ff72dff1c3 100644 --- a/Wox.Infrastructure/Logger/Log.cs +++ b/Wox.Infrastructure/Logger/Log.cs @@ -87,7 +87,7 @@ namespace Wox.Infrastructure.Logger do { - logger.Error($"Exception fulle name:\n <{e.GetType().FullName}>"); + logger.Error($"Exception full name:\n <{e.GetType().FullName}>"); logger.Error($"Exception message:\n <{e.Message}>"); logger.Error($"Exception stack trace:\n <{e.StackTrace}>"); logger.Error($"Exception source:\n <{e.Source}>"); diff --git a/Wox.Infrastructure/Storage/WoxJsonStorage.cs b/Wox.Infrastructure/Storage/WoxJsonStorage.cs index f117aee229..da0dbd073d 100644 --- a/Wox.Infrastructure/Storage/WoxJsonStorage.cs +++ b/Wox.Infrastructure/Storage/WoxJsonStorage.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Wox.Infrastructure.Storage { - class WoxJsonStorage : JsonStrorage where T : new() + public class WoxJsonStorage : JsonStrorage where T : new() { public WoxJsonStorage() { @@ -18,4 +18,4 @@ namespace Wox.Infrastructure.Storage FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); } } -} +} \ No newline at end of file diff --git a/Wox.Infrastructure/StringMatcher.cs b/Wox.Infrastructure/StringMatcher.cs index 5b82b18918..58ffa336fc 100644 --- a/Wox.Infrastructure/StringMatcher.cs +++ b/Wox.Infrastructure/StringMatcher.cs @@ -1,16 +1,19 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Text; using Wox.Infrastructure.Logger; using Wox.Infrastructure.UserSettings; +using static Wox.Infrastructure.StringMatcher; -namespace Wox.Infrastructure +namespace Wox.Infrastructure { public static class StringMatcher { public static MatchOption DefaultMatchOption = new MatchOption(); public static string UserSettingSearchPrecision { get; set; } + public static bool ShouldUsePinyin { get; set; } [Obsolete("This method is obsolete and should not be used. Please use the static function StringMatcher.FuzzySearch")] public static int Score(string source, string target) @@ -54,6 +57,9 @@ namespace Wox.Infrastructure var firstMatchIndex = -1; var lastMatchIndex = 0; char ch; + + var indexList = new List(); + for (var idx = 0; idx < len; idx++) { ch = stringToCompare[idx]; @@ -63,6 +69,7 @@ namespace Wox.Infrastructure firstMatchIndex = idx; lastMatchIndex = idx + 1; + indexList.Add(idx); sb.Append(opt.Prefix + ch + opt.Suffix); patternIdx += 1; } @@ -82,27 +89,38 @@ namespace Wox.Infrastructure // return rendered string if we have a match for every char if (patternIdx == pattern.Length) { - return new MatchResult + var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex); + var pinyinScore = ScoreForPinyin(stringToCompare, query); + + var result = new MatchResult { Success = true, - Value = sb.ToString(), - Score = CalScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex) + MatchData = indexList, + RawScore = Math.Max(score, pinyinScore) }; + + return result; } return new MatchResult { Success = false }; } - private static int CalScore(string query, string stringToCompare, int firstIndex, int matchLen) + private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen) { - //a match found near the beginning of a string is scored more than a match found near the end - //a match is scored more if the characters in the patterns are closer to each other, while the score is lower if they are more spread out + // A match found near the beginning of a string is scored more than a match found near the end + // A match is scored more if the characters in the patterns are closer to each other, + // while the score is lower if they are more spread out var score = 100 * (query.Length + 1) / ((1 + firstIndex) + (matchLen + 1)); - //a match with less characters assigning more weights + + // A match with less characters assigning more weights if (stringToCompare.Length - query.Length < 5) + { score += 20; + } else if (stringToCompare.Length - query.Length < 10) + { score += 10; + } return score; } @@ -114,21 +132,13 @@ namespace Wox.Infrastructure None = 0 } - public static bool IsSearchPrecisionScoreMet(this MatchResult matchResult) - { - var precisionScore = (SearchPrecisionScore)Enum.Parse(typeof(SearchPrecisionScore), - UserSettingSearchPrecision ?? SearchPrecisionScore.Regular.ToString()); - return matchResult.Score >= (int)precisionScore; - } - - public static int ScoreAfterSearchPrecisionFilter(this MatchResult matchResult) - { - return matchResult.IsSearchPrecisionScoreMet() ? matchResult.Score : 0; - - } - public static int ScoreForPinyin(string source, string target) { + if (!ShouldUsePinyin) + { + return 0; + } + if (!string.IsNullOrEmpty(source) && !string.IsNullOrEmpty(target)) { if (Alphabet.ContainsChinese(source)) @@ -158,12 +168,48 @@ namespace Wox.Infrastructure public class MatchResult { public bool Success { get; set; } - public int Score { get; set; } - + /// - /// highlight string + /// The final score of the match result with all search precision filters applied. /// - public string Value { get; set; } + public int Score { get; private set; } + + /// + /// The raw calculated search score without any search precision filtering applied. + /// + private int _rawScore; + public int RawScore + { + get { return _rawScore; } + set + { + _rawScore = value; + Score = ApplySearchPrecisionFilter(_rawScore); + } + } + + /// + /// Matched data to highlight. + /// + public List MatchData { get; set; } + + public bool IsSearchPrecisionScoreMet() + { + return IsSearchPrecisionScoreMet(Score); + } + + private bool IsSearchPrecisionScoreMet(int score) + { + var precisionScore = (SearchPrecisionScore)Enum.Parse( + typeof(SearchPrecisionScore), + UserSettingSearchPrecision ?? SearchPrecisionScore.Regular.ToString()); + return score >= (int)precisionScore; + } + + private int ApplySearchPrecisionFilter(int score) + { + return IsSearchPrecisionScoreMet(score) ? score : 0; + } } public class MatchOption diff --git a/Wox.Infrastructure/UserSettings/Settings.cs b/Wox.Infrastructure/UserSettings/Settings.cs index 7371ea5d05..de5a2e6624 100644 --- a/Wox.Infrastructure/UserSettings/Settings.cs +++ b/Wox.Infrastructure/UserSettings/Settings.cs @@ -24,7 +24,17 @@ namespace Wox.Infrastructure.UserSettings /// /// when false Alphabet static service will always return empty results /// - public bool ShouldUsePinyin { get; set; } = true; + private bool _shouldUsePinyin = true; + public bool ShouldUsePinyin + { + get { return _shouldUsePinyin; } + set + { + _shouldUsePinyin = value; + StringMatcher.ShouldUsePinyin = value; + } + } + private string _querySearchPrecision { get; set; } = StringMatcher.SearchPrecisionScore.Regular.ToString(); public string QuerySearchPrecision diff --git a/Wox.Plugin/Result.cs b/Wox.Plugin/Result.cs index 2f74591703..6e0559d35f 100644 --- a/Wox.Plugin/Result.cs +++ b/Wox.Plugin/Result.cs @@ -42,6 +42,16 @@ namespace Wox.Plugin public int Score { get; set; } + /// + /// A list of indexes for the characters to be highlighted in Title + /// + public IList TitleHighlightData { get; set; } + + /// + /// A list of indexes for the characters to be highlighted in SubTitle + /// + public IList SubTitleHighlightData { get; set; } + /// /// Only resulsts that originQuery match with curren query will be displayed in the panel /// @@ -69,7 +79,9 @@ namespace Wox.Plugin var equality = string.Equals(r?.Title, Title) && string.Equals(r?.SubTitle, SubTitle) && - string.Equals(r?.IcoPath, IcoPath); + string.Equals(r?.IcoPath, IcoPath) && + TitleHighlightData == r.TitleHighlightData && + SubTitleHighlightData == r.SubTitleHighlightData; return equality; } @@ -108,7 +120,7 @@ namespace Wox.Plugin public object ContextData { get; set; } /// - /// Plugin ID that generate this result + /// Plugin ID that generated this result /// public string PluginID { get; internal set; } } diff --git a/Wox.Plugin/SharedCommands/ShellCommand.cs b/Wox.Plugin/SharedCommands/ShellCommand.cs new file mode 100644 index 0000000000..9da535150c --- /dev/null +++ b/Wox.Plugin/SharedCommands/ShellCommand.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Wox.Plugin.SharedCommands +{ + public static class ShellCommand + { + public delegate bool EnumThreadDelegate(IntPtr hwnd, IntPtr lParam); + [DllImport("user32.dll")] static extern bool EnumThreadWindows(uint threadId, EnumThreadDelegate lpfn, IntPtr lParam); + [DllImport("user32.dll")] static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount); + [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hwnd); + + private static bool containsSecurityWindow; + + public static Process RunAsDifferentUser(ProcessStartInfo processStartInfo) + { + processStartInfo.Verb = "RunAsUser"; + var process = Process.Start(processStartInfo); + + containsSecurityWindow = false; + while (!containsSecurityWindow) // wait for windows to bring up the "Windows Security" dialog + { + CheckSecurityWindow(); + Thread.Sleep(25); + } + while (containsSecurityWindow) // while this process contains a "Windows Security" dialog, stay open + { + containsSecurityWindow = false; + CheckSecurityWindow(); + Thread.Sleep(50); + } + + return process; + } + + private static void CheckSecurityWindow() + { + ProcessThreadCollection ptc = Process.GetCurrentProcess().Threads; + for (int i = 0; i < ptc.Count; i++) + EnumThreadWindows((uint)ptc[i].Id, CheckSecurityThread, IntPtr.Zero); + } + + private static bool CheckSecurityThread(IntPtr hwnd, IntPtr lParam) + { + if (GetWindowTitle(hwnd) == "Windows Security") + containsSecurityWindow = true; + return true; + } + + private static string GetWindowTitle(IntPtr hwnd) + { + StringBuilder sb = new StringBuilder(GetWindowTextLength(hwnd) + 1); + GetWindowText(hwnd, sb, sb.Capacity); + return sb.ToString(); + } + + public static ProcessStartInfo SetProcessStartInfo(this string fileName, string workingDirectory = "", string arguments = "", string verb = "") + { + var info = new ProcessStartInfo + { + FileName = fileName, + WorkingDirectory = workingDirectory, + Arguments = arguments, + Verb = verb + }; + + return info; + } + } +} diff --git a/Wox.Plugin/Wox.Plugin.csproj b/Wox.Plugin/Wox.Plugin.csproj index 843cc7d8bf..3a486fb42b 100644 --- a/Wox.Plugin/Wox.Plugin.csproj +++ b/Wox.Plugin/Wox.Plugin.csproj @@ -78,6 +78,7 @@ + diff --git a/Wox.Test/FuzzyMatcherTest.cs b/Wox.Test/FuzzyMatcherTest.cs index ce1746a3a3..21563f91f9 100644 --- a/Wox.Test/FuzzyMatcherTest.cs +++ b/Wox.Test/FuzzyMatcherTest.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; using NUnit.Framework; using Wox.Infrastructure; +using Wox.Infrastructure.UserSettings; using Wox.Plugin; namespace Wox.Test @@ -11,7 +12,7 @@ namespace Wox.Test [TestFixture] public class FuzzyMatcherTest { - public List GetSearchStrings() + public List GetSearchStrings() => new List { "Chrome", @@ -48,14 +49,13 @@ namespace Wox.Test "aac" }; - var results = new List(); foreach (var str in sources) { results.Add(new Result { Title = str, - Score = StringMatcher.FuzzySearch("inst", str).Score + Score = StringMatcher.FuzzySearch("inst", str).RawScore }); } @@ -72,7 +72,7 @@ namespace Wox.Test { var compareString = "Can have rum only in my glass"; - var scoreResult = StringMatcher.FuzzySearch(searchString, compareString).Score; + var scoreResult = StringMatcher.FuzzySearch(searchString, compareString).RawScore; Assert.True(scoreResult == 0); } @@ -129,13 +129,12 @@ namespace Wox.Test .ToList(); var results = new List(); - foreach (var str in searchStrings) { results.Add(new Result { Title = str, - Score = StringMatcher.FuzzySearch(searchTerm, str).Score + Score = StringMatcher.FuzzySearch(searchTerm, str).RawScore }); } @@ -168,8 +167,11 @@ namespace Wox.Test [TestCase("ccs", "Candy Crush Saga from King", (int)StringMatcher.SearchPrecisionScore.Low, true)] [TestCase("cand", "Candy Crush Saga from King", (int)StringMatcher.SearchPrecisionScore.Regular, true)] [TestCase("cand", "Help cure hope raise on mind entity Chrome", (int)StringMatcher.SearchPrecisionScore.Regular, false)] - public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual(string queryString, string compareString, - int expectedPrecisionScore, bool expectedPrecisionResult) + public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual( + string queryString, + string compareString, + int expectedPrecisionScore, + bool expectedPrecisionResult) { var expectedPrecisionString = (StringMatcher.SearchPrecisionScore)expectedPrecisionScore; StringMatcher.UserSettingSearchPrecision = expectedPrecisionString.ToString(); diff --git a/Wox/App.xaml.cs b/Wox/App.xaml.cs index 0ec1614bf9..9436df4758 100644 --- a/Wox/App.xaml.cs +++ b/Wox/App.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using System.Timers; @@ -56,6 +56,7 @@ namespace Wox Alphabet.Initialize(_settings); StringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision; + StringMatcher.ShouldUsePinyin = _settings.ShouldUsePinyin; PluginManager.LoadPlugins(_settings.PluginSettings); _mainVM = new MainViewModel(_settings); diff --git a/Wox/Converters/HighlightTextConverter.cs b/Wox/Converters/HighlightTextConverter.cs new file mode 100644 index 0000000000..b7d6d06838 --- /dev/null +++ b/Wox/Converters/HighlightTextConverter.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Data; +using System.Windows.Documents; + +namespace Wox.Converters +{ + public class HighlightTextConverter : IMultiValueConverter + { + public object Convert(object[] value, Type targetType, object parameter, CultureInfo cultureInfo) + { + var text = value[0] as string; + var highlightData = value[1] as List; + + var textBlock = new Span(); + + if (highlightData == null || !highlightData.Any()) + { + // No highlight data, just return the text + return new Run(text); + } + + for (var i = 0; i < text.Length; i++) + { + var currentCharacter = text.Substring(i, 1); + if (this.ShouldHighlight(highlightData, i)) + { + textBlock.Inlines.Add(new Bold(new Run(currentCharacter))); + } + else + { + textBlock.Inlines.Add(new Run(currentCharacter)); + } + } + return textBlock; + } + + public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture) + { + return new[] { DependencyProperty.UnsetValue, DependencyProperty.UnsetValue }; + } + + private bool ShouldHighlight(List highlightData, int index) + { + return highlightData.Contains(index); + } + } +} diff --git a/Wox/MainWindow.xaml b/Wox/MainWindow.xaml index d1e610dfc8..7dfe8cd1e9 100644 --- a/Wox/MainWindow.xaml +++ b/Wox/MainWindow.xaml @@ -36,6 +36,7 @@ + diff --git a/Wox/ResultListBox.xaml b/Wox/ResultListBox.xaml index cad02d5198..dc3100e0e7 100644 --- a/Wox/ResultListBox.xaml +++ b/Wox/ResultListBox.xaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:vm="clr-namespace:Wox.ViewModel" + xmlns:converter="clr-namespace:Wox.Converters" mc:Ignorable="d" d:DesignWidth="100" d:DesignHeight="100" d:DataContext="{d:DesignInstance vm:ResultsViewModel}" MaxHeight="{Binding MaxHeight}" @@ -30,6 +31,9 @@ + + + @@ -44,9 +48,23 @@ + Text="{Binding Result.Title}"> + + + + + + + + Grid.Row="1" x:Name="SubTitle" Text="{Binding Result.SubTitle}"> + + + + + + + diff --git a/Wox/Storage/TopMostRecord.cs b/Wox/Storage/TopMostRecord.cs index 4d33f91ebb..5f940f2184 100644 --- a/Wox/Storage/TopMostRecord.cs +++ b/Wox/Storage/TopMostRecord.cs @@ -1,47 +1,50 @@ using System.Collections.Generic; using System.Linq; -using Wox.Infrastructure.Storage; +using Newtonsoft.Json; using Wox.Plugin; namespace Wox.Storage { + // todo this class is not thread safe.... but used from multiple threads. public class TopMostRecord { - public Dictionary records = new Dictionary(); + [JsonProperty] + private Dictionary records = new Dictionary(); internal bool IsTopMost(Result result) { + if (records.Count == 0) + { + return false; + } + + // since this dictionary should be very small (or empty) going over it should be pretty fast. return records.Any(o => o.Value.Title == result.Title - && o.Value.SubTitle == result.SubTitle - && o.Value.PluginID == result.PluginID - && o.Key == result.OriginQuery.RawQuery); + && o.Value.SubTitle == result.SubTitle + && o.Value.PluginID == result.PluginID + && o.Key == result.OriginQuery.RawQuery); } internal void Remove(Result result) { - if (records.ContainsKey(result.OriginQuery.RawQuery)) - { - records.Remove(result.OriginQuery.RawQuery); - } + records.Remove(result.OriginQuery.RawQuery); } internal void AddOrUpdate(Result result) { - if (records.ContainsKey(result.OriginQuery.RawQuery)) + var record = new Record { - records[result.OriginQuery.RawQuery].Title = result.Title; - records[result.OriginQuery.RawQuery].SubTitle = result.SubTitle; - records[result.OriginQuery.RawQuery].PluginID = result.PluginID; - } - else - { - records.Add(result.OriginQuery.RawQuery, new Record - { - PluginID = result.PluginID, - Title = result.Title, - SubTitle = result.SubTitle - }); - } + PluginID = result.PluginID, + Title = result.Title, + SubTitle = result.SubTitle + }; + records[result.OriginQuery.RawQuery] = record; + + } + + public void Load(Dictionary dictionary) + { + records = dictionary; } } diff --git a/Wox/Storage/UserSelectedRecord.cs b/Wox/Storage/UserSelectedRecord.cs index 9005590410..ef8bf7f8ef 100644 --- a/Wox/Storage/UserSelectedRecord.cs +++ b/Wox/Storage/UserSelectedRecord.cs @@ -12,21 +12,23 @@ namespace Wox.Storage public void Add(Result result) { - if (records.ContainsKey(result.ToString())) + var key = result.ToString(); + if (records.TryGetValue(key, out int value)) { - records[result.ToString()] += 1; + records[key] = value + 1; } else { - records.Add(result.ToString(), 1); + records.Add(key, 1); + } } public int GetSelectedCount(Result result) { - if (records.ContainsKey(result.ToString())) + if (records.TryGetValue(result.ToString(), out int value)) { - return records[result.ToString()]; + return value; } return 0; } diff --git a/Wox/ViewModel/MainViewModel.cs b/Wox/ViewModel/MainViewModel.cs index 4e2ee5818f..a7e17d9a7e 100644 --- a/Wox/ViewModel/MainViewModel.cs +++ b/Wox/ViewModel/MainViewModel.cs @@ -126,6 +126,8 @@ namespace Wox.ViewModel SelectedResults.SelectPrevPage(); }); + SelectFirstResultCommand = new RelayCommand(_ => SelectedResults.SelectFirstResult()); + StartHelpCommand = new RelayCommand(_ => { Process.Start("http://doc.wox.one/"); @@ -268,6 +270,7 @@ namespace Wox.ViewModel public ICommand SelectPrevItemCommand { get; set; } public ICommand SelectNextPageCommand { get; set; } public ICommand SelectPrevPageCommand { get; set; } + public ICommand SelectFirstResultCommand { get; set; } public ICommand StartHelpCommand { get; set; } public ICommand LoadContextMenuCommand { get; set; } public ICommand LoadHistoryCommand { get; set; } @@ -415,11 +418,15 @@ namespace Wox.ViewModel var config = _settings.PluginSettings.Plugins[plugin.Metadata.ID]; if (!config.Disabled) { - var results = PluginManager.QueryForPlugin(plugin, query); UpdateResultView(results, plugin.Metadata, query); } }); + + // this should happen once after all queries are done so progress bar should continue + // until the end of all querying + _queryHasReturn = true; + ProgressBarVisibility = Visibility.Hidden; }, _updateToken); } } @@ -628,9 +635,6 @@ namespace Wox.ViewModel /// public void UpdateResultView(List list, PluginMetadata metadata, Query originQuery) { - _queryHasReturn = true; - ProgressBarVisibility = Visibility.Hidden; - foreach (var result in list) { if (_topMostRecord.IsTopMost(result)) diff --git a/Wox/ViewModel/RelayCommand.cs b/Wox/ViewModel/RelayCommand.cs index 3a52f30007..b1dbc551c1 100644 --- a/Wox/ViewModel/RelayCommand.cs +++ b/Wox/ViewModel/RelayCommand.cs @@ -5,7 +5,6 @@ namespace Wox.ViewModel { public class RelayCommand : ICommand { - private Action _action; public RelayCommand(Action action) diff --git a/Wox/ViewModel/ResultsViewModel.cs b/Wox/ViewModel/ResultsViewModel.cs index 674923228e..e36ddc94d3 100644 --- a/Wox/ViewModel/ResultsViewModel.cs +++ b/Wox/ViewModel/ResultsViewModel.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows; +using System.Windows.Controls; using System.Windows.Data; +using System.Windows.Documents; using Wox.Infrastructure.UserSettings; using Wox.Plugin; @@ -107,6 +109,11 @@ namespace Wox.ViewModel SelectedIndex = NewIndex(SelectedIndex - MaxResults); } + public void SelectFirstResult() + { + SelectedIndex = NewIndex(0); + } + public void Clear() { Results.Clear(); @@ -155,7 +162,6 @@ namespace Wox.ViewModel // Find the same results in A (old results) and B (new newResults) var sameResults = oldResults .Where(t1 => newResults.Any(x => x.Result.Equals(t1.Result))) - .Select(t1 => t1) .ToList(); // remove result of relative complement of B in A @@ -193,8 +199,37 @@ namespace Wox.ViewModel return results; } + #endregion + #region FormattedText Dependency Property + public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached( + "FormattedText", + typeof(Inline), + typeof(ResultsViewModel), + new PropertyMetadata(null, FormattedTextPropertyChanged)); + public static void SetFormattedText(DependencyObject textBlock, IList value) + { + textBlock.SetValue(FormattedTextProperty, value); + } + + public static Inline GetFormattedText(DependencyObject textBlock) + { + return (Inline)textBlock.GetValue(FormattedTextProperty); + } + + private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var textBlock = d as TextBlock; + if (textBlock == null) return; + + var inline = (Inline)e.NewValue; + + textBlock.Inlines.Clear(); + if (inline == null) return; + + textBlock.Inlines.Add(inline); + } #endregion public class ResultCollection : ObservableCollection diff --git a/Wox/Wox.csproj b/Wox/Wox.csproj index ef77148291..4a4dc62bfc 100644 --- a/Wox/Wox.csproj +++ b/Wox/Wox.csproj @@ -158,6 +158,7 @@ Properties\SolutionAssemblyInfo.cs +