mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into 250412-ImportThemePreset
This commit is contained in:
commit
643e531c12
14 changed files with 354 additions and 148 deletions
|
|
@ -166,6 +166,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
|
|||
public bool UseAnimation { get; set; } = true;
|
||||
public bool UseSound { get; set; } = true;
|
||||
public double SoundVolume { get; set; } = 50;
|
||||
public bool ShowBadges { get; set; } = false;
|
||||
public bool ShowBadgesGlobalOnly { get; set; } = false;
|
||||
|
||||
public bool UseClock { get; set; } = true;
|
||||
public bool UseDate { get; set; } = false;
|
||||
|
|
|
|||
|
|
@ -12,12 +12,19 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public class Result
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users.
|
||||
/// </summary>
|
||||
public const int MaxScore = int.MaxValue;
|
||||
|
||||
private string _pluginDirectory;
|
||||
|
||||
private string _icoPath;
|
||||
|
||||
private string _copyText = string.Empty;
|
||||
|
||||
private string _badgeIcoPath;
|
||||
|
||||
/// <summary>
|
||||
/// The title of the result. This is always required.
|
||||
/// </summary>
|
||||
|
|
@ -60,7 +67,7 @@ namespace Flow.Launcher.Plugin
|
|||
/// <remarks>GlyphInfo is prioritized if not null</remarks>
|
||||
public string IcoPath
|
||||
{
|
||||
get { return _icoPath; }
|
||||
get => _icoPath;
|
||||
set
|
||||
{
|
||||
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
|
||||
|
|
@ -80,6 +87,33 @@ namespace Flow.Launcher.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The image to be displayed for the badge of the result.
|
||||
/// </summary>
|
||||
/// <value>Can be a local file path or a URL.</value>
|
||||
/// <remarks>If null or empty, will use plugin icon</remarks>
|
||||
public string BadgeIcoPath
|
||||
{
|
||||
get => _badgeIcoPath;
|
||||
set
|
||||
{
|
||||
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
|
||||
if (!string.IsNullOrEmpty(value)
|
||||
&& !string.IsNullOrEmpty(PluginDirectory)
|
||||
&& !Path.IsPathRooted(value)
|
||||
&& !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !value.StartsWith("data:image", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_badgeIcoPath = Path.Combine(PluginDirectory, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_badgeIcoPath = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if Icon has a border radius
|
||||
/// </summary>
|
||||
|
|
@ -94,14 +128,18 @@ namespace Flow.Launcher.Plugin
|
|||
/// <summary>
|
||||
/// Delegate to load an icon for this result.
|
||||
/// </summary>
|
||||
public IconDelegate Icon;
|
||||
public IconDelegate Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delegate to load an icon for the badge of this result.
|
||||
/// </summary>
|
||||
public IconDelegate BadgeIcon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
|
||||
/// </summary>
|
||||
public GlyphInfo Glyph { get; init; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An action to take in the form of a function call when the result has been selected.
|
||||
/// </summary>
|
||||
|
|
@ -143,59 +181,19 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public string PluginDirectory
|
||||
{
|
||||
get { return _pluginDirectory; }
|
||||
get => _pluginDirectory;
|
||||
set
|
||||
{
|
||||
_pluginDirectory = value;
|
||||
|
||||
// When the Result object is returned from the query call, PluginDirectory is not provided until
|
||||
// UpdatePluginMetadata call is made at PluginManager.cs L196. Once the PluginDirectory becomes available
|
||||
// we need to update (only if not Uri path) the IcoPath with the full absolute path so the image can be loaded.
|
||||
// we need to update (only if not Uri path) the IcoPath and BadgeIcoPath with the full absolute path so the image can be loaded.
|
||||
IcoPath = _icoPath;
|
||||
BadgeIcoPath = _badgeIcoPath;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Title + SubTitle + Score;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the current result
|
||||
/// </summary>
|
||||
public Result Clone()
|
||||
{
|
||||
return new Result
|
||||
{
|
||||
Title = Title,
|
||||
SubTitle = SubTitle,
|
||||
ActionKeywordAssigned = ActionKeywordAssigned,
|
||||
CopyText = CopyText,
|
||||
AutoCompleteText = AutoCompleteText,
|
||||
IcoPath = IcoPath,
|
||||
RoundedIcon = RoundedIcon,
|
||||
Icon = Icon,
|
||||
Glyph = Glyph,
|
||||
Action = Action,
|
||||
AsyncAction = AsyncAction,
|
||||
Score = Score,
|
||||
TitleHighlightData = TitleHighlightData,
|
||||
OriginQuery = OriginQuery,
|
||||
PluginDirectory = PluginDirectory,
|
||||
ContextData = ContextData,
|
||||
PluginID = PluginID,
|
||||
TitleToolTip = TitleToolTip,
|
||||
SubTitleToolTip = SubTitleToolTip,
|
||||
PreviewPanel = PreviewPanel,
|
||||
ProgressBar = ProgressBar,
|
||||
ProgressBarColor = ProgressBarColor,
|
||||
Preview = Preview,
|
||||
AddSelectedCount = AddSelectedCount,
|
||||
RecordKey = RecordKey
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Additional data associated with this result
|
||||
/// </summary>
|
||||
|
|
@ -224,16 +222,6 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public Lazy<UserControl> PreviewPanel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Run this result, asynchronously
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public ValueTask<bool> ExecuteAsync(ActionContext context)
|
||||
{
|
||||
return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result
|
||||
/// </summary>
|
||||
|
|
@ -255,11 +243,6 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public bool AddSelectedCount { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum score. This can be useful when set one result to the top by default. This is the score for the results set to the topmost by users.
|
||||
/// </summary>
|
||||
public const int MaxScore = int.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records.
|
||||
/// This can be useful when your plugin will change the Title or SubTitle of the result dynamically.
|
||||
|
|
@ -268,6 +251,66 @@ namespace Flow.Launcher.Plugin
|
|||
/// </summary>
|
||||
public string RecordKey { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the badge icon should be shown.
|
||||
/// If users want to show the result badges and here you set this to true, the results will show the badge icon.
|
||||
/// </summary>
|
||||
public bool ShowBadge { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Run this result, asynchronously
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public ValueTask<bool> ExecuteAsync(ActionContext context)
|
||||
{
|
||||
return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Title + SubTitle + Score;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the current result
|
||||
/// </summary>
|
||||
public Result Clone()
|
||||
{
|
||||
return new Result
|
||||
{
|
||||
Title = Title,
|
||||
SubTitle = SubTitle,
|
||||
ActionKeywordAssigned = ActionKeywordAssigned,
|
||||
CopyText = CopyText,
|
||||
AutoCompleteText = AutoCompleteText,
|
||||
IcoPath = IcoPath,
|
||||
BadgeIcoPath = BadgeIcoPath,
|
||||
RoundedIcon = RoundedIcon,
|
||||
Icon = Icon,
|
||||
BadgeIcon = BadgeIcon,
|
||||
Glyph = Glyph,
|
||||
Action = Action,
|
||||
AsyncAction = AsyncAction,
|
||||
Score = Score,
|
||||
TitleHighlightData = TitleHighlightData,
|
||||
OriginQuery = OriginQuery,
|
||||
PluginDirectory = PluginDirectory,
|
||||
ContextData = ContextData,
|
||||
PluginID = PluginID,
|
||||
TitleToolTip = TitleToolTip,
|
||||
SubTitleToolTip = SubTitleToolTip,
|
||||
PreviewPanel = PreviewPanel,
|
||||
ProgressBar = ProgressBar,
|
||||
ProgressBarColor = ProgressBarColor,
|
||||
Preview = Preview,
|
||||
AddSelectedCount = AddSelectedCount,
|
||||
RecordKey = RecordKey,
|
||||
ShowBadge = ShowBadge,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Info of the preview section of a <see cref="Result"/>
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ namespace Flow.Launcher.Test.Plugins
|
|||
}
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")]
|
||||
[TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")]
|
||||
[TestCase("C:\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
|
||||
[TestCase("C:\\SomeFolder\\", $"SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY {QueryConstructor.OrderIdentifier}")]
|
||||
public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
|
||||
{
|
||||
// Given
|
||||
|
|
@ -59,7 +59,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
[TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" +
|
||||
" FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" +
|
||||
" AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" +
|
||||
" ORDER BY System.FileName")]
|
||||
$" ORDER BY {QueryConstructor.OrderIdentifier}")]
|
||||
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
|
||||
string folderPath, string userSearchString, string expectedString)
|
||||
{
|
||||
|
|
@ -87,8 +87,8 @@ namespace Flow.Launcher.Test.Plugins
|
|||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +
|
||||
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
|
||||
"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")]
|
||||
[TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")]
|
||||
$"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
|
||||
[TestCase("", $"SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
|
||||
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
|
||||
string userSearchString, string expectedString)
|
||||
{
|
||||
|
|
@ -107,7 +107,6 @@ namespace Flow.Launcher.Test.Plugins
|
|||
ClassicAssert.AreEqual(expectedString, resultString);
|
||||
}
|
||||
|
||||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase(@"some words", @"FREETEXT('some words')")]
|
||||
public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString(
|
||||
|
|
@ -127,7 +126,7 @@ namespace Flow.Launcher.Test.Plugins
|
|||
|
||||
[SupportedOSPlatform("windows7.0")]
|
||||
[TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
|
||||
"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")]
|
||||
$"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY {QueryConstructor.OrderIdentifier}")]
|
||||
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
|
||||
string userSearchString, string expectedString)
|
||||
{
|
||||
|
|
|
|||
32
Flow.Launcher/Converters/BadgePositionConverter.cs
Normal file
32
Flow.Launcher/Converters/BadgePositionConverter.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class BadgePositionConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is double actualWidth && parameter is string param)
|
||||
{
|
||||
double offset = actualWidth / 2 - 8;
|
||||
|
||||
if (param == "1") // X-Offset
|
||||
{
|
||||
return offset + 2;
|
||||
}
|
||||
else if (param == "2") // Y-Offset
|
||||
{
|
||||
return offset + 2;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
27
Flow.Launcher/Converters/SizeRatioConverter.cs
Normal file
27
Flow.Launcher/Converters/SizeRatioConverter.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System.Windows.Data;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
|
||||
namespace Flow.Launcher.Converters;
|
||||
|
||||
public class SizeRatioConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is double size && parameter is string ratioString)
|
||||
{
|
||||
if (double.TryParse(ratioString, NumberStyles.Any, CultureInfo.InvariantCulture, out double ratio))
|
||||
{
|
||||
return size * ratio;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -302,6 +302,10 @@
|
|||
<system:String x:Key="useGlyphUI">Use Segoe Fluent Icons</system:String>
|
||||
<system:String x:Key="useGlyphUIEffect">Use Segoe Fluent Icons for query results where supported</system:String>
|
||||
<system:String x:Key="flowlauncherPressHotkey">Press Key</system:String>
|
||||
<system:String x:Key="showBadges">Show Result Badges</system:String>
|
||||
<system:String x:Key="showBadgesToolTip">Show badges for query results where supported</system:String>
|
||||
<system:String x:Key="showBadgesGlobalOnly">Show Result Badges for Global Query Only</system:String>
|
||||
<system:String x:Key="showBadgesGlobalOnlyToolTip">Show badges for global query results only</system:String>
|
||||
|
||||
<!-- Setting Proxy -->
|
||||
<system:String x:Key="proxy">HTTP Proxy</system:String>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@
|
|||
<!-- IsSynchronizedWithCurrentItem: http://stackoverflow.com/a/7833798/2833083 -->
|
||||
|
||||
<ListBox.Resources>
|
||||
<converter:SizeRatioConverter x:Key="SizeRatioConverter" />
|
||||
<converter:BadgePositionConverter x:Key="BadgePositionConverter" />
|
||||
<converter:IconRadiusConverter x:Key="IconRadiusConverter" />
|
||||
<converter:DiameterToCenterPointConverter x:Key="DiameterToCenterPointConverter" />
|
||||
</ListBox.Resources>
|
||||
|
|
@ -90,60 +92,64 @@
|
|||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
x:Name="Bullet"
|
||||
Grid.Column="0"
|
||||
Style="{DynamicResource BulletStyle}" />
|
||||
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Margin="9 0 0 0"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="1">
|
||||
<Image
|
||||
x:Name="ImageIcon"
|
||||
Margin="0 0 0 0"
|
||||
HorizontalAlignment="Center"
|
||||
IsHitTestVisible="False"
|
||||
RenderOptions.BitmapScalingMode="Fant"
|
||||
Source="{Binding Image, TargetNullValue={x:Null}}"
|
||||
Stretch="Uniform"
|
||||
StretchDirection="DownOnly"
|
||||
Style="{DynamicResource ImageIconStyle}"
|
||||
Visibility="{Binding ShowIcon}">
|
||||
<Image.Clip>
|
||||
<EllipseGeometry Center="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource DiameterToCenterPointConverter}}">
|
||||
<EllipseGeometry.RadiusX>
|
||||
<MultiBinding Converter="{StaticResource IconRadiusConverter}">
|
||||
<Binding ElementName="ImageIcon" Path="ActualWidth" />
|
||||
<Binding Path="Result.RoundedIcon" />
|
||||
</MultiBinding>
|
||||
</EllipseGeometry.RadiusX>
|
||||
<EllipseGeometry.RadiusY>
|
||||
<MultiBinding Converter="{StaticResource IconRadiusConverter}">
|
||||
<Binding ElementName="ImageIcon" Path="ActualWidth" />
|
||||
<Binding Path="Result.RoundedIcon" />
|
||||
</MultiBinding>
|
||||
</EllipseGeometry.RadiusY>
|
||||
</EllipseGeometry>
|
||||
</Image.Clip>
|
||||
</Image>
|
||||
</Border>
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Margin="9 0 0 0"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0">
|
||||
<TextBlock
|
||||
x:Name="GlyphIcon"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{Binding Glyph.FontFamily}"
|
||||
Style="{DynamicResource ItemGlyph}"
|
||||
Text="{Binding Glyph.Glyph}"
|
||||
Visibility="{Binding ShowGlyph}" />
|
||||
<Grid>
|
||||
<Image
|
||||
x:Name="ImageIcon"
|
||||
IsHitTestVisible="False"
|
||||
RenderOptions.BitmapScalingMode="Fant"
|
||||
Source="{Binding Image, TargetNullValue={x:Null}}"
|
||||
Stretch="Uniform"
|
||||
StretchDirection="DownOnly"
|
||||
Style="{DynamicResource ImageIconStyle}"
|
||||
Visibility="{Binding ShowIcon}">
|
||||
<Image.Clip>
|
||||
<EllipseGeometry Center="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource DiameterToCenterPointConverter}}">
|
||||
<EllipseGeometry.RadiusX>
|
||||
<MultiBinding Converter="{StaticResource IconRadiusConverter}">
|
||||
<Binding ElementName="ImageIcon" Path="ActualWidth" />
|
||||
<Binding Path="Result.RoundedIcon" />
|
||||
</MultiBinding>
|
||||
</EllipseGeometry.RadiusX>
|
||||
<EllipseGeometry.RadiusY>
|
||||
<MultiBinding Converter="{StaticResource IconRadiusConverter}">
|
||||
<Binding ElementName="ImageIcon" Path="ActualWidth" />
|
||||
<Binding Path="Result.RoundedIcon" />
|
||||
</MultiBinding>
|
||||
</EllipseGeometry.RadiusY>
|
||||
</EllipseGeometry>
|
||||
</Image.Clip>
|
||||
</Image>
|
||||
|
||||
<TextBlock
|
||||
x:Name="GlyphIcon"
|
||||
FontFamily="{Binding Glyph.FontFamily}"
|
||||
Style="{DynamicResource ItemGlyph}"
|
||||
Text="{Binding Glyph.Glyph}"
|
||||
Visibility="{Binding ShowGlyph}" />
|
||||
|
||||
<Image
|
||||
x:Name="BadgeIcon"
|
||||
Width="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource SizeRatioConverter}, ConverterParameter=0.6}"
|
||||
Height="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource SizeRatioConverter}, ConverterParameter=0.6}"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
Source="{Binding BadgeImage, TargetNullValue={x:Null}}"
|
||||
Visibility="{Binding ShowBadge}">
|
||||
<Image.RenderTransform>
|
||||
<TranslateTransform X="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource BadgePositionConverter}, ConverterParameter=1}" Y="{Binding ElementName=ImageIcon, Path=ActualWidth, Converter={StaticResource BadgePositionConverter}, ConverterParameter=2}" />
|
||||
</Image.RenderTransform>
|
||||
</Image>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
|
|
|
|||
|
|
@ -716,11 +716,10 @@
|
|||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
<!-- Fonts and icons -->
|
||||
<cc:Card
|
||||
Title="{DynamicResource useGlyphUI}"
|
||||
Margin="0 0 0 0"
|
||||
Margin="0 18 0 0"
|
||||
Icon=""
|
||||
Sub="{DynamicResource useGlyphUIEffect}">
|
||||
<ui:ToggleSwitch
|
||||
|
|
@ -728,6 +727,30 @@
|
|||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}" />
|
||||
</cc:Card>
|
||||
|
||||
<!-- Badges -->
|
||||
<cc:ExCard
|
||||
Title="{DynamicResource showBadges}"
|
||||
Margin="0 0 0 0"
|
||||
Icon=""
|
||||
Sub="{DynamicResource showBadgesToolTip}">
|
||||
<cc:ExCard.SideContent>
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding Settings.ShowBadges}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}" />
|
||||
</cc:ExCard.SideContent>
|
||||
<cc:Card
|
||||
Title="{DynamicResource showBadgesGlobalOnly}"
|
||||
Sub="{DynamicResource showBadgesGlobalOnlyToolTip}"
|
||||
Type="InsideFit">
|
||||
<ui:ToggleSwitch
|
||||
IsOn="{Binding Settings.ShowBadgesGlobalOnly}"
|
||||
OffContent="{DynamicResource disable}"
|
||||
OnContent="{DynamicResource enable}" />
|
||||
</cc:Card>
|
||||
</cc:ExCard>
|
||||
|
||||
<!-- Settings color scheme -->
|
||||
<cc:Card
|
||||
Title="{DynamicResource ColorScheme}"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
BasedOn="{StaticResource BaseGlyphStyle}"
|
||||
TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
|
||||
<Setter Property="Margin" Value="-4 0 0 0" />
|
||||
<Setter Property="Margin" Value="0 0 0 0" />
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="QueryBoxStyle"
|
||||
|
|
@ -182,7 +182,7 @@
|
|||
<Setter Property="Foreground" Value="{DynamicResource Color05B}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="-4 0 0 0" />
|
||||
<Setter Property="Margin" Value="0 0 0 0" />
|
||||
<Setter Property="Width" Value="25" />
|
||||
<Setter Property="Height" Value="25" />
|
||||
<Setter Property="FontSize" Value="25" />
|
||||
|
|
|
|||
|
|
@ -245,6 +245,14 @@ namespace Flow.Launcher.ViewModel
|
|||
// make a clone to avoid possible issue that plugin will also change the list and items when updating view model
|
||||
var resultsCopy = DeepCloneResults(e.Results, token);
|
||||
|
||||
foreach (var result in resultsCopy)
|
||||
{
|
||||
if (string.IsNullOrEmpty(result.BadgeIcoPath))
|
||||
{
|
||||
result.BadgeIcoPath = pair.Metadata.IcoPath;
|
||||
}
|
||||
}
|
||||
|
||||
PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query);
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query,
|
||||
token)))
|
||||
|
|
@ -1200,11 +1208,11 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
_lastQuery = query;
|
||||
|
||||
if (query.ActionKeyword == Plugin.Query.GlobalPluginWildcardSign)
|
||||
if (string.IsNullOrEmpty(query.ActionKeyword))
|
||||
{
|
||||
// Wait 45 millisecond for query change in global query
|
||||
// Wait 15 millisecond for query change in global query
|
||||
// if query changes, return so that it won't be calculated
|
||||
await Task.Delay(45, _updateSource.Token);
|
||||
await Task.Delay(15, _updateSource.Token);
|
||||
if (_updateSource.Token.IsCancellationRequested)
|
||||
return;
|
||||
}
|
||||
|
|
@ -1268,8 +1276,7 @@ namespace Flow.Launcher.ViewModel
|
|||
// Task.Yield will force it to run in ThreadPool
|
||||
await Task.Yield();
|
||||
|
||||
IReadOnlyList<Result> results =
|
||||
await PluginManager.QueryForPluginAsync(plugin, query, token);
|
||||
var results = await PluginManager.QueryForPluginAsync(plugin, query, token);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
|
@ -1285,6 +1292,14 @@ namespace Flow.Launcher.ViewModel
|
|||
resultsCopy = DeepCloneResults(results, token);
|
||||
}
|
||||
|
||||
foreach (var result in resultsCopy)
|
||||
{
|
||||
if (string.IsNullOrEmpty(result.BadgeIcoPath))
|
||||
{
|
||||
result.BadgeIcoPath = plugin.Metadata.IcoPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query,
|
||||
token, reSelect)))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing.Text;
|
||||
using System.IO;
|
||||
|
|
@ -108,7 +109,6 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
return IconXY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Visibility ShowGlyph
|
||||
|
|
@ -124,10 +124,32 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
public Visibility ShowBadge
|
||||
{
|
||||
get
|
||||
{
|
||||
// If results do not allow badges, or user has disabled badges in settings,
|
||||
// or badge icon is not available, then do not show badge
|
||||
if (!Result.ShowBadge || !Settings.ShowBadges || !BadgeIconAvailable)
|
||||
return Visibility.Collapsed;
|
||||
|
||||
// If user has set to show badges only for global results, and this is not a global result,
|
||||
// then do not show badge
|
||||
if (Settings.ShowBadgesGlobalOnly && !IsGlobalQuery)
|
||||
return Visibility.Collapsed;
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsGlobalQuery => string.IsNullOrEmpty(Result.OriginQuery.ActionKeyword);
|
||||
|
||||
private bool GlyphAvailable => Glyph is not null;
|
||||
|
||||
private bool ImgIconAvailable => !string.IsNullOrEmpty(Result.IcoPath) || Result.Icon is not null;
|
||||
|
||||
private bool BadgeIconAvailable => !string.IsNullOrEmpty(Result.BadgeIcoPath) || Result.BadgeIcon is not null;
|
||||
|
||||
private bool PreviewImageAvailable => !string.IsNullOrEmpty(Result.Preview.PreviewImagePath) || Result.Preview.PreviewDelegate != null;
|
||||
|
||||
public string OpenResultModifiers => Settings.OpenResultModifiers;
|
||||
|
|
@ -141,9 +163,11 @@ namespace Flow.Launcher.ViewModel
|
|||
: Result.SubTitleToolTip;
|
||||
|
||||
private volatile bool _imageLoaded;
|
||||
private volatile bool _badgeImageLoaded;
|
||||
private volatile bool _previewImageLoaded;
|
||||
|
||||
private ImageSource _image = ImageLoader.LoadingImage;
|
||||
private ImageSource _badgeImage = ImageLoader.LoadingImage;
|
||||
private ImageSource _previewImage = ImageLoader.LoadingImage;
|
||||
|
||||
public ImageSource Image
|
||||
|
|
@ -161,6 +185,21 @@ namespace Flow.Launcher.ViewModel
|
|||
private set => _image = value;
|
||||
}
|
||||
|
||||
public ImageSource BadgeImage
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_badgeImageLoaded)
|
||||
{
|
||||
_badgeImageLoaded = true;
|
||||
_ = LoadBadgeImageAsync();
|
||||
}
|
||||
|
||||
return _badgeImage;
|
||||
}
|
||||
private set => _badgeImage = value;
|
||||
}
|
||||
|
||||
public ImageSource PreviewImage
|
||||
{
|
||||
get
|
||||
|
|
@ -206,7 +245,7 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
var imagePath = Result.IcoPath;
|
||||
var iconDelegate = Result.Icon;
|
||||
if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img))
|
||||
if (ImageLoader.TryGetValue(imagePath, false, out var img))
|
||||
{
|
||||
_image = img;
|
||||
}
|
||||
|
|
@ -217,11 +256,26 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
private async Task LoadBadgeImageAsync()
|
||||
{
|
||||
var badgeImagePath = Result.BadgeIcoPath;
|
||||
var badgeIconDelegate = Result.BadgeIcon;
|
||||
if (ImageLoader.TryGetValue(badgeImagePath, false, out var img))
|
||||
{
|
||||
_badgeImage = img;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We need to modify the property not field here to trigger the OnPropertyChanged event
|
||||
BadgeImage = await LoadImageInternalAsync(badgeImagePath, badgeIconDelegate, false).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPreviewImageAsync()
|
||||
{
|
||||
var imagePath = Result.Preview.PreviewImagePath ?? Result.IcoPath;
|
||||
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
|
||||
if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img))
|
||||
if (ImageLoader.TryGetValue(imagePath, true, out var img))
|
||||
{
|
||||
_previewImage = img;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
public ResultCollection Results { get; }
|
||||
|
||||
private readonly object _collectionLock = new object();
|
||||
private readonly object _collectionLock = new();
|
||||
private readonly Settings _settings;
|
||||
private int MaxResults => _settings?.MaxResultsToShow ?? 6;
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
#region Private Methods
|
||||
|
||||
private int InsertIndexOf(int newScore, IList<ResultViewModel> list)
|
||||
private static int InsertIndexOf(int newScore, IList<ResultViewModel> list)
|
||||
{
|
||||
int index = 0;
|
||||
for (; index < list.Count; index++)
|
||||
|
|
@ -118,7 +118,6 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
|
@ -190,10 +189,10 @@ namespace Flow.Launcher.ViewModel
|
|||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
UpdateResults(newResults, token, reselect);
|
||||
UpdateResults(newResults, reselect, token);
|
||||
}
|
||||
|
||||
private void UpdateResults(List<ResultViewModel> newResults, CancellationToken token = default, bool reselect = true)
|
||||
private void UpdateResults(List<ResultViewModel> newResults, bool reselect = true, CancellationToken token = default)
|
||||
{
|
||||
lock (_collectionLock)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
{
|
||||
public class QueryConstructor
|
||||
{
|
||||
private static Regex _specialCharacterMatcher = new(@"[\@\@\#\#\&\&*_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled);
|
||||
private static Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled);
|
||||
private static readonly Regex _specialCharacterMatcher = new(@"[\@\@\#\#\&\&*_;,\%\|\!\(\)\{\}\[\]\^\~\?\\""\/\:\=\-]+", RegexOptions.Compiled);
|
||||
private static readonly Regex _multiWhiteSpacesMatcher = new(@"\s+", RegexOptions.Compiled);
|
||||
|
||||
private Settings settings { get; }
|
||||
private Settings Settings { get; }
|
||||
|
||||
private const string SystemIndex = "SystemIndex";
|
||||
|
||||
public QueryConstructor(Settings settings)
|
||||
{
|
||||
this.settings = settings;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public CSearchQueryHelper CreateBaseQuery()
|
||||
|
|
@ -23,7 +23,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
var baseQuery = CreateQueryHelper();
|
||||
|
||||
// Set the number of results we want. Don't set this property if all results are needed.
|
||||
baseQuery.QueryMaxResults = settings.MaxResult;
|
||||
baseQuery.QueryMaxResults = Settings.MaxResult;
|
||||
|
||||
// Set list of columns we want to display, getting the path presently
|
||||
baseQuery.QuerySelectColumns = "System.FileName, System.ItemUrl, System.ItemType";
|
||||
|
|
@ -37,7 +37,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
return baseQuery;
|
||||
}
|
||||
|
||||
internal CSearchQueryHelper CreateQueryHelper()
|
||||
internal static CSearchQueryHelper CreateQueryHelper()
|
||||
{
|
||||
// This uses the Microsoft.Search.Interop assembly
|
||||
// Throws COMException if Windows Search service is not running/disabled, this needs to be caught
|
||||
|
|
@ -54,20 +54,19 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
|
||||
public static string TopLevelDirectoryConstraint(ReadOnlySpan<char> path) => $"directory='file:{path}'";
|
||||
public static string RecursiveDirectoryConstraint(ReadOnlySpan<char> path) => $"scope='file:{path}'";
|
||||
|
||||
|
||||
///<summary>
|
||||
/// Search will be performed on all folders and files on the first level of a specified directory.
|
||||
///</summary>
|
||||
public string Directory(ReadOnlySpan<char> path, ReadOnlySpan<char> searchString = default, bool recursive = false)
|
||||
{
|
||||
var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND ({FileName} LIKE '{searchString}%' OR CONTAINS({FileName},'\"{searchString}*\"'))";
|
||||
var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND (System.FileName LIKE '{searchString}%' OR CONTAINS(System.FileName,'\"{searchString}*\"'))";
|
||||
|
||||
var scopeConstraint = recursive
|
||||
? RecursiveDirectoryConstraint(path)
|
||||
: TopLevelDirectoryConstraint(path);
|
||||
|
||||
var query = $"SELECT TOP {settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {FileName}";
|
||||
var query = $"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {OrderIdentifier}";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
|
@ -84,7 +83,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
var replacedSearchString = ReplaceSpecialCharacterWithTwoSideWhiteSpace(userSearchString);
|
||||
|
||||
// Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause
|
||||
return $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
|
||||
return $"{CreateBaseQuery().GenerateSQLFromUserQuery(replacedSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -121,10 +120,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
public const string RestrictionsForAllFilesAndFoldersSearch = "scope='file:'";
|
||||
|
||||
/// <summary>
|
||||
/// Order identifier: file name
|
||||
/// Order identifier: System.Search.Rank DESC
|
||||
/// </summary>
|
||||
public const string FileName = "System.FileName";
|
||||
|
||||
/// <remarks>
|
||||
/// <see href="https://docs.microsoft.com/en-us/windows/win32/properties/props-system-search-rank"/>
|
||||
/// </remarks>
|
||||
public const string OrderIdentifier = "System.Search.Rank DESC";
|
||||
|
||||
///<summary>
|
||||
/// Search will be performed on all indexed file contents for the specified search keywords.
|
||||
|
|
@ -132,7 +133,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
public string FileContent(ReadOnlySpan<char> userSearchString)
|
||||
{
|
||||
string query =
|
||||
$"SELECT TOP {settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
|
||||
$"SELECT TOP {Settings.MaxResult} {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {OrderIdentifier}";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,14 +82,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
return HandledEngineNotAvailableExceptionAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<SearchResult> SearchAsync(string search, CancellationToken token)
|
||||
{
|
||||
return WindowsIndexFilesAndFoldersSearchAsync(search, token: token);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<SearchResult> ContentSearchAsync(string plainSearch, string contentSearch, CancellationToken token)
|
||||
{
|
||||
return WindowsIndexFileContentSearchAsync(contentSearch, token);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<SearchResult> EnumerateAsync(string path, string search, bool recursive, CancellationToken token)
|
||||
{
|
||||
return WindowsIndexTopLevelFolderSearchAsync(search, path, recursive, token);
|
||||
|
|
@ -100,19 +103,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
|
|||
if (!Settings.WarnWindowsSearchServiceOff)
|
||||
return AsyncEnumerable.Empty<SearchResult>();
|
||||
|
||||
var api = Main.Context.API;
|
||||
|
||||
throw new EngineNotAvailableException(
|
||||
"Windows Index",
|
||||
api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
|
||||
api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
|
||||
Main.Context.API.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
|
||||
Main.Context.API.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
|
||||
Constants.WindowsIndexErrorImagePath,
|
||||
c =>
|
||||
{
|
||||
Settings.WarnWindowsSearchServiceOff = false;
|
||||
|
||||
// Clears the warning message so user is not mistaken that it has not worked
|
||||
api.ChangeQuery(string.Empty);
|
||||
Main.Context.API.ChangeQuery(string.Empty);
|
||||
|
||||
return ValueTask.FromResult(false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue