diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 895ec1ecd..0bd1a809b 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -166,6 +166,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool UseAnimation { get; set; } = true; public bool UseSound { get; set; } = true; public double SoundVolume { get; set; } = 50; + public bool ShowBadges { get; set; } = false; + public bool ShowBadgesGlobalOnly { get; set; } = false; public bool UseClock { get; set; } = true; public bool UseDate { get; set; } = false; diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 910485438..f561fcb1d 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -12,12 +12,19 @@ namespace Flow.Launcher.Plugin /// public class Result { + /// + /// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users. + /// + public const int MaxScore = int.MaxValue; + private string _pluginDirectory; private string _icoPath; private string _copyText = string.Empty; + private string _badgeIcoPath; + /// /// The title of the result. This is always required. /// @@ -60,7 +67,7 @@ namespace Flow.Launcher.Plugin /// GlyphInfo is prioritized if not null public string IcoPath { - get { return _icoPath; } + get => _icoPath; set { // As a standard this property will handle prepping and converting to absolute local path for icon image processing @@ -80,6 +87,33 @@ namespace Flow.Launcher.Plugin } } + /// + /// The image to be displayed for the badge of the result. + /// + /// Can be a local file path or a URL. + /// If null or empty, will use plugin icon + public string BadgeIcoPath + { + get => _badgeIcoPath; + set + { + // As a standard this property will handle prepping and converting to absolute local path for icon image processing + if (!string.IsNullOrEmpty(value) + && !string.IsNullOrEmpty(PluginDirectory) + && !Path.IsPathRooted(value) + && !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) + { + _badgeIcoPath = Path.Combine(PluginDirectory, value); + } + else + { + _badgeIcoPath = value; + } + } + } + /// /// Determines if Icon has a border radius /// @@ -94,14 +128,18 @@ namespace Flow.Launcher.Plugin /// /// Delegate to load an icon for this result. /// - public IconDelegate Icon; + public IconDelegate Icon { get; set; } + + /// + /// Delegate to load an icon for the badge of this result. + /// + public IconDelegate BadgeIcon { get; set; } /// /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) /// public GlyphInfo Glyph { get; init; } - /// /// An action to take in the form of a function call when the result has been selected. /// @@ -143,59 +181,19 @@ namespace Flow.Launcher.Plugin /// public string PluginDirectory { - get { return _pluginDirectory; } + get => _pluginDirectory; set { _pluginDirectory = value; // When the Result object is returned from the query call, PluginDirectory is not provided until // UpdatePluginMetadata call is made at PluginManager.cs L196. Once the PluginDirectory becomes available - // we need to update (only if not Uri path) the IcoPath with the full absolute path so the image can be loaded. + // we need to update (only if not Uri path) the IcoPath and BadgeIcoPath with the full absolute path so the image can be loaded. IcoPath = _icoPath; + BadgeIcoPath = _badgeIcoPath; } } - /// - public override string ToString() - { - return Title + SubTitle + Score; - } - - /// - /// Clones the current result - /// - public Result Clone() - { - return new Result - { - Title = Title, - SubTitle = SubTitle, - ActionKeywordAssigned = ActionKeywordAssigned, - CopyText = CopyText, - AutoCompleteText = AutoCompleteText, - IcoPath = IcoPath, - RoundedIcon = RoundedIcon, - Icon = Icon, - Glyph = Glyph, - Action = Action, - AsyncAction = AsyncAction, - Score = Score, - TitleHighlightData = TitleHighlightData, - OriginQuery = OriginQuery, - PluginDirectory = PluginDirectory, - ContextData = ContextData, - PluginID = PluginID, - TitleToolTip = TitleToolTip, - SubTitleToolTip = SubTitleToolTip, - PreviewPanel = PreviewPanel, - ProgressBar = ProgressBar, - ProgressBarColor = ProgressBarColor, - Preview = Preview, - AddSelectedCount = AddSelectedCount, - RecordKey = RecordKey - }; - } - /// /// Additional data associated with this result /// @@ -224,16 +222,6 @@ namespace Flow.Launcher.Plugin /// public Lazy PreviewPanel { get; set; } - /// - /// Run this result, asynchronously - /// - /// - /// - public ValueTask ExecuteAsync(ActionContext context) - { - return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false); - } - /// /// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result /// @@ -255,11 +243,6 @@ namespace Flow.Launcher.Plugin /// public bool AddSelectedCount { get; set; } = true; - /// - /// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users. - /// - public const int MaxScore = int.MaxValue; - /// /// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records. /// This can be useful when your plugin will change the Title or SubTitle of the result dynamically. @@ -268,6 +251,66 @@ namespace Flow.Launcher.Plugin /// public string RecordKey { get; set; } = null; + /// + /// Determines if the badge icon should be shown. + /// If users want to show the result badges and here you set this to true, the results will show the badge icon. + /// + public bool ShowBadge { get; set; } = false; + + /// + /// Run this result, asynchronously + /// + /// + /// + public ValueTask ExecuteAsync(ActionContext context) + { + return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false); + } + + /// + public override string ToString() + { + return Title + SubTitle + Score; + } + + /// + /// Clones the current result + /// + public Result Clone() + { + return new Result + { + Title = Title, + SubTitle = SubTitle, + ActionKeywordAssigned = ActionKeywordAssigned, + CopyText = CopyText, + AutoCompleteText = AutoCompleteText, + IcoPath = IcoPath, + BadgeIcoPath = BadgeIcoPath, + RoundedIcon = RoundedIcon, + Icon = Icon, + BadgeIcon = BadgeIcon, + Glyph = Glyph, + Action = Action, + AsyncAction = AsyncAction, + Score = Score, + TitleHighlightData = TitleHighlightData, + OriginQuery = OriginQuery, + PluginDirectory = PluginDirectory, + ContextData = ContextData, + PluginID = PluginID, + TitleToolTip = TitleToolTip, + SubTitleToolTip = SubTitleToolTip, + PreviewPanel = PreviewPanel, + ProgressBar = ProgressBar, + ProgressBarColor = ProgressBarColor, + Preview = Preview, + AddSelectedCount = AddSelectedCount, + RecordKey = RecordKey, + ShowBadge = ShowBadge, + }; + } + /// /// Info of the preview section of a /// diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 420da266d..9ec952155 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -39,8 +39,8 @@ namespace Flow.Launcher.Test.Plugins } [SupportedOSPlatform("windows7.0")] - [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")] - [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")] + [TestCase("C:\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY {QueryConstructor.OrderIdentifier}")] + [TestCase("C:\\SomeFolder\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY {QueryConstructor.OrderIdentifier}")] public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString) { // Given @@ -59,7 +59,7 @@ namespace Flow.Launcher.Test.Plugins [TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" + " FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" + " AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" + - " ORDER BY System.FileName")] + $" ORDER BY {QueryConstructor.OrderIdentifier}")] public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString( string folderPath, string userSearchString, string expectedString) { @@ -87,8 +87,8 @@ namespace Flow.Launcher.Test.Plugins [SupportedOSPlatform("windows7.0")] [TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " + "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + - "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")] - [TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")] + $"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")] + [TestCase("", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")] public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { @@ -107,7 +107,6 @@ namespace Flow.Launcher.Test.Plugins ClassicAssert.AreEqual(expectedString, resultString); } - [SupportedOSPlatform("windows7.0")] [TestCase(@"some words", @"FREETEXT('some words')")] public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString( @@ -127,7 +126,7 @@ namespace Flow.Launcher.Test.Plugins [SupportedOSPlatform("windows7.0")] [TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " + - "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")] + $"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")] public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString( string userSearchString, string expectedString) { diff --git a/Flow.Launcher/Converters/BadgePositionConverter.cs b/Flow.Launcher/Converters/BadgePositionConverter.cs new file mode 100644 index 000000000..66a7446f2 --- /dev/null +++ b/Flow.Launcher/Converters/BadgePositionConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace Flow.Launcher.Converters; + +public class BadgePositionConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double actualWidth && parameter is string param) + { + double offset = actualWidth / 2 - 8; + + if (param == "1") // X-Offset + { + return offset + 2; + } + else if (param == "2") // Y-Offset + { + return offset + 2; + } + } + + return 0.0; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/Flow.Launcher/Converters/SizeRatioConverter.cs b/Flow.Launcher/Converters/SizeRatioConverter.cs new file mode 100644 index 000000000..e61eeaf9b --- /dev/null +++ b/Flow.Launcher/Converters/SizeRatioConverter.cs @@ -0,0 +1,27 @@ +using System.Windows.Data; +using System; +using System.Globalization; +using System.Windows; + +namespace Flow.Launcher.Converters; + +public class SizeRatioConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double size && parameter is string ratioString) + { + if (double.TryParse(ratioString, NumberStyles.Any, CultureInfo.InvariantCulture, out double ratio)) + { + return size * ratio; + } + } + + return 0.0; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index f3a645767..5a6fd3d74 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -302,6 +302,10 @@ Use Segoe Fluent Icons Use Segoe Fluent Icons for query results where supported Press Key + Show Result Badges + Show badges for query results where supported + Show Result Badges for Global Query Only + Show badges for global query results only HTTP Proxy diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml index 3744e0449..8cb15400f 100644 --- a/Flow.Launcher/ResultListBox.xaml +++ b/Flow.Launcher/ResultListBox.xaml @@ -32,6 +32,8 @@ + + @@ -90,60 +92,64 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + - +