diff --git a/Plugins/Wox.Plugin.Folder/Main.cs b/Plugins/Wox.Plugin.Folder/Main.cs index d6b4a8665..170799029 100644 --- a/Plugins/Wox.Plugin.Folder/Main.cs +++ b/Plugins/Wox.Plugin.Folder/Main.cs @@ -191,9 +191,14 @@ namespace Wox.Plugin.Folder if (incompleteName.StartsWith(">")) { searchOption = SearchOption.AllDirectories; - incompleteName = incompleteName.Substring(1); + + // match everything before and after search term using supported wildcard '*', ie. *searchterm* + incompleteName = "*" + incompleteName.Substring(1); } - + + var folderList = new List(); + var fileList = new List(); + try { // search folder and add results @@ -204,11 +209,14 @@ namespace Wox.Plugin.Folder { 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); + if(fileSystemInfo is DirectoryInfo) + { + folderList.Add(CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName, query)); + } + else + { + fileList.Add(CreateFileResult(fileSystemInfo.FullName, query)); + } } } catch (Exception e) @@ -223,7 +231,8 @@ namespace Wox.Plugin.Folder throw; } - return results; + // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. + return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList(); } private static Result CreateFileResult(string filePath, Query query) diff --git a/README.md b/README.md index d0659d0fa..2b9da637f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ WoX === -![Maintenance](https://img.shields.io/maintenance/yes/2019) +![Maintenance](https://img.shields.io/maintenance/yes/2020) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/jjw24/wox)](https://github.com/jjw24/Wox/releases/latest) ![GitHub Release Date](https://img.shields.io/github/release-date/jjw24/wox) ![GitHub commits since latest release](https://img.shields.io/github/commits-since/jjw24/wox/v1.3.524) [![Build Status](https://dev.azure.com/Wox-Launcher/Wox/_apis/build/status/jjw24.Wox?branchName=master)](https://dev.azure.com/Wox-Launcher/Wox/_build/latest?definitionId=1&branchName=master) -[![Github All Releases](https://img.shields.io/github/downloads/Wox-launcher/Wox/total.svg)](https://github.com/Wox-launcher/Wox/releases) +[![Github All Releases](https://img.shields.io/github/downloads/jjw24/Wox/total.svg)](https://github.com/jjw24/Wox/releases) [![RamenBless](https://cdn.rawgit.com/LunaGao/BlessYourCodeTag/master/tags/ramen.svg)](https://github.com/LunaGao/BlessYourCodeTag) **WoX** is a launcher for Windows that simply works. It's an alternative to [Alfred](https://www.alfredapp.com/) and [Launchy](http://www.launchy.net/). You can call it Windows omni-eXecutor if you want a long name. diff --git a/Wox.Core/Updater.cs b/Wox.Core/Updater.cs index d2461baf4..971995f5e 100644 --- a/Wox.Core/Updater.cs +++ b/Wox.Core/Updater.cs @@ -16,18 +16,23 @@ using Wox.Infrastructure.Logger; namespace Wox.Core { - public static class Updater + public class Updater { - private static readonly Internationalization Translater = InternationalizationManager.Instance; + public string GitHubRepository { get; } - public static async Task UpdateApp() + public Updater(string gitHubRepository) + { + GitHubRepository = gitHubRepository; + } + + public async Task UpdateApp() { UpdateManager m; UpdateInfo u; try { - m = await GitHubUpdateManager(Constant.Repository); + m = await GitHubUpdateManager(GitHubRepository); } catch (Exception e) when (e is HttpRequestException || e is WebException || e is SocketException) { @@ -66,8 +71,8 @@ namespace Wox.Core await m.ApplyReleases(u); await m.CreateUninstallerRegistryEntry(); - var newVersionTips = Translater.GetTranslation("newVersionTips"); - newVersionTips = string.Format(newVersionTips, fr.Version); + var newVersionTips = this.NewVersinoTips(fr.Version.ToString()); + MessageBox.Show(newVersionTips); Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); } @@ -90,7 +95,7 @@ namespace Wox.Core } /// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs - private static async Task GitHubUpdateManager(string repository) + private async Task GitHubUpdateManager(string repository) { var uri = new Uri(repository); var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases"; @@ -109,7 +114,7 @@ namespace Wox.Core return manager; } - public static string NewVersinoTips(string version) + public string NewVersinoTips(string version) { var translater = InternationalizationManager.Instance; var tips = string.Format(translater.GetTranslation("newVersionTips"), version); diff --git a/Wox.Infrastructure/Http/Http.cs b/Wox.Infrastructure/Http/Http.cs index d79f3481f..0c8597a5d 100644 --- a/Wox.Infrastructure/Http/Http.cs +++ b/Wox.Infrastructure/Http/Http.cs @@ -13,6 +13,16 @@ namespace Wox.Infrastructure.Http { private const string UserAgent = @"Mozilla/5.0 (Trident/7.0; rv:11.0) like Gecko"; + static Http() + { + // need to be added so it would work on a win10 machine + ServicePointManager.Expect100Continue = true; + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls + | SecurityProtocolType.Tls11 + | SecurityProtocolType.Tls12 + | SecurityProtocolType.Ssl3; + } + public static HttpProxy Proxy { private get; set; } public static IWebProxy WebProxy() { diff --git a/Wox.Infrastructure/Image/ImageCache.cs b/Wox.Infrastructure/Image/ImageCache.cs index 599b69066..5e74a2a38 100644 --- a/Wox.Infrastructure/Image/ImageCache.cs +++ b/Wox.Infrastructure/Image/ImageCache.cs @@ -39,6 +39,19 @@ namespace Wox.Infrastructure.Image var contains = _data.ContainsKey(key); return contains; } + + public int CacheSize() + { + return _data.Count; + } + + /// + /// return the number of unique images in the cache (by reference not by checking images content) + /// + public int UniqueImagesInCache() + { + return _data.Values.Distinct().Count(); + } } } diff --git a/Wox.Infrastructure/Image/ImageHashGenerator.cs b/Wox.Infrastructure/Image/ImageHashGenerator.cs new file mode 100644 index 000000000..9ace8b74f --- /dev/null +++ b/Wox.Infrastructure/Image/ImageHashGenerator.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace Wox.Infrastructure.Image +{ + public interface IImageHashGenerator + { + string GetHashFromImage(ImageSource image); + } + public class ImageHashGenerator : IImageHashGenerator + { + public string GetHashFromImage(ImageSource imageSource) + { + if (!(imageSource is BitmapSource image)) + { + return null; + } + + try + { + using (var outStream = new MemoryStream()) + { + // PngBitmapEncoder enc2 = new PngBitmapEncoder(); + // enc2.Frames.Add(BitmapFrame.Create(tt)); + + var enc = new JpegBitmapEncoder(); + var bitmapFrame = BitmapFrame.Create(image); + bitmapFrame.Freeze(); + enc.Frames.Add(bitmapFrame); + enc.Save(outStream); + var byteArray = outStream.GetBuffer(); + using (var sha1 = new SHA1CryptoServiceProvider()) + { + var hash = Convert.ToBase64String(sha1.ComputeHash(byteArray)); + return hash; + } + } + } + catch + { + return null; + } + + } + } +} \ No newline at end of file diff --git a/Wox.Infrastructure/Image/ImageLoader.cs b/Wox.Infrastructure/Image/ImageLoader.cs index 3498e4f3b..528900ce7 100644 --- a/Wox.Infrastructure/Image/ImageLoader.cs +++ b/Wox.Infrastructure/Image/ImageLoader.cs @@ -14,6 +14,8 @@ namespace Wox.Infrastructure.Image { private static readonly ImageCache ImageCache = new ImageCache(); private static BinaryStorage> _storage; + private static readonly ConcurrentDictionary GuidToKey = new ConcurrentDictionary(); + private static IImageHashGenerator _hashGenerator; private static readonly string[] ImageExtensions = @@ -30,7 +32,8 @@ namespace Wox.Infrastructure.Image public static void Initialize() { - _storage = new BinaryStorage> ("Image"); + _storage = new BinaryStorage>("Image"); + _hashGenerator = new ImageHashGenerator(); ImageCache.Usage = _storage.TryLoad(new ConcurrentDictionary()); foreach (var icon in new[] { Constant.DefaultIcon, Constant.ErrorIcon }) @@ -43,16 +46,12 @@ namespace Wox.Infrastructure.Image { Stopwatch.Normal("|ImageLoader.Initialize|Preload images cost", () => { - ImageCache.Usage.AsParallel().Where(i => !ImageCache.ContainsKey(i.Key)).ForAll(i => + ImageCache.Usage.AsParallel().ForAll(x => { - var img = Load(i.Key); - if (img != null) - { - ImageCache[i.Key] = img; - } + Load(x.Key); }); }); - Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.Usage.Count}>"); + Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.Usage.Count}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}"); }); } @@ -61,31 +60,56 @@ namespace Wox.Infrastructure.Image ImageCache.Cleanup(); _storage.Save(ImageCache.Usage); } - - public static ImageSource Load(string path, bool loadFullImage = false) + + private class ImageResult + { + public ImageResult(ImageSource imageSource, ImageType imageType) + { + ImageSource = imageSource; + ImageType = imageType; + } + + public ImageType ImageType { get; } + public ImageSource ImageSource { get; } + } + + private enum ImageType + { + File, + Folder, + Data, + ImageFile, + Error, + Cache + } + + private static ImageResult LoadInternal(string path, bool loadFullImage = false) { ImageSource image; + ImageType type = ImageType.Error; try { if (string.IsNullOrEmpty(path)) { - return ImageCache[Constant.ErrorIcon]; + return new ImageResult(ImageCache[Constant.ErrorIcon], ImageType.Error); } if (ImageCache.ContainsKey(path)) { - return ImageCache[path]; + return new ImageResult(ImageCache[path], ImageType.Cache); } - + if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) { - return new BitmapImage(new Uri(path)); + var imageSource = new BitmapImage(new Uri(path)); + imageSource.Freeze(); + return new ImageResult(imageSource, ImageType.Data); } if (!Path.IsPathRooted(path)) { path = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path)); } - + if (Directory.Exists(path)) { /* Directories can also have thumbnails instead of shell icons. @@ -94,14 +118,17 @@ namespace Wox.Infrastructure.Image * Wox responsibility. * - Solution: just load the icon */ + type = ImageType.Folder; image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.IconOnly); + } else if (File.Exists(path)) { var extension = Path.GetExtension(path).ToLower(); if (ImageExtensions.Contains(extension)) { + type = ImageType.ImageFile; if (loadFullImage) { image = LoadFullImage(path); @@ -119,6 +146,7 @@ namespace Wox.Infrastructure.Image } else { + type = ImageType.File; image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, Constant.ThumbnailSize, ThumbnailOptions.None); } @@ -128,17 +156,50 @@ namespace Wox.Infrastructure.Image image = ImageCache[Constant.ErrorIcon]; path = Constant.ErrorIcon; } - ImageCache[path] = image; - image.Freeze(); + + if (type != ImageType.Error) + { + image.Freeze(); + } } catch (System.Exception e) { Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path}", e); - + type = ImageType.Error; image = ImageCache[Constant.ErrorIcon]; ImageCache[path] = image; } - return image; + return new ImageResult(image, type); + } + + private static bool EnableImageHash = true; + + public static ImageSource Load(string path, bool loadFullImage = false) + { + var imageResult = LoadInternal(path, loadFullImage); + + var img = imageResult.ImageSource; + if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache) + { // we need to get image hash + string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null; + if (hash != null) + { + if (GuidToKey.TryGetValue(hash, out string key)) + { // image already exists + img = ImageCache[key]; + } + else + { // new guid + GuidToKey[hash] = path; + } + } + + // update cache + ImageCache[path] = img; + } + + + return img; } private static BitmapImage LoadFullImage(string path) diff --git a/Wox.Infrastructure/Image/ThumbnailReader.cs b/Wox.Infrastructure/Image/ThumbnailReader.cs index e0ea9bba3..bd65fc700 100644 --- a/Wox.Infrastructure/Image/ThumbnailReader.cs +++ b/Wox.Infrastructure/Image/ThumbnailReader.cs @@ -110,7 +110,6 @@ namespace Wox.Infrastructure.Image try { - return Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } finally diff --git a/Wox.Infrastructure/Logger/Log.cs b/Wox.Infrastructure/Logger/Log.cs index ff72dff1c..10e8128c5 100644 --- a/Wox.Infrastructure/Logger/Log.cs +++ b/Wox.Infrastructure/Logger/Log.cs @@ -11,18 +11,20 @@ namespace Wox.Infrastructure.Logger { public const string DirectoryName = "Logs"; + public static string CurrentLogDirectory { get; private set; } + static Log() { - var path = Path.Combine(Constant.DataDirectory, DirectoryName, Constant.Version); - if (!Directory.Exists(path)) + CurrentLogDirectory = Path.Combine(Constant.DataDirectory, DirectoryName, Constant.Version); + if (!Directory.Exists(CurrentLogDirectory)) { - Directory.CreateDirectory(path); + Directory.CreateDirectory(CurrentLogDirectory); } var configuration = new LoggingConfiguration(); var target = new FileTarget(); configuration.AddTarget("file", target); - target.FileName = path.Replace(@"\", "/") + "/${shortdate}.txt"; + target.FileName = CurrentLogDirectory.Replace(@"\", "/") + "/${shortdate}.txt"; #if DEBUG var rule = new LoggingRule("*", LogLevel.Debug, target); #else @@ -47,8 +49,53 @@ namespace Wox.Infrastructure.Logger return valid; } - /// example: "|prefix|unprefixed" - public static void Error(string message) + + + [MethodImpl(MethodImplOptions.Synchronized)] + public static void Exception(string className, string message, System.Exception exception, [CallerMemberName] string methodName = "") + { + if (string.IsNullOrWhiteSpace(className)) + { + LogFaultyFormat($"Fail to specify a class name during logging of message: {message ?? "no message entered"}"); + } + + if (string.IsNullOrWhiteSpace(message)) + { // todo: not sure we really need that + LogFaultyFormat($"Fail to specify a message during logging"); + } + + if (!string.IsNullOrWhiteSpace(methodName)) + { + className += "." + methodName; + } + + ExceptionInternal(className, message, exception); + } + + private static void ExceptionInternal(string classAndMethod, string message, System.Exception e) + { + var logger = LogManager.GetLogger(classAndMethod); + + System.Diagnostics.Debug.WriteLine($"ERROR|{message}"); + + logger.Error("-------------------------- Begin exception --------------------------"); + logger.Error(message); + + do + { + logger.Error($"Exception full name:\n <{e.GetType().FullName}>"); + logger.Error($"Exception message:\n <{e.Message}>"); + logger.Error($"Exception stack trace:\n <{e.StackTrace}>"); + logger.Error($"Exception source:\n <{e.Source}>"); + logger.Error($"Exception target site:\n <{e.TargetSite}>"); + logger.Error($"Exception HResult:\n <{e.HResult}>"); + e = e.InnerException; + } while (e != null); + + logger.Error("-------------------------- End exception --------------------------"); + } + + private static void LogInternal(string message, LogLevel level) { if (FormatValid(message)) { @@ -57,8 +104,8 @@ namespace Wox.Infrastructure.Logger var unprefixed = parts[2]; var logger = LogManager.GetLogger(prefix); - System.Diagnostics.Debug.WriteLine($"ERROR|{message}"); - logger.Error(unprefixed); + System.Diagnostics.Debug.WriteLine($"{level.Name}|{message}"); + logger.Log(level, unprefixed); } else { @@ -78,25 +125,7 @@ namespace Wox.Infrastructure.Logger var parts = message.Split('|'); var prefix = parts[1]; var unprefixed = parts[2]; - var logger = LogManager.GetLogger(prefix); - - System.Diagnostics.Debug.WriteLine($"ERROR|{message}"); - - logger.Error("-------------------------- Begin exception --------------------------"); - logger.Error(unprefixed); - - do - { - logger.Error($"Exception full name:\n <{e.GetType().FullName}>"); - logger.Error($"Exception message:\n <{e.Message}>"); - logger.Error($"Exception stack trace:\n <{e.StackTrace}>"); - logger.Error($"Exception source:\n <{e.Source}>"); - logger.Error($"Exception target site:\n <{e.TargetSite}>"); - logger.Error($"Exception HResult:\n <{e.HResult}>"); - e = e.InnerException; - } while (e != null); - - logger.Error("-------------------------- End exception --------------------------"); + ExceptionInternal(prefix, unprefixed, e); } else { @@ -104,62 +133,29 @@ namespace Wox.Infrastructure.Logger } #endif } - + + /// example: "|prefix|unprefixed" + public static void Error(string message) + { + LogInternal(message, LogLevel.Error); + } + /// example: "|prefix|unprefixed" public static void Debug(string message) { - if (FormatValid(message)) - { - var parts = message.Split('|'); - var prefix = parts[1]; - var unprefixed = parts[2]; - var logger = LogManager.GetLogger(prefix); - - System.Diagnostics.Debug.WriteLine($"DEBUG|{message}"); - logger.Debug(unprefixed); - } - else - { - LogFaultyFormat(message); - } + LogInternal(message, LogLevel.Debug); } /// example: "|prefix|unprefixed" public static void Info(string message) { - if (FormatValid(message)) - { - var parts = message.Split('|'); - var prefix = parts[1]; - var unprefixed = parts[2]; - var logger = LogManager.GetLogger(prefix); - - System.Diagnostics.Debug.WriteLine($"INFO|{message}"); - logger.Info(unprefixed); - } - else - { - LogFaultyFormat(message); - } + LogInternal(message, LogLevel.Info); } /// example: "|prefix|unprefixed" public static void Warn(string message) { - if (FormatValid(message)) - { - var parts = message.Split('|'); - var prefix = parts[1]; - var unprefixed = parts[2]; - var logger = LogManager.GetLogger(prefix); - - System.Diagnostics.Debug.WriteLine($"WARN|{message}"); - logger.Warn(unprefixed); - } - else - { - LogFaultyFormat(message); - } + LogInternal(message, LogLevel.Warn); } } } \ No newline at end of file diff --git a/Wox.Infrastructure/StringMatcher.cs b/Wox.Infrastructure/StringMatcher.cs index 58ffa336f..d1cc1fdec 100644 --- a/Wox.Infrastructure/StringMatcher.cs +++ b/Wox.Infrastructure/StringMatcher.cs @@ -6,13 +6,14 @@ 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 SearchPrecisionScore 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")] @@ -40,56 +41,104 @@ namespace Wox.Infrastructure } /// - /// refer to https://github.com/mattyork/fuzzy + /// Current method: + /// Character matching + substring matching; + /// 1. Query search string is split into substrings, separator is whitespace. + /// 2. Check each query substring's characters against full compare string, + /// 3. if a character in the substring is matched, loop back to verify the previous character. + /// 4. If previous character also matches, and is the start of the substring, update list. + /// 5. Once the previous character is verified, move on to the next character in the query substring. + /// 6. Move onto the next substring's characters until all substrings are checked. + /// 7. Consider success and move onto scoring if every char or substring without whitespaces matched /// public static MatchResult FuzzySearch(string query, string stringToCompare, MatchOption opt) { if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) return new MatchResult { Success = false }; - + query = query.Trim(); - var len = stringToCompare.Length; - var compareString = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare; - var pattern = opt.IgnoreCase ? query.ToLower() : query; + var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare; + + var queryWithoutCase = opt.IgnoreCase ? query.ToLower() : query; + + var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + int currentQuerySubstringIndex = 0; + var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; + var currentQuerySubstringCharacterIndex = 0; - var sb = new StringBuilder(stringToCompare.Length + (query.Length * (opt.Prefix.Length + opt.Suffix.Length))); - var patternIdx = 0; var firstMatchIndex = -1; + var firstMatchIndexInWord = -1; var lastMatchIndex = 0; - char ch; + bool allQuerySubstringsMatched = false; + bool matchFoundInPreviousLoop = false; + bool allSubstringsContainedInCompareString = true; var indexList = new List(); - for (var idx = 0; idx < len; idx++) + for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++) { - ch = stringToCompare[idx]; - if (compareString[idx] == pattern[patternIdx]) + if (fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex]) { - if (firstMatchIndex < 0) - firstMatchIndex = idx; - lastMatchIndex = idx + 1; - - indexList.Add(idx); - sb.Append(opt.Prefix + ch + opt.Suffix); - patternIdx += 1; - } - else - { - sb.Append(ch); + matchFoundInPreviousLoop = false; + continue; } - // match success, append remain char - if (patternIdx == pattern.Length && (idx + 1) != compareString.Length) + if (firstMatchIndex < 0) { - sb.Append(stringToCompare.Substring(idx + 1)); - break; + // first matched char will become the start of the compared string + firstMatchIndex = compareStringIndex; + } + + if (currentQuerySubstringCharacterIndex == 0) + { + // first letter of current word + matchFoundInPreviousLoop = true; + firstMatchIndexInWord = compareStringIndex; + } + else if (!matchFoundInPreviousLoop) + { + // we want to verify that there is not a better match if this is not a full word + // in order to do so we need to verify all previous chars are part of the pattern + var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex; + + if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, fullStringToCompareWithoutCase, currentQuerySubstring)) + { + matchFoundInPreviousLoop = true; + + // if it's the beginning character of the first query substring that is matched then we need to update start index + firstMatchIndex = currentQuerySubstringIndex == 0 ? startIndexToVerify : firstMatchIndex; + + indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, firstMatchIndexInWord, indexList); + } + } + + lastMatchIndex = compareStringIndex + 1; + indexList.Add(compareStringIndex); + + currentQuerySubstringCharacterIndex++; + + // if finished looping through every character in the current substring + if (currentQuerySubstringCharacterIndex == currentQuerySubstring.Length) + { + // if any of the substrings was not matched then consider as all are not matched + allSubstringsContainedInCompareString = matchFoundInPreviousLoop && allSubstringsContainedInCompareString; + + currentQuerySubstringIndex++; + + allQuerySubstringsMatched = AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length); + if (allQuerySubstringsMatched) + break; + + // otherwise move to the next query substring + currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; + currentQuerySubstringCharacterIndex = 0; } } - - // return rendered string if we have a match for every char - if (patternIdx == pattern.Length) + + // proceed to calculate score if every char or substring without whitespaces matched + if (allQuerySubstringsMatched) { - var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex); + var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); var pinyinScore = ScoreForPinyin(stringToCompare, query); var result = new MatchResult @@ -105,7 +154,44 @@ namespace Wox.Infrastructure return new MatchResult { Success = false }; } - private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen) + private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex, + string fullStringToCompareWithoutCase, string currentQuerySubstring) + { + var allMatch = true; + for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++) + { + if (fullStringToCompareWithoutCase[startIndexToVerify + indexToCheck] != + currentQuerySubstring[indexToCheck]) + { + allMatch = false; + } + } + + return allMatch; + } + + private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, int firstMatchIndexInWord, List indexList) + { + var updatedList = new List(); + + indexList.RemoveAll(x => x >= firstMatchIndexInWord); + + updatedList.AddRange(indexList); + + for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++) + { + updatedList.Add(startIndexToVerify + indexToCheck); + } + + return updatedList; + } + + private static bool AllQuerySubstringsMatched(int currentQuerySubstringIndex, int querySubstringsLength) + { + return currentQuerySubstringIndex >= querySubstringsLength; + } + + private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, bool allSubstringsContainedInCompareString) { // 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, @@ -122,6 +208,13 @@ namespace Wox.Infrastructure score += 10; } + if (allSubstringsContainedInCompareString) + { + int count = query.Count(c => !char.IsWhiteSpace(c)); + int factor = count < 4 ? 10 : 5; + score += factor * count; + } + return score; } @@ -143,11 +236,11 @@ namespace Wox.Infrastructure { if (Alphabet.ContainsChinese(source)) { - var combination = Alphabet.PinyinComination(source); + var combination = Alphabet.PinyinComination(source); var pinyinScore = combination .Select(pinyin => FuzzySearch(target, string.Join("", pinyin)).Score) .Max(); - var acronymScore = combination.Select(Alphabet.Acronym) + var acronymScore = combination.Select(Alphabet.Acronym) .Select(pinyin => FuzzySearch(target, pinyin).Score) .Max(); var score = Math.Max(pinyinScore, acronymScore); @@ -162,7 +255,7 @@ namespace Wox.Infrastructure { return 0; } - } + } } public class MatchResult @@ -178,6 +271,7 @@ namespace Wox.Infrastructure /// The raw calculated search score without any search precision filtering applied. /// private int _rawScore; + public int RawScore { get { return _rawScore; } @@ -200,10 +294,7 @@ namespace Wox.Infrastructure private bool IsSearchPrecisionScoreMet(int score) { - var precisionScore = (SearchPrecisionScore)Enum.Parse( - typeof(SearchPrecisionScore), - UserSettingSearchPrecision ?? SearchPrecisionScore.Regular.ToString()); - return score >= (int)precisionScore; + return score >= (int)UserSettingSearchPrecision; } private int ApplySearchPrecisionFilter(int score) @@ -214,22 +305,18 @@ namespace Wox.Infrastructure public class MatchOption { - public MatchOption() - { - Prefix = ""; - Suffix = ""; - IgnoreCase = true; - } - /// /// prefix of match char, use for hightlight /// - public string Prefix { get; set; } + [Obsolete("this is never used")] + public string Prefix { get; set; } = ""; + /// /// suffix of match char, use for hightlight /// - public string Suffix { get; set; } + [Obsolete("this is never used")] + public string Suffix { get; set; } = ""; - public bool IgnoreCase { get; set; } + public bool IgnoreCase { get; set; } = true; } -} +} \ No newline at end of file diff --git a/Wox.Infrastructure/UserSettings/Settings.cs b/Wox.Infrastructure/UserSettings/Settings.cs index de5a2e662..b11ec069c 100644 --- a/Wox.Infrastructure/UserSettings/Settings.cs +++ b/Wox.Infrastructure/UserSettings/Settings.cs @@ -36,14 +36,30 @@ namespace Wox.Infrastructure.UserSettings } - private string _querySearchPrecision { get; set; } = StringMatcher.SearchPrecisionScore.Regular.ToString(); - public string QuerySearchPrecision + internal StringMatcher.SearchPrecisionScore QuerySearchPrecision { get; private set; } = StringMatcher.SearchPrecisionScore.Regular; + + public string QuerySearchPrecisionString { - get { return _querySearchPrecision; } + get { return QuerySearchPrecision.ToString(); } set { - _querySearchPrecision = value; - StringMatcher.UserSettingSearchPrecision = value; + try + { + var precisionScore = (StringMatcher.SearchPrecisionScore)Enum + .Parse(typeof(StringMatcher.SearchPrecisionScore), value); + + QuerySearchPrecision = precisionScore; + StringMatcher.UserSettingSearchPrecision = precisionScore; + } + catch (ArgumentException e) + { + Logger.Log.Exception(nameof(Settings), "Failed to load QuerySearchPrecisionString value from Settings file", e); + + QuerySearchPrecision = StringMatcher.SearchPrecisionScore.Regular; + StringMatcher.UserSettingSearchPrecision = StringMatcher.SearchPrecisionScore.Regular; + + throw; + } } } diff --git a/Wox.Infrastructure/Wox.Infrastructure.csproj b/Wox.Infrastructure/Wox.Infrastructure.csproj index af76894ed..bd14c5603 100644 --- a/Wox.Infrastructure/Wox.Infrastructure.csproj +++ b/Wox.Infrastructure/Wox.Infrastructure.csproj @@ -71,6 +71,7 @@ + diff --git a/Wox.Infrastructure/Wox.cs b/Wox.Infrastructure/Wox.cs index fbab671ed..396ee0bb1 100644 --- a/Wox.Infrastructure/Wox.cs +++ b/Wox.Infrastructure/Wox.cs @@ -29,7 +29,6 @@ namespace Wox.Infrastructure public static readonly string DataDirectory = DetermineDataDirectory(); public static readonly string PluginsDirectory = Path.Combine(DataDirectory, Plugins); public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins); - public const string Repository = "https://github.com/Wox-launcher/Wox"; public const string Issue = "https://github.com/Wox-launcher/Wox/issues/new"; public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion; diff --git a/Wox.Test/FuzzyMatcherTest.cs b/Wox.Test/FuzzyMatcherTest.cs index 21563f91f..f2de39a28 100644 --- a/Wox.Test/FuzzyMatcherTest.cs +++ b/Wox.Test/FuzzyMatcherTest.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Linq; using NUnit.Framework; using Wox.Infrastructure; -using Wox.Infrastructure.UserSettings; using Wox.Plugin; namespace Wox.Test @@ -12,17 +11,25 @@ namespace Wox.Test [TestFixture] public class FuzzyMatcherTest { + private const string Chrome = "Chrome"; + private const string CandyCrushSagaFromKing = "Candy Crush Saga from King"; + private const string HelpCureHopeRaiseOnMindEntityChrome = "Help cure hope raise on mind entity Chrome"; + private const string UninstallOrChangeProgramsOnYourComputer = "Uninstall or change programs on your computer"; + private const string LastIsChrome = "Last is chrome"; + private const string OneOneOneOne = "1111"; + private const string MicrosoftSqlServerManagementStudio = "Microsoft SQL Server Management Studio"; + public List GetSearchStrings() => new List { - "Chrome", + Chrome, "Choose which programs you want Windows to use for activities like web browsing, editing photos, sending e-mail, and playing music.", - "Help cure hope raise on mind entity Chrome ", - "Candy Crush Saga from King", - "Uninstall or change programs on your computer", + HelpCureHopeRaiseOnMindEntityChrome, + CandyCrushSagaFromKing, + UninstallOrChangeProgramsOnYourComputer, "Add, change, and manage fonts on your computer", - "Last is chrome", - "1111" + LastIsChrome, + OneOneOneOne }; public List GetPrecisionScores() @@ -76,17 +83,17 @@ namespace Wox.Test Assert.True(scoreResult == 0); } - + [TestCase("chr")] [TestCase("chrom")] - [TestCase("chrome")] + [TestCase("chrome")] [TestCase("cand")] [TestCase("cpywa")] [TestCase("ccs")] public void WhenGivenStringsAndAppliedPrecisionFilteringThenShouldReturnGreaterThanPrecisionScoreResults(string searchTerm) { var results = new List(); - + foreach (var str in GetSearchStrings()) { results.Add(new Result @@ -94,7 +101,7 @@ namespace Wox.Test Title = str, Score = StringMatcher.FuzzySearch(searchTerm, str).Score }); - } + } foreach (var precisionScore in GetPrecisionScores()) { @@ -114,78 +121,103 @@ namespace Wox.Test } } - [TestCase("chrome")] - public void WhenGivenStringsForCalScoreMethodThenShouldReturnCurrentScoring(string searchTerm) + [TestCase(Chrome, Chrome, 137)] + [TestCase(Chrome, LastIsChrome, 83)] + [TestCase(Chrome, HelpCureHopeRaiseOnMindEntityChrome, 21)] + [TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 15)] + [TestCase(Chrome, CandyCrushSagaFromKing, 0)] + [TestCase("sql", MicrosoftSqlServerManagementStudio, 56)] + [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 79)]//double spacing intended + public void WhenGivenQueryStringThenShouldReturnCurrentScoring(string queryString, string compareString, int expectedScore) { - var searchStrings = new List - { - "Chrome",//SCORE: 107 - "Last is chrome",//SCORE: 53 - "Help cure hope raise on mind entity Chrome",//SCORE: 21 - "Uninstall or change programs on your computer", //SCORE: 15 - "Candy Crush Saga from King"//SCORE: 0 - } - .OrderByDescending(x => x) - .ToList(); + // When, Given + var rawScore = StringMatcher.FuzzySearch(queryString, compareString).RawScore; - var results = new List(); - foreach (var str in searchStrings) - { - results.Add(new Result - { - Title = str, - Score = StringMatcher.FuzzySearch(searchTerm, str).RawScore - }); - } - - var orderedResults = results.OrderByDescending(x => x.Title).ToList(); - - Debug.WriteLine(""); - Debug.WriteLine("###############################################"); - Debug.WriteLine("SEARCHTERM: " + searchTerm); - foreach (var item in orderedResults) - { - Debug.WriteLine("SCORE: " + item.Score.ToString() + ", FoundString: " + item.Title); - } - Debug.WriteLine("###############################################"); - Debug.WriteLine(""); - - Assert.IsTrue(orderedResults[0].Score == 15 && orderedResults[0].Title == searchStrings[0]); - Assert.IsTrue(orderedResults[1].Score == 53 && orderedResults[1].Title == searchStrings[1]); - Assert.IsTrue(orderedResults[2].Score == 21 && orderedResults[2].Title == searchStrings[2]); - Assert.IsTrue(orderedResults[3].Score == 107 && orderedResults[3].Title == searchStrings[3]); - Assert.IsTrue(orderedResults[4].Score == 0 && orderedResults[4].Title == searchStrings[4]); + // Should + Assert.AreEqual(expectedScore, rawScore, $"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}"); } - [TestCase("goo", "Google Chrome", (int)StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("chr", "Google Chrome", (int)StringMatcher.SearchPrecisionScore.Low, true)] - [TestCase("chr", "Chrome", (int)StringMatcher.SearchPrecisionScore.Regular, true)] - [TestCase("chr", "Help cure hope raise on mind entity Chrome", (int)StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("chr", "Help cure hope raise on mind entity Chrome", (int)StringMatcher.SearchPrecisionScore.Low, true)] - [TestCase("chr", "Candy Crush Saga from King", (int)StringMatcher.SearchPrecisionScore.Regular, false)] - [TestCase("chr", "Candy Crush Saga from King", (int)StringMatcher.SearchPrecisionScore.None, true)] - [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)] + [TestCase("goo", "Google Chrome", StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Google Chrome", StringMatcher.SearchPrecisionScore.Low, true)] + [TestCase("chr", "Chrome", StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("chr", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Low, true)] + [TestCase("chr", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("chr", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.None, true)] + [TestCase("ccs", "Candy Crush Saga from King", StringMatcher.SearchPrecisionScore.Low, true)] + [TestCase("cand", "Candy Crush Saga from King",StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("cand", "Help cure hope raise on mind entity Chrome", StringMatcher.SearchPrecisionScore.Regular, false)] public void WhenGivenDesiredPrecisionThenShouldReturnAllResultsGreaterOrEqual( - string queryString, - string compareString, - int expectedPrecisionScore, + string queryString, + string compareString, + StringMatcher.SearchPrecisionScore expectedPrecisionScore, bool expectedPrecisionResult) { - var expectedPrecisionString = (StringMatcher.SearchPrecisionScore)expectedPrecisionScore; - StringMatcher.UserSettingSearchPrecision = expectedPrecisionString.ToString(); + // When + StringMatcher.UserSettingSearchPrecision = expectedPrecisionScore; + + // Given var matchResult = StringMatcher.FuzzySearch(queryString, compareString); Debug.WriteLine(""); Debug.WriteLine("###############################################"); - Debug.WriteLine($"SearchTerm: {queryString} PrecisionLevelSetAt: {expectedPrecisionString} ({expectedPrecisionScore})"); - Debug.WriteLine($"SCORE: {matchResult.Score.ToString()}, ComparedString: {compareString}"); + Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); + Debug.WriteLine($"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); - var matchPrecisionResult = matchResult.IsSearchPrecisionScoreMet(); - Assert.IsTrue(matchPrecisionResult == expectedPrecisionResult); + // Should + Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + $"Query:{queryString}{Environment.NewLine} " + + $"Compare:{compareString}{Environment.NewLine}" + + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + + $"Precision Score: {(int)expectedPrecisionScore}"); + } + + [TestCase("exce", "OverLeaf-Latex: An online LaTeX editor", StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("term", "Windows Terminal (Preview)", StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("sql s managa", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("sql' s manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("sql s manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("sql manag", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("sql", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("sql serv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("sqlserv", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("sql servman", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("sql serv man", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("sql studio", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("mic", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("chr", "Shutdown", StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("mssms", MicrosoftSqlServerManagementStudio, StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("chr", "Change settings for text-to-speech and for speech recognition (if installed).", StringMatcher.SearchPrecisionScore.Regular, false)] + [TestCase("ch r", "Change settings for text-to-speech and for speech recognition (if installed).", StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("a test", "This is a test", StringMatcher.SearchPrecisionScore.Regular, true)] + [TestCase("test", "This is a test", StringMatcher.SearchPrecisionScore.Regular, true)] + public void WhenGivenQueryShouldReturnResultsContainingAllQuerySubstrings( + string queryString, + string compareString, + StringMatcher.SearchPrecisionScore expectedPrecisionScore, + bool expectedPrecisionResult) + { + // When + StringMatcher.UserSettingSearchPrecision = expectedPrecisionScore; + + // Given + var matchResult = StringMatcher.FuzzySearch(queryString, compareString); + + Debug.WriteLine(""); + Debug.WriteLine("###############################################"); + Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); + Debug.WriteLine($"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})"); + Debug.WriteLine("###############################################"); + Debug.WriteLine(""); + + // Should + Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + $"Query:{queryString}{Environment.NewLine} " + + $"Compare:{compareString}{Environment.NewLine}" + + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + + $"Precision Score: {(int)expectedPrecisionScore}"); } } -} +} \ No newline at end of file diff --git a/Wox/App.config b/Wox/App.config index 0fa0fe8bd..aef034f76 100644 --- a/Wox/App.config +++ b/Wox/App.config @@ -1,7 +1,19 @@  + + +
+ + + + + + https://github.com/Wox-launcher/Wox + + + \ No newline at end of file diff --git a/Wox/App.xaml.cs b/Wox/App.xaml.cs index 9436df475..6d6716a86 100644 --- a/Wox/App.xaml.cs +++ b/Wox/App.xaml.cs @@ -25,6 +25,7 @@ namespace Wox private Settings _settings; private MainViewModel _mainVM; private SettingWindowViewModel _settingsVM; + private readonly Updater _updater = new Updater(Wox.Properties.Settings.Default.GithubRepo); [STAThread] public static void Main() @@ -50,7 +51,7 @@ namespace Wox ImageLoader.Initialize(); - _settingsVM = new SettingWindowViewModel(); + _settingsVM = new SettingWindowViewModel(_updater); _settings = _settingsVM.Settings; Alphabet.Initialize(_settings); @@ -111,12 +112,12 @@ namespace Wox var timer = new Timer(1000 * 60 * 60 * 5); timer.Elapsed += async (s, e) => { - await Updater.UpdateApp(); + await _updater.UpdateApp(); }; timer.Start(); // check updates on startup - await Updater.UpdateApp(); + await _updater.UpdateApp(); } }); } diff --git a/Wox/MainWindow.xaml b/Wox/MainWindow.xaml index 7dfe8cd1e..d50411b83 100644 --- a/Wox/MainWindow.xaml +++ b/Wox/MainWindow.xaml @@ -87,4 +87,4 @@ - \ No newline at end of file + diff --git a/Wox/Properties/Settings.Designer.cs b/Wox/Properties/Settings.Designer.cs index 7a4226349..a61339f5e 100644 --- a/Wox/Properties/Settings.Designer.cs +++ b/Wox/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Wox.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.3.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -22,5 +22,14 @@ namespace Wox.Properties { return defaultInstance; } } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("https://github.com/Wox-launcher/Wox")] + public string GithubRepo { + get { + return ((string)(this["GithubRepo"])); + } + } } } diff --git a/Wox/Properties/Settings.settings b/Wox/Properties/Settings.settings index a585a6308..1fc52390b 100644 --- a/Wox/Properties/Settings.settings +++ b/Wox/Properties/Settings.settings @@ -1,7 +1,9 @@  - - - - - + + + + + https://github.com/Wox-launcher/Wox + + \ No newline at end of file diff --git a/Wox/ReportWindow.xaml.cs b/Wox/ReportWindow.xaml.cs index 38509a728..ac7e986fd 100644 --- a/Wox/ReportWindow.xaml.cs +++ b/Wox/ReportWindow.xaml.cs @@ -23,7 +23,7 @@ namespace Wox private void SetException(Exception exception) { - string path = Path.Combine(Constant.DataDirectory, Log.DirectoryName, Constant.Version); + string path = Log.CurrentLogDirectory; var directory = new DirectoryInfo(path); var log = directory.GetFiles().OrderByDescending(f => f.LastWriteTime).First(); diff --git a/Wox/SettingWindow.xaml b/Wox/SettingWindow.xaml index 9a5146c7e..a6f23814f 100644 --- a/Wox/SettingWindow.xaml +++ b/Wox/SettingWindow.xaml @@ -62,7 +62,7 @@ + SelectedItem="{Binding Settings.QuerySearchPrecisionString}" /> diff --git a/Wox/SettingWindow.xaml.cs b/Wox/SettingWindow.xaml.cs index c041d7ddd..67d67351e 100644 --- a/Wox/SettingWindow.xaml.cs +++ b/Wox/SettingWindow.xaml.cs @@ -260,53 +260,16 @@ namespace Wox #region Proxy private void OnTestProxyClick(object sender, RoutedEventArgs e) - { - if (string.IsNullOrEmpty(_settings.Proxy.Server)) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("serverCantBeEmpty")); - return; - } - if (_settings.Proxy.Port <= 0) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("portCantBeEmpty")); - return; - } - - HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Infrastructure.Constant.Repository); - if (string.IsNullOrEmpty(_settings.Proxy.UserName) || string.IsNullOrEmpty(_settings.Proxy.Password)) - { - request.Proxy = new WebProxy(_settings.Proxy.Server, _settings.Proxy.Port); - } - else - { - request.Proxy = new WebProxy(_settings.Proxy.Server, _settings.Proxy.Port) - { - Credentials = new NetworkCredential(_settings.Proxy.UserName, _settings.Proxy.Password) - }; - } - try - { - var response = (HttpWebResponse)request.GetResponse(); - if (response.StatusCode == HttpStatusCode.OK) - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("proxyIsCorrect")); - } - else - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("proxyConnectFailed")); - } - } - catch - { - MessageBox.Show(InternationalizationManager.Instance.GetTranslation("proxyConnectFailed")); - } + { // TODO: change to command + var msg = _viewModel.TestProxy(); + MessageBox.Show(msg); // TODO: add message box service } #endregion private async void OnCheckUpdates(object sender, RoutedEventArgs e) { - await Updater.UpdateApp(); + _viewModel.UpdateApp(); // TODO: change to command } private void OnRequestNavigate(object sender, RequestNavigateEventArgs e) diff --git a/Wox/Settings.cs b/Wox/Settings.cs new file mode 100644 index 000000000..b45d4a5a1 --- /dev/null +++ b/Wox/Settings.cs @@ -0,0 +1,28 @@ +namespace Wox.Properties { + + + // This class allows you to handle specific events on the settings class: + // The SettingChanging event is raised before a setting's value is changed. + // The PropertyChanged event is raised after a setting's value is changed. + // The SettingsLoaded event is raised after the setting values are loaded. + // The SettingsSaving event is raised before the setting values are saved. + internal sealed partial class Settings { + + public Settings() { + // // To add event handlers for saving and changing settings, uncomment the lines below: + // + // this.SettingChanging += this.SettingChangingEventHandler; + // + // this.SettingsSaving += this.SettingsSavingEventHandler; + // + } + + private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e) { + // Add code to handle the SettingChangingEvent event here. + } + + private void SettingsSavingEventHandler(object sender, System.ComponentModel.CancelEventArgs e) { + // Add code to handle the SettingsSaving event here. + } + } +} diff --git a/Wox/ViewModel/MainViewModel.cs b/Wox/ViewModel/MainViewModel.cs index a7e17d9a7..bd1314a27 100644 --- a/Wox/ViewModel/MainViewModel.cs +++ b/Wox/ViewModel/MainViewModel.cs @@ -13,7 +13,6 @@ using Wox.Core.Resource; using Wox.Helper; using Wox.Infrastructure; using Wox.Infrastructure.Hotkey; -using Wox.Infrastructure.Image; using Wox.Infrastructure.Storage; using Wox.Infrastructure.UserSettings; using Wox.Plugin; @@ -25,7 +24,7 @@ namespace Wox.ViewModel { #region Private Fields - private bool _queryHasReturn; + private bool _isQueryRunning; private Query _lastQuery; private string _queryTextBeforeLeaveResults; @@ -41,7 +40,7 @@ namespace Wox.ViewModel private CancellationToken _updateToken; private bool _saved; - private Internationalization _translator = InternationalizationManager.Instance; + private readonly Internationalization _translator = InternationalizationManager.Instance; #endregion @@ -312,7 +311,7 @@ namespace Wox.ViewModel { var filtered = results.Where ( - r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() + r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() || StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() ).ToList(); ContextMenu.AddResults(filtered, id); @@ -371,63 +370,59 @@ namespace Wox.ViewModel if (!string.IsNullOrEmpty(QueryText)) { _updateSource?.Cancel(); - _updateSource = new CancellationTokenSource(); - _updateToken = _updateSource.Token; + var currentUpdateSource = new CancellationTokenSource(); + _updateSource = currentUpdateSource; + var currentCancellationToken = _updateSource.Token; + _updateToken = currentCancellationToken; ProgressBarVisibility = Visibility.Hidden; - _queryHasReturn = false; + _isQueryRunning = true; var query = PluginManager.QueryInit(QueryText.Trim()); if (query != null) { // handle the exclusiveness of plugin using action keyword - string lastKeyword = _lastQuery.ActionKeyword; - string keyword = query.ActionKeyword; - if (string.IsNullOrEmpty(lastKeyword)) - { - if (!string.IsNullOrEmpty(keyword)) - { - Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); - } - } - else - { - if (string.IsNullOrEmpty(keyword)) - { - Results.RemoveResultsFor(PluginManager.NonGlobalPlugins[lastKeyword].Metadata); - } - else if (lastKeyword != keyword) - { - Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); - } - } + RemoveOldQueryResults(query); _lastQuery = query; - Task.Delay(200, _updateToken).ContinueWith(_ => - { - if (query.RawQuery == _lastQuery.RawQuery && !_queryHasReturn) + Task.Delay(200, currentCancellationToken).ContinueWith(_ => + { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet + if (currentUpdateSource == _updateSource && _isQueryRunning) { ProgressBarVisibility = Visibility.Visible; } - }, _updateToken); + }, currentCancellationToken); var plugins = PluginManager.ValidPluginsForQuery(query); Task.Run(() => { - Parallel.ForEach(plugins, plugin => + // so looping will stop once it was cancelled + var parallelOptions = new ParallelOptions { CancellationToken = currentCancellationToken }; + try { - var config = _settings.PluginSettings.Plugins[plugin.Metadata.ID]; - if (!config.Disabled) + Parallel.ForEach(plugins, parallelOptions, plugin => { - var results = PluginManager.QueryForPlugin(plugin, query); - UpdateResultView(results, plugin.Metadata, query); - } - }); + var config = _settings.PluginSettings.Plugins[plugin.Metadata.ID]; + if (!config.Disabled) + { + var results = PluginManager.QueryForPlugin(plugin, query); + UpdateResultView(results, plugin.Metadata, query); + } + }); + } + catch (OperationCanceledException) + { + // nothing to do here + } + // 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); + _isQueryRunning = false; + if (currentUpdateSource == _updateSource) + { // update to hidden if this is still the current query + ProgressBarVisibility = Visibility.Hidden; + } + }, currentCancellationToken); } } else @@ -437,6 +432,30 @@ namespace Wox.ViewModel } } + private void RemoveOldQueryResults(Query query) + { + string lastKeyword = _lastQuery.ActionKeyword; + string keyword = query.ActionKeyword; + if (string.IsNullOrEmpty(lastKeyword)) + { + if (!string.IsNullOrEmpty(keyword)) + { + Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); + } + } + else + { + if (string.IsNullOrEmpty(keyword)) + { + Results.RemoveResultsFor(PluginManager.NonGlobalPlugins[lastKeyword].Metadata); + } + else if (lastKeyword != keyword) + { + Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata); + } + } + } + private Result ContextMenuTopMost(Result result) { @@ -660,4 +679,4 @@ namespace Wox.ViewModel #endregion } -} +} \ No newline at end of file diff --git a/Wox/ViewModel/ResultsViewModel.cs b/Wox/ViewModel/ResultsViewModel.cs index e36ddc94d..76a7ee75b 100644 --- a/Wox/ViewModel/ResultsViewModel.cs +++ b/Wox/ViewModel/ResultsViewModel.cs @@ -156,14 +156,14 @@ namespace Wox.ViewModel private List NewResults(List newRawResults, string resultId) { var results = Results.ToList(); - var newResults = newRawResults.Select(r => new ResultViewModel(r)).ToList(); + var newResults = newRawResults.Select(r => new ResultViewModel(r)).ToList(); var oldResults = results.Where(r => r.Result.PluginID == resultId).ToList(); // 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))) .ToList(); - + // remove result of relative complement of B in A foreach (var result in oldResults.Except(sameResults)) { @@ -248,6 +248,10 @@ namespace Wox.ViewModel } } + /// + /// Update the results collection with new results, try to keep identical results + /// + /// public void Update(List newItems) { int newCount = newItems.Count; @@ -259,7 +263,7 @@ namespace Wox.ViewModel ResultViewModel oldResult = this[i]; ResultViewModel newResult = newItems[i]; if (!oldResult.Equals(newResult)) - { + { // result is not the same update it in the current index this[i] = newResult; } else if (oldResult.Result.Score != newResult.Result.Score) @@ -286,4 +290,4 @@ namespace Wox.ViewModel } } } -} +} \ No newline at end of file diff --git a/Wox/ViewModel/SettingWindowViewModel.cs b/Wox/ViewModel/SettingWindowViewModel.cs index 67b8d7af0..923e456b0 100644 --- a/Wox/ViewModel/SettingWindowViewModel.cs +++ b/Wox/ViewModel/SettingWindowViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Media; @@ -20,10 +21,12 @@ namespace Wox.ViewModel { public class SettingWindowViewModel : BaseModel { + private readonly Updater _updater; private readonly WoxJsonStorage _storage; - public SettingWindowViewModel() + public SettingWindowViewModel(Updater updater) { + _updater = updater; _storage = new WoxJsonStorage(); Settings = _storage.Load(); Settings.PropertyChanged += (s, e) => @@ -39,6 +42,10 @@ namespace Wox.ViewModel public Settings Settings { get; set; } + public async void UpdateApp() + { + await _updater.UpdateApp(); + } public void Save() { @@ -73,7 +80,7 @@ namespace Wox.ViewModel public List QuerySearchPrecisionStrings { get - { + { var precisionStrings = new List(); var enumList = Enum.GetValues(typeof(StringMatcher.SearchPrecisionScore)).Cast().ToList(); @@ -88,6 +95,50 @@ namespace Wox.ViewModel public List Languages => _translater.LoadAvailableLanguages(); public IEnumerable MaxResultsRange => Enumerable.Range(2, 16); + public string TestProxy() + { + var proxyServer = Settings.Proxy.Server; + var proxyUserName = Settings.Proxy.UserName; + if (string.IsNullOrEmpty(proxyServer)) + { + return InternationalizationManager.Instance.GetTranslation("serverCantBeEmpty"); + } + if (Settings.Proxy.Port <= 0) + { + return InternationalizationManager.Instance.GetTranslation("portCantBeEmpty"); + } + + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_updater.GitHubRepository); + + if (string.IsNullOrEmpty(proxyUserName) || string.IsNullOrEmpty(Settings.Proxy.Password)) + { + request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port); + } + else + { + request.Proxy = new WebProxy(proxyServer, Settings.Proxy.Port) + { + Credentials = new NetworkCredential(proxyUserName, Settings.Proxy.Password) + }; + } + try + { + var response = (HttpWebResponse)request.GetResponse(); + if (response.StatusCode == HttpStatusCode.OK) + { + return InternationalizationManager.Instance.GetTranslation("proxyIsCorrect"); + } + else + { + return InternationalizationManager.Instance.GetTranslation("proxyConnectFailed"); + } + } + catch + { + return InternationalizationManager.Instance.GetTranslation("proxyConnectFailed"); + } + } + #endregion #region plugin @@ -220,7 +271,7 @@ namespace Wox.ViewModel }, new Result { - Title = $"Open Source: {Constant.Repository}", + Title = $"Open Source: {_updater.GitHubRepository}", SubTitle = "Please star it!" } }; @@ -330,8 +381,8 @@ namespace Wox.ViewModel #region about - public static string Github => Constant.Repository; - public static string ReleaseNotes => @"https://github.com/Wox-launcher/Wox/releases/latest"; + public string Github => _updater.GitHubRepository; + public string ReleaseNotes => _updater.GitHubRepository + @"/releases/latest"; public static string Version => Constant.Version; public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); #endregion diff --git a/Wox/Wox.csproj b/Wox/Wox.csproj index 4a4dc62bf..77b7e16d0 100644 --- a/Wox/Wox.csproj +++ b/Wox/Wox.csproj @@ -165,6 +165,7 @@ ResultListBox.xaml + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 32aa5f677..5307ec006 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,7 +8,7 @@ trigger: - dev pool: - vmImage: 'vs2017-win2016' #'windows-latest' + vmImage: 'vs2017-win2016' #'due to windows SDK dependency for building UWP project' variables: solution: '**/*.sln'