diff --git a/Flow.Launcher.Infrastructure/Logger/Log.cs b/Flow.Launcher.Infrastructure/Logger/Log.cs index 75f208c9e..b8f1408e7 100644 --- a/Flow.Launcher.Infrastructure/Logger/Log.cs +++ b/Flow.Launcher.Infrastructure/Logger/Log.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using NLog; diff --git a/Flow.Launcher.Infrastructure/StringMatcher.cs b/Flow.Launcher.Infrastructure/StringMatcher.cs index 3ffa9f7b1..46165a849 100644 --- a/Flow.Launcher.Infrastructure/StringMatcher.cs +++ b/Flow.Launcher.Infrastructure/StringMatcher.cs @@ -202,7 +202,11 @@ namespace Flow.Launcher.Infrastructure if (allQuerySubstringsMatched) { var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex); - var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, + + // 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, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList(); @@ -296,7 +300,7 @@ namespace Flow.Launcher.Infrastructure return currentQuerySubstringIndex >= querySubstringsLength; } - private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, 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 @@ -304,6 +308,14 @@ namespace Flow.Launcher.Infrastructure // while the score is lower if they are more spread out var score = 100 * (query.Length + 1) / ((1 + firstIndex) + (matchLen + 1)); + // Give more weight to a match that is closer to the start of the string. + // if the first matched char is immediately before space and all strings are contained in the compare string e.g. 'world' matching 'hello world' + // and 'world hello', because both have 'world' immediately preceded by space, their firstIndex will be 0 when distance is calculated, + // to prevent them scoring the same, we adjust the score by deducting the number of spaces it has from the start of the string, so 'world hello' + // will score slightly higher than 'hello world' because 'hello world' has one additional space. + if (firstIndex == 0 && allSubstringsContainedInCompareString) + score -= spaceIndices.Count; + // A match with less characters assigning more weights if (stringToCompare.Length - query.Length < 5) { diff --git a/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs new file mode 100644 index 000000000..71020369a --- /dev/null +++ b/Flow.Launcher.Infrastructure/UserSettings/CustomShortcutModel.cs @@ -0,0 +1,65 @@ +using System; +using System.Text.Json.Serialization; + +namespace Flow.Launcher.Infrastructure.UserSettings +{ + public abstract class ShortcutBaseModel + { + public string Key { get; set; } + + [JsonIgnore] + public Func Expand { get; set; } = () => { return ""; }; + + public override bool Equals(object obj) + { + return obj is ShortcutBaseModel other && + Key == other.Key; + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + } + + public class CustomShortcutModel : ShortcutBaseModel + { + public string Value { get; set; } + + [JsonConstructorAttribute] + public CustomShortcutModel(string key, string value) + { + Key = key; + Value = value; + Expand = () => { return Value; }; + } + + public void Deconstruct(out string key, out string value) + { + key = Key; + value = Value; + } + + public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut) + { + return (shortcut.Key, shortcut.Value); + } + + public static implicit operator CustomShortcutModel((string Key, string Value) shortcut) + { + return new CustomShortcutModel(shortcut.Key, shortcut.Value); + } + } + + public class BuiltinShortcutModel : ShortcutBaseModel + { + public string Description { get; set; } + + public BuiltinShortcutModel(string key, string description, Func expand) + { + Key = key; + Description = description; + Expand = expand ?? (() => { return ""; }); + } + } +} diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 27afff5b6..7ead7459f 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Drawing; using System.Text.Json.Serialization; +using System.Windows; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher; @@ -129,8 +130,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings PrivateArg = "-private", EnablePrivate = false, Editable = false - } - , + }, new() { Name = "MS Edge", @@ -186,6 +186,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings public ObservableCollection CustomPluginHotkeys { get; set; } = new ObservableCollection(); + public ObservableCollection CustomShortcuts { get; set; } = new ObservableCollection(); + + [JsonIgnore] + public ObservableCollection BuiltinShortcuts { get; set; } = new ObservableCollection() { + new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText) + }; + public bool DontPromptUpdateMsg { get; set; } public bool EnableUpdateLog { get; set; } diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 7633e34a7..a1d3b83ab 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -37,7 +37,11 @@ namespace Flow.Launcher.Plugin /// user's clipboard when Ctrl + C is pressed on a result. If the text is a file/directory path /// flow will copy the actual file/folder instead of just the path text. /// - public string CopyText { get; set; } = string.Empty; + public string CopyText + { + get => string.IsNullOrEmpty(_copyText) ? SubTitle : _copyText; + set => _copyText = value; + } /// /// This holds the text which can be provided by plugin to help Flow autocomplete text @@ -81,6 +85,7 @@ namespace Flow.Launcher.Plugin /// Delegate to Get Image Source /// public IconDelegate Icon; + private string _copyText = string.Empty; /// /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs index bbddcbd2a..46c848c7a 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -129,14 +129,20 @@ namespace Flow.Launcher.Test } } + + /// + /// These are standard match scenarios + /// The intention of this test is provide a bench mark for how much the score has increased from a change. + /// Usually the increase in scoring should not be drastic, increase of less than 10 is acceptable. + /// [TestCase(Chrome, Chrome, 157)] - [TestCase(Chrome, LastIsChrome, 147)] + [TestCase(Chrome, LastIsChrome, 145)] [TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)] [TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)] [TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)] [TestCase(Chrome, CandyCrushSagaFromKing, 0)] - [TestCase("sql", MicrosoftSqlServerManagementStudio, 110)] - [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended + [TestCase("sql", MicrosoftSqlServerManagementStudio, 109)] + [TestCase("sql manag", MicrosoftSqlServerManagementStudio, 120)] //double spacing intended public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring( string queryString, string compareString, int expectedScore) { @@ -275,7 +281,40 @@ namespace Flow.Launcher.Test $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + - $"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}"); + $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); + } + + [TestCase("red", "red colour", "metro red")] + [TestCase("red", "this red colour", "this colour red")] + [TestCase("red", "this red colour", "this colour is very red")] + [TestCase("red", "this red colour", "this colour is surprisingly super awesome red and cool")] + [TestCase("red", "this colour is surprisingly super red very and cool", "this colour is surprisingly super very red and cool")] + public void WhenGivenTwoStrings_Scoring_ShouldGiveMoreWeightToTheStringCloserToIndexZero( + string queryString, string compareString1, string compareString2) + { + // When + var matcher = new StringMatcher { UserSettingSearchPrecision = SearchPrecisionScore.Regular }; + + // Given + var compareString1Result = matcher.FuzzyMatch(queryString, compareString1); + var compareString2Result = matcher.FuzzyMatch(queryString, compareString2); + + Debug.WriteLine(""); + Debug.WriteLine("###############################################"); + Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}"); + Debug.WriteLine( + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}"); + Debug.WriteLine( + $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); + Debug.WriteLine("###############################################"); + Debug.WriteLine(""); + + // Should + Assert.True(compareString1Result.Score > compareString2Result.Score, + $"Query: \"{queryString}\"{Environment.NewLine} " + + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + + $"Should be greater than{Environment.NewLine}" + + $"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}"); } [TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")] diff --git a/Flow.Launcher/Converters/TranslationConverter.cs b/Flow.Launcher/Converters/TranslationConverter.cs new file mode 100644 index 000000000..e1e8a58e3 --- /dev/null +++ b/Flow.Launcher/Converters/TranslationConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using Flow.Launcher.Core.Resource; + +namespace Flow.Launcher.Converters +{ + public class TranlationConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var key = value.ToString(); + if (String.IsNullOrEmpty(key)) + return key; + return InternationalizationManager.Instance.GetTranslation(key); + } + + public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException(); + } +} diff --git a/Flow.Launcher/CustomShortcutSetting.xaml b/Flow.Launcher/CustomShortcutSetting.xaml new file mode 100644 index 000000000..78b392f3e --- /dev/null +++ b/Flow.Launcher/CustomShortcutSetting.xaml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher/CustomShortcutSetting.xaml.cs b/Flow.Launcher/CustomShortcutSetting.xaml.cs new file mode 100644 index 000000000..097d6a53b --- /dev/null +++ b/Flow.Launcher/CustomShortcutSetting.xaml.cs @@ -0,0 +1,73 @@ +using Flow.Launcher.Core.Resource; +using Flow.Launcher.ViewModel; +using System; +using System.Windows; +using System.Windows.Input; + +namespace Flow.Launcher +{ + public partial class CustomShortcutSetting : Window + { + private SettingWindowViewModel viewModel; + public string Key { get; set; } = String.Empty; + public string Value { get; set; } = String.Empty; + private string originalKey { get; init; } = null; + private string originalValue { get; init; } = null; + private bool update { get; init; } = false; + + public CustomShortcutSetting(SettingWindowViewModel vm) + { + viewModel = vm; + InitializeComponent(); + } + + public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm) + { + viewModel = vm; + Key = key; + Value = value; + originalKey = key; + originalValue = value; + update = true; + InitializeComponent(); + } + + private void BtnCancel_OnClick(object sender, RoutedEventArgs e) + { + DialogResult = false; + Close(); + } + + private void BtnAdd_OnClick(object sender, RoutedEventArgs e) + { + if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value)) + { + MessageBox.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut")); + return; + } + // Check if key is modified or adding a new one + if (((update && originalKey != Key) || !update) + && viewModel.ShortcutExists(Key)) + { + MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut")); + return; + } + DialogResult = !update || originalKey != Key || originalValue != Value; + Close(); + } + + private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e) + { + DialogResult = false; + Close(); + } + + private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e) + { + App.API.ChangeQuery(tbExpand.Text); + Application.Current.MainWindow.Show(); + Application.Current.MainWindow.Opacity = 1; + Application.Current.MainWindow.Focus(); + } + } +} diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 0eff97f4a..2e3922d06 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -144,12 +144,18 @@ Show Hotkey Show result selection hotkey with results. Custom Query Hotkey + Custom Query Shortcut Query + Shortcut + Expanded + Description Delete Edit Add Please select an item Are you sure you want to delete {0} plugin hotkey? + Are you sure you want to delete shortcut: {0} with expansion {1}? + Get text from clipboard. Query window shadow effect Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited. Window Width Size @@ -241,6 +247,12 @@ Invalid plugin hotkey Update + + Custom Query Shortcut + Enter a shortcut that automatically expands to the specified query. + Shortcut already exists, please enter a new Shortcut or edit the existing one. + Shortcut and/or its expansion is empty. + Hotkey Unavailable diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 6f3915076..922f8b933 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -300,7 +300,7 @@ + PreviewMouseLeftButtonUp="OnPreviewMouseButtonDown" /> diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 630daf42e..6a65a38f2 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Threading.Tasks; using System.Windows; @@ -20,6 +20,11 @@ using Flow.Launcher.Infrastructure; using System.Windows.Media; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Plugin.SharedCommands; +using System.Text; +using DataObject = System.Windows.DataObject; +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using System.IO; using System.Windows.Threading; using System.Windows.Data; diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml index 5c497b925..1ece390d1 100644 --- a/Flow.Launcher/ResultListBox.xaml +++ b/Flow.Launcher/ResultListBox.xaml @@ -25,12 +25,15 @@ VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Standard" Visibility="{Binding Visbility}" - mc:Ignorable="d"> + mc:Ignorable="d" + PreviewMouseMove="ResultList_MouseMove" + PreviewMouseLeftButtonDown="ResultList_PreviewMouseLeftButtonDown"> -