diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index 2882cb8f03e..2a422e8bfcc 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -1,16 +1,18 @@ -using CommunityToolkit.Mvvm.DependencyInjection; -using Flow.Launcher.Plugin.SharedModels; -using System; +using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Infrastructure { public class StringMatcher { private readonly MatchOption _defaultMatchOption = new(); - + private readonly Settings _settings; public SearchPrecisionScore UserSettingSearchPrecision { get; set; } private readonly IAlphabet _alphabet; @@ -18,13 +20,23 @@ public class StringMatcher public StringMatcher(IAlphabet alphabet, Settings settings) { _alphabet = alphabet; - UserSettingSearchPrecision = settings.QuerySearchPrecision; + _settings = settings; + UserSettingSearchPrecision = _settings.QuerySearchPrecision; + + _settings.PropertyChanged += (sender, e) => + { + switch (e.PropertyName) + { + case nameof(Settings.QuerySearchPrecision): + UserSettingSearchPrecision = _settings.QuerySearchPrecision; + break; + } + }; } // This is a workaround to allow unit tests to set the instance - public StringMatcher(IAlphabet alphabet) + public StringMatcher(IAlphabet alphabet) : this(alphabet, new Settings()) { - _alphabet = alphabet; } public static MatchResult FuzzySearch(string query, string stringToCompare) @@ -80,10 +92,20 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption int acronymsTotalCount = 0; int acronymsMatched = 0; - var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToLower() : stringToCompare; - var queryWithoutCase = opt.IgnoreCase ? query.ToLower() : query; + var queryToCompare = query; + bool ignoreAccents = _settings.IgnoreAccents; + bool ignoreCase = opt.IgnoreCase; + + if (ignoreAccents) + { + queryToCompare = StringNormalize.Normalize(queryToCompare); + } + else if (ignoreCase) + { + queryToCompare = queryToCompare.ToLower(); + } - var querySubstrings = queryWithoutCase.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var querySubstrings = queryToCompare.Split([' '], StringSplitOptions.RemoveEmptyEntries); int currentQuerySubstringIndex = 0; var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; var currentQuerySubstringCharacterIndex = 0; @@ -98,7 +120,9 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption var indexList = new List(); List spaceIndices = new List(); - for (var compareStringIndex = 0; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++) + for (var compareStringIndex = 0; + compareStringIndex < stringToCompare.Length; + compareStringIndex++) { // If acronyms matching successfully finished, this gets the remaining not matched acronyms for score calculation if (currentAcronymQueryIndex >= query.Length && acronymsMatched == query.Length) @@ -112,16 +136,26 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption currentAcronymQueryIndex >= query.Length && allQuerySubstringsMatched) break; + char compareChar = stringToCompare[compareStringIndex]; + if (ignoreAccents) + { + compareChar = StringNormalize.NormalizeChar(compareChar); + } + else if (ignoreCase) + { + compareChar = char.ToLower(compareChar); + } + // To maintain a list of indices which correspond to spaces in the string to compare // To populate the list only for the first query substring - if (fullStringToCompareWithoutCase[compareStringIndex] == ' ' && currentQuerySubstringIndex == 0) + if (compareChar == ' ' && currentQuerySubstringIndex == 0) spaceIndices.Add(compareStringIndex); // Acronym Match if (IsAcronym(stringToCompare, compareStringIndex)) { - if (fullStringToCompareWithoutCase[compareStringIndex] == - queryWithoutCase[currentAcronymQueryIndex]) + if (compareChar == + queryToCompare[currentAcronymQueryIndex]) { acronymMatchData.Add(compareStringIndex); acronymsMatched++; @@ -133,7 +167,7 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption if (IsAcronymCount(stringToCompare, compareStringIndex)) acronymsTotalCount++; - if (allQuerySubstringsMatched || fullStringToCompareWithoutCase[compareStringIndex] != + if (allQuerySubstringsMatched || compareChar != currentQuerySubstring[currentQuerySubstringCharacterIndex]) { matchFoundInPreviousLoop = false; @@ -160,7 +194,7 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex; if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, - fullStringToCompareWithoutCase, currentQuerySubstring)) + stringToCompare, currentQuerySubstring, ignoreAccents, ignoreCase)) { matchFoundInPreviousLoop = true; @@ -205,7 +239,8 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption if (acronymScore >= (int)UserSettingSearchPrecision) { - acronymMatchData = acronymMatchData.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); + acronymMatchData = acronymMatchData.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x) + .Distinct().ToList(); return new MatchResult(true, UserSettingSearchPrecision, acronymMatchData, acronymScore); } } @@ -218,19 +253,23 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption // firstMatchIndex - nearestSpaceIndex - 1 is to set the firstIndex as the index of the first matched char // preceded by a space e.g. 'world' matching 'hello world' firstIndex would be 0 not 6 // giving more weight than 'we or donald' by allowing the distance calculation to treat the starting position at after the space. - var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, spaceIndices, + var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, + spaceIndices, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); - var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); + var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct() + .ToList(); return new MatchResult(true, UserSettingSearchPrecision, resultList, score); } return new MatchResult(false, UserSettingSearchPrecision); } + private static bool IsAcronym(string stringToCompare, int compareStringIndex) { - if (IsAcronymChar(stringToCompare, compareStringIndex) || IsAcronymNumber(stringToCompare, compareStringIndex)) + if (IsAcronymChar(stringToCompare, compareStringIndex) || + IsAcronymNumber(stringToCompare, compareStringIndex)) return true; return false; @@ -274,19 +313,19 @@ private static int CalculateClosestSpaceIndex(List spaceIndices, int firstM } private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex, - string fullStringToCompareWithoutCase, string currentQuerySubstring) + string stringToCompare, string currentQuerySubstring, bool ignoreAccents, bool ignoreCase) { - var allMatch = true; for (int indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++) { - if (fullStringToCompareWithoutCase[startIndexToVerify + indexToCheck] != - currentQuerySubstring[indexToCheck]) - { - allMatch = false; - } + char c = stringToCompare[startIndexToVerify + indexToCheck]; + if (ignoreAccents) c = StringNormalize.NormalizeChar(c); + else if (ignoreCase) c = char.ToLower(c); + + if (c != currentQuerySubstring[indexToCheck]) + return false; } - return allMatch; + return true; } private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, @@ -312,7 +351,8 @@ private static bool AllQuerySubstringsMatched(int currentQuerySubstringIndex, in return currentQuerySubstringIndex >= querySubstringsLength; } - private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, List spaceIndices, int matchLen, + private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, + List spaceIndices, int matchLen, bool allSubstringsContainedInCompareString) { // A match found near the beginning of a string is scored more than a match found near the end diff --git a/Flow.Launcher.Infrastructure/StringNormalize.cs b/Flow.Launcher.Infrastructure/StringNormalize.cs new file mode 100644 index 00000000000..2c37203fc5c --- /dev/null +++ b/Flow.Launcher.Infrastructure/StringNormalize.cs @@ -0,0 +1,156 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Flow.Launcher.Infrastructure; + +public static class StringNormalize +{ + private static readonly Dictionary AccentMap = new() + { + ['á'] = 'a', + ['à'] = 'a', + ['ã'] = 'a', + ['â'] = 'a', + ['ä'] = 'a', + ['å'] = 'a', + ['ā'] = 'a', + ['ă'] = 'a', + ['ą'] = 'a', + ['é'] = 'e', + ['è'] = 'e', + ['ê'] = 'e', + ['ë'] = 'e', + ['ē'] = 'e', + ['ĕ'] = 'e', + ['ė'] = 'e', + ['ę'] = 'e', + ['ě'] = 'e', + ['í'] = 'i', + ['ì'] = 'i', + ['î'] = 'i', + ['ï'] = 'i', + ['ī'] = 'i', + ['ĭ'] = 'i', + ['į'] = 'i', + ['ı'] = 'i', + ['ó'] = 'o', + ['ò'] = 'o', + ['õ'] = 'o', + ['ô'] = 'o', + ['ö'] = 'o', + ['ø'] = 'o', + ['ō'] = 'o', + ['ŏ'] = 'o', + ['ő'] = 'o', + ['ú'] = 'u', + ['ù'] = 'u', + ['û'] = 'u', + ['ü'] = 'u', + ['ū'] = 'u', + ['ŭ'] = 'u', + ['ů'] = 'u', + ['ű'] = 'u', + ['ų'] = 'u', + ['ç'] = 'c', + ['ć'] = 'c', + ['ĉ'] = 'c', + ['ċ'] = 'c', + ['č'] = 'c', + ['ñ'] = 'n', + ['ń'] = 'n', + ['ņ'] = 'n', + ['ň'] = 'n', + ['ŋ'] = 'n', + ['ý'] = 'y', + ['ÿ'] = 'y', + ['ŷ'] = 'y', + ['ś'] = 's', + ['ŝ'] = 's', + ['ş'] = 's', + ['š'] = 's', + ['ß'] = 's', + ['ź'] = 'z', + ['ż'] = 'z', + ['ž'] = 'z', + ['ł'] = 'l', + ['ď'] = 'd', + ['đ'] = 'd', + ['ĝ'] = 'g', + ['ğ'] = 'g', + ['ġ'] = 'g', + ['ģ'] = 'g', + ['ĥ'] = 'h', + ['ħ'] = 'h', + ['ĵ'] = 'j', + ['ķ'] = 'k', + ['ŕ'] = 'r', + ['ř'] = 'r', + ['ţ'] = 't', + ['ť'] = 't', + ['ŧ'] = 't', + ['æ'] = 'a', + ['œ'] = 'o' + }; + + private const char AccentRangeStart = '\u00DF'; + private const char AccentRangeEnd = '\u017E'; + private static readonly char[] AccentLookup = BuildAccentLookup(); + + private static char[] BuildAccentLookup() + { + var lookup = new char[AccentRangeEnd - AccentRangeStart + 1]; + foreach (var (key, value) in AccentMap) + lookup[key - AccentRangeStart] = value; + return lookup; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static char NormalizeChar(char c) + { + c = char.ToLowerInvariant(c); + if (c >= AccentRangeStart && c <= AccentRangeEnd) + { + var mapped = AccentLookup[c - AccentRangeStart]; + if (mapped != 0) return mapped; + } + + return c; + } + + public static string Normalize(string value) + { + if (string.IsNullOrEmpty(value)) return value; + + int firstChange = -1; + for (int i = 0; i < value.Length; i++) + { + if (NormalizeChar(value[i]) != value[i]) + { + firstChange = i; + break; + } + } + + if (firstChange < 0) return value; + + char[] arrayFromPool = null; + Span buffer = value.Length <= 512 + ? stackalloc char[value.Length] + : (arrayFromPool = ArrayPool.Shared.Rent(value.Length)); + try + { + value.AsSpan(0, firstChange).CopyTo(buffer); + for (int i = firstChange; i < value.Length; i++) + buffer[i] = NormalizeChar(value[i]); + + return new string(buffer.Slice(0, value.Length)); + } + finally + { + if (arrayFromPool != null) + ArrayPool.Shared.Return(arrayFromPool); + } + } +} diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 7524d6c1a79..18840dbdbda 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Text.Json.Serialization; using System.Windows; using System.Windows.Media; -using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; @@ -16,7 +17,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings public class Settings : BaseModel, IHotkeySettings { private FlowLauncherJsonStorage _storage; - private StringMatcher _stringMatcher = null; + + public event EventHandler StringMatcherBehaviorChanged; public void SetStorage(FlowLauncherJsonStorage storage) { @@ -25,13 +27,26 @@ public void SetStorage(FlowLauncherJsonStorage storage) public void Initialize() { - // Initialize dependency injection instances after Ioc.Default is created - _stringMatcher = Ioc.Default.GetRequiredService(); - // Initialize application resources after application is created var settingWindowFont = new FontFamily(SettingWindowFont); Application.Current.Resources["SettingWindowFont"] = settingWindowFont; Application.Current.Resources["ContentControlThemeFontFamily"] = settingWindowFont; + + PropertyChanged += Settings_PropertyChanged; + } + + private void Settings_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(QuerySearchPrecision): + case nameof(ShouldUsePinyin): + case nameof(UseDoublePinyin): + case nameof(DoublePinyinSchema): + case nameof(IgnoreAccents): + StringMatcherBehaviorChanged?.Invoke(this, EventArgs.Empty); + break; + } } public void Save() @@ -403,8 +418,21 @@ public SearchPrecisionScore QuerySearchPrecision if (_querySearchPrecision != value) { _querySearchPrecision = value; - if (_stringMatcher != null) - _stringMatcher.UserSettingSearchPrecision = value; + OnPropertyChanged(); + } + } + } + + private bool _IgnoreAccents = false; + public bool IgnoreAccents + { + get => _IgnoreAccents; + set + { + if (_IgnoreAccents != value) + { + _IgnoreAccents = value; + OnPropertyChanged(); } } } diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index c7d3e49869f..bb903fb2810 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -646,5 +646,10 @@ public interface IPublicAPI /// /// string GetLogDirectory(); + + /// + /// Invoked when the behavior of string matcher has changed which can effect the result of . + /// + event EventHandler StringMatcherBehaviorChanged; } } diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs index 090719642ed..1643b67aff8 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using NUnit.Framework.Legacy; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; @@ -62,7 +63,7 @@ public void MatchTest() }; var results = new List(); - var matcher = new StringMatcher(alphabet); + var matcher = new StringMatcher(alphabet, new Settings()); foreach (var str in sources) { results.Add(new Result @@ -84,7 +85,7 @@ public void MatchTest() public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(string searchString) { var compareString = "Can have rum only in my glass"; - var matcher = new StringMatcher(alphabet); + var matcher = new StringMatcher(alphabet, new Settings()); var scoreResult = matcher.FuzzyMatch(searchString, compareString).RawScore; ClassicAssert.True(scoreResult == 0); @@ -100,7 +101,7 @@ public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreat string searchTerm) { var results = new List(); - var matcher = new StringMatcher(alphabet); + var matcher = new StringMatcher(alphabet, new Settings()); foreach (var str in GetSearchStrings()) { results.Add(new Result @@ -150,7 +151,7 @@ public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring( string queryString, string compareString, int expectedScore) { // When, Given - var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; + var matcher = new StringMatcher(alphabet, new Settings()) { UserSettingSearchPrecision = SearchPrecisionScore.Regular }; var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore; // Should @@ -184,7 +185,7 @@ public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual( bool expectedPrecisionResult) { // When - var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore}; + var matcher = new StringMatcher(alphabet, new Settings()) { UserSettingSearchPrecision = expectedPrecisionScore }; // Given var matchResult = matcher.FuzzyMatch(queryString, compareString); @@ -193,7 +194,7 @@ public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual( Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); Debug.WriteLine( - $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); + $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); @@ -235,7 +236,7 @@ public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings( bool expectedPrecisionResult) { // When - var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = expectedPrecisionScore}; + var matcher = new StringMatcher(alphabet, new Settings()) { UserSettingSearchPrecision = expectedPrecisionScore }; // Given var matchResult = matcher.FuzzyMatch(queryString, compareString); @@ -244,7 +245,7 @@ public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings( Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); Debug.WriteLine( - $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); + $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int)expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); @@ -263,7 +264,7 @@ public void WhenGivenAQuery_Scoring_ShouldGiveMoreWeightToStartOfNewWord( string queryString, string compareString1, string compareString2) { // When - var matcher = new StringMatcher(alphabet) {UserSettingSearchPrecision = SearchPrecisionScore.Regular}; + var matcher = new StringMatcher(alphabet, new Settings()) { UserSettingSearchPrecision = SearchPrecisionScore.Regular }; // Given var compareString1Result = matcher.FuzzyMatch(queryString, compareString1); @@ -296,7 +297,7 @@ public void WhenGivenTwoStrings_Scoring_ShouldGiveMoreWeightToTheStringCloserToI string queryString, string compareString1, string compareString2) { // When - var matcher = new StringMatcher(alphabet) { UserSettingSearchPrecision = SearchPrecisionScore.Regular }; + var matcher = new StringMatcher(alphabet, new Settings()) { UserSettingSearchPrecision = SearchPrecisionScore.Regular }; // Given var compareString1Result = matcher.FuzzyMatch(queryString, compareString1); @@ -326,7 +327,7 @@ public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore( string secondName, string secondDescription, string secondExecutableName) { // Act - var matcher = new StringMatcher(alphabet); + var matcher = new StringMatcher(alphabet, new Settings()); var firstNameMatch = matcher.FuzzyMatch(queryString, firstName).RawScore; var firstDescriptionMatch = matcher.FuzzyMatch(queryString, firstDescription).RawScore; var firstExecutableNameMatch = matcher.FuzzyMatch(queryString, firstExecutableName).RawScore; @@ -335,8 +336,8 @@ public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore( var secondDescriptionMatch = matcher.FuzzyMatch(queryString, secondDescription).RawScore; var secondExecutableNameMatch = matcher.FuzzyMatch(queryString, secondExecutableName).RawScore; - var firstScore = new[] {firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch}.Max(); - var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max(); + var firstScore = new[] { firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch }.Max(); + var secondScore = new[] { secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch }.Max(); // Assert ClassicAssert.IsTrue(firstScore > secondScore, @@ -361,7 +362,7 @@ public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore( public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, string compareString, int desiredScore) { - var matcher = new StringMatcher(alphabet); + var matcher = new StringMatcher(alphabet, new Settings()); var score = matcher.FuzzyMatch(queryString, compareString).Score; ClassicAssert.IsTrue(score == desiredScore, $@"Query: ""{queryString}"" diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 729146a435d..8cf255ca63a 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -125,6 +125,8 @@ Flow Launcher search window is hidden in the tray after starting up. Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. + Ignore accents when searching results + When enabled, you will be able to find results that contain accented characters more easily. Query Search Precision Changes minimum match score required for results. None diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 5cefd2298cb..25c6d788a85 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -651,6 +651,12 @@ public event ActualApplicationThemeChangedEventHandler ActualApplicationThemeCha public string GetLogDirectory() => DataLocation.VersionLogDirectory; + public event EventHandler StringMatcherBehaviorChanged + { + add => _settings.StringMatcherBehaviorChanged += value; + remove => _settings.StringMatcherBehaviorChanged -= value; + } + #endregion #region Private Methods diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index 5ce5187d5dc..1c61e1d62ee 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -196,6 +196,20 @@ public int SearchDelayTimeValue } } + + public bool IgnoreAccents + { + get => Settings.IgnoreAccents; + set + { + if(Settings.IgnoreAccents != value) + { + Settings.IgnoreAccents = value; + OnPropertyChanged(); + } + } + } + public int MaxHistoryResultsToShowValue { get => Settings.MaxHistoryResultsToShowForHomePage; diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 38a0e429f68..5779071534f 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -560,6 +560,20 @@ + + + + + + + + diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 9c84747d265..616df2f8d6a 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -327,6 +327,8 @@ static void MoveFile(string sourcePath, string destinationPath) WatchProgramUpdate(); } + Context.API.StringMatcherBehaviorChanged += API_StringMatcherBehaviorChanged; + static void WatchProgramUpdate() { Win32.WatchProgramUpdate(_settings); @@ -334,6 +336,13 @@ static void WatchProgramUpdate() } } + private void API_StringMatcherBehaviorChanged(object sender, EventArgs e) + { + // Since cache stores the search results based on the old string matcher behavior, + // we need to reset the cache when the string matcher behavior changes + ResetCache(); + } + public static async Task IndexWin32ProgramsAsync(bool resetCache) { await _win32sLock.WaitAsync(); @@ -558,6 +567,7 @@ public async Task ReloadDataAsync() public void Dispose() { + Context.API.StringMatcherBehaviorChanged -= API_StringMatcherBehaviorChanged; Win32.Dispose(); } }