diff --git a/Flow.Launcher.Infrastructure/IAlphabet.cs b/Flow.Launcher.Infrastructure/IAlphabet.cs new file mode 100644 index 00000000000..e79ec0c6d6f --- /dev/null +++ b/Flow.Launcher.Infrastructure/IAlphabet.cs @@ -0,0 +1,22 @@ +namespace Flow.Launcher.Infrastructure +{ + /// + /// Translate a language to English letters using a given rule. + /// + public interface IAlphabet + { + /// + /// Translate a string to English letters, using a given rule. + /// + /// String to translate. + /// + public (string translation, TranslationMapping map) Translate(string stringToTranslate); + + /// + /// Determine if a string can be translated to English letter with this Alphabet. + /// + /// String to translate. + /// + public bool ShouldTranslate(string stringToTranslate); + } +} diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index 7d72359684d..10799c676f1 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -1,203 +1,196 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Text; using JetBrains.Annotations; using Flow.Launcher.Infrastructure.UserSettings; using ToolGood.Words.Pinyin; +using System.Collections.Generic; +using System.Collections.ObjectModel; namespace Flow.Launcher.Infrastructure { - public class TranslationMapping + public class PinyinAlphabet : IAlphabet { - private bool constructed; + private readonly ConcurrentDictionary _pinyinCache = + new(); - private List originalIndexs = new List(); - private List translatedIndexs = new List(); - private int translatedLength = 0; - - public string key { get; private set; } + private Settings _settings; - public void setKey(string key) + public void Initialize([NotNull] Settings settings) { - this.key = key; + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - public void AddNewIndex(int originalIndex, int translatedIndex, int length) + public bool ShouldTranslate(string stringToTranslate) { - if (constructed) - throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); - - originalIndexs.Add(originalIndex); - translatedIndexs.Add(translatedIndex); - translatedIndexs.Add(translatedIndex + length); - translatedLength += length - 1; + return _settings.UseDoublePinyin ? + (!WordsHelper.HasChinese(stringToTranslate) && stringToTranslate.Length % 2 == 0) : + !WordsHelper.HasChinese(stringToTranslate); } - public int MapToOriginalIndex(int translatedIndex) + public (string translation, TranslationMapping map) Translate(string content) { - if (translatedIndex > translatedIndexs.Last()) - return translatedIndex - translatedLength - 1; - - int lowerBound = 0; - int upperBound = originalIndexs.Count - 1; - - int count = 0; - - // Corner case handle - if (translatedIndex < translatedIndexs[0]) - return translatedIndex; - if (translatedIndex > translatedIndexs.Last()) + if (_settings.ShouldUsePinyin) { - int indexDef = 0; - for (int k = 0; k < originalIndexs.Count; k++) + if (!_pinyinCache.TryGetValue(content, out var value)) + { + return BuildCacheFromContent(content); + } + else { - indexDef += translatedIndexs[k * 2 + 1] - translatedIndexs[k * 2]; + return value; } + } + return (content, null); + } - return translatedIndex - indexDef - 1; + private (string translation, TranslationMapping map) BuildCacheFromContent(string content) + { + if (!WordsHelper.HasChinese(content)) + { + return (content, null); } - // Binary Search with Range - for (int i = originalIndexs.Count / 2;; count++) + var resultList = WordsHelper.GetPinyinList(content); + + StringBuilder resultBuilder = new StringBuilder(); + TranslationMapping map = new TranslationMapping(); + + bool pre = false; + + for (int i = 0; i < resultList.Length; i++) { - if (translatedIndex < translatedIndexs[i * 2]) - { - // move to lower middle - upperBound = i; - i = (i + lowerBound) / 2; - } - else if (translatedIndex > translatedIndexs[i * 2 + 1] - 1) + if (content[i] >= 0x3400 && content[i] <= 0x9FD5) { - lowerBound = i; - // move to upper middle - // due to floor of integer division, move one up on corner case - i = (i + upperBound + 1) / 2; + string dp = _settings.UseDoublePinyin ? ToDoublePin(resultList[i]) : resultList[i]; + map.AddNewIndex(i, resultBuilder.Length, dp.Length + 1); + resultBuilder.Append(' '); + resultBuilder.Append(dp); + pre = true; } else - return originalIndexs[i]; - - if (upperBound - lowerBound <= 1 && - translatedIndex > translatedIndexs[lowerBound * 2 + 1] && - translatedIndex < translatedIndexs[upperBound * 2]) { - int indexDef = 0; - - for (int j = 0; j < upperBound; j++) + if (pre) { - indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2]; + pre = false; + resultBuilder.Append(' '); } - return translatedIndex - indexDef - 1; + resultBuilder.Append(resultList[i]); } } - } - public void endConstruct() - { - if (constructed) - throw new InvalidOperationException("Mapping has already been constructed"); - constructed = true; + map.endConstruct(); + + var key = resultBuilder.ToString(); + + return _pinyinCache[content] = (key, map); } - } - /// - /// Translate a language to English letters using a given rule. - /// - public interface IAlphabet - { - /// - /// Translate a string to English letters, using a given rule. - /// - /// String to translate. - /// - public (string translation, TranslationMapping map) Translate(string stringToTranslate); - - /// - /// Determine if a string can be translated to English letter with this Alphabet. - /// - /// String to translate. - /// - public bool CanBeTranslated(string stringToTranslate); - } + #region Double Pinyin - public class PinyinAlphabet : IAlphabet - { - private ConcurrentDictionary _pinyinCache = - new ConcurrentDictionary(); + private static readonly ReadOnlyDictionary special = new(new Dictionary(){ + {"A", "aa"}, + {"Ai", "ai"}, + {"An", "an"}, + {"Ang", "ah"}, + {"Ao", "ao"}, + {"E", "ee"}, + {"Ei", "ei"}, + {"En", "en"}, + {"Er", "er"}, + {"O", "oo"}, + {"Ou", "ou"} + }); - private Settings _settings; - public void Initialize([NotNull] Settings settings) - { - _settings = settings ?? throw new ArgumentNullException(nameof(settings)); - } + private static readonly ReadOnlyDictionary first = new(new Dictionary(){ + {"Ch", "i"}, + {"Sh", "u"}, + {"Zh", "v"} + }); - public bool CanBeTranslated(string stringToTranslate) - { - return WordsHelper.HasChinese(stringToTranslate); - } - public (string translation, TranslationMapping map) Translate(string content) + private static readonly ReadOnlyDictionary second = new(new Dictionary() { - if (_settings.ShouldUsePinyin) + {"ua", "x"}, + {"ei", "w"}, + {"e", "e"}, + {"ou", "z"}, + {"iu", "q"}, + {"ve", "t"}, + {"ue", "t"}, + {"u", "u"}, + {"i", "i"}, + {"o", "o"}, + {"uo", "o"}, + {"ie", "p"}, + {"a", "a"}, + {"ong", "s"}, + {"iong", "s"}, + {"ai", "d"}, + {"ing", "k"}, + {"uai", "k"}, + {"ang", "h"}, + {"uan", "r"}, + {"an", "j"}, + {"en", "f"}, + {"ia", "x"}, + {"iang", "l"}, + {"uang", "l"}, + {"eng", "g"}, + {"in", "b"}, + {"ao", "c"}, + {"v", "v"}, + {"ui", "v"}, + {"un", "y"}, + {"iao", "n"}, + {"ian", "m"} + }); + + private static string ToDoublePin(string fullPinyin) + { + // Assuming s is valid + StringBuilder doublePin = new StringBuilder(); + + if (fullPinyin.Length <= 3 && (fullPinyin[0] == 'a' || fullPinyin[0] == 'e' || fullPinyin[0] == 'o')) { - if (!_pinyinCache.ContainsKey(content)) + if (special.TryGetValue(fullPinyin, out var value)) { - return BuildCacheFromContent(content); - } - else - { - return _pinyinCache[content]; + return value; } } - return (content, null); - } - private (string translation, TranslationMapping map) BuildCacheFromContent(string content) - { - if (WordsHelper.HasChinese(content)) + // zh, ch, sh + if (fullPinyin.Length >= 2 && first.ContainsKey(fullPinyin[..2])) { - var resultList = WordsHelper.GetPinyinList(content); - - StringBuilder resultBuilder = new StringBuilder(); - TranslationMapping map = new TranslationMapping(); - - bool pre = false; + doublePin.Append(first[fullPinyin[..2]]); - for (int i = 0; i < resultList.Length; i++) + if (second.TryGetValue(fullPinyin[2..], out string tmp)) { - if (content[i] >= 0x3400 && content[i] <= 0x9FD5) - { - map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1); - resultBuilder.Append(' '); - resultBuilder.Append(resultList[i]); - pre = true; - } - else - { - if (pre) - { - pre = false; - resultBuilder.Append(' '); - } - - resultBuilder.Append(resultList[i]); - } + doublePin.Append(tmp); + } + else + { + doublePin.Append(fullPinyin[2..]); } - - map.endConstruct(); - - var key = resultBuilder.ToString(); - map.setKey(key); - - return _pinyinCache[content] = (key, map); } else { - return (content, null); + doublePin.Append(fullPinyin[0]); + + if (second.TryGetValue(fullPinyin[1..], out string tmp)) + { + doublePin.Append(tmp); + } + else + { + doublePin.Append(fullPinyin[1..]); + } } + + return doublePin.ToString(); } + #endregion } } diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index bd5dbdda9be..4929e4cd2cf 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -61,7 +61,7 @@ public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption query = query.Trim(); TranslationMapping translationMapping = null; - if (_alphabet is not null && !_alphabet.CanBeTranslated(query)) + if (_alphabet is not null && _alphabet.ShouldTranslate(query)) { // We assume that if a query can be translated (containing characters of a language, like Chinese) // it actually means user doesn't want it to be translated to English letters. diff --git a/Flow.Launcher.Infrastructure/TranslationMapping.cs b/Flow.Launcher.Infrastructure/TranslationMapping.cs new file mode 100644 index 00000000000..c976fc522d6 --- /dev/null +++ b/Flow.Launcher.Infrastructure/TranslationMapping.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flow.Launcher.Infrastructure +{ + public class TranslationMapping + { + private bool constructed; + + private List originalIndexs = new List(); + private List translatedIndexs = new List(); + private int translatedLength = 0; + + public void AddNewIndex(int originalIndex, int translatedIndex, int length) + { + if (constructed) + throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); + + originalIndexs.Add(originalIndex); + translatedIndexs.Add(translatedIndex); + translatedIndexs.Add(translatedIndex + length); + translatedLength += length - 1; + } + + public int MapToOriginalIndex(int translatedIndex) + { + if (translatedIndex > translatedIndexs.Last()) + return translatedIndex - translatedLength - 1; + + int lowerBound = 0; + int upperBound = originalIndexs.Count - 1; + + int count = 0; + + // Corner case handle + if (translatedIndex < translatedIndexs[0]) + return translatedIndex; + if (translatedIndex > translatedIndexs.Last()) + { + int indexDef = 0; + for (int k = 0; k < originalIndexs.Count; k++) + { + indexDef += translatedIndexs[k * 2 + 1] - translatedIndexs[k * 2]; + } + + return translatedIndex - indexDef - 1; + } + + // Binary Search with Range + for (int i = originalIndexs.Count / 2;; count++) + { + if (translatedIndex < translatedIndexs[i * 2]) + { + // move to lower middle + upperBound = i; + i = (i + lowerBound) / 2; + } + else if (translatedIndex > translatedIndexs[i * 2 + 1] - 1) + { + lowerBound = i; + // move to upper middle + // due to floor of integer division, move one up on corner case + i = (i + upperBound + 1) / 2; + } + else + return originalIndexs[i]; + + if (upperBound - lowerBound <= 1 && + translatedIndex > translatedIndexs[lowerBound * 2 + 1] && + translatedIndex < translatedIndexs[upperBound * 2]) + { + int indexDef = 0; + + for (int j = 0; j < upperBound; j++) + { + indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2]; + } + + return translatedIndex - indexDef - 1; + } + } + } + + public void endConstruct() + { + if (constructed) + throw new InvalidOperationException("Mapping has already been constructed"); + constructed = true; + } + } +} diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 4588466652e..30e3b77beee 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -185,6 +185,9 @@ public CustomBrowserViewModel CustomBrowser /// when false Alphabet static service will always return empty results /// public bool ShouldUsePinyin { get; set; } = false; + + public bool UseDoublePinyin { get; set; } = true; //For developing + public bool AlwaysPreview { get; set; } = false; public bool AlwaysStartEn { get; set; } = false; diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 765a1a5593a..83870837a0b 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -74,7 +74,7 @@ private async void OnStartupAsync(object sender, StartupEventArgs e) PluginManager.LoadPlugins(_settings.PluginSettings); _mainVM = new MainViewModel(_settings); - API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet); + API = new PublicAPIInstance(_settingsVM, _mainVM); Http.API = API; Http.Proxy = _settings.Proxy; diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index b49bf39d3c5..e14d692cd65 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -32,15 +32,13 @@ public class PublicAPIInstance : IPublicAPI { private readonly SettingWindowViewModel _settingsVM; private readonly MainViewModel _mainVM; - private readonly PinyinAlphabet _alphabet; #region Constructor - public PublicAPIInstance(SettingWindowViewModel settingsVM, MainViewModel mainVM, PinyinAlphabet alphabet) + public PublicAPIInstance(SettingWindowViewModel settingsVM, MainViewModel mainVM) { _settingsVM = settingsVM; _mainVM = mainVM; - _alphabet = alphabet; GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback; WebRequest.RegisterPrefix("data", new DataWebRequestFactory()); }