From 28366fe5c4518962b1fd2e4ea3d02a2829807a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=88=B0=E9=98=9F=E7=9A=84=E5=81=B6=E5=83=8F-=E5=B2=9B?= =?UTF-8?q?=E9=A3=8E=E9=85=B1=EF=BC=81?= Date: Thu, 17 Nov 2022 00:25:57 +0800 Subject: [PATCH] [PTRun]Add Pinyin Support (#21465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Pinyin Support for Powertoys Run using hyjiacan.pinyin4net Revert microsoft/PowerToys#7455 closed microsoft/PowerToys#3587 closed microsoft/PowerToys#20370 Signed-off-by: 舰队的偶像-岛风酱! * Remove JetBrains annotations Signed-off-by: 舰队的偶像-岛风酱! --- installer/PowerToysSetup/Product.wxs | 2 +- .../launcher/PowerLauncher/App.xaml.cs | 14 +- .../PowerLauncher/PublicAPIInstance.cs | 12 +- .../launcher/Wox.Infrastructure/Alphabet.cs | 212 ++++++++++++++++++ .../launcher/Wox.Infrastructure/IAlphabet.cs | 10 + .../Wox.Infrastructure/StringMatcher.cs | 13 ++ .../UserSettings/PowerToysRunSettings.cs | 7 + .../Wox.Infrastructure.csproj | 1 + 8 files changed, 267 insertions(+), 4 deletions(-) create mode 100644 src/modules/launcher/Wox.Infrastructure/Alphabet.cs create mode 100644 src/modules/launcher/Wox.Infrastructure/IAlphabet.cs diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 945be73512..6a58606b56 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -2066,7 +2066,7 @@ - + diff --git a/src/modules/launcher/PowerLauncher/App.xaml.cs b/src/modules/launcher/PowerLauncher/App.xaml.cs index 8f147b9ca7..d13aa99aa8 100644 --- a/src/modules/launcher/PowerLauncher/App.xaml.cs +++ b/src/modules/launcher/PowerLauncher/App.xaml.cs @@ -9,20 +9,27 @@ using System.Linq; using System.Text; using System.Threading; using System.Windows; + using Common.UI; + using interop; + using ManagedCommon; + using Microsoft.PowerLauncher.Telemetry; using Microsoft.PowerToys.Telemetry; + using PowerLauncher.Helper; using PowerLauncher.Plugin; using PowerLauncher.ViewModel; + using Wox; using Wox.Infrastructure; using Wox.Infrastructure.Image; using Wox.Infrastructure.UserSettings; using Wox.Plugin; using Wox.Plugin.Logger; + using Stopwatch = Wox.Infrastructure.Stopwatch; namespace PowerLauncher @@ -31,6 +38,8 @@ namespace PowerLauncher { public static PublicAPIInstance API { get; private set; } + private readonly Alphabet _alphabet = new Alphabet(); + public static CancellationTokenSource NativeThreadCTS { get; private set; } private static bool _disposed; @@ -126,13 +135,14 @@ namespace PowerLauncher _settings = _settingsVM.Settings; _settings.StartedFromPowerToysRunner = e.Args.Contains("--started-from-runner"); - _stringMatcher = new StringMatcher(); + _alphabet.Initialize(_settings); + _stringMatcher = new StringMatcher(_alphabet); StringMatcher.Instance = _stringMatcher; _stringMatcher.UserSettingSearchPrecision = _settings.QuerySearchPrecision; _mainVM = new MainViewModel(_settings, NativeThreadCTS.Token); _mainWindow = new MainWindow(_settings, _mainVM, NativeThreadCTS.Token); - API = new PublicAPIInstance(_settingsVM, _mainVM, _themeManager); + API = new PublicAPIInstance(_settingsVM, _mainVM, _alphabet, _themeManager); _settingsReader = new SettingsReader(_settings, _themeManager); _settingsReader.ReadSettings(); diff --git a/src/modules/launcher/PowerLauncher/PublicAPIInstance.cs b/src/modules/launcher/PowerLauncher/PublicAPIInstance.cs index b3a0687916..c60ba20315 100644 --- a/src/modules/launcher/PowerLauncher/PublicAPIInstance.cs +++ b/src/modules/launcher/PowerLauncher/PublicAPIInstance.cs @@ -6,12 +6,19 @@ using System; using System.Collections.Generic; using System.Linq; using System.Windows; + using Common.UI; + using ManagedCommon; + using Microsoft.Toolkit.Uwp.Notifications; + using PowerLauncher.Plugin; using PowerLauncher.ViewModel; + using Windows.UI.Notifications; + +using Wox.Infrastructure; using Wox.Infrastructure.Image; using Wox.Plugin; @@ -21,15 +28,17 @@ namespace Wox { private readonly SettingWindowViewModel _settingsVM; private readonly MainViewModel _mainVM; + private readonly Alphabet _alphabet; private readonly ThemeManager _themeManager; private bool _disposed; public event ThemeChangedHandler ThemeChanged; - public PublicAPIInstance(SettingWindowViewModel settingsVM, MainViewModel mainVM, ThemeManager themeManager) + public PublicAPIInstance(SettingWindowViewModel settingsVM, MainViewModel mainVM, Alphabet alphabet, ThemeManager themeManager) { _settingsVM = settingsVM ?? throw new ArgumentNullException(nameof(settingsVM)); _mainVM = mainVM ?? throw new ArgumentNullException(nameof(mainVM)); + _alphabet = alphabet ?? throw new ArgumentNullException(nameof(alphabet)); _themeManager = themeManager ?? throw new ArgumentNullException(nameof(themeManager)); _themeManager.ThemeChanged += OnThemeChanged; @@ -60,6 +69,7 @@ namespace Wox _settingsVM.Save(); PluginManager.Save(); ImageLoader.Save(); + _alphabet.Save(); } public void ReloadAllPluginData() diff --git a/src/modules/launcher/Wox.Infrastructure/Alphabet.cs b/src/modules/launcher/Wox.Infrastructure/Alphabet.cs new file mode 100644 index 0000000000..0d067c7953 --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/Alphabet.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using hyjiacan.py4n; + +using Wox.Infrastructure.Storage; +using Wox.Infrastructure.UserSettings; +using Wox.Plugin.Logger; + +namespace Wox.Infrastructure; + +public class Alphabet : IAlphabet +{ + private readonly PinyinFormat _pinyinFormat = + PinyinFormat.CAPITALIZE_FIRST_LETTER | + PinyinFormat.WITH_V | + PinyinFormat.WITHOUT_TONE; + + private ConcurrentDictionary _pinyinCache; + private WoxJsonStorage> _pinyinStorage; + private PowerToysRunSettings _settings; + private Dictionary __cache; + + public void Initialize(PowerToysRunSettings settings) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + InitializePinyinHelpers(); + } + + private void InitializePinyinHelpers() + { + Stopwatch.Normal("|Wox.Infrastructure.Alphabet.Initialize|Preload pinyin cache", () => + { + _pinyinStorage = new WoxJsonStorage>("Pinyin"); + SetPinyinCacheAsDictionary(__cache = _pinyinStorage.Load()); + + // force pinyin library static constructor initialize + Pinyin4Net.GetPinyin('一', _pinyinFormat); + }); + Log.Info($"Number of preload pinyin combination<{_pinyinCache.Count}>", GetType()); + } + + public string Translate(string stringToTranslate) + { + return ConvertChineseCharactersToPinyin(stringToTranslate); + } + + public string ConvertChineseCharactersToPinyin(string source) + { + if (!_settings.ShouldUsePinyin) + { + return source; + } + + if (string.IsNullOrEmpty(source)) + { + return source; + } + + if (!ContainsChinese(source)) + { + return source; + } + + var combination = PinyinCombination(source); + + var pinyinArray = combination.Select(x => string.Join(string.Empty, x)); + var acronymArray = combination.Select(Acronym).Distinct(); + + var joinedSingleStringCombination = new StringBuilder(); + var all = acronymArray.Concat(pinyinArray); + all.ToList().ForEach(x => joinedSingleStringCombination.Append(x)); + + return joinedSingleStringCombination.ToString(); + } + + public void Save() + { + if (!_settings.ShouldUsePinyin) + { + return; + } + + GetPinyinCacheAsDictionary(); + _pinyinStorage.Save(); + } + + private static readonly string[] _emptyStringArray = Array.Empty(); + private static readonly string[][] _empty2DStringArray = Array.Empty(); + + /// + /// replace chinese character with pinyin, non chinese character won't be modified + /// should be word or sentence, instead of single character. e.g. 微软 + /// + [Obsolete("Not accurate, eg 音乐 will not return yinyue but returns yinle ")] + public string[] Pinyin(string word) + { + if (!_settings.ShouldUsePinyin) + { + return _emptyStringArray; + } + + var pinyin = word.Select(c => + { + string result = c.ToString(); + if (PinyinUtil.IsHanzi(c)) + { + var pinyins = Pinyin4Net.GetPinyin(c); + result = pinyins[0]; + } + + return result; + }).ToArray(); + return pinyin; + } + + /// + /// replace chinese character with pinyin, non chinese character won't be modified + /// Because we don't have words dictionary, so we can only return all possibly pinyin combination + /// e.g. 音乐 will return yinyue and yinle + /// should be word or sentence, instead of single character. e.g. 微软 + /// + public string[][] PinyinCombination(string characters) + { + if (!_settings.ShouldUsePinyin || string.IsNullOrEmpty(characters)) + { + return _empty2DStringArray; + } + + if (!_pinyinCache.ContainsKey(characters)) + { + var allPinyins = new List(); + foreach (var c in characters) + { + if (PinyinUtil.IsHanzi(c)) + { + var pinyins = Pinyin4Net.GetPinyin(c, _pinyinFormat); + var r = pinyins.Distinct().ToArray(); + allPinyins.Add(r); + } + else + { + var r = new[] { c.ToString() }; + allPinyins.Add(r); + } + } + + var combination = allPinyins.Aggregate(Combination).Select(c => c.Split(';')).ToArray(); + _pinyinCache[characters] = combination; + return combination; + } + else + { + return _pinyinCache[characters]; + } + } + + public string Acronym(string[] pinyin) + { + var acronym = string.Join(string.Empty, pinyin.Select(p => p[0])); + return acronym; + } + + public bool ContainsChinese(string word) + { + if (!_settings.ShouldUsePinyin) + { + return false; + } + + if (word.Length > 40) + { + // Skip strings that are too long string for Pinyin conversion. + return false; + } + + var chinese = word.Any(PinyinUtil.IsHanzi); + return chinese; + } + + private string[] Combination(string[] array1, string[] array2) + { + if (!_settings.ShouldUsePinyin) + { + return _emptyStringArray; + } + + var combination = ( + from a1 in array1 + from a2 in array2 + select $"{a1};{a2}" + ).ToArray(); + return combination; + } + + private Dictionary GetPinyinCacheAsDictionary() + { + return new Dictionary(_pinyinCache); + } + + private void SetPinyinCacheAsDictionary(Dictionary usage) + { + _pinyinCache = new ConcurrentDictionary(usage); + } +} diff --git a/src/modules/launcher/Wox.Infrastructure/IAlphabet.cs b/src/modules/launcher/Wox.Infrastructure/IAlphabet.cs new file mode 100644 index 0000000000..addeac6eef --- /dev/null +++ b/src/modules/launcher/Wox.Infrastructure/IAlphabet.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Wox.Infrastructure; + +public interface IAlphabet +{ + string Translate(string stringToTranslate); +} diff --git a/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs b/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs index 9c0b47a74f..f74c4d75b4 100644 --- a/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs +++ b/src/modules/launcher/Wox.Infrastructure/StringMatcher.cs @@ -20,6 +20,13 @@ namespace Wox.Infrastructure public SearchPrecisionScore UserSettingSearchPrecision { get; set; } + private readonly IAlphabet _alphabet; + + public StringMatcher(IAlphabet alphabet = null) + { + _alphabet = alphabet; + } + public static StringMatcher Instance { get; internal set; } [Obsolete("This method is obsolete and should not be used. Please use the static function StringMatcher.FuzzySearch")] @@ -90,6 +97,12 @@ namespace Wox.Infrastructure query = query.Trim(); + if (_alphabet != null) + { + query = _alphabet.Translate(query); + stringToCompare = _alphabet.Translate(stringToCompare); + } + // Using InvariantCulture since this is internal var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToUpper(CultureInfo.InvariantCulture) : stringToCompare; var queryWithoutCase = opt.IgnoreCase ? query.ToUpper(CultureInfo.InvariantCulture) : query; diff --git a/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs b/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs index 5f4a5ab19a..30cf0d827d 100644 --- a/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs +++ b/src/modules/launcher/Wox.Infrastructure/UserSettings/PowerToysRunSettings.cs @@ -6,7 +6,9 @@ using System; using System.Collections.ObjectModel; using System.Drawing; using System.Text.Json.Serialization; + using ManagedCommon; + using Wox.Plugin; namespace Wox.Infrastructure.UserSettings @@ -204,6 +206,11 @@ namespace Wox.Infrastructure.UserSettings public string ResultFontStretch { get; set; } + /// + /// Gets or sets a value indicating whether when false Alphabet static service will always return empty results + /// + public bool ShouldUsePinyin { get; set; } + internal StringMatcher.SearchPrecisionScore QuerySearchPrecision { get; private set; } = StringMatcher.SearchPrecisionScore.Regular; [JsonIgnore] diff --git a/src/modules/launcher/Wox.Infrastructure/Wox.Infrastructure.csproj b/src/modules/launcher/Wox.Infrastructure/Wox.Infrastructure.csproj index a30d796b41..49c8bddf25 100644 --- a/src/modules/launcher/Wox.Infrastructure/Wox.Infrastructure.csproj +++ b/src/modules/launcher/Wox.Infrastructure/Wox.Infrastructure.csproj @@ -42,6 +42,7 @@ +