diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 649d59ad7..b5ba36f98 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -72,9 +72,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 2c9b8d4fd..9404d1107 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -4,11 +4,13 @@ using System.IO; using System.Threading.Tasks; using System.Windows.Controls; using System.Windows.Media; +using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { /// - /// Describes a result of a executed by a plugin + /// Describes a result of a executed by a plugin. + /// This or its child classes is serializable. /// public class Result { @@ -21,6 +23,8 @@ namespace Flow.Launcher.Plugin private string _icoPath; + private string _icoPathAbsolute; + private string _copyText = string.Empty; private string _badgeIcoPath; @@ -64,15 +68,27 @@ namespace Flow.Launcher.Plugin public string AutoCompleteText { get; set; } /// - /// The image to be displayed for the result. + /// Path or URI to the icon image for this result. + /// Updates appropriately when set. /// - /// Can be a local file path or a URL. - /// GlyphInfo is prioritized if not null + /// + /// Preferred usage: provide a path relative to the plugin directory (for example: "Images\icon.png"). + /// Because is serialized, using relative paths keeps the icon reference portable + /// when Flow is moved. + /// + /// Accepted formats: + /// - Relative file paths (resolved against into ) + /// - Absolute file paths (left as-is) + /// - HTTP/HTTPS URLs (left as-is) + /// - Data URIs (left as-is) + /// public string IcoPath { get => _icoPath; set { + _icoPath = value; + // 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) @@ -81,15 +97,23 @@ namespace Flow.Launcher.Plugin && !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase) && !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase)) { - _icoPath = Path.Combine(PluginDirectory, value); + _icoPathAbsolute = Path.Combine(PluginDirectory, value); } else { - _icoPath = value; + _icoPathAbsolute = value; } } } + /// + /// Absolute path or URI which is used to load and display the result icon for Flow. + /// This is populated by the setter. + /// If a relative path was provided to , this property will contain the resolved + /// absolute local path after combining with . + /// + public string IcoPathAbsolute => _icoPathAbsolute; + /// /// The image to be displayed for the badge of the result. /// @@ -131,17 +155,34 @@ namespace Flow.Launcher.Plugin /// /// Delegate to load an icon for this result. /// + [JsonIgnore] public IconDelegate Icon = null; /// /// Delegate to load an icon for the badge of this result. /// + [JsonIgnore] public IconDelegate BadgeIcon = null; + private GlyphInfo _glyph; + /// /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) /// - public GlyphInfo Glyph { get; init; } + public GlyphInfo Glyph + { + get => _glyph; + init => _glyph = value; + } + + /// + /// Set the Glyph Icon after initialization + /// + /// + public void SetGlyph(GlyphInfo glyph) + { + _glyph = glyph; + } /// /// An action to take in the form of a function call when the result has been selected. @@ -151,6 +192,7 @@ namespace Flow.Launcher.Plugin /// Its result determines what happens to Flow Launcher's query form: /// when true, the form will be hidden; when false, it will stay in focus. /// + [JsonIgnore] public Func Action { get; set; } /// @@ -161,6 +203,7 @@ namespace Flow.Launcher.Plugin /// Its result determines what happens to Flow Launcher's query form: /// when true, the form will be hidden; when false, it will stay in focus. /// + [JsonIgnore] public Func> AsyncAction { get; set; } /// @@ -203,11 +246,13 @@ namespace Flow.Launcher.Plugin /// /// As external information for ContextMenu /// + [JsonIgnore] public object ContextData { get; set; } /// /// Plugin ID that generated this result /// + [JsonInclude] public string PluginID { get; internal set; } /// @@ -223,6 +268,7 @@ namespace Flow.Launcher.Plugin /// /// Customized Preview Panel /// + [JsonIgnore] public Lazy PreviewPanel { get; set; } /// @@ -352,6 +398,7 @@ namespace Flow.Launcher.Plugin /// /// Delegate to get the preview panel's image /// + [JsonIgnore] public IconDelegate PreviewDelegate { get; set; } = null; /// diff --git a/Flow.Launcher.Plugin/packages.lock.json b/Flow.Launcher.Plugin/packages.lock.json index 70f71f20d..662192b4a 100644 --- a/Flow.Launcher.Plugin/packages.lock.json +++ b/Flow.Launcher.Plugin/packages.lock.json @@ -16,23 +16,23 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.102, )", + "resolved": "10.0.102", + "contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.102", + "Microsoft.SourceLink.Common": "10.0.102" } }, "Microsoft.Windows.CsWin32": { "type": "Direct", - "requested": "[0.3.205, )", - "resolved": "0.3.205", - "contentHash": "U5wGAnyKd7/I2YMd43nogm81VMtjiKzZ9dsLMVI4eAB7jtv5IEj0gprj0q/F3iRmAIaGv5omOf8iSYx2+nE6BQ==", + "requested": "[0.3.269, )", + "resolved": "0.3.269", + "contentHash": "O4GVJ0ymxcoFRGS07VcoEClj7A9PIciHIjWDrPymzonhYlOfM7V0ZqGBUK19cUH3BPca9MfSOH0KLK/9JzQ8+Q==", "dependencies": { "Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha", - "Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview", - "Microsoft.Windows.WDK.Win32Metadata": "0.12.8-experimental" + "Microsoft.Windows.SDK.Win32Metadata": "69.0.7-preview", + "Microsoft.Windows.WDK.Win32Metadata": "0.13.25-experimental" } }, "PropertyChanged.Fody": { @@ -46,13 +46,13 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.102", + "contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg==" }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.102", + "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A==" }, "Microsoft.Windows.SDK.Win32Docs": { "type": "Transitive", @@ -61,15 +61,15 @@ }, "Microsoft.Windows.SDK.Win32Metadata": { "type": "Transitive", - "resolved": "61.0.15-preview", - "contentHash": "cysex3dazKtCPALCluC2XX3f5Aedy9H2pw5jb+TW5uas2rkem1Z7FRnbUrg2vKx0pk0Qz+4EJNr37HdYTEcvEQ==" + "resolved": "69.0.7-preview", + "contentHash": "RJoNjQJVCIDNLPbvYuaygCFknTyAxOUE45of1voj0jjOgJa9MB2m1/G8L8F3IYc+2EFG5aqa/9y8PEx7Tk2tLQ==" }, "Microsoft.Windows.WDK.Win32Metadata": { "type": "Transitive", - "resolved": "0.12.8-experimental", - "contentHash": "3n8R44/Z96Ly+ty4eYVJfESqbzvpw96lRLs3zOzyDmr1x1Kw7FNn5CyE416q+bZQV3e1HRuMUvyegMeRE/WedA==", + "resolved": "0.13.25-experimental", + "contentHash": "IM50tb/+UIwBr9FMr6ZKcZjCMW+Axo6NjGqKxgjUfyCY8dRnYUfrJEXxAaXoWtYP4X8EmASmC1Jtwh4XucseZg==", "dependencies": { - "Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview" + "Microsoft.Windows.SDK.Win32Metadata": "63.0.31-preview" } } } diff --git a/Flow.Launcher.Test/Plugins/CalculatorTest.cs b/Flow.Launcher.Test/Plugins/CalculatorTest.cs index b075813db..4e40d3645 100644 --- a/Flow.Launcher.Test/Plugins/CalculatorTest.cs +++ b/Flow.Launcher.Test/Plugins/CalculatorTest.cs @@ -16,14 +16,15 @@ namespace Flow.Launcher.Test.Plugins { DecimalSeparator = DecimalSeparator.UseSystemLocale, MaxDecimalPlaces = 10, - ShowErrorMessage = false // Make sure we return the empty results when error occurs + ShowErrorMessage = false, // Make sure we return the empty results when error occurs + UseThousandsSeparator = true // Default value }; private readonly Engine _engine = new(new Configuration { Scope = new Dictionary - { - { "e", Math.E }, // e is not contained in the default mages engine - } + { + { "e", Math.E }, // e is not contained in the default mages engine + } }); public CalculatorPluginTest() @@ -41,6 +42,44 @@ namespace Flow.Launcher.Test.Plugins engineField.SetValue(null, _engine); } + [Test] + public void ThousandsSeparatorTest_Enabled() + { + _settings.UseThousandsSeparator = true; + + _settings.DecimalSeparator = DecimalSeparator.Dot; + var result = GetCalculationResult("1000+234"); + // When thousands separator is enabled, the result should contain a separator + // Since decimal separator is dot, thousands separator should be comma + ClassicAssert.AreEqual("1,234", result); + + _settings.DecimalSeparator = DecimalSeparator.Comma; + var result2 = GetCalculationResult("1000+234"); + // When thousands separator is enabled, the result should contain a separator + // Since decimal separator is comma, thousands separator should be dot + ClassicAssert.AreEqual("1.234", result2); + } + + [Test] + public void ThousandsSeparatorTest_Disabled() + { + _settings.UseThousandsSeparator = false; + _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale; + + var result = GetCalculationResult("1000+234"); + ClassicAssert.AreEqual("1234", result); + } + + [Test] + public void ThousandsSeparatorTest_LargeNumber() + { + _settings.UseThousandsSeparator = false; + _settings.DecimalSeparator = DecimalSeparator.UseSystemLocale; + + var result = GetCalculationResult("1000000+234567"); + ClassicAssert.AreEqual("1234567", result); + } + // Basic operations [TestCase(@"1+1", "2")] [TestCase(@"2-1", "1")] @@ -77,6 +116,9 @@ namespace Flow.Launcher.Test.Plugins [TestCase(@"invalid_expression", "")] public void CalculatorTest(string expression, string result) { + _settings.UseThousandsSeparator = false; + _settings.DecimalSeparator = DecimalSeparator.Dot; + ClassicAssert.AreEqual(GetCalculationResult(expression), result); } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index b45bbc549..da11380b8 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -259,6 +259,9 @@ namespace Flow.Launcher await PluginManager.InitializePluginsAsync(_mainVM); + // Refresh the history results after plugins are initialized so that we can parse the absolute icon paths + _mainVM.RefreshLastOpenedHistoryResults(); + // Refresh home page after plugins are initialized because users may open main window during plugin initialization // And home page is created without full plugin list if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText)) diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index f3c614702..576bf6f2f 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -138,7 +138,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Flow.Launcher/Helper/ResultHelper.cs b/Flow.Launcher/Helper/ResultHelper.cs index b8b7ff98e..017651fdf 100644 --- a/Flow.Launcher/Helper/ResultHelper.cs +++ b/Flow.Launcher/Helper/ResultHelper.cs @@ -11,7 +11,7 @@ namespace Flow.Launcher.Helper; public static class ResultHelper { - public static async Task PopulateResultsAsync(LastOpenedHistoryItem item) + public static async Task PopulateResultsAsync(LastOpenedHistoryResult item) { return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey); } @@ -24,7 +24,7 @@ public static class ResultHelper if (query == null) return null; try { - var freshResults = await plugin.Plugin.QueryAsync(query, CancellationToken.None); + var freshResults = await PluginManager.QueryForPluginAsync(plugin, query, CancellationToken.None); // Try to match by record key first if it is valid, otherwise fall back to title + subtitle match if (string.IsNullOrEmpty(recordKey)) { diff --git a/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs b/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs new file mode 100644 index 000000000..78985108c --- /dev/null +++ b/Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs @@ -0,0 +1,253 @@ +using iNKORE.UI.WPF.Modern.Controls; +using iNKORE.UI.WPF.Modern.Controls.Helpers; +using iNKORE.UI.WPF.Modern.Controls.Primitives; +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace Flow.Launcher.Resources.Controls +{ + // TODO: Use IsScrollAnimationEnabled property in future: https://github.com/iNKORE-NET/UI.WPF.Modern/pull/347 + public class CustomScrollViewerEx : ScrollViewer + { + private double LastVerticalLocation = 0; + private double LastHorizontalLocation = 0; + + public CustomScrollViewerEx() + { + Loaded += OnLoaded; + var valueSource = DependencyPropertyHelper.GetValueSource(this, AutoPanningMode.IsEnabledProperty).BaseValueSource; + if (valueSource == BaseValueSource.Default) + { + AutoPanningMode.SetIsEnabled(this, true); + } + } + + #region Orientation + + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(CustomScrollViewerEx), + new PropertyMetadata(Orientation.Vertical)); + + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + #endregion + + #region AutoHideScrollBars + + public static readonly DependencyProperty AutoHideScrollBarsProperty = + ScrollViewerHelper.AutoHideScrollBarsProperty + .AddOwner( + typeof(CustomScrollViewerEx), + new PropertyMetadata(true, OnAutoHideScrollBarsChanged)); + + public bool AutoHideScrollBars + { + get => (bool)GetValue(AutoHideScrollBarsProperty); + set => SetValue(AutoHideScrollBarsProperty, value); + } + + private static void OnAutoHideScrollBarsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is CustomScrollViewerEx sv) + { + sv.UpdateVisualState(); + } + } + + #endregion + + private void OnLoaded(object sender, RoutedEventArgs e) + { + LastVerticalLocation = VerticalOffset; + LastHorizontalLocation = HorizontalOffset; + UpdateVisualState(false); + } + + /// + protected override void OnInitialized(EventArgs e) + { + base.OnInitialized(e); + + if (Style == null && ReadLocalValue(StyleProperty) == DependencyProperty.UnsetValue) + { + SetResourceReference(StyleProperty, typeof(ScrollViewer)); + } + } + + /// + protected override void OnMouseWheel(MouseWheelEventArgs e) + { + var Direction = GetDirection(); + ScrollViewerBehavior.SetIsAnimating(this, true); + + if (Direction == Orientation.Vertical) + { + if (ScrollableHeight > 0) + { + e.Handled = true; + } + + var WheelChange = e.Delta * (ViewportHeight / 1.5) / ActualHeight; + var newOffset = LastVerticalLocation - WheelChange; + + if (newOffset < 0) + { + newOffset = 0; + } + + if (newOffset > ScrollableHeight) + { + newOffset = ScrollableHeight; + } + + if (newOffset == LastVerticalLocation) + { + return; + } + + ScrollToVerticalOffset(LastVerticalLocation); + + ScrollToValue(newOffset, Direction); + LastVerticalLocation = newOffset; + } + else + { + if (ScrollableWidth > 0) + { + e.Handled = true; + } + + var WheelChange = e.Delta * (ViewportWidth / 1.5) / ActualWidth; + var newOffset = LastHorizontalLocation - WheelChange; + + if (newOffset < 0) + { + newOffset = 0; + } + + if (newOffset > ScrollableWidth) + { + newOffset = ScrollableWidth; + } + + if (newOffset == LastHorizontalLocation) + { + return; + } + + ScrollToHorizontalOffset(LastHorizontalLocation); + + ScrollToValue(newOffset, Direction); + LastHorizontalLocation = newOffset; + } + } + + /// + protected override void OnScrollChanged(ScrollChangedEventArgs e) + { + base.OnScrollChanged(e); + if (!ScrollViewerBehavior.GetIsAnimating(this)) + { + LastVerticalLocation = VerticalOffset; + LastHorizontalLocation = HorizontalOffset; + } + } + + private Orientation GetDirection() + { + var isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + + if (Orientation == Orientation.Horizontal) + { + return isShiftDown ? Orientation.Vertical : Orientation.Horizontal; + } + else + { + return isShiftDown ? Orientation.Horizontal : Orientation.Vertical; + } + } + + /// + /// Causes the to load a new view into the viewport using the specified offsets and zoom factor. + /// + /// A value between 0 and that specifies the distance the content should be scrolled horizontally. + /// A value between 0 and that specifies the distance the content should be scrolled vertically. + /// A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor. + /// if the view is changed; otherwise, . + public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor) + { + return ChangeView(horizontalOffset, verticalOffset, zoomFactor, false); + } + + /// + /// Causes the to load a new view into the viewport using the specified offsets and zoom factor, and optionally disables scrolling animation. + /// + /// A value between 0 and that specifies the distance the content should be scrolled horizontally. + /// A value between 0 and that specifies the distance the content should be scrolled vertically. + /// A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor. + /// to disable zoom/pan animations while changing the view; otherwise, . The default is false. + /// if the view is changed; otherwise, . + public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor, bool disableAnimation) + { + if (disableAnimation) + { + if (horizontalOffset.HasValue) + { + ScrollToHorizontalOffset(horizontalOffset.Value); + } + + if (verticalOffset.HasValue) + { + ScrollToVerticalOffset(verticalOffset.Value); + } + } + else + { + if (horizontalOffset.HasValue) + { + ScrollToHorizontalOffset(LastHorizontalLocation); + ScrollToValue(Math.Min(ScrollableWidth, horizontalOffset.Value), Orientation.Horizontal); + LastHorizontalLocation = horizontalOffset.Value; + } + + if (verticalOffset.HasValue) + { + ScrollToVerticalOffset(LastVerticalLocation); + ScrollToValue(Math.Min(ScrollableHeight, verticalOffset.Value), Orientation.Vertical); + LastVerticalLocation = verticalOffset.Value; + } + } + + return true; + } + + private void ScrollToValue(double value, Orientation Direction) + { + if (Direction == Orientation.Vertical) + { + ScrollToVerticalOffset(value); + } + else + { + ScrollToHorizontalOffset(value); + } + + ScrollViewerBehavior.SetIsAnimating(this, false); + } + + private void UpdateVisualState(bool useTransitions = true) + { + var stateName = AutoHideScrollBars ? "NoIndicator" : "MouseIndicator"; + VisualStateManager.GoToState(this, stateName, useTransitions); + } + } +} diff --git a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml index aa027e19e..4d37dc93a 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPanePluginStore.xaml @@ -333,7 +333,7 @@ Margin="18 24 0 0" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="Fant" - Source="{Binding IcoPath, IsAsync=True}" /> + Source="{Binding IcoPathAbsolute, IsAsync=True}" /> +/// A serializable result used to record the last opened history for reopening results. +/// Inherits common result fields from and adds the original query and execution time. +/// +public class LastOpenedHistoryResult : Result +{ + /// + /// The query string from Query.TrimmedQuery property, it is stored as a string instead of the entire Query class . + /// This is used so results can be reopened or re-run using the serialized query string. + /// + public string Query { get; set; } = string.Empty; + + /// + /// The local date and time when this result was executed/opened. + /// + public DateTime ExecutedDateTime { get; set; } + + /// + /// Initializes a new instance of . + /// + public LastOpenedHistoryResult() + { + } + + /// + /// Creates a from an existing . + /// Copies required fields and sets up default reopening actions. + /// + /// The original result to create history from. + public LastOpenedHistoryResult(Result result) + { + Title = result.Title; + SubTitle = result.SubTitle; + PluginID = result.PluginID; + Query = result.OriginQuery.TrimmedQuery; + OriginQuery = result.OriginQuery; + RecordKey = result.RecordKey; + IcoPath = result.IcoPath; + PluginDirectory = result.PluginDirectory; + Glyph = result.Glyph; + ExecutedDateTime = DateTime.Now; + // Used for Query History style reopening + Action = _ => + { + App.API.BackToQueryResults(); + App.API.ChangeQuery(result.OriginQuery.TrimmedQuery); + return false; + }; + // Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs + AsyncAction = null; + } + + /// + /// Selectively creates a deep copy of the required properties for + /// based on the style of history- Last Opened or Query. + /// This copy should be independent of original and full isolated. + /// + /// A new containing the same required data. + public LastOpenedHistoryResult DeepCopyForHistoryStyle(bool isHistoryStyleLastOpened) + { + // queryValue and glyphValue are captured to ensure they are correctly referenced in the Action delegate. + var queryValue = Query; + var glyphValue = Glyph; + + var title = string.Empty; + var showBadge = false; + var badgeIcoPath = string.Empty; + var icoPath = string.Empty; + var glyph = null as GlyphInfo; + + if (isHistoryStyleLastOpened) + { + title = Title; + icoPath = IcoPath; + glyph = glyphValue != null + ? new GlyphInfo(glyphValue.FontFamily, glyphValue.Glyph) + : null; + showBadge = true; + badgeIcoPath = Constant.HistoryIcon; + } + else + { + title = Localize.executeQuery(Query); + icoPath = Constant.HistoryIcon; + glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C"); + showBadge = false; + } + + return new LastOpenedHistoryResult + { + Title = title, + // Subtitle has datetime which can cause duplicates when saving. + SubTitle = Localize.lastExecuteTime(ExecutedDateTime), + // Empty PluginID so the source of last opened history results won't be updated, this copy is meant to be temporary. + PluginID = string.Empty, + Query = Query, + OriginQuery = new Query { TrimmedQuery = Query }, + RecordKey = RecordKey, + IcoPath = icoPath, + ShowBadge = showBadge, + BadgeIcoPath = badgeIcoPath, + PluginDirectory = PluginDirectory, + // Used for Query History style reopening + Action = _ => + { + App.API.BackToQueryResults(); + App.API.ChangeQuery(queryValue); + return false; + }, + // Used for Last Opened History style reopening, currently need to be assigned at MainViewModel.cs + AsyncAction = null, + Glyph = glyph, + ExecutedDateTime = ExecutedDateTime + // Note: Other properties are left as default — copy if needed. + }; + } + + /// + /// Determines whether the specified is equivalent to this history result. + /// Comparison uses when available; otherwise falls back to title/subtitle/plugin id and query. + /// + /// The result to compare to. + /// true if the results are considered equal; otherwise false. + public bool Equals(Result r) + { + if (string.IsNullOrEmpty(RecordKey) || string.IsNullOrEmpty(r.RecordKey)) + { + return Title == r.Title + && SubTitle == r.SubTitle + && PluginID == r.PluginID + && Query == r.OriginQuery.TrimmedQuery; + } + else + { + return RecordKey == r.RecordKey + && PluginID == r.PluginID + && Query == r.OriginQuery.TrimmedQuery; + } + } +} diff --git a/Flow.Launcher/Storage/QueryHistory.cs b/Flow.Launcher/Storage/QueryHistory.cs index 6998a4ae3..7bf948399 100644 --- a/Flow.Launcher/Storage/QueryHistory.cs +++ b/Flow.Launcher/Storage/QueryHistory.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage @@ -14,28 +15,50 @@ namespace Flow.Launcher.Storage #pragma warning restore CS0618 // Type or member is obsolete [JsonInclude] - public List LastOpenedHistoryItems { get; private set; } = []; + public List LastOpenedHistoryItems { get; private set; } = []; private readonly int _maxHistory = 300; + /// + /// Migrate legacy history data (stored in ) into the new + /// format and append them to + /// . + /// + [Obsolete("For backwards compatibility. Remove after release v2.3.0")] public void PopulateHistoryFromLegacyHistory() { if (Items.Count == 0) return; // Migrate old history items to new LastOpenedHistoryItems foreach (var item in Items) { - LastOpenedHistoryItems.Add(new LastOpenedHistoryItem + LastOpenedHistoryItems.Add(new LastOpenedHistoryResult { + Title = Localize.executeQuery(item.Query), + OriginQuery = new Query { TrimmedQuery = item.Query }, Query = item.Query, + Action = _ => + { + App.API.BackToQueryResults(); + App.API.ChangeQuery(item.Query); + return false; + }, ExecutedDateTime = item.ExecutedDateTime }); } Items.Clear(); } + /// + /// Records a result into the last-opened history list (). + /// This will also update the IcoPath if existing history item has one that is different. + /// + /// The result to add to history. Must have a non-empty .. public void Add(Result result) { if (string.IsNullOrEmpty(result.OriginQuery.TrimmedQuery)) return; + // History results triggered from homepage do not contain PluginID, + // these are intentionally not saved otherwise cause duplicates due to subtitle + // containing datetime string. if (string.IsNullOrEmpty(result.PluginID)) return; // Maintain the max history limit @@ -44,23 +67,53 @@ namespace Flow.Launcher.Storage LastOpenedHistoryItems.RemoveAt(0); } - // If the last item is the same as the current result, just update the timestamp - if (LastOpenedHistoryItems.Count > 0 && - LastOpenedHistoryItems.Last().Equals(result)) + // If the last item is the same as the current result, just update the timestamp and the icon path + if (LastOpenedHistoryItems.Count > 0 && + TryGetLastOpenedHistoryResult(result, out var existingHistoryItem)) { - LastOpenedHistoryItems.Last().ExecutedDateTime = DateTime.Now; + existingHistoryItem.ExecutedDateTime = DateTime.Now; + + if (existingHistoryItem.IcoPath != result.IcoPath) + existingHistoryItem.IcoPath = result.IcoPath; + + if (existingHistoryItem.Glyph?.Glyph != result.Glyph?.Glyph + || existingHistoryItem.Glyph?.FontFamily != result.Glyph?.FontFamily) + existingHistoryItem.SetGlyph(result.Glyph); } else { - LastOpenedHistoryItems.Add(new LastOpenedHistoryItem - { - Title = result.Title, - SubTitle = result.SubTitle, - PluginID = result.PluginID, - Query = result.OriginQuery.TrimmedQuery, - RecordKey = result.RecordKey, - ExecutedDateTime = DateTime.Now - }); + LastOpenedHistoryItems.Add(new LastOpenedHistoryResult(result)); + } + } + + /// + /// Attempts to find an existing in + /// that is considered equal to the supplied . + /// + private bool TryGetLastOpenedHistoryResult(Result result, out LastOpenedHistoryResult historyItem) + { + historyItem = LastOpenedHistoryItems.FirstOrDefault(x => x.Equals(result)); + return historyItem is not null; + } + + /// + /// Flow uses IcoPathAbsolute property to display result the icons. This refreshes the IcoPathAbsolute + /// property using current plugin metadata by updating the PluginDirectory property, which in turn also + /// updates IcoPath. This keeps the saved icon paths of results updated correctly if flow is moved around. + /// + /// Call this after plugins are loaded/initialized. + public void UpdateIcoPathAbsolute() + { + if (LastOpenedHistoryItems.Count == 0) return; + + foreach (var item in LastOpenedHistoryItems) + { + if (string.IsNullOrEmpty(item.PluginID)) continue; + + var pluginPair = PluginManager.GetPluginForId(item.PluginID); + if (pluginPair == null) continue; + + item.PluginDirectory = pluginPair.Metadata.PluginDirectory; } } } diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml index c3831e68f..c5b45890b 100644 --- a/Flow.Launcher/Themes/Base.xaml +++ b/Flow.Launcher/Themes/Base.xaml @@ -252,14 +252,12 @@ - - - - + - + diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index a28b2b33e..333ac3652 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -37,16 +38,16 @@ namespace Flow.Launcher.ViewModel private Query _lastQuery; private bool _previousIsHomeQuery; - private Query _progressQuery; // Used for QueryResultAsync + private readonly ConcurrentDictionary _progressQueryDict = new(); // Used for QueryResultAsync private Query _updateQuery; // Used for ResultsUpdated private string _queryTextBeforeLeaveResults; private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results private readonly FlowLauncherJsonStorage _historyItemsStorage; - private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; - private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord; private readonly History _history; private int lastHistoryIndex = 1; + private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; + private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord; private readonly UserSelectedRecord _userSelectedRecord; private CancellationTokenSource _updateSource; // Used to cancel old query flows @@ -152,11 +153,10 @@ namespace Flow.Launcher.ViewModel }; _historyItemsStorage = new FlowLauncherJsonStorage(); - _userSelectedRecordStorage = new FlowLauncherJsonStorage(); - _topMostRecord = new FlowLauncherJsonStorageTopMostRecord(); _history = _historyItemsStorage.Load(); - _history.PopulateHistoryFromLegacyHistory(); + _userSelectedRecordStorage = new FlowLauncherJsonStorage(); _userSelectedRecord = _userSelectedRecordStorage.Load(); + _topMostRecord = new FlowLauncherJsonStorageTopMostRecord(); ContextMenu = new ResultsViewModel(Settings, this) { @@ -355,11 +355,17 @@ namespace Flow.Launcher.ViewModel if (QueryResultsSelected()) { SelectedResults = History; - History.SelectedIndex = _history.LastOpenedHistoryItems.Count - 1; + if (History.Results.Count > 0) + { + SelectedResults.SelectedIndex = 0; + SelectedResults.SelectedItem = History.Results[0]; + } } else { SelectedResults = Results; + PreviewSelectedItem = Results.SelectedItem; + _ = UpdatePreviewAsync(); } } @@ -431,7 +437,8 @@ namespace Flow.Launcher.ViewModel { // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing - if (SelectedResults.SelectedItem != null) + if (SelectedResults.SelectedItem?.Result != null && + !string.IsNullOrEmpty(SelectedResults.SelectedItem.Result.PluginID)) // Do not show context menu for history results { SelectedResults = ContextMenu; } @@ -439,6 +446,8 @@ namespace Flow.Launcher.ViewModel else { SelectedResults = Results; + PreviewSelectedItem = Results.SelectedItem; + _ = UpdatePreviewAsync(); } } @@ -642,6 +651,8 @@ namespace Flow.Launcher.ViewModel if (!QueryResultsSelected()) { SelectedResults = Results; + PreviewSelectedItem = Results.SelectedItem; + _ = UpdatePreviewAsync(); } else { @@ -1252,22 +1263,12 @@ namespace Flow.Launcher.ViewModel var selected = Results.SelectedItem?.Result; - if (selected != null) // SelectedItem returns null if selection is empty. + if (selected != null && // SelectedItem returns null if selection is empty. + !string.IsNullOrEmpty(selected.PluginID)) // SelectedItem must have a valid PluginID, history results do not. { - List results; - if (selected.PluginID == null) // SelectedItem from history in home page. - { - results = new() - { - ContextMenuTopMost(selected) - }; - } - else - { - results = PluginManager.GetContextMenusForPlugin(selected); - results.Add(ContextMenuTopMost(selected)); - results.Add(ContextMenuPluginInfo(selected)); - } + List results = PluginManager.GetContextMenusForPlugin(selected); + results.Add(ContextMenuTopMost(selected)); + results.Add(ContextMenuPluginInfo(selected)); if (!string.IsNullOrEmpty(query)) { @@ -1318,68 +1319,77 @@ namespace Flow.Launcher.ViewModel } } - private List GetHistoryItems(IEnumerable historyItems) + private List GetHistoryItems(IEnumerable historyItems, int? maxResult = null) { var results = new List(); - if (Settings.HistoryStyle == HistoryStyle.Query) - { - foreach (var h in historyItems) - { - var result = new Result - { - Title = Localize.executeQuery(h.Query), - SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime), - IcoPath = Constant.HistoryIcon, - OriginQuery = new Query { TrimmedQuery = h.Query }, - Action = _ => - { - App.API.BackToQueryResults(); - App.API.ChangeQuery(h.Query); - return false; - }, - Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C") - }; - results.Add(result); - } - } - else - { - foreach (var h in historyItems) - { - var result = new Result - { - Title = string.IsNullOrEmpty(h.Title) ? // Old migrated history items have no title - Localize.executeQuery(h.Query) : - h.Title, - SubTitle = Localize.lastExecuteTime(h.ExecutedDateTime), - IcoPath = Constant.HistoryIcon, - OriginQuery = new Query { TrimmedQuery = h.Query }, - AsyncAction = async c => - { - var reflectResult = await ResultHelper.PopulateResultsAsync(h); - if (reflectResult != null) - { - // Record the user selected record for result ranking - _userSelectedRecord.Add(reflectResult); - // Since some actions may need to hide the Flow window to execute - // So let us populate the results of them - return await reflectResult.ExecuteAsync(c); - } + // Order by executed time descending: Latest -> Oldest + historyItems = historyItems.OrderByDescending(x => x.ExecutedDateTime); - // If we cannot get the result, fallback to re-query - App.API.BackToQueryResults(); - App.API.ChangeQuery(h.Query); - return false; - }, - Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C") - }; - results.Add(result); - } + if (Settings.HistoryStyle == HistoryStyle.LastOpened) + { + // Items saved to disk are differentiated by Query also, but LastOpened style only cares about unique results + historyItems = historyItems + .GroupBy(r => new { r.Title, r.SubTitle, r.PluginID, r.RecordKey }) + .Select(g => g.First()); } + + // Max history results to return for display + if (maxResult.HasValue) + { + historyItems = historyItems.Take(maxResult.Value); + } + + foreach (var item in historyItems) + { + var copiedItem = item.DeepCopyForHistoryStyle(Settings.HistoryStyle == HistoryStyle.LastOpened); + + if (Settings.HistoryStyle == HistoryStyle.LastOpened) + { + copiedItem.AsyncAction = async c => + { + // Use original history item to reflect correct result because properties like subtitle have been modified in copiedItem + var reflectResult = await ResultHelper.PopulateResultsAsync(item); + if (reflectResult != null) + { + // Since some actions may need to hide the Flow window to execute + // So let us populate the results of them + return await reflectResult.ExecuteAsync(c); + } + + // If we cannot get the result, fallback to re-query + App.API.BackToQueryResults(); + App.API.ChangeQuery(copiedItem.Query); + return false; + }; + } + + results.Add(copiedItem); + } + return results; } + /// + /// Refreshes the last-opened history storage by migrating legacy entries and + /// updating stored icon paths to their resolved (absolute) locations. + /// + /// + /// Calls to refresh absolute icon + /// paths on the migrated/saved history entries by updating each item's + /// PluginDirectory (which in turn resolves IcoPathAbsolute). + /// + /// Important: + /// - Plugins must be initialized (their metadata and PluginDirectory set) + /// before calling this method; otherwise icon resolution cannot be performed. + /// + internal void RefreshLastOpenedHistoryResults() + { + _history.PopulateHistoryFromLegacyHistory(); + + _history.UpdateIcoPathAbsolute(); + } + private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true) { _updateSource?.Cancel(); @@ -1406,6 +1416,9 @@ namespace Flow.Launcher.ViewModel return; } + // Create a Guid for this update session so that we can filter out in progress checking + var updateGuid = Guid.NewGuid(); + try { _updateSource?.Dispose(); @@ -1417,7 +1430,7 @@ namespace Flow.Launcher.ViewModel ProgressBarVisibility = Visibility.Hidden; - _progressQuery = query; + _progressQueryDict.TryAdd(updateGuid, query); _updateQuery = query; // Switch to ThreadPool thread @@ -1472,7 +1485,8 @@ namespace Flow.Launcher.ViewModel _ = 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 (_progressQuery != null && _progressQuery.OriginalQuery == query.OriginalQuery) + if (_progressQueryDict.TryGetValue(updateGuid, out var progressQuery) && + progressQuery.OriginalQuery == query.OriginalQuery) { ProgressBarVisibility = Visibility.Visible; } @@ -1528,7 +1542,7 @@ namespace Flow.Launcher.ViewModel // this should happen once after all queries are done so progress bar should continue // until the end of all querying - _progressQuery = null; + _progressQueryDict.Remove(updateGuid, out _); if (!currentCancellationToken.IsCancellationRequested) { @@ -1538,8 +1552,8 @@ namespace Flow.Launcher.ViewModel } finally { - // this make sures progress query is null when this query is canceled - _progressQuery = null; + // this ensures the query is removed from the progress tracking dictionary when this query is canceled or completes + _progressQueryDict.Remove(updateGuid, out _); } // Local function @@ -1617,10 +1631,8 @@ namespace Flow.Launcher.ViewModel void QueryHistoryTask(CancellationToken token) { - // Select last history results and revert its order to make sure last history results are on top - var historyItems = _history.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse(); - - var results = GetHistoryItems(historyItems); + // Select last history results + var results = GetHistoryItems(_history.LastOpenedHistoryItems, Settings.MaxHistoryResultsToShowForHomePage); if (token.IsCancellationRequested) return; diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index f2f49f8f1..077acfce7 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -141,7 +141,7 @@ namespace Flow.Launcher.ViewModel private bool GlyphAvailable => Glyph is not null; - private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPath) || Result.Icon is not null; + private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPathAbsolute) || Result.Icon is not null; private bool BadgeIconAvailable => !string.IsNullOrEmpty(Result.BadgeIcoPath) || Result.BadgeIcon is not null; @@ -236,7 +236,7 @@ namespace Flow.Launcher.ViewModel private async Task LoadImageAsync() { - var imagePath = Result.IcoPath; + var imagePath = Result.IcoPathAbsolute; var iconDelegate = Result.Icon; if (ImageLoader.TryGetValue(imagePath, false, out var img)) { @@ -266,7 +266,7 @@ namespace Flow.Launcher.ViewModel private async Task LoadPreviewImageAsync() { - var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPath; + var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPathAbsolute; var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon; if (ImageLoader.TryGetValue(imagePath, true, out var img)) { diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json index 8c3a16e5e..b4b929d19 100644 --- a/Flow.Launcher/packages.lock.json +++ b/Flow.Launcher/packages.lock.json @@ -28,9 +28,9 @@ }, "iNKORE.UI.WPF.Modern": { "type": "Direct", - "requested": "[0.10.2.1, )", - "resolved": "0.10.2.1", - "contentHash": "nGwuuVul+TcLCTgPmaAZCc0fYFqUpCNZ8PiulVT3gZnsWt/AvxMZ0DSPpuyI/iRPc/NhFIg9lSIR7uaHWV0I/Q==", + "requested": "[0.10.1, )", + "resolved": "0.10.1", + "contentHash": "nRYmBosiL+42eUpLbHeqP7qJqtp5EpzuIMZTpvq4mFV33VB/JjkFg1y82gk50pjkXlAQWDvRyrfSAmPR5AM+3g==", "dependencies": { "iNKORE.UI.WPF": "1.2.8" } @@ -1619,7 +1619,7 @@ "FSharp.Core": "[9.0.303, )", "Flow.Launcher.Infrastructure": "[1.0.0, )", "Flow.Launcher.Localization": "[0.0.6, )", - "Flow.Launcher.Plugin": "[5.1.0, )", + "Flow.Launcher.Plugin": "[5.0.0, )", "Meziantou.Framework.Win32.Jobs": "[3.4.5, )", "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "SemanticVersioning": "[3.0.0, )", @@ -1634,7 +1634,7 @@ "BitFaster.Caching": "[2.5.4, )", "CommunityToolkit.Mvvm": "[8.4.0, )", "Flow.Launcher.Localization": "[0.0.6, )", - "Flow.Launcher.Plugin": "[5.1.0, )", + "Flow.Launcher.Plugin": "[5.0.0, )", "InputSimulator": "[1.0.4, )", "MemoryPack": "[1.21.4, )", "Microsoft.VisualStudio.Threading": "[17.14.15, )", diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index ed121375b..cb86f719e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -105,9 +105,9 @@ - - - + + + diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml index b12972b1b..5f9a3ca5a 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml @@ -14,6 +14,7 @@ Comma (,) Dot (.) Max. decimal places + Show thousands separator in results Copy failed, please try later Show error message when calculation fails \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index a20a1ad5d..1b9a38e18 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -363,7 +363,7 @@ namespace Flow.Launcher.Plugin.Calculator string integerPart = parts[0]; string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty; - if (integerPart.Length > 3) + if (_settings.UseThousandsSeparator && integerPart.Length > 3) { integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator); } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs index 1544dc41f..69cd17ed2 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Settings.cs @@ -7,4 +7,6 @@ public class Settings public int MaxDecimalPlaces { get; set; } = 10; public bool ShowErrorMessage { get; set; } = false; + + public bool UseThousandsSeparator { get; set; } = true; } diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml index 9e7549b2d..82f8eec60 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Views/CalculatorSettings.xaml @@ -16,6 +16,7 @@ + @@ -66,6 +67,16 @@ Margin="{StaticResource SettingPanelItemTopBottomMargin}" HorizontalAlignment="Left" VerticalAlignment="Center" + Content="{DynamicResource flowlauncher_plugin_calculator_use_thousands_separator}" + IsChecked="{Binding Settings.UseThousandsSeparator, Mode=TwoWay}" /> + + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 956c84db2..30c2c7f14 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -532,7 +532,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels [RelayCommand] private void OpenShellPath() { - var path = PromptUserSelectPath(ResultType.File, Settings.EditorPath != null ? Path.GetDirectoryName(Settings.EditorPath) : null); + var path = PromptUserSelectPath(ResultType.File, Settings.ShellPath != null ? Path.GetDirectoryName(Settings.ShellPath) : null); if (path is null) return; diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj index 39586771f..ec8a32b95 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj @@ -54,7 +54,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj index 4cf09baab..1ef6dc495 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj +++ b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj @@ -60,7 +60,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 57b9749f7..e0da6cbe1 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -175,7 +175,7 @@ namespace Flow.Launcher.Plugin.Sys Privileges = new() { e0 = new LUID_AND_ATTRIBUTES { Luid = luid, Attributes = TOKEN_PRIVILEGES_ATTRIBUTES.SE_PRIVILEGE_ENABLED } } }; - if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, &privileges, 0, null, null)) + if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, &privileges, null, out var _)) { return false; } diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj index 42176376b..3b3add106 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj @@ -50,6 +50,10 @@ + + + + diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml index 5d65e4462..2b2b06fae 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Languages/en.xaml @@ -21,6 +21,7 @@ URL Search Use Search Query Autocomplete + Max Suggestions Autocomplete Data from: Please select a web search Are you sure you want to delete {0}? diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs index b9b8a0b19..299e27499 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Main.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -91,6 +91,7 @@ namespace Flow.Launcher.Plugin.WebSearch if (token.IsCancellationRequested) return null; + } return results; @@ -126,7 +127,7 @@ namespace Flow.Launcher.Plugin.WebSearch token.ThrowIfCancellationRequested(); - var resultsFromSuggestion = suggestions?.Select(o => new Result + var resultsFromSuggestion = suggestions?.Take(_settings.MaxSuggestions).Select(o => new Result { Title = o, SubTitle = subtitle, diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs index 0c0ac4b84..31540ad92 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Settings.cs @@ -205,6 +205,23 @@ namespace Flow.Launcher.Plugin.WebSearch } } + private int maxSuggestions = 1; + public int MaxSuggestions + { + get => maxSuggestions; + set + { + if (value > 0 && value <= 1000) + { + if (maxSuggestions != value) + { + maxSuggestions = value; + OnPropertyChanged(); + } + } + } + } + [JsonIgnore] public SuggestionSource[] Suggestions { get; set; } = { new Google(), diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml index 152558e81..e4f3485cd 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml @@ -3,7 +3,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern" xmlns:vm="clr-namespace:Flow.Launcher.Plugin.WebSearch" d:DataContext="{d:DesignInstance vm:SettingsViewModel}" d:DesignHeight="300" @@ -45,17 +47,17 @@ x:Name="SearchSourcesListView" Grid.Row="0" Margin="{StaticResource SettingPanelItemTopBottomMargin}" + AllowDrop="True" BorderBrush="DarkGray" BorderThickness="1" + Drop="ListView_Drop" GridViewColumnHeader.Click="SortByColumn" ItemsSource="{Binding Settings.SearchSources}" MouseDoubleClick="MouseDoubleClickItem" - SelectedItem="{Binding Settings.SelectedSearchSource}" - SizeChanged="ListView_SizeChanged" PreviewMouseLeftButtonDown="ListView_PreviewMouseLeftButtonDown" PreviewMouseMove="ListView_PreviewMouseMove" - AllowDrop="True" - Drop="ListView_Drop" + SelectedItem="{Binding Settings.SelectedSearchSource}" + SizeChanged="ListView_SizeChanged" Style="{StaticResource {x:Static GridView.GridViewStyleKey}}"> @@ -146,31 +148,43 @@ - - - + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs index 6dc766fff..1c1dd3116 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs @@ -240,5 +240,15 @@ namespace Flow.Launcher.Plugin.WebSearch } return null; } + + // This is used for NumberBox to force its value to be 1 when the user clears the value + private void NumberBox_ValueChanged(iNKORE.UI.WPF.Modern.Controls.NumberBox sender, iNKORE.UI.WPF.Modern.Controls.NumberBoxValueChangedEventArgs args) + { + if (double.IsNaN(args.NewValue)) + { + sender.Value = 1; + _settings.MaxSuggestions = (int)sender.Value; + } + } } }