From b50f738f4207ddf9d41be726d4eaa44c4d28fcdd Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Sun, 15 Mar 2020 20:05:34 +1100 Subject: [PATCH] Feature: Add auto-complete query text suggestion (#153) * Add query suggestion logic * Add UI element * update * Make caret more appealing by following the color of the query font * Add suggestions when using actionkeyword * Add feature to readme * Per suggestion add explaination * Per review comment * Per review comment * change to use converter * Correct typo Co-authored-by: clueless <14300910+theClueless@users.noreply.github.com> --- Plugins/Wox.Plugin.WebSearch/Main.cs | 2 + README.md | 1 + Wox.Core/Plugin/PluginManager.cs | 5 ++ Wox.Core/Resource/Theme.cs | 6 ++ Wox.Plugin/Result.cs | 13 +++- Wox/Converters/QuerySuggestionBoxConverter.cs | 61 +++++++++++++++++++ Wox/MainWindow.xaml | 47 +++++++++----- Wox/Themes/Base.xaml | 1 + Wox/ViewModel/MainViewModel.cs | 6 +- Wox/Wox.csproj | 1 + 10 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 Wox/Converters/QuerySuggestionBoxConverter.cs diff --git a/Plugins/Wox.Plugin.WebSearch/Main.cs b/Plugins/Wox.Plugin.WebSearch/Main.cs index 429e51cc0..d8c6ccafb 100644 --- a/Plugins/Wox.Plugin.WebSearch/Main.cs +++ b/Plugins/Wox.Plugin.WebSearch/Main.cs @@ -72,6 +72,7 @@ public List Query(Query query) SubTitle = subtitle, Score = 6, IcoPath = searchSource.IconPath, + ActionKeywordAssigned = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? string.Empty : searchSource.ActionKeyword, Action = c => { if (_settings.OpenInNewBrowser) @@ -137,6 +138,7 @@ private async Task> Suggestions(string keyword, string subti SubTitle = subtitle, Score = 5, IcoPath = searchSource.IconPath, + ActionKeywordAssigned = searchSource.ActionKeyword == SearchSourceGlobalPluginWildCardSign ? string.Empty : searchSource.ActionKeyword, Action = c => { if (_settings.OpenInNewBrowser) diff --git a/README.md b/README.md index 8db5d4d9c..732dd242f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Features **New from this fork:** - Portable mode - Drastically improved search experience +- Auto-complete text suggestion - Search all subfolders and files - Option to always run CMD or Powershell as administrator - Run CMD, Powershell and programs as a different user diff --git a/Wox.Core/Plugin/PluginManager.cs b/Wox.Core/Plugin/PluginManager.cs index 0166690b7..3d1450648 100644 --- a/Wox.Core/Plugin/PluginManager.cs +++ b/Wox.Core/Plugin/PluginManager.cs @@ -191,6 +191,11 @@ public static void UpdatePluginMetadata(List results, PluginMetadata met r.PluginDirectory = metadata.PluginDirectory; r.PluginID = metadata.ID; r.OriginQuery = query; + + // ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions + // Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level + if (metadata.ActionKeywords.Count == 1) + r.ActionKeywordAssigned = query.ActionKeyword; } } diff --git a/Wox.Core/Resource/Theme.cs b/Wox.Core/Resource/Theme.cs index 4b8c37ccf..fc5d55046 100644 --- a/Wox.Core/Resource/Theme.cs +++ b/Wox.Core/Resource/Theme.cs @@ -124,6 +124,12 @@ public ResourceDictionary GetResourceDictionary() queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, FontHelper.GetFontStyleFromInvariantStringOrNormal(Settings.QueryBoxFontStyle))); queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, FontHelper.GetFontWeightFromInvariantStringOrNormal(Settings.QueryBoxFontWeight))); queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, FontHelper.GetFontStretchFromInvariantStringOrNormal(Settings.QueryBoxFontStretch))); + + var caretBrushPropertyValue = queryBoxStyle.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); + var foregroundPropertyValue = queryBoxStyle.Setters.OfType().Where(x => x.Property.Name == "Foreground") + .Select(x => x.Value).FirstOrDefault(); + if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling + queryBoxStyle.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue)); } Style resultItemStyle = dict["ItemTitleStyle"] as Style; diff --git a/Wox.Plugin/Result.cs b/Wox.Plugin/Result.cs index 6e0559d35..4c8f2b1ed 100644 --- a/Wox.Plugin/Result.cs +++ b/Wox.Plugin/Result.cs @@ -14,6 +14,12 @@ public class Result public string Title { get; set; } public string SubTitle { get; set; } + /// + /// This holds the action keyword that triggered the result. + /// If result is triggered by global keyword: *, this should be empty. + /// + public string ActionKeywordAssigned { get; set; } + public string IcoPath { get { return _icoPath; } @@ -53,7 +59,7 @@ public string IcoPath public IList SubTitleHighlightData { get; set; } /// - /// Only resulsts that originQuery match with curren query will be displayed in the panel + /// Only results that originQuery match with current query will be displayed in the panel /// internal Query OriginQuery { get; set; } @@ -98,13 +104,14 @@ public override string ToString() return Title + SubTitle; } - [Obsolete("Use IContextMenu instead")] + /// /// Context menus associate with this result /// + [Obsolete("Use IContextMenu instead")] public List ContextMenu { get; set; } - [Obsolete("Use Object initializers instead")] + [Obsolete("Use Object initializer instead")] public Result(string Title, string IcoPath, string SubTitle = null) { this.Title = Title; diff --git a/Wox/Converters/QuerySuggestionBoxConverter.cs b/Wox/Converters/QuerySuggestionBoxConverter.cs new file mode 100644 index 000000000..68b891ce6 --- /dev/null +++ b/Wox/Converters/QuerySuggestionBoxConverter.cs @@ -0,0 +1,61 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using Wox.Infrastructure.Logger; +using Wox.ViewModel; + +namespace Wox.Converters +{ + public class QuerySuggestionBoxConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Length != 2) + { + return string.Empty; + } + + // first prop is the current query string + var queryText = (string)values[0]; + + if (string.IsNullOrEmpty(queryText)) + return "Type here to search"; + + // second prop is the current selected item result + var val = values[1]; + if (val == null) + { + return string.Empty; + } + if (!(val is ResultViewModel)) + { + return System.Windows.Data.Binding.DoNothing; + } + + try + { + var selectedItem = (ResultViewModel)val; + + var selectedResult = selectedItem.Result; + var selectedResultActionKeyword = string.IsNullOrEmpty(selectedResult.ActionKeywordAssigned) ? "" : selectedResult.ActionKeywordAssigned + " "; + var selectedResultPossibleSuggestion = selectedResultActionKeyword + selectedResult.Title; + + if (!selectedResultPossibleSuggestion.StartsWith(queryText, StringComparison.CurrentCultureIgnoreCase)) + return string.Empty; + + // When user typed lower case and result title is uppercase, we still want to display suggestion + return queryText + selectedResultPossibleSuggestion.Substring(queryText.Length); + } + catch (Exception e) + { + Log.Exception(nameof(QuerySuggestionBoxConverter), "fail to convert text for suggestion box", e); + return string.Empty; + } + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Wox/MainWindow.xaml b/Wox/MainWindow.xaml index d50411b83..7c5afb3ef 100644 --- a/Wox/MainWindow.xaml +++ b/Wox/MainWindow.xaml @@ -4,7 +4,9 @@ xmlns:wox="clr-namespace:Wox" xmlns:vm="clr-namespace:Wox.ViewModel" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:converters="clr-namespace:Wox.Converters" + mc:Ignorable="d" Title="Wox" Topmost="True" SizeToContent="Height" @@ -25,6 +27,9 @@ PreviewKeyDown="OnKeyDown" Visibility="{Binding MainWindowVisibility, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" d:DataContext="{d:DesignInstance vm:MainViewModel}"> + + + @@ -55,29 +60,43 @@ - + + + + + + + + + - - - - - - - - - - + Background="Transparent"> + + + + + + + + + + + - + diff --git a/Wox/Themes/Base.xaml b/Wox/Themes/Base.xaml index ae195feaa..6f6083035 100644 --- a/Wox/Themes/Base.xaml +++ b/Wox/Themes/Base.xaml @@ -8,6 +8,7 @@ + diff --git a/Wox/ViewModel/MainViewModel.cs b/Wox/ViewModel/MainViewModel.cs index acdd7c040..c2a277963 100644 --- a/Wox/ViewModel/MainViewModel.cs +++ b/Wox/ViewModel/MainViewModel.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Threading; @@ -210,7 +211,7 @@ public string QueryText Query(); } } - + /// /// we need move cursor to end when we manually changed query /// but we don't want to move cursor to end when query is updated from TextBox @@ -455,7 +456,6 @@ private void RemoveOldQueryResults(Query query) } } - private Result ContextMenuTopMost(Result result) { Result menu; diff --git a/Wox/Wox.csproj b/Wox/Wox.csproj index 0aa060ddd..56a4ed97b 100644 --- a/Wox/Wox.csproj +++ b/Wox/Wox.csproj @@ -93,6 +93,7 @@ + ResultListBox.xaml