diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 3b4a6e445fc..6a12fceae50 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -164,20 +164,22 @@ public static async Task InitializePluginsAsync(IPublicAPI api) } } - public static ICollection ValidPluginsForQuery(Query query) + public static PluginPair[] ValidPluginsForQuery(Query query) { if (query is null) + { return Array.Empty(); + } - if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword)) - return GlobalPlugins; - - - var plugin = NonGlobalPlugins[query.ActionKeyword]; - return new List + string actionKeyword = query.ActionKeyword; + if (NonGlobalPlugins.TryGetValue(actionKeyword, out var plugin)) { - plugin - }; + return plugin.Metadata.Disabled ? Array.Empty() : new[] { plugin }; + } + else + { + return GlobalPlugins.Where(x => !x.Metadata.Disabled).ToArray(); + } } public static async Task> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index bfd7e4b8757..299debedb63 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -59,6 +59,8 @@ public string Theme public string TimeFormat { get; set; } = "hh:mm tt"; public string DateFormat { get; set; } = "MM'/'dd ddd"; public bool FirstLaunch { get; set; } = true; + + public int GlobalSearchDelay { get; set; } = 50; public double SettingWindowWidth { get; set; } = 1000; public double SettingWindowHeight { get; set; } = 700; diff --git a/Flow.Launcher.Plugin/PluginMetadata.cs b/Flow.Launcher.Plugin/PluginMetadata.cs index e8f5cf74432..152b44cd39f 100644 --- a/Flow.Launcher.Plugin/PluginMetadata.cs +++ b/Flow.Launcher.Plugin/PluginMetadata.cs @@ -8,16 +8,51 @@ namespace Flow.Launcher.Plugin public class PluginMetadata : BaseModel { private string _pluginDirectory; + /// + /// Unique ID of the plugin + /// public string ID { get; set; } + /// + /// Name of the plugin + /// public string Name { get; set; } + /// + /// Author of the plugin + /// public string Author { get; set; } + /// + /// Plugin Version + /// public string Version { get; set; } + /// + /// Programming Language of the plugin + /// public string Language { get; set; } + /// + /// Description of the plugin + /// public string Description { get; set; } + /// + /// Website of the plugin + /// public string Website { get; set; } + /// + /// Whether the plugin is enabled + /// public bool Disabled { get; set; } + /// + /// Executable file path of the plugin + /// public string ExecuteFilePath { get; private set;} + /// + /// Plugin Specified Search Delay + /// + public int? SearchDelay { get; set; } = null; + + /// + /// Executable file Name of the plugin + /// public string ExecuteFileName { get; set; } public string PluginDirectory @@ -31,17 +66,33 @@ internal set } } + /// + /// Action keyword of the plugin (Obsolete) + /// public string ActionKeyword { get; set; } + /// + /// Action keywords of the plugin + /// public List ActionKeywords { get; set; } + /// + /// Icon path of the plugin + /// public string IcoPath { get; set;} + /// + /// Metadata ToString + /// + /// Full Name of Plugin public override string ToString() { return Name; } + /// + /// Plugin Priority + /// [JsonIgnore] public int Priority { get; set; } @@ -50,8 +101,14 @@ public override string ToString() /// [JsonIgnore] public long InitTime { get; set; } + /// + /// Plugin Average Query Time (Statistics) + /// [JsonIgnore] public long AvgQueryTime { get; set; } + /// + /// Plugin Query Count (Statistics) + /// [JsonIgnore] public int QueryCount { get; set; } } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 49c603dc851..158ead811a3 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -68,6 +68,8 @@ Hide Flow Launcher on startup Hide tray icon When the icon is hidden from the tray, the Settings menu can be opened by right-clicking on the search window. + Global Search Delay + Sets the search delay when you stops typing. Query Search Precision Changes minimum match score required for results. Search with Pinyin diff --git a/Flow.Launcher/Languages/ko.xaml b/Flow.Launcher/Languages/ko.xaml index e69e29c8c7c..0997ee1c506 100644 --- a/Flow.Launcher/Languages/ko.xaml +++ b/Flow.Launcher/Languages/ko.xaml @@ -1,5 +1,8 @@ - - + + 단축키 등록 실패: {0} {0}을 실행할 수 없습니다. @@ -66,6 +69,8 @@ 시작 시 Flow Launcher 숨김 트레이 아이콘 숨기기 트레이에서 아이콘을 숨길 경우, 검색창 우클릭으로 설정창을 열 수 있습니다. + 검색 지연 시간 + 타이핑이 멈췄을 때 검색 결과가 표시되는 속도를 지정합니다. 빠를수록 타이핑 도중 결과가 갱신됩니다. 기본값은 50ms입니다. 쿼리 검색 정밀도 검색 결과에 필요한 최소 매치 점수를 변경합니다. 항상 Pinyin 사용 diff --git a/Flow.Launcher/SettingWindow.xaml b/Flow.Launcher/SettingWindow.xaml index daf8ec608c4..56e12d95ad4 100644 --- a/Flow.Launcher/SettingWindow.xaml +++ b/Flow.Launcher/SettingWindow.xaml @@ -788,7 +788,40 @@ Padding="0" CornerRadius="5" Style="{DynamicResource SettingGroupBox}"> + + + + + + + + + +  + + + + @@ -108,6 +115,7 @@ public MainViewModel(Settings settings) { case nameof(Results.SelectedItem): UpdatePreview(); + break; } }; @@ -133,6 +141,7 @@ async Task updateAction() while (await channelReader.WaitToReadAsync()) { await Task.Delay(20); + while (channelReader.TryRead(out var item)) { if (!item.Token.IsCancellationRequested) @@ -173,6 +182,7 @@ private void RegisterResultsUpdatedEvent() var token = e.Token == default ? _updateToken : e.Token; PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query); + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(e.Results, pair.Metadata, e.Query, token))) { Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); @@ -237,6 +247,7 @@ private void Backspace(object index) private void AutocompleteQuery() { var result = SelectedResults.SelectedItem?.Result; + if (result != null && SelectedIsFromQueryResults()) // SelectedItem returns null if selection is empty. { var autoCompleteText = result.Title; @@ -251,6 +262,7 @@ private void AutocompleteQuery() } var specialKeyState = GlobalHotkey.CheckModifiers(); + if (specialKeyState.ShiftPressed) { autoCompleteText = result.SubTitle; @@ -264,15 +276,19 @@ private void AutocompleteQuery() private async Task OpenResultAsync(string index) { var results = SelectedResults; + if (index is not null) { results.SelectedIndex = int.Parse(index); } + var result = results.SelectedItem?.Result; + if (result == null) { return; } + var hideWindow = await result.ExecuteAsync(new ActionContext { SpecialKeyState = GlobalHotkey.CheckModifiers() @@ -361,18 +377,23 @@ public void ToggleGameMode() #region ViewModel Properties public Settings Settings { get; } + public string ClockText { get; private set; } + public string DateText { get; private set; } + public CultureInfo Culture => CultureInfo.DefaultThreadCurrentCulture; private async Task RegisterClockAndDateUpdateAsync() { var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + // ReSharper disable once MethodSupportsCancellation while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) { if (Settings.UseClock) ClockText = DateTime.Now.ToString(Settings.TimeFormat, Culture); + if (Settings.UseDate) DateText = DateTime.Now.ToString(Settings.DateFormat, Culture); } @@ -410,6 +431,7 @@ private void IncreaseWidth() Settings.WindowSize += 100; Settings.WindowLeft -= 50; } + OnPropertyChanged(); } @@ -425,6 +447,7 @@ private void DecreaseWidth() Settings.WindowLeft += 50; Settings.WindowSize -= 100; } + OnPropertyChanged(); } @@ -516,6 +539,7 @@ public void ChangeQueryText(string queryText, bool reQuery = false) { Query(); } + QueryTextCursorMovedToEnd = true; }); } @@ -533,6 +557,7 @@ private ResultsViewModel SelectedResults set { _selectedResults = value; + if (SelectedIsFromQueryResults()) { ContextMenu.Visibility = Visibility.Collapsed; @@ -564,7 +589,9 @@ private ResultsViewModel SelectedResults } public Visibility ProgressBarVisibility { get; set; } + public Visibility MainWindowVisibility { get; set; } + public double MainWindowOpacity { get; set; } = 1; // This is to be used for determining the visibility status of the mainwindow instead of MainWindowVisibility @@ -634,6 +661,7 @@ private void QueryContextMenu() r => { var match = StringMatcher.FuzzySearch(query, r.Title); + if (!match.IsSearchPrecisionScoreMet()) { match = StringMatcher.FuzzySearch(query, r.SubTitle); @@ -642,9 +670,11 @@ private void QueryContextMenu() if (!match.IsSearchPrecisionScoreMet()) return false; r.Score = match.Score; + return true; }).ToList(); + ContextMenu.AddResults(filtered, id); } else @@ -661,6 +691,7 @@ private void QueryHistory() History.Clear(); var results = new List(); + foreach (var h in _history.Items) { var title = _translator.GetTranslation("executeQuery"); @@ -678,9 +709,11 @@ private void QueryHistory() { SelectedResults = Results; ChangeQueryText(h.Query); + return false; } }; + results.Add(result); } @@ -691,6 +724,7 @@ private void QueryHistory() r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() || StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet() ).ToList(); + History.AddResults(filtered, id); } else @@ -707,7 +741,9 @@ private async void QueryResults() var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts); - if (query == null) // shortcut expanded + var plugins = PluginManager.ValidPluginsForQuery(query); + + if (query == null || plugins.Length == 0) // shortcut expanded { Results.Clear(); Results.Visibility = Visibility.Collapsed; @@ -715,9 +751,18 @@ private async void QueryResults() SearchIconVisibility = Visibility.Visible; return; } - + else if (plugins.Length == 1) + { + PluginIconPath = plugins.Single().Metadata.IcoPath; + SearchIconVisibility = Visibility.Hidden; + } + else + { + PluginIconPath = null; + SearchIconVisibility = Visibility.Visible; + } + _updateSource?.Dispose(); - var currentUpdateSource = new CancellationTokenSource(); _updateSource = currentUpdateSource; var currentCancellationToken = _updateSource.Token; @@ -738,51 +783,38 @@ private async void QueryResults() _lastQuery = query; - var plugins = PluginManager.ValidPluginsForQuery(query); - - if (plugins.Count == 1) - { - PluginIconPath = plugins.Single().Metadata.IcoPath; - SearchIconVisibility = Visibility.Hidden; - } - else - { - PluginIconPath = null; - SearchIconVisibility = Visibility.Visible; - } - - - if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign) - { - // Wait 45 millisecond for query change in global query - // if query changes, return so that it won't be calculated - await Task.Delay(45, currentCancellationToken); - if (currentCancellationToken.IsCancellationRequested) - return; - } _ = 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 (!currentCancellationToken.IsCancellationRequested && _isQueryRunning) { - ProgressBarVisibility = Visibility.Visible; - } - }, currentCancellationToken, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default); - - // plugins is ICollection, meaning LINQ will get the Count and preallocate Array + // 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 (!currentCancellationToken.IsCancellationRequested && _isQueryRunning) + { + ProgressBarVisibility = Visibility.Visible; + } + }, + currentCancellationToken, + TaskContinuationOptions.NotOnCanceled, + TaskScheduler.Default); - var tasks = plugins.Select(plugin => plugin.Metadata.Disabled switch + try { - false => QueryTask(plugin), - true => Task.CompletedTask - }).ToArray(); + await Parallel.ForEachAsync(plugins, + currentCancellationToken, + async (pair, token) => + { + if(token.IsCancellationRequested) + return; + await Task.Delay(pair.Metadata.SearchDelay + ?? (string.IsNullOrEmpty(query.ActionKeyword) + ? Settings.GlobalSearchDelay + : 0)); - try - { - // Check the code, WhenAll will translate all type of IEnumerable or Collection to Array, so make an array at first - await Task.WhenAll(tasks); + if (token.IsCancellationRequested) + return; + + await QueryTask(pair, token); + }); } catch (OperationCanceledException) { @@ -795,6 +827,7 @@ private async void QueryResults() // this should happen once after all queries are done so progress bar should continue // until the end of all querying _isQueryRunning = false; + if (!currentCancellationToken.IsCancellationRequested) { // update to hidden if this is still the current query @@ -802,15 +835,12 @@ private async void QueryResults() } // Local function - async Task QueryTask(PluginPair plugin) + async ValueTask QueryTask(PluginPair plugin, CancellationToken token = default) { - // Since it is wrapped within a ThreadPool Thread, the synchronous context is null - // Task.Yield will force it to run in ThreadPool - await Task.Yield(); - - IReadOnlyList results = await PluginManager.QueryForPluginAsync(plugin, query, currentCancellationToken); + IReadOnlyList results = await PluginManager.QueryForPluginAsync(plugin, query, token); - currentCancellationToken.ThrowIfCancellationRequested(); + if (token.IsCancellationRequested) + return; results ??= _emptyResult; @@ -868,6 +898,7 @@ private Query ConstructQuery(string queryText, IEnumerable _queryText = queryBuilderTmp.ToString(); var query = QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins); + return query; } @@ -882,6 +913,7 @@ private void RemoveOldQueryResults(Query query) private Result ContextMenuTopMost(Result result) { Result menu; + if (_topMostRecord.IsTopMost(result)) { menu = new Result @@ -893,6 +925,7 @@ private Result ContextMenuTopMost(Result result) { _topMostRecord.Remove(result); App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success")); + return false; } }; @@ -909,6 +942,7 @@ private Result ContextMenuTopMost(Result result) { _topMostRecord.AddOrUpdate(result); App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success")); + return false; } }; @@ -939,27 +973,32 @@ private Result ContextMenuPluginInfo(string id) Action = _ => { App.API.OpenUrl(metadata.Website); + return true; } }; + return menu; } internal bool SelectedIsFromQueryResults() { var selected = SelectedResults == Results; + return selected; } private bool ContextMenuSelected() { var selected = SelectedResults == ContextMenu; + return selected; } private bool HistorySelected() { var selected = SelectedResults == History; + return selected; } @@ -1001,16 +1040,21 @@ public async void Hide() case LastQueryMode.Empty: ChangeQueryText(string.Empty); await Task.Delay(100); //Time for change to opacity + break; case LastQueryMode.Preserved: if (Settings.UseAnimation) await Task.Delay(100); + LastQuerySelected = true; + break; case LastQueryMode.Selected: if (Settings.UseAnimation) await Task.Delay(100); + LastQuerySelected = false; + break; default: throw new ArgumentException($"wrong LastQueryMode: <{Settings.LastQueryMode}>"); @@ -1046,6 +1090,7 @@ public void UpdateResultView(IEnumerable resultsForUpdates) { if (!resultsForUpdates.Any()) return; + CancellationToken token; try @@ -1096,11 +1141,13 @@ public void ResultCopy(string stringToCopy) if (string.IsNullOrEmpty(stringToCopy)) { var result = Results.SelectedItem?.Result; + if (result != null) { string copyText = result.CopyText; var isFile = File.Exists(copyText); var isFolder = Directory.Exists(copyText); + if (isFile || isFolder) { var paths = new StringCollection diff --git a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs index 37587ce5949..017f808ed4e 100644 --- a/Flow.Launcher/ViewModel/SettingWindowViewModel.cs +++ b/Flow.Launcher/ViewModel/SettingWindowViewModel.cs @@ -217,6 +217,26 @@ private void UpdateLastQueryModeDisplay() } } + + public class SearchDelay + { + public string Display { get; init; } + public int Value { get; init; } + + public SearchDelay(int value) + { + Value = value; + Display = value.ToString() + " ms"; + } + } + + public List SearchDelays { get; init; } = new() + { + new SearchDelay(50), + new SearchDelay(100), + new SearchDelay(150), + }; + public string Language { get