mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into optimize
This commit is contained in:
commit
5e87156258
32 changed files with 800 additions and 216 deletions
|
|
@ -72,9 +72,9 @@
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.102" PrivateAssets="All" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
||||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.205">
|
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.269">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Flow.Launcher.Plugin
|
namespace Flow.Launcher.Plugin
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Describes a result of a <see cref="Query"/> executed by a plugin
|
/// Describes a result of a <see cref="Query"/> executed by a plugin.
|
||||||
|
/// This or its child classes is serializable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Result
|
public class Result
|
||||||
{
|
{
|
||||||
|
|
@ -21,6 +23,8 @@ namespace Flow.Launcher.Plugin
|
||||||
|
|
||||||
private string _icoPath;
|
private string _icoPath;
|
||||||
|
|
||||||
|
private string _icoPathAbsolute;
|
||||||
|
|
||||||
private string _copyText = string.Empty;
|
private string _copyText = string.Empty;
|
||||||
|
|
||||||
private string _badgeIcoPath;
|
private string _badgeIcoPath;
|
||||||
|
|
@ -64,15 +68,27 @@ namespace Flow.Launcher.Plugin
|
||||||
public string AutoCompleteText { get; set; }
|
public string AutoCompleteText { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The image to be displayed for the result.
|
/// Path or URI to the icon image for this result.
|
||||||
|
/// Updates <see cref="IcoPathAbsolute"/> appropriately when set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>Can be a local file path or a URL.</value>
|
/// <remarks>
|
||||||
/// <remarks>GlyphInfo is prioritized if not null</remarks>
|
/// Preferred usage: provide a path relative to the plugin directory (for example: "Images\icon.png").
|
||||||
|
/// Because <see cref="IcoPath"/> is serialized, using relative paths keeps the icon reference portable
|
||||||
|
/// when Flow is moved.
|
||||||
|
///
|
||||||
|
/// Accepted formats:
|
||||||
|
/// - Relative file paths (resolved against <see cref="PluginDirectory"/> into <see cref="IcoPathAbsolute"/>)
|
||||||
|
/// - Absolute file paths (left as-is)
|
||||||
|
/// - HTTP/HTTPS URLs (left as-is)
|
||||||
|
/// - Data URIs (left as-is)
|
||||||
|
/// </remarks>
|
||||||
public string IcoPath
|
public string IcoPath
|
||||||
{
|
{
|
||||||
get => _icoPath;
|
get => _icoPath;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
_icoPath = value;
|
||||||
|
|
||||||
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
|
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
|
||||||
if (!string.IsNullOrEmpty(value)
|
if (!string.IsNullOrEmpty(value)
|
||||||
&& !string.IsNullOrEmpty(PluginDirectory)
|
&& !string.IsNullOrEmpty(PluginDirectory)
|
||||||
|
|
@ -81,15 +97,23 @@ namespace Flow.Launcher.Plugin
|
||||||
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_icoPath = Path.Combine(PluginDirectory, value);
|
_icoPathAbsolute = Path.Combine(PluginDirectory, value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_icoPath = value;
|
_icoPathAbsolute = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Absolute path or URI which is used to load and display the result icon for Flow.
|
||||||
|
/// This is populated by the <see cref="IcoPath"/> setter.
|
||||||
|
/// If a relative path was provided to <see cref="IcoPath"/>, this property will contain the resolved
|
||||||
|
/// absolute local path after combining with <see cref="PluginDirectory"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string IcoPathAbsolute => _icoPathAbsolute;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The image to be displayed for the badge of the result.
|
/// The image to be displayed for the badge of the result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -131,17 +155,34 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delegate to load an icon for this result.
|
/// Delegate to load an icon for this result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
public IconDelegate Icon = null;
|
public IconDelegate Icon = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delegate to load an icon for the badge of this result.
|
/// Delegate to load an icon for the badge of this result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
public IconDelegate BadgeIcon = null;
|
public IconDelegate BadgeIcon = null;
|
||||||
|
|
||||||
|
private GlyphInfo _glyph;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
|
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GlyphInfo Glyph { get; init; }
|
public GlyphInfo Glyph
|
||||||
|
{
|
||||||
|
get => _glyph;
|
||||||
|
init => _glyph = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the Glyph Icon after initialization
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="glyph"></param>
|
||||||
|
public void SetGlyph(GlyphInfo glyph)
|
||||||
|
{
|
||||||
|
_glyph = glyph;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An action to take in the form of a function call when the result has been selected.
|
/// 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:
|
/// 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.
|
/// when true, the form will be hidden; when false, it will stay in focus.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
public Func<ActionContext, bool> Action { get; set; }
|
public Func<ActionContext, bool> Action { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -161,6 +203,7 @@ namespace Flow.Launcher.Plugin
|
||||||
/// Its result determines what happens to Flow Launcher's query form:
|
/// 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.
|
/// when true, the form will be hidden; when false, it will stay in focus.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
public Func<ActionContext, ValueTask<bool>> AsyncAction { get; set; }
|
public Func<ActionContext, ValueTask<bool>> AsyncAction { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -203,11 +246,13 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <example>
|
/// <example>
|
||||||
/// As external information for ContextMenu
|
/// As external information for ContextMenu
|
||||||
/// </example>
|
/// </example>
|
||||||
|
[JsonIgnore]
|
||||||
public object ContextData { get; set; }
|
public object ContextData { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plugin ID that generated this result
|
/// Plugin ID that generated this result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonInclude]
|
||||||
public string PluginID { get; internal set; }
|
public string PluginID { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -223,6 +268,7 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Customized Preview Panel
|
/// Customized Preview Panel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
public Lazy<UserControl> PreviewPanel { get; set; }
|
public Lazy<UserControl> PreviewPanel { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -352,6 +398,7 @@ namespace Flow.Launcher.Plugin
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delegate to get the preview panel's image
|
/// Delegate to get the preview panel's image
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
public IconDelegate PreviewDelegate { get; set; } = null;
|
public IconDelegate PreviewDelegate { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -16,23 +16,23 @@
|
||||||
},
|
},
|
||||||
"Microsoft.SourceLink.GitHub": {
|
"Microsoft.SourceLink.GitHub": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[8.0.0, )",
|
"requested": "[10.0.102, )",
|
||||||
"resolved": "8.0.0",
|
"resolved": "10.0.102",
|
||||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
"Microsoft.Build.Tasks.Git": "10.0.102",
|
||||||
"Microsoft.SourceLink.Common": "8.0.0"
|
"Microsoft.SourceLink.Common": "10.0.102"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Windows.CsWin32": {
|
"Microsoft.Windows.CsWin32": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[0.3.205, )",
|
"requested": "[0.3.269, )",
|
||||||
"resolved": "0.3.205",
|
"resolved": "0.3.269",
|
||||||
"contentHash": "U5wGAnyKd7/I2YMd43nogm81VMtjiKzZ9dsLMVI4eAB7jtv5IEj0gprj0q/F3iRmAIaGv5omOf8iSYx2+nE6BQ==",
|
"contentHash": "O4GVJ0ymxcoFRGS07VcoEClj7A9PIciHIjWDrPymzonhYlOfM7V0ZqGBUK19cUH3BPca9MfSOH0KLK/9JzQ8+Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
|
"Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha",
|
||||||
"Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview",
|
"Microsoft.Windows.SDK.Win32Metadata": "69.0.7-preview",
|
||||||
"Microsoft.Windows.WDK.Win32Metadata": "0.12.8-experimental"
|
"Microsoft.Windows.WDK.Win32Metadata": "0.13.25-experimental"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"PropertyChanged.Fody": {
|
"PropertyChanged.Fody": {
|
||||||
|
|
@ -46,13 +46,13 @@
|
||||||
},
|
},
|
||||||
"Microsoft.Build.Tasks.Git": {
|
"Microsoft.Build.Tasks.Git": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.0",
|
"resolved": "10.0.102",
|
||||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
|
||||||
},
|
},
|
||||||
"Microsoft.SourceLink.Common": {
|
"Microsoft.SourceLink.Common": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "8.0.0",
|
"resolved": "10.0.102",
|
||||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
|
||||||
},
|
},
|
||||||
"Microsoft.Windows.SDK.Win32Docs": {
|
"Microsoft.Windows.SDK.Win32Docs": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
|
|
@ -61,15 +61,15 @@
|
||||||
},
|
},
|
||||||
"Microsoft.Windows.SDK.Win32Metadata": {
|
"Microsoft.Windows.SDK.Win32Metadata": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "61.0.15-preview",
|
"resolved": "69.0.7-preview",
|
||||||
"contentHash": "cysex3dazKtCPALCluC2XX3f5Aedy9H2pw5jb+TW5uas2rkem1Z7FRnbUrg2vKx0pk0Qz+4EJNr37HdYTEcvEQ=="
|
"contentHash": "RJoNjQJVCIDNLPbvYuaygCFknTyAxOUE45of1voj0jjOgJa9MB2m1/G8L8F3IYc+2EFG5aqa/9y8PEx7Tk2tLQ=="
|
||||||
},
|
},
|
||||||
"Microsoft.Windows.WDK.Win32Metadata": {
|
"Microsoft.Windows.WDK.Win32Metadata": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "0.12.8-experimental",
|
"resolved": "0.13.25-experimental",
|
||||||
"contentHash": "3n8R44/Z96Ly+ty4eYVJfESqbzvpw96lRLs3zOzyDmr1x1Kw7FNn5CyE416q+bZQV3e1HRuMUvyegMeRE/WedA==",
|
"contentHash": "IM50tb/+UIwBr9FMr6ZKcZjCMW+Axo6NjGqKxgjUfyCY8dRnYUfrJEXxAaXoWtYP4X8EmASmC1Jtwh4XucseZg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Windows.SDK.Win32Metadata": "61.0.15-preview"
|
"Microsoft.Windows.SDK.Win32Metadata": "63.0.31-preview"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,15 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
{
|
{
|
||||||
DecimalSeparator = DecimalSeparator.UseSystemLocale,
|
DecimalSeparator = DecimalSeparator.UseSystemLocale,
|
||||||
MaxDecimalPlaces = 10,
|
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
|
private readonly Engine _engine = new(new Configuration
|
||||||
{
|
{
|
||||||
Scope = new Dictionary<string, object>
|
Scope = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "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()
|
public CalculatorPluginTest()
|
||||||
|
|
@ -41,6 +42,44 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
engineField.SetValue(null, _engine);
|
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
|
// Basic operations
|
||||||
[TestCase(@"1+1", "2")]
|
[TestCase(@"1+1", "2")]
|
||||||
[TestCase(@"2-1", "1")]
|
[TestCase(@"2-1", "1")]
|
||||||
|
|
@ -77,6 +116,9 @@ namespace Flow.Launcher.Test.Plugins
|
||||||
[TestCase(@"invalid_expression", "")]
|
[TestCase(@"invalid_expression", "")]
|
||||||
public void CalculatorTest(string expression, string result)
|
public void CalculatorTest(string expression, string result)
|
||||||
{
|
{
|
||||||
|
_settings.UseThousandsSeparator = false;
|
||||||
|
_settings.DecimalSeparator = DecimalSeparator.Dot;
|
||||||
|
|
||||||
ClassicAssert.AreEqual(GetCalculationResult(expression), result);
|
ClassicAssert.AreEqual(GetCalculationResult(expression), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,9 @@ namespace Flow.Launcher
|
||||||
|
|
||||||
await PluginManager.InitializePluginsAsync(_mainVM);
|
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
|
// 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
|
// And home page is created without full plugin list
|
||||||
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
|
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.10.2.1" />
|
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.10.1" />
|
||||||
<PackageReference Include="MdXaml" Version="1.27.0" />
|
<PackageReference Include="MdXaml" Version="1.27.0" />
|
||||||
<PackageReference Include="MdXaml.AnimatedGif" Version="1.27.0" />
|
<PackageReference Include="MdXaml.AnimatedGif" Version="1.27.0" />
|
||||||
<PackageReference Include="MdXaml.Html" Version="1.27.0" />
|
<PackageReference Include="MdXaml.Html" Version="1.27.0" />
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace Flow.Launcher.Helper;
|
||||||
|
|
||||||
public static class ResultHelper
|
public static class ResultHelper
|
||||||
{
|
{
|
||||||
public static async Task<Result?> PopulateResultsAsync(LastOpenedHistoryItem item)
|
public static async Task<Result?> PopulateResultsAsync(LastOpenedHistoryResult item)
|
||||||
{
|
{
|
||||||
return await PopulateResultsAsync(item.PluginID, item.Query, item.Title, item.SubTitle, item.RecordKey);
|
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;
|
if (query == null) return null;
|
||||||
try
|
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
|
// Try to match by record key first if it is valid, otherwise fall back to title + subtitle match
|
||||||
if (string.IsNullOrEmpty(recordKey))
|
if (string.IsNullOrEmpty(recordKey))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
253
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
Normal file
253
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override void OnInitialized(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnInitialized(e);
|
||||||
|
|
||||||
|
if (Style == null && ReadLocalValue(StyleProperty) == DependencyProperty.UnsetValue)
|
||||||
|
{
|
||||||
|
SetResourceReference(StyleProperty, typeof(ScrollViewer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Causes the <see cref="ScrollViewerEx"/> to load a new view into the viewport using the specified offsets and zoom factor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="horizontalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableWidth"/> that specifies the distance the content should be scrolled horizontally.</param>
|
||||||
|
/// <param name="verticalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableHeight"/> that specifies the distance the content should be scrolled vertically.</param>
|
||||||
|
/// <param name="zoomFactor">A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor.</param>
|
||||||
|
/// <returns><see langword="true"/> if the view is changed; otherwise, <see langword="false"/>.</returns>
|
||||||
|
public bool ChangeView(double? horizontalOffset, double? verticalOffset, float? zoomFactor)
|
||||||
|
{
|
||||||
|
return ChangeView(horizontalOffset, verticalOffset, zoomFactor, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Causes the <see cref="ScrollViewerEx"/> to load a new view into the viewport using the specified offsets and zoom factor, and optionally disables scrolling animation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="horizontalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableWidth"/> that specifies the distance the content should be scrolled horizontally.</param>
|
||||||
|
/// <param name="verticalOffset">A value between 0 and <see cref="ScrollViewer.ScrollableHeight"/> that specifies the distance the content should be scrolled vertically.</param>
|
||||||
|
/// <param name="zoomFactor">A value between MinZoomFactor and MaxZoomFactor that specifies the required target ZoomFactor.</param>
|
||||||
|
/// <param name="disableAnimation"><see langword="true"/> to disable zoom/pan animations while changing the view; otherwise, <see langword="false"/>. The default is false.</param>
|
||||||
|
/// <returns><see langword="true"/> if the view is changed; otherwise, <see langword="false"/>.</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -333,7 +333,7 @@
|
||||||
Margin="18 24 0 0"
|
Margin="18 24 0 0"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
RenderOptions.BitmapScalingMode="Fant"
|
RenderOptions.BitmapScalingMode="Fant"
|
||||||
Source="{Binding IcoPath, IsAsync=True}" />
|
Source="{Binding IcoPathAbsolute, IsAsync=True}" />
|
||||||
<Border
|
<Border
|
||||||
x:Name="LabelUpdate"
|
x:Name="LabelUpdate"
|
||||||
Height="12"
|
Height="12"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace Flow.Launcher.Storage
|
namespace Flow.Launcher.Storage
|
||||||
{
|
{
|
||||||
[Obsolete("Use LastOpenedHistoryItem instead. This class will be removed in future versions.")]
|
[Obsolete("Use LastOpenedHistoryResult instead. This class will be removed in future versions.")]
|
||||||
public class HistoryItem
|
public class HistoryItem
|
||||||
{
|
{
|
||||||
public string Query { get; set; }
|
public string Query { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
using System;
|
|
||||||
using Flow.Launcher.Plugin;
|
|
||||||
|
|
||||||
namespace Flow.Launcher.Storage;
|
|
||||||
|
|
||||||
public class LastOpenedHistoryItem
|
|
||||||
{
|
|
||||||
public string Title { get; set; } = string.Empty;
|
|
||||||
public string SubTitle { get; set; } = string.Empty;
|
|
||||||
public string PluginID { get; set; } = string.Empty;
|
|
||||||
public string Query { get; set; } = string.Empty;
|
|
||||||
public string RecordKey { get; set; } = string.Empty;
|
|
||||||
public DateTime ExecutedDateTime { get; set; }
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
146
Flow.Launcher/Storage/LastOpenedHistoryResult.cs
Normal file
146
Flow.Launcher/Storage/LastOpenedHistoryResult.cs
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
using System;
|
||||||
|
using Flow.Launcher.Infrastructure;
|
||||||
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
|
namespace Flow.Launcher.Storage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A serializable result used to record the last opened history for reopening results.
|
||||||
|
/// Inherits common result fields from <see cref="Result"/> and adds the original query and execution time.
|
||||||
|
/// </summary>
|
||||||
|
public class LastOpenedHistoryResult : Result
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The query string from Query.TrimmedQuery property, it is stored as a string instead of the entire Query class <see cref="Result"/>.
|
||||||
|
/// This is used so results can be reopened or re-run using the serialized query string.
|
||||||
|
/// </summary>
|
||||||
|
public string Query { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The local date and time when this result was executed/opened.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ExecutedDateTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="LastOpenedHistoryResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
public LastOpenedHistoryResult()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <see cref="LastOpenedHistoryResult"/> from an existing <see cref="Result"/>.
|
||||||
|
/// Copies required fields and sets up default reopening actions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The original result to create history from.</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selectively creates a deep copy of the required properties for <see cref="LastOpenedHistoryResult"/>
|
||||||
|
/// based on the style of history- Last Opened or Query.
|
||||||
|
/// This copy should be independent of original and full isolated.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A new <see cref="LastOpenedHistoryResult"/> containing the same required data.</returns>
|
||||||
|
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.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the specified <see cref="Result"/> is equivalent to this history result.
|
||||||
|
/// Comparison uses <see cref="Result.RecordKey"/> when available; otherwise falls back to title/subtitle/plugin id and query.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="r">The result to compare to.</param>
|
||||||
|
/// <returns><c>true</c> if the results are considered equal; otherwise <c>false</c>.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Flow.Launcher.Core.Plugin;
|
||||||
using Flow.Launcher.Plugin;
|
using Flow.Launcher.Plugin;
|
||||||
|
|
||||||
namespace Flow.Launcher.Storage
|
namespace Flow.Launcher.Storage
|
||||||
|
|
@ -14,28 +15,50 @@ namespace Flow.Launcher.Storage
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
[JsonInclude]
|
[JsonInclude]
|
||||||
public List<LastOpenedHistoryItem> LastOpenedHistoryItems { get; private set; } = [];
|
public List<LastOpenedHistoryResult> LastOpenedHistoryItems { get; private set; } = [];
|
||||||
|
|
||||||
private readonly int _maxHistory = 300;
|
private readonly int _maxHistory = 300;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Migrate legacy history data (stored in <see cref="Items"/>) into the new
|
||||||
|
/// <see cref="LastOpenedHistoryResult"/> format and append them to
|
||||||
|
/// <see cref="LastOpenedHistoryItems"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("For backwards compatibility. Remove after release v2.3.0")]
|
||||||
public void PopulateHistoryFromLegacyHistory()
|
public void PopulateHistoryFromLegacyHistory()
|
||||||
{
|
{
|
||||||
if (Items.Count == 0) return;
|
if (Items.Count == 0) return;
|
||||||
// Migrate old history items to new LastOpenedHistoryItems
|
// Migrate old history items to new LastOpenedHistoryItems
|
||||||
foreach (var item in Items)
|
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,
|
Query = item.Query,
|
||||||
|
Action = _ =>
|
||||||
|
{
|
||||||
|
App.API.BackToQueryResults();
|
||||||
|
App.API.ChangeQuery(item.Query);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
ExecutedDateTime = item.ExecutedDateTime
|
ExecutedDateTime = item.ExecutedDateTime
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Items.Clear();
|
Items.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records a result into the last-opened history list (<see cref="LastOpenedHistoryItems"/>).
|
||||||
|
/// This will also update the IcoPath if existing history item has one that is different.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The result to add to history. Must have a non-empty <see cref="Result.OriginQuery"/>.<see cref="Query.TrimmedQuery"/>.</param>
|
||||||
public void Add(Result result)
|
public void Add(Result result)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(result.OriginQuery.TrimmedQuery)) return;
|
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;
|
if (string.IsNullOrEmpty(result.PluginID)) return;
|
||||||
|
|
||||||
// Maintain the max history limit
|
// Maintain the max history limit
|
||||||
|
|
@ -44,23 +67,53 @@ namespace Flow.Launcher.Storage
|
||||||
LastOpenedHistoryItems.RemoveAt(0);
|
LastOpenedHistoryItems.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the last item is the same as the current result, just update the timestamp
|
// If the last item is the same as the current result, just update the timestamp and the icon path
|
||||||
if (LastOpenedHistoryItems.Count > 0 &&
|
if (LastOpenedHistoryItems.Count > 0 &&
|
||||||
LastOpenedHistoryItems.Last().Equals(result))
|
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
|
else
|
||||||
{
|
{
|
||||||
LastOpenedHistoryItems.Add(new LastOpenedHistoryItem
|
LastOpenedHistoryItems.Add(new LastOpenedHistoryResult(result));
|
||||||
{
|
}
|
||||||
Title = result.Title,
|
}
|
||||||
SubTitle = result.SubTitle,
|
|
||||||
PluginID = result.PluginID,
|
/// <summary>
|
||||||
Query = result.OriginQuery.TrimmedQuery,
|
/// Attempts to find an existing <see cref="LastOpenedHistoryResult"/> in <see cref="LastOpenedHistoryItems"/>
|
||||||
RecordKey = result.RecordKey,
|
/// that is considered equal to the supplied <paramref name="result"/>.
|
||||||
ExecutedDateTime = DateTime.Now
|
/// </summary>
|
||||||
});
|
private bool TryGetLastOpenedHistoryResult(Result result, out LastOpenedHistoryResult historyItem)
|
||||||
|
{
|
||||||
|
historyItem = LastOpenedHistoryItems.FirstOrDefault(x => x.Equals(result));
|
||||||
|
return historyItem is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks> Call this after plugins are loaded/initialized.</remarks>
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -252,14 +252,12 @@
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="ListBox">
|
<ControlTemplate TargetType="ListBox">
|
||||||
<ui:ScrollViewerEx
|
<cc:CustomScrollViewerEx
|
||||||
x:Name="ListBoxScrollViewer"
|
x:Name="ListBoxScrollViewer"
|
||||||
Focusable="False"
|
Focusable="False"
|
||||||
IsScrollAnimationEnabled="False"
|
|
||||||
RewriteWheelChange="True"
|
|
||||||
Template="{DynamicResource ScrollViewerControlTemplate}">
|
Template="{DynamicResource ScrollViewerControlTemplate}">
|
||||||
<ui:ScrollViewerEx.Style>
|
<cc:CustomScrollViewerEx.Style>
|
||||||
<Style TargetType="ui:ScrollViewerEx">
|
<Style TargetType="cc:CustomScrollViewerEx">
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<Trigger Property="ComputedVerticalScrollBarVisibility" Value="Visible">
|
<Trigger Property="ComputedVerticalScrollBarVisibility" Value="Visible">
|
||||||
<Setter Property="Margin" Value="0 0 0 0" />
|
<Setter Property="Margin" Value="0 0 0 0" />
|
||||||
|
|
@ -271,9 +269,9 @@
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</ui:ScrollViewerEx.Style>
|
</cc:CustomScrollViewerEx.Style>
|
||||||
<VirtualizingStackPanel IsItemsHost="True" />
|
<VirtualizingStackPanel IsItemsHost="True" />
|
||||||
</ui:ScrollViewerEx>
|
</cc:CustomScrollViewerEx>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
@ -37,16 +38,16 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
private Query _lastQuery;
|
private Query _lastQuery;
|
||||||
private bool _previousIsHomeQuery;
|
private bool _previousIsHomeQuery;
|
||||||
private Query _progressQuery; // Used for QueryResultAsync
|
private readonly ConcurrentDictionary<Guid, Query> _progressQueryDict = new(); // Used for QueryResultAsync
|
||||||
private Query _updateQuery; // Used for ResultsUpdated
|
private Query _updateQuery; // Used for ResultsUpdated
|
||||||
private string _queryTextBeforeLeaveResults;
|
private string _queryTextBeforeLeaveResults;
|
||||||
private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results
|
private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results
|
||||||
|
|
||||||
private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
|
private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
|
||||||
private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
|
|
||||||
private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord;
|
|
||||||
private readonly History _history;
|
private readonly History _history;
|
||||||
private int lastHistoryIndex = 1;
|
private int lastHistoryIndex = 1;
|
||||||
|
private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
|
||||||
|
private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord;
|
||||||
private readonly UserSelectedRecord _userSelectedRecord;
|
private readonly UserSelectedRecord _userSelectedRecord;
|
||||||
|
|
||||||
private CancellationTokenSource _updateSource; // Used to cancel old query flows
|
private CancellationTokenSource _updateSource; // Used to cancel old query flows
|
||||||
|
|
@ -152,11 +153,10 @@ namespace Flow.Launcher.ViewModel
|
||||||
};
|
};
|
||||||
|
|
||||||
_historyItemsStorage = new FlowLauncherJsonStorage<History>();
|
_historyItemsStorage = new FlowLauncherJsonStorage<History>();
|
||||||
_userSelectedRecordStorage = new FlowLauncherJsonStorage<UserSelectedRecord>();
|
|
||||||
_topMostRecord = new FlowLauncherJsonStorageTopMostRecord();
|
|
||||||
_history = _historyItemsStorage.Load();
|
_history = _historyItemsStorage.Load();
|
||||||
_history.PopulateHistoryFromLegacyHistory();
|
_userSelectedRecordStorage = new FlowLauncherJsonStorage<UserSelectedRecord>();
|
||||||
_userSelectedRecord = _userSelectedRecordStorage.Load();
|
_userSelectedRecord = _userSelectedRecordStorage.Load();
|
||||||
|
_topMostRecord = new FlowLauncherJsonStorageTopMostRecord();
|
||||||
|
|
||||||
ContextMenu = new ResultsViewModel(Settings, this)
|
ContextMenu = new ResultsViewModel(Settings, this)
|
||||||
{
|
{
|
||||||
|
|
@ -355,11 +355,17 @@ namespace Flow.Launcher.ViewModel
|
||||||
if (QueryResultsSelected())
|
if (QueryResultsSelected())
|
||||||
{
|
{
|
||||||
SelectedResults = History;
|
SelectedResults = History;
|
||||||
History.SelectedIndex = _history.LastOpenedHistoryItems.Count - 1;
|
if (History.Results.Count > 0)
|
||||||
|
{
|
||||||
|
SelectedResults.SelectedIndex = 0;
|
||||||
|
SelectedResults.SelectedItem = History.Results[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SelectedResults = Results;
|
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
|
// 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
|
// 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;
|
SelectedResults = ContextMenu;
|
||||||
}
|
}
|
||||||
|
|
@ -439,6 +446,8 @@ namespace Flow.Launcher.ViewModel
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SelectedResults = Results;
|
SelectedResults = Results;
|
||||||
|
PreviewSelectedItem = Results.SelectedItem;
|
||||||
|
_ = UpdatePreviewAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -642,6 +651,8 @@ namespace Flow.Launcher.ViewModel
|
||||||
if (!QueryResultsSelected())
|
if (!QueryResultsSelected())
|
||||||
{
|
{
|
||||||
SelectedResults = Results;
|
SelectedResults = Results;
|
||||||
|
PreviewSelectedItem = Results.SelectedItem;
|
||||||
|
_ = UpdatePreviewAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1252,22 +1263,12 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
var selected = Results.SelectedItem?.Result;
|
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<Result> results;
|
List<Result> results = PluginManager.GetContextMenusForPlugin(selected);
|
||||||
if (selected.PluginID == null) // SelectedItem from history in home page.
|
results.Add(ContextMenuTopMost(selected));
|
||||||
{
|
results.Add(ContextMenuPluginInfo(selected));
|
||||||
results = new()
|
|
||||||
{
|
|
||||||
ContextMenuTopMost(selected)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
results = PluginManager.GetContextMenusForPlugin(selected);
|
|
||||||
results.Add(ContextMenuTopMost(selected));
|
|
||||||
results.Add(ContextMenuPluginInfo(selected));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(query))
|
if (!string.IsNullOrEmpty(query))
|
||||||
{
|
{
|
||||||
|
|
@ -1318,68 +1319,77 @@ namespace Flow.Launcher.ViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Result> GetHistoryItems(IEnumerable<LastOpenedHistoryItem> historyItems)
|
private List<Result> GetHistoryItems(IEnumerable<LastOpenedHistoryResult> historyItems, int? maxResult = null)
|
||||||
{
|
{
|
||||||
var results = new List<Result>();
|
var results = new List<Result>();
|
||||||
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
|
// Order by executed time descending: Latest -> Oldest
|
||||||
// So let us populate the results of them
|
historyItems = historyItems.OrderByDescending(x => x.ExecutedDateTime);
|
||||||
return await reflectResult.ExecuteAsync(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we cannot get the result, fallback to re-query
|
if (Settings.HistoryStyle == HistoryStyle.LastOpened)
|
||||||
App.API.BackToQueryResults();
|
{
|
||||||
App.API.ChangeQuery(h.Query);
|
// Items saved to disk are differentiated by Query also, but LastOpened style only cares about unique results
|
||||||
return false;
|
historyItems = historyItems
|
||||||
},
|
.GroupBy(r => new { r.Title, r.SubTitle, r.PluginID, r.RecordKey })
|
||||||
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uE81C")
|
.Select(g => g.First());
|
||||||
};
|
|
||||||
results.Add(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the last-opened history storage by migrating legacy entries and
|
||||||
|
/// updating stored icon paths to their resolved (absolute) locations.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Calls <see cref="History.UpdateIcoPathAbsolute"/> to refresh absolute icon
|
||||||
|
/// paths on the migrated/saved history entries by updating each item's
|
||||||
|
/// <c>PluginDirectory</c> (which in turn resolves <c>IcoPathAbsolute</c>).
|
||||||
|
///
|
||||||
|
/// Important:
|
||||||
|
/// - Plugins must be initialized (their metadata and <c>PluginDirectory</c> set)
|
||||||
|
/// before calling this method; otherwise icon resolution cannot be performed.
|
||||||
|
/// </remarks>
|
||||||
|
internal void RefreshLastOpenedHistoryResults()
|
||||||
|
{
|
||||||
|
_history.PopulateHistoryFromLegacyHistory();
|
||||||
|
|
||||||
|
_history.UpdateIcoPathAbsolute();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
|
private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
|
||||||
{
|
{
|
||||||
_updateSource?.Cancel();
|
_updateSource?.Cancel();
|
||||||
|
|
@ -1406,6 +1416,9 @@ namespace Flow.Launcher.ViewModel
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a Guid for this update session so that we can filter out in progress checking
|
||||||
|
var updateGuid = Guid.NewGuid();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_updateSource?.Dispose();
|
_updateSource?.Dispose();
|
||||||
|
|
@ -1417,7 +1430,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
ProgressBarVisibility = Visibility.Hidden;
|
ProgressBarVisibility = Visibility.Hidden;
|
||||||
|
|
||||||
_progressQuery = query;
|
_progressQueryDict.TryAdd(updateGuid, query);
|
||||||
_updateQuery = query;
|
_updateQuery = query;
|
||||||
|
|
||||||
// Switch to ThreadPool thread
|
// Switch to ThreadPool thread
|
||||||
|
|
@ -1472,7 +1485,8 @@ namespace Flow.Launcher.ViewModel
|
||||||
_ = Task.Delay(200, currentCancellationToken).ContinueWith(_ =>
|
_ = 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
|
// 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;
|
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
|
// this should happen once after all queries are done so progress bar should continue
|
||||||
// until the end of all querying
|
// until the end of all querying
|
||||||
_progressQuery = null;
|
_progressQueryDict.Remove(updateGuid, out _);
|
||||||
|
|
||||||
if (!currentCancellationToken.IsCancellationRequested)
|
if (!currentCancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
|
@ -1538,8 +1552,8 @@ namespace Flow.Launcher.ViewModel
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// this make sures progress query is null when this query is canceled
|
// this ensures the query is removed from the progress tracking dictionary when this query is canceled or completes
|
||||||
_progressQuery = null;
|
_progressQueryDict.Remove(updateGuid, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local function
|
// Local function
|
||||||
|
|
@ -1617,10 +1631,8 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
void QueryHistoryTask(CancellationToken token)
|
void QueryHistoryTask(CancellationToken token)
|
||||||
{
|
{
|
||||||
// Select last history results and revert its order to make sure last history results are on top
|
// Select last history results
|
||||||
var historyItems = _history.LastOpenedHistoryItems.TakeLast(Settings.MaxHistoryResultsToShowForHomePage).Reverse();
|
var results = GetHistoryItems(_history.LastOpenedHistoryItems, Settings.MaxHistoryResultsToShowForHomePage);
|
||||||
|
|
||||||
var results = GetHistoryItems(historyItems);
|
|
||||||
|
|
||||||
if (token.IsCancellationRequested) return;
|
if (token.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
private bool GlyphAvailable => Glyph is not null;
|
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;
|
private bool BadgeIconAvailable => !string.IsNullOrEmpty(Result.BadgeIcoPath) || Result.BadgeIcon is not null;
|
||||||
|
|
||||||
|
|
@ -236,7 +236,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
private async Task LoadImageAsync()
|
private async Task LoadImageAsync()
|
||||||
{
|
{
|
||||||
var imagePath = Result.IcoPath;
|
var imagePath = Result.IcoPathAbsolute;
|
||||||
var iconDelegate = Result.Icon;
|
var iconDelegate = Result.Icon;
|
||||||
if (ImageLoader.TryGetValue(imagePath, false, out var img))
|
if (ImageLoader.TryGetValue(imagePath, false, out var img))
|
||||||
{
|
{
|
||||||
|
|
@ -266,7 +266,7 @@ namespace Flow.Launcher.ViewModel
|
||||||
|
|
||||||
private async Task LoadPreviewImageAsync()
|
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;
|
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
|
||||||
if (ImageLoader.TryGetValue(imagePath, true, out var img))
|
if (ImageLoader.TryGetValue(imagePath, true, out var img))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@
|
||||||
},
|
},
|
||||||
"iNKORE.UI.WPF.Modern": {
|
"iNKORE.UI.WPF.Modern": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[0.10.2.1, )",
|
"requested": "[0.10.1, )",
|
||||||
"resolved": "0.10.2.1",
|
"resolved": "0.10.1",
|
||||||
"contentHash": "nGwuuVul+TcLCTgPmaAZCc0fYFqUpCNZ8PiulVT3gZnsWt/AvxMZ0DSPpuyI/iRPc/NhFIg9lSIR7uaHWV0I/Q==",
|
"contentHash": "nRYmBosiL+42eUpLbHeqP7qJqtp5EpzuIMZTpvq4mFV33VB/JjkFg1y82gk50pjkXlAQWDvRyrfSAmPR5AM+3g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iNKORE.UI.WPF": "1.2.8"
|
"iNKORE.UI.WPF": "1.2.8"
|
||||||
}
|
}
|
||||||
|
|
@ -1619,7 +1619,7 @@
|
||||||
"FSharp.Core": "[9.0.303, )",
|
"FSharp.Core": "[9.0.303, )",
|
||||||
"Flow.Launcher.Infrastructure": "[1.0.0, )",
|
"Flow.Launcher.Infrastructure": "[1.0.0, )",
|
||||||
"Flow.Launcher.Localization": "[0.0.6, )",
|
"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, )",
|
"Meziantou.Framework.Win32.Jobs": "[3.4.5, )",
|
||||||
"Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )",
|
"Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )",
|
||||||
"SemanticVersioning": "[3.0.0, )",
|
"SemanticVersioning": "[3.0.0, )",
|
||||||
|
|
@ -1634,7 +1634,7 @@
|
||||||
"BitFaster.Caching": "[2.5.4, )",
|
"BitFaster.Caching": "[2.5.4, )",
|
||||||
"CommunityToolkit.Mvvm": "[8.4.0, )",
|
"CommunityToolkit.Mvvm": "[8.4.0, )",
|
||||||
"Flow.Launcher.Localization": "[0.0.6, )",
|
"Flow.Launcher.Localization": "[0.0.6, )",
|
||||||
"Flow.Launcher.Plugin": "[5.1.0, )",
|
"Flow.Launcher.Plugin": "[5.0.0, )",
|
||||||
"InputSimulator": "[1.0.4, )",
|
"InputSimulator": "[1.0.4, )",
|
||||||
"MemoryPack": "[1.21.4, )",
|
"MemoryPack": "[1.21.4, )",
|
||||||
"Microsoft.VisualStudio.Threading": "[17.14.15, )",
|
"Microsoft.VisualStudio.Threading": "[17.14.15, )",
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,9 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.1" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
|
||||||
<PackageReference Include="Svg.Skia" Version="3.2.1" />
|
<PackageReference Include="Svg.Skia" Version="3.4.1" />
|
||||||
<PackageReference Include="SkiaSharp" Version="3.119.1" />
|
<PackageReference Include="SkiaSharp" Version="3.119.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
<system:String x:Key="flowlauncher_plugin_calculator_decimal_separator_comma">Comma (,)</system:String>
|
<system:String x:Key="flowlauncher_plugin_calculator_decimal_separator_comma">Comma (,)</system:String>
|
||||||
<system:String x:Key="flowlauncher_plugin_calculator_decimal_separator_dot">Dot (.)</system:String>
|
<system:String x:Key="flowlauncher_plugin_calculator_decimal_separator_dot">Dot (.)</system:String>
|
||||||
<system:String x:Key="flowlauncher_plugin_calculator_max_decimal_places">Max. decimal places</system:String>
|
<system:String x:Key="flowlauncher_plugin_calculator_max_decimal_places">Max. decimal places</system:String>
|
||||||
|
<system:String x:Key="flowlauncher_plugin_calculator_use_thousands_separator">Show thousands separator in results</system:String>
|
||||||
<system:String x:Key="flowlauncher_plugin_calculator_failed_to_copy">Copy failed, please try later</system:String>
|
<system:String x:Key="flowlauncher_plugin_calculator_failed_to_copy">Copy failed, please try later</system:String>
|
||||||
<system:String x:Key="flowlauncher_plugin_calculator_show_error_message">Show error message when calculation fails</system:String>
|
<system:String x:Key="flowlauncher_plugin_calculator_show_error_message">Show error message when calculation fails</system:String>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
@ -363,7 +363,7 @@ namespace Flow.Launcher.Plugin.Calculator
|
||||||
string integerPart = parts[0];
|
string integerPart = parts[0];
|
||||||
string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty;
|
string fractionalPart = parts.Length > 1 ? parts[1] : string.Empty;
|
||||||
|
|
||||||
if (integerPart.Length > 3)
|
if (_settings.UseThousandsSeparator && integerPart.Length > 3)
|
||||||
{
|
{
|
||||||
integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator);
|
integerPart = ThousandGroupRegex.Replace(integerPart, groupSeparator);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,6 @@ public class Settings
|
||||||
public int MaxDecimalPlaces { get; set; } = 10;
|
public int MaxDecimalPlaces { get; set; } = 10;
|
||||||
|
|
||||||
public bool ShowErrorMessage { get; set; } = false;
|
public bool ShowErrorMessage { get; set; } = false;
|
||||||
|
|
||||||
|
public bool UseThousandsSeparator { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
<RowDefinition Height="auto" />
|
<RowDefinition Height="auto" />
|
||||||
<RowDefinition Height="auto" />
|
<RowDefinition Height="auto" />
|
||||||
<RowDefinition Height="auto" />
|
<RowDefinition Height="auto" />
|
||||||
|
<RowDefinition Height="auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
|
@ -66,6 +67,16 @@
|
||||||
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
|
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
Content="{DynamicResource flowlauncher_plugin_calculator_use_thousands_separator}"
|
||||||
|
IsChecked="{Binding Settings.UseThousandsSeparator, Mode=TwoWay}" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
Content="{DynamicResource flowlauncher_plugin_calculator_show_error_message}"
|
Content="{DynamicResource flowlauncher_plugin_calculator_show_error_message}"
|
||||||
IsChecked="{Binding Settings.ShowErrorMessage, Mode=TwoWay}" />
|
IsChecked="{Binding Settings.ShowErrorMessage, Mode=TwoWay}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -532,7 +532,7 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenShellPath()
|
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)
|
if (path is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
||||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.205">
|
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.269">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
<PackageReference Include="Flow.Launcher.Localization" Version="0.0.6" />
|
||||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.205">
|
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.269">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
|
||||||
|
|
@ -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 } }
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,10 @@
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="iNKORE.UI.WPF.Modern" Version="0.10.2.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
<system:String x:Key="flowlauncher_plugin_websearch_url">URL</system:String>
|
<system:String x:Key="flowlauncher_plugin_websearch_url">URL</system:String>
|
||||||
<system:String x:Key="flowlauncher_plugin_websearch_search">Search</system:String>
|
<system:String x:Key="flowlauncher_plugin_websearch_search">Search</system:String>
|
||||||
<system:String x:Key="flowlauncher_plugin_websearch_enable_suggestion">Use Search Query Autocomplete</system:String>
|
<system:String x:Key="flowlauncher_plugin_websearch_enable_suggestion">Use Search Query Autocomplete</system:String>
|
||||||
|
<system:String x:Key="flowlauncher_plugin_websearch_max_suggestions">Max Suggestions</system:String>
|
||||||
<system:String x:Key="flowlauncher_plugin_websearch_enable_suggestion_provider">Autocomplete Data from:</system:String>
|
<system:String x:Key="flowlauncher_plugin_websearch_enable_suggestion_provider">Autocomplete Data from:</system:String>
|
||||||
<system:String x:Key="flowlauncher_plugin_websearch_pls_select_web_search">Please select a web search</system:String>
|
<system:String x:Key="flowlauncher_plugin_websearch_pls_select_web_search">Please select a web search</system:String>
|
||||||
<system:String x:Key="flowlauncher_plugin_websearch_delete_warning">Are you sure you want to delete {0}?</system:String>
|
<system:String x:Key="flowlauncher_plugin_websearch_delete_warning">Are you sure you want to delete {0}?</system:String>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -91,6 +91,7 @@ namespace Flow.Launcher.Plugin.WebSearch
|
||||||
|
|
||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
@ -126,7 +127,7 @@ namespace Flow.Launcher.Plugin.WebSearch
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var resultsFromSuggestion = suggestions?.Select(o => new Result
|
var resultsFromSuggestion = suggestions?.Take(_settings.MaxSuggestions).Select(o => new Result
|
||||||
{
|
{
|
||||||
Title = o,
|
Title = o,
|
||||||
SubTitle = subtitle,
|
SubTitle = subtitle,
|
||||||
|
|
|
||||||
|
|
@ -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]
|
[JsonIgnore]
|
||||||
public SuggestionSource[] Suggestions { get; set; } = {
|
public SuggestionSource[] Suggestions { get; set; } = {
|
||||||
new Google(),
|
new Google(),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
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: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"
|
xmlns:vm="clr-namespace:Flow.Launcher.Plugin.WebSearch"
|
||||||
d:DataContext="{d:DesignInstance vm:SettingsViewModel}"
|
d:DataContext="{d:DesignInstance vm:SettingsViewModel}"
|
||||||
d:DesignHeight="300"
|
d:DesignHeight="300"
|
||||||
|
|
@ -45,17 +47,17 @@
|
||||||
x:Name="SearchSourcesListView"
|
x:Name="SearchSourcesListView"
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
|
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
|
||||||
|
AllowDrop="True"
|
||||||
BorderBrush="DarkGray"
|
BorderBrush="DarkGray"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
|
Drop="ListView_Drop"
|
||||||
GridViewColumnHeader.Click="SortByColumn"
|
GridViewColumnHeader.Click="SortByColumn"
|
||||||
ItemsSource="{Binding Settings.SearchSources}"
|
ItemsSource="{Binding Settings.SearchSources}"
|
||||||
MouseDoubleClick="MouseDoubleClickItem"
|
MouseDoubleClick="MouseDoubleClickItem"
|
||||||
SelectedItem="{Binding Settings.SelectedSearchSource}"
|
|
||||||
SizeChanged="ListView_SizeChanged"
|
|
||||||
PreviewMouseLeftButtonDown="ListView_PreviewMouseLeftButtonDown"
|
PreviewMouseLeftButtonDown="ListView_PreviewMouseLeftButtonDown"
|
||||||
PreviewMouseMove="ListView_PreviewMouseMove"
|
PreviewMouseMove="ListView_PreviewMouseMove"
|
||||||
AllowDrop="True"
|
SelectedItem="{Binding Settings.SelectedSearchSource}"
|
||||||
Drop="ListView_Drop"
|
SizeChanged="ListView_SizeChanged"
|
||||||
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
|
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
|
||||||
<ListView.View>
|
<ListView.View>
|
||||||
<GridView>
|
<GridView>
|
||||||
|
|
@ -146,31 +148,43 @@
|
||||||
|
|
||||||
<Separator Grid.Row="2" Style="{StaticResource SettingPanelSeparatorStyle}" />
|
<Separator Grid.Row="2" Style="{StaticResource SettingPanelSeparatorStyle}" />
|
||||||
|
|
||||||
<DockPanel
|
<WrapPanel Grid.Row="3" HorizontalAlignment="Stretch">
|
||||||
Grid.Row="3"
|
<StackPanel Orientation="Horizontal">
|
||||||
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
|
<TextBlock
|
||||||
HorizontalAlignment="Right">
|
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
|
||||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
|
||||||
<Label
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Content="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion_provider}" />
|
Text="{DynamicResource flowlauncher_plugin_websearch_max_suggestions}" />
|
||||||
|
<ui:NumberBox
|
||||||
|
Width="120"
|
||||||
|
MinWidth="120"
|
||||||
|
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
|
||||||
|
Maximum="1000"
|
||||||
|
Minimum="1"
|
||||||
|
SmallChange="10"
|
||||||
|
SpinButtonPlacementMode="Compact"
|
||||||
|
ValidationMode="InvalidInputOverwritten"
|
||||||
|
ValueChanged="NumberBox_ValueChanged"
|
||||||
|
Value="{Binding Settings.MaxSuggestions, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox
|
||||||
|
Name="EnableSuggestion"
|
||||||
|
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion}"
|
||||||
|
IsChecked="{Binding Settings.EnableSuggestion}" />
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock
|
||||||
|
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion_provider}" />
|
||||||
<ComboBox
|
<ComboBox
|
||||||
Height="30"
|
Height="30"
|
||||||
Margin="{StaticResource SettingPanelItemLeftMargin}"
|
Margin="{StaticResource SettingPanelItemTopBottomMargin}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontSize="11"
|
|
||||||
IsEnabled="{Binding Settings.EnableSuggestion}"
|
IsEnabled="{Binding Settings.EnableSuggestion}"
|
||||||
ItemsSource="{Binding Settings.Suggestions}"
|
ItemsSource="{Binding Settings.Suggestions}"
|
||||||
SelectedItem="{Binding Settings.SelectedSuggestion}" />
|
SelectedItem="{Binding Settings.SelectedSuggestion}" />
|
||||||
<CheckBox
|
|
||||||
Name="EnableSuggestion"
|
|
||||||
Margin="{StaticResource SettingPanelItemLeftMargin}"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Content="{DynamicResource flowlauncher_plugin_websearch_enable_suggestion}"
|
|
||||||
IsChecked="{Binding Settings.EnableSuggestion}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DockPanel>
|
</WrapPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
@ -240,5 +240,15 @@ namespace Flow.Launcher.Plugin.WebSearch
|
||||||
}
|
}
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue