Merge branch 'FixMemoryLeak' of https://github.com/onesounds/Flow.Launcher into FixMemoryLeak

This commit is contained in:
DB p 2022-11-08 18:46:06 +09:00
commit 03c9a1aab4
23 changed files with 777 additions and 56 deletions

View file

@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using NLog;

View file

@ -202,7 +202,11 @@ namespace Flow.Launcher.Infrastructure
if (allQuerySubstringsMatched)
{
var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex);
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1,
// firstMatchIndex - nearestSpaceIndex - 1 is to set the firstIndex as the index of the first matched char
// preceded by a space e.g. 'world' matching 'hello world' firstIndex would be 0 not 6
// giving more weight than 'we or donald' by allowing the distance calculation to treat the starting position at after the space.
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, spaceIndices,
lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList();
@ -296,7 +300,7 @@ namespace Flow.Launcher.Infrastructure
return currentQuerySubstringIndex >= querySubstringsLength;
}
private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen,
private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, List<int> spaceIndices, int matchLen,
bool allSubstringsContainedInCompareString)
{
// A match found near the beginning of a string is scored more than a match found near the end
@ -304,6 +308,14 @@ namespace Flow.Launcher.Infrastructure
// while the score is lower if they are more spread out
var score = 100 * (query.Length + 1) / ((1 + firstIndex) + (matchLen + 1));
// Give more weight to a match that is closer to the start of the string.
// if the first matched char is immediately before space and all strings are contained in the compare string e.g. 'world' matching 'hello world'
// and 'world hello', because both have 'world' immediately preceded by space, their firstIndex will be 0 when distance is calculated,
// to prevent them scoring the same, we adjust the score by deducting the number of spaces it has from the start of the string, so 'world hello'
// will score slightly higher than 'hello world' because 'hello world' has one additional space.
if (firstIndex == 0 && allSubstringsContainedInCompareString)
score -= spaceIndices.Count;
// A match with less characters assigning more weights
if (stringToCompare.Length - query.Length < 5)
{

View file

@ -0,0 +1,65 @@
using System;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Infrastructure.UserSettings
{
public abstract class ShortcutBaseModel
{
public string Key { get; set; }
[JsonIgnore]
public Func<string> Expand { get; set; } = () => { return ""; };
public override bool Equals(object obj)
{
return obj is ShortcutBaseModel other &&
Key == other.Key;
}
public override int GetHashCode()
{
return Key.GetHashCode();
}
}
public class CustomShortcutModel : ShortcutBaseModel
{
public string Value { get; set; }
[JsonConstructorAttribute]
public CustomShortcutModel(string key, string value)
{
Key = key;
Value = value;
Expand = () => { return Value; };
}
public void Deconstruct(out string key, out string value)
{
key = Key;
value = Value;
}
public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut)
{
return (shortcut.Key, shortcut.Value);
}
public static implicit operator CustomShortcutModel((string Key, string Value) shortcut)
{
return new CustomShortcutModel(shortcut.Key, shortcut.Value);
}
}
public class BuiltinShortcutModel : ShortcutBaseModel
{
public string Description { get; set; }
public BuiltinShortcutModel(string key, string description, Func<string> expand)
{
Key = key;
Description = description;
Expand = expand ?? (() => { return ""; });
}
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Text.Json.Serialization;
using System.Windows;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher;
@ -129,8 +130,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
PrivateArg = "-private",
EnablePrivate = false,
Editable = false
}
,
},
new()
{
Name = "MS Edge",
@ -186,6 +186,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();
public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new ObservableCollection<CustomShortcutModel>();
[JsonIgnore]
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new ObservableCollection<BuiltinShortcutModel>() {
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText)
};
public bool DontPromptUpdateMsg { get; set; }
public bool EnableUpdateLog { get; set; }

View file

@ -37,7 +37,11 @@ namespace Flow.Launcher.Plugin
/// user's clipboard when Ctrl + C is pressed on a result. If the text is a file/directory path
/// flow will copy the actual file/folder instead of just the path text.
/// </summary>
public string CopyText { get; set; } = string.Empty;
public string CopyText
{
get => string.IsNullOrEmpty(_copyText) ? SubTitle : _copyText;
set => _copyText = value;
}
/// <summary>
/// This holds the text which can be provided by plugin to help Flow autocomplete text
@ -81,6 +85,7 @@ namespace Flow.Launcher.Plugin
/// Delegate to Get Image Source
/// </summary>
public IconDelegate Icon;
private string _copyText = string.Empty;
/// <summary>
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)

View file

@ -129,14 +129,20 @@ namespace Flow.Launcher.Test
}
}
/// <summary>
/// These are standard match scenarios
/// The intention of this test is provide a bench mark for how much the score has increased from a change.
/// Usually the increase in scoring should not be drastic, increase of less than 10 is acceptable.
/// </summary>
[TestCase(Chrome, Chrome, 157)]
[TestCase(Chrome, LastIsChrome, 147)]
[TestCase(Chrome, LastIsChrome, 145)]
[TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)]
[TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)]
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)]
[TestCase(Chrome, CandyCrushSagaFromKing, 0)]
[TestCase("sql", MicrosoftSqlServerManagementStudio, 110)]
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended
[TestCase("sql", MicrosoftSqlServerManagementStudio, 109)]
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 120)] //double spacing intended
public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring(
string queryString, string compareString, int expectedScore)
{
@ -275,7 +281,40 @@ namespace Flow.Launcher.Test
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
$"Should be greater than{Environment.NewLine}" +
$"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}");
$"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
}
[TestCase("red", "red colour", "metro red")]
[TestCase("red", "this red colour", "this colour red")]
[TestCase("red", "this red colour", "this colour is very red")]
[TestCase("red", "this red colour", "this colour is surprisingly super awesome red and cool")]
[TestCase("red", "this colour is surprisingly super red very and cool", "this colour is surprisingly super very red and cool")]
public void WhenGivenTwoStrings_Scoring_ShouldGiveMoreWeightToTheStringCloserToIndexZero(
string queryString, string compareString1, string compareString2)
{
// When
var matcher = new StringMatcher { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
// Given
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
var compareString2Result = matcher.FuzzyMatch(queryString, compareString2);
Debug.WriteLine("");
Debug.WriteLine("###############################################");
Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}");
Debug.WriteLine(
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}");
Debug.WriteLine(
$"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
Debug.WriteLine("###############################################");
Debug.WriteLine("");
// Should
Assert.True(compareString1Result.Score > compareString2Result.Score,
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
$"Should be greater than{Environment.NewLine}" +
$"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
}
[TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")]

View file

@ -0,0 +1,20 @@
using System;
using System.Globalization;
using System.Windows.Data;
using Flow.Launcher.Core.Resource;
namespace Flow.Launcher.Converters
{
public class TranlationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var key = value.ToString();
if (String.IsNullOrEmpty(key))
return key;
return InternationalizationManager.Instance.GetTranslation(key);
}
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
}
}

View file

@ -0,0 +1,160 @@
<Window
x:Class="Flow.Launcher.CustomShortcutSetting"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
Title="{DynamicResource customeQueryShortcutTitle}"
Width="530"
Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}"
Icon="Images\app.png"
ResizeMode="NoResize"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Window.InputBindings>
<KeyBinding Key="Escape" Command="Close" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Close" Executed="cmdEsc_OnPress" />
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="80" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="4"
Click="BtnCancel_OnClick"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
</Grid>
</StackPanel>
<StackPanel Margin="26,0,26,0">
<StackPanel Grid.Row="0" Margin="0,0,0,12">
<TextBlock
Grid.Column="0"
Margin="0,0,0,0"
FontFamily="Segoe UI"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource customQueryShortcut}"
TextAlignment="Left" />
</StackPanel>
<StackPanel>
<TextBlock
FontSize="14"
Text="{DynamicResource customeQueryShortcutTips}"
TextAlignment="Left"
TextWrapping="WrapWithOverflow" />
</StackPanel>
<StackPanel Margin="0,20,0,0" Orientation="Horizontal">
<Grid Width="470">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource customShortcut}" />
<TextBox
Grid.Row="0"
Grid.Column="1"
Margin="10"
Text="{Binding Key}"
/>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource customShortcutExpansion}" />
<DockPanel
Grid.Row="1"
Grid.Column="1"
LastChildFill="True">
<Button
x:Name="btnTestShortcut"
Margin="0,0,10,0"
Padding="10,5,10,5"
Click="BtnTestShortcut_OnClick"
Content="{DynamicResource preview}"
DockPanel.Dock="Right" />
<TextBox
x:Name="tbExpand"
Margin="10"
HorizontalAlignment="Stretch"
Text="{Binding Value}"
VerticalAlignment="Center" />
</DockPanel>
</Grid>
</StackPanel>
</StackPanel>
</StackPanel>
<Border
Grid.Row="1"
Margin="0,14,0,0"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0,1,0,0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
x:Name="btnCancel"
MinWidth="140"
Margin="10,0,5,0"
Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" />
<Button
x:Name="btnAdd"
MinWidth="140"
Margin="5,0,10,0"
Click="BtnAdd_OnClick"
Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
</Button>
</StackPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,73 @@
using Flow.Launcher.Core.Resource;
using Flow.Launcher.ViewModel;
using System;
using System.Windows;
using System.Windows.Input;
namespace Flow.Launcher
{
public partial class CustomShortcutSetting : Window
{
private SettingWindowViewModel viewModel;
public string Key { get; set; } = String.Empty;
public string Value { get; set; } = String.Empty;
private string originalKey { get; init; } = null;
private string originalValue { get; init; } = null;
private bool update { get; init; } = false;
public CustomShortcutSetting(SettingWindowViewModel vm)
{
viewModel = vm;
InitializeComponent();
}
public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm)
{
viewModel = vm;
Key = key;
Value = value;
originalKey = key;
originalValue = value;
update = true;
InitializeComponent();
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
{
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
return;
}
// Check if key is modified or adding a new one
if (((update && originalKey != Key) || !update)
&& viewModel.ShortcutExists(Key))
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
return;
}
DialogResult = !update || originalKey != Key || originalValue != Value;
Close();
}
private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e)
{
App.API.ChangeQuery(tbExpand.Text);
Application.Current.MainWindow.Show();
Application.Current.MainWindow.Opacity = 1;
Application.Current.MainWindow.Focus();
}
}
}

View file

@ -144,12 +144,18 @@
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
<system:String x:Key="customQueryHotkey">Custom Query Hotkey</system:String>
<system:String x:Key="customQueryShortcut">Custom Query Shortcut</system:String>
<system:String x:Key="customQuery">Query</system:String>
<system:String x:Key="customShortcut">Shortcut</system:String>
<system:String x:Key="customShortcutExpansion">Expanded</system:String>
<system:String x:Key="builtinShortcutDescription">Description</system:String>
<system:String x:Key="delete">Delete</system:String>
<system:String x:Key="edit">Edit</system:String>
<system:String x:Key="add">Add</system:String>
<system:String x:Key="pleaseSelectAnItem">Please select an item</system:String>
<system:String x:Key="deleteCustomHotkeyWarning">Are you sure you want to delete {0} plugin hotkey?</system:String>
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
<system:String x:Key="shortcut_clipboard_description">Get text from clipboard.</system:String>
<system:String x:Key="queryWindowShadowEffect">Query window shadow effect</system:String>
<system:String x:Key="shadowEffectCPUUsage">Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited.</system:String>
<system:String x:Key="windowWidthSize">Window Width Size</system:String>
@ -241,6 +247,12 @@
<system:String x:Key="invalidPluginHotkey">Invalid plugin hotkey</system:String>
<system:String x:Key="update">Update</system:String>
<!-- Custom Query Shortcut Dialog -->
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
<system:String x:Key="customeQueryShortcutTips">Enter a shortcut that automatically expands to the specified query.</system:String>
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
<!-- Hotkey Control -->
<system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>

View file

@ -300,7 +300,7 @@
<ContentControl>
<flowlauncher:ResultListBox x:Name="ResultListBox"
DataContext="{Binding Results}"
PreviewMouseDown="OnPreviewMouseButtonDown" />
PreviewMouseLeftButtonUp="OnPreviewMouseButtonDown" />
</ContentControl>
</Border>
<Border Style="{DynamicResource WindowRadius}">

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
@ -20,6 +20,11 @@ using Flow.Launcher.Infrastructure;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Plugin.SharedCommands;
using System.Text;
using DataObject = System.Windows.DataObject;
using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Windows.Threading;
using System.Windows.Data;

View file

@ -25,12 +25,15 @@
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard"
Visibility="{Binding Visbility}"
mc:Ignorable="d">
mc:Ignorable="d"
PreviewMouseMove="ResultList_MouseMove"
PreviewMouseLeftButtonDown="ResultList_PreviewMouseLeftButtonDown">
<!-- IsSynchronizedWithCurrentItem: http://stackoverflow.com/a/7833798/2833083 -->
<ListBox.ItemTemplate>
<DataTemplate>
<Button HorizontalAlignment="Stretch">
<Button
HorizontalAlignment="Stretch">
<Button.Template>
<ControlTemplate>
<ContentPresenter Content="{TemplateBinding Button.Content}" />
@ -84,7 +87,7 @@
x:Name="ImageIcon"
Width="{Binding IconXY}"
Height="{Binding IconXY}"
Margin="0,0,0,0"
Margin="0,0,0,0" IsHitTestVisible="False"
HorizontalAlignment="Center"
Source="{Binding Image, TargetNullValue={x:Null}}"
Stretch="Uniform"
@ -134,6 +137,7 @@
x:Name="Title"
VerticalAlignment="Center"
DockPanel.Dock="Left"
IsHitTestVisible="False"
Style="{DynamicResource ItemTitleStyle}"
Text="{Binding Result.Title}"
ToolTip="{Binding ShowTitleToolTip}">
@ -147,6 +151,7 @@
<TextBlock
x:Name="SubTitle"
Grid.Row="1"
IsHitTestVisible="False"
Style="{DynamicResource ItemSubTitleStyle}"
Text="{Binding Result.SubTitle}"
ToolTip="{Binding ShowSubTitleToolTip}" />

View file

@ -1,6 +1,9 @@
using System.Windows;
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Flow.Launcher.ViewModel;
namespace Flow.Launcher
{
@ -24,7 +27,7 @@ namespace Flow.Launcher
private void OnMouseEnter(object sender, MouseEventArgs e)
{
lock(_lock)
lock (_lock)
{
curItem = (ListBoxItem)sender;
var p = e.GetPosition((IInputElement)sender);
@ -34,7 +37,7 @@ namespace Flow.Launcher
private void OnMouseMove(object sender, MouseEventArgs e)
{
lock(_lock)
lock (_lock)
{
var p = e.GetPosition((IInputElement)sender);
if (_lastpos != p)
@ -46,7 +49,7 @@ namespace Flow.Launcher
private void ListBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
lock(_lock)
lock (_lock)
{
if (curItem != null)
{
@ -54,5 +57,51 @@ namespace Flow.Launcher
}
}
}
private Point start;
private string path;
private string query;
private void ResultList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (Mouse.DirectlyOver is not FrameworkElement { DataContext: ResultViewModel result })
return;
path = result.Result.CopyText;
query = result.Result.OriginQuery.RawQuery;
start = e.GetPosition(null);
}
private void ResultList_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
{
start = default;
path = string.Empty;
query = string.Empty;
return;
}
if (!File.Exists(path) && !Directory.Exists(path))
return;
Point mousePosition = e.GetPosition(null);
Vector diff = this.start - mousePosition;
if (Math.Abs(diff.X) < SystemParameters.MinimumHorizontalDragDistance
|| Math.Abs(diff.Y) < SystemParameters.MinimumVerticalDragDistance)
return;
var data = new DataObject(DataFormats.FileDrop, new[]
{
path
});
DragDrop.DoDragDrop((DependencyObject)sender, data, DragDropEffects.Move | DragDropEffects.Copy);
App.API.ChangeQuery(query, true);
e.Handled = true;
}
}
}

View file

@ -42,6 +42,7 @@
<converters:BorderClipConverter x:Key="BorderClipConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:TextConverter x:Key="TextConverter" />
<converters:TranlationConverter x:Key="TranlationConverter" />
<CollectionViewSource x:Key="SortedFonts" Source="{Binding Source={x:Static Fonts.SystemFontFamilies}}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Source" />
@ -2257,9 +2258,17 @@
<RowDefinition Height="43" />
<RowDefinition Height="80" />
<RowDefinition Height="146" />
<RowDefinition Height="50" />
<RowDefinition Height="*" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
<RowDefinition Height="*" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
@ -2364,6 +2373,7 @@
Text="{DynamicResource customQueryHotkey}" />
<ListView
Grid.Row="4"
MinHeight="160"
Margin="0,0,0,0"
Background="{DynamicResource Color02B}"
BorderBrush="DarkGray"
@ -2404,7 +2414,7 @@
<Button
MinWidth="100"
Margin="10"
Click="OnnEditCustomHotkeyClick"
Click="OnEditCustomHotkeyClick"
Content="{DynamicResource edit}" />
<Button
MinWidth="100"
@ -2412,6 +2422,104 @@
Click="OnAddCustomHotkeyClick"
Content="{DynamicResource add}" />
</StackPanel>
<TextBlock
Grid.Row="6"
Margin="0,0,12,2"
Padding="0,12,0,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="{DynamicResource Color05B}"
Text="{DynamicResource customQueryShortcut}" />
<ListView
Grid.Row="7"
Name="customShortcutView"
MinHeight="160"
Margin="0,6,0,0"
Background="{DynamicResource Color02B}"
BorderBrush="DarkGray"
BorderThickness="1"
ItemsSource="{Binding CustomShortcuts}"
SelectedItem="{Binding SelectedCustomShortcut}"
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
<ListView.View>
<GridView>
<GridViewColumn Width="180" Header="{DynamicResource customShortcut}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Key}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="540" Header="{DynamicResource customShortcutExpansion}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<StackPanel
Grid.Row="8"
Margin="0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Horizontal">
<Button
MinWidth="100"
Margin="10"
Click="OnDeleteCustomShortCutClick"
Content="{DynamicResource delete}" />
<Button
MinWidth="100"
Margin="10"
Click="OnEditCustomShortCutClick"
Content="{DynamicResource edit}" />
<Button
MinWidth="100"
Margin="10,10,0,10"
Click="OnAddCustomShortCutClick"
Content="{DynamicResource add}" />
</StackPanel>
<TextBlock
Grid.Row="9"
Margin="0,0,12,2"
Padding="0,12,0,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="{DynamicResource Color05B}"
Text="Built-in Shortcuts" />
<ListView
Grid.Row="10"
MinHeight="160"
Margin="0,6,0,0"
Background="{DynamicResource Color02B}"
BorderBrush="DarkGray"
BorderThickness="1"
ItemsSource="{Binding BuiltinShortcuts}"
Style="{StaticResource {x:Static GridView.GridViewStyleKey}}">
<ListView.View>
<GridView>
<GridViewColumn Width="180" Header="{DynamicResource customShortcut}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Key}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="540" Header="{DynamicResource builtinShortcutDescription}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description, Converter={StaticResource TranlationConverter}}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Border>
</ScrollViewer>

View file

@ -158,7 +158,7 @@ namespace Flow.Launcher
}
}
private void OnnEditCustomHotkeyClick(object sender, RoutedEventArgs e)
private void OnEditCustomHotkeyClick(object sender, RoutedEventArgs e)
{
var item = viewModel.SelectedCustomPluginHotkey;
if (item != null)
@ -385,11 +385,34 @@ namespace Flow.Launcher
restoreButton.Visibility = Visibility.Collapsed;
}
}
private void Window_StateChanged(object sender, EventArgs e)
{
RefreshMaximizeRestoreButton();
}
#region Shortcut
private void OnDeleteCustomShortCutClick(object sender, RoutedEventArgs e)
{
viewModel.DeleteSelectedCustomShortcut();
}
private void OnEditCustomShortCutClick(object sender, RoutedEventArgs e)
{
if (viewModel.EditSelectedCustomShortcut())
{
customShortcutView.Items.Refresh();
}
}
private void OnAddCustomShortCutClick(object sender, RoutedEventArgs e)
{
viewModel.AddCustomShortcut();
}
#endregion
private CollectionView pluginListView;
private CollectionView pluginStoreView;
@ -467,6 +490,7 @@ namespace Flow.Launcher
{
ClockDisplay();
}
public void ClockDisplay()
{
if (settings.UseClock)

View file

@ -17,6 +17,7 @@ using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Storage;
using Flow.Launcher.Infrastructure.Logger;
using Microsoft.VisualStudio.Threading;
using System.Text;
using System.Threading.Channels;
using ISavable = Flow.Launcher.Plugin.ISavable;
using System.IO;
@ -621,7 +622,9 @@ namespace Flow.Launcher.ViewModel
{
_updateSource?.Cancel();
if (string.IsNullOrWhiteSpace(QueryText))
var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts);
if (query == null) // shortcut expanded
{
Results.Clear();
Results.Visbility = Visibility.Collapsed;
@ -645,8 +648,7 @@ namespace Flow.Launcher.ViewModel
if (currentCancellationToken.IsCancellationRequested)
return;
var query = QueryBuilder.Build(QueryText, PluginManager.NonGlobalPlugins);
// handle the exclusiveness of plugin using action keyword
RemoveOldQueryResults(query);
@ -736,6 +738,40 @@ namespace Flow.Launcher.ViewModel
}
}
private Query ConstructQuery(string queryText, IEnumerable<CustomShortcutModel> customShortcuts, IEnumerable<BuiltinShortcutModel> builtInShortcuts)
{
if (string.IsNullOrWhiteSpace(queryText))
{
return null;
}
StringBuilder queryBuilder = new(queryText);
StringBuilder queryBuilderTmp = new(queryText);
foreach (var shortcut in customShortcuts)
{
if (queryBuilder.Equals(shortcut.Key))
{
queryBuilder.Replace(shortcut.Key, shortcut.Expand());
}
queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand());
}
foreach (var shortcut in builtInShortcuts)
{
queryBuilder.Replace(shortcut.Key, shortcut.Expand());
queryBuilderTmp.Replace(shortcut.Key, shortcut.Expand());
}
// show expanded builtin shortcuts
// use private field to avoid infinite recursion
_queryText = queryBuilderTmp.ToString();
var query = QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
return query;
}
private void RemoveOldQueryResults(Query query)
{
if (_lastQuery?.ActionKeyword != query?.ActionKeyword)
@ -966,28 +1002,26 @@ namespace Flow.Launcher.ViewModel
var result = Results.SelectedItem?.Result;
if (result != null)
{
string copyText = string.IsNullOrEmpty(result.CopyText) ? result.SubTitle : result.CopyText;
string copyText = result.CopyText;
var isFile = File.Exists(copyText);
var isFolder = Directory.Exists(copyText);
if (isFile || isFolder)
{
var paths = new StringCollection();
paths.Add(copyText);
var paths = new StringCollection
{
copyText
};
Clipboard.SetFileDropList(paths);
App.API.ShowMsg(
App.API.GetTranslation("copy")
+ " "
+ (isFile ? App.API.GetTranslation("fileTitle") : App.API.GetTranslation("folderTitle")),
$"{App.API.GetTranslation("copy")} {(isFile ? App.API.GetTranslation("fileTitle") : App.API.GetTranslation("folderTitle"))}",
App.API.GetTranslation("completedSuccessfully"));
}
else
{
Clipboard.SetDataObject(copyText.ToString());
Clipboard.SetDataObject(copyText);
App.API.ShowMsg(
App.API.GetTranslation("copy")
+ " "
+ App.API.GetTranslation("textTitle"),
$"{App.API.GetTranslation("copy")} {App.API.GetTranslation("textTitle")}",
App.API.GetTranslation("completedSuccessfully"));
}
}

View file

@ -19,6 +19,7 @@ using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using System.Collections.ObjectModel;
namespace Flow.Launcher.ViewModel
{
@ -212,11 +213,19 @@ namespace Flow.Launcher.ViewModel
}
}
public List<string> OpenResultModifiersList => new List<string> { KeyConstant.Alt, KeyConstant.Ctrl, $"{KeyConstant.Ctrl}+{KeyConstant.Alt}" };
public List<string> OpenResultModifiersList => new List<string>
{
KeyConstant.Alt,
KeyConstant.Ctrl,
$"{KeyConstant.Ctrl}+{KeyConstant.Alt}"
};
private Internationalization _translater => InternationalizationManager.Instance;
public List<Language> Languages => _translater.LoadAvailableLanguages();
public IEnumerable<int> MaxResultsRange => Enumerable.Range(2, 16);
public ObservableCollection<CustomShortcutModel> CustomShortcuts => Settings.CustomShortcuts;
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts => Settings.BuiltinShortcuts;
public string TestProxy()
{
var proxyServer = Settings.Proxy.Server;
@ -275,7 +284,10 @@ namespace Flow.Launcher.ViewModel
var metadatas = PluginManager.AllPlugins
.OrderBy(x => x.Metadata.Disabled)
.ThenBy(y => y.Metadata.Name)
.Select(p => new PluginViewModel { PluginPair = p })
.Select(p => new PluginViewModel
{
PluginPair = p
})
.ToList();
return metadatas;
}
@ -397,7 +409,11 @@ namespace Flow.Launcher.ViewModel
{
var key = $"ColorScheme{e}";
var display = _translater.GetTranslation(key);
var m = new ColorScheme { Display = display, Value = e, };
var m = new ColorScheme
{
Display = display,
Value = e,
};
modes.Add(m);
}
return modes;
@ -522,7 +538,10 @@ namespace Flow.Launcher.ViewModel
bitmap.DecodePixelWidth = 800;
bitmap.DecodePixelHeight = 600;
bitmap.EndInit();
var brush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill };
var brush = new ImageBrush(bitmap)
{
Stretch = Stretch.UniformToFill
};
return brush;
}
else
@ -550,19 +569,19 @@ namespace Flow.Launcher.ViewModel
{
Title = "WebSearch",
SubTitle = "Search the web with different search engine support",
IcoPath =Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png")
IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png")
},
new Result
{
Title = "Program",
SubTitle = "Launch programs as admin or a different user",
IcoPath =Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png")
IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png")
},
new Result
{
Title = "ProcessKiller",
SubTitle = "Terminate unwanted processes",
IcoPath =Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png")
IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png")
}
};
var vm = new ResultsViewModel(Settings);
@ -576,8 +595,8 @@ namespace Flow.Launcher.ViewModel
get
{
if (Fonts.SystemFontFamilies.Count(o =>
o.FamilyNames.Values != null &&
o.FamilyNames.Values.Contains(Settings.QueryBoxFont)) > 0)
o.FamilyNames.Values != null &&
o.FamilyNames.Values.Contains(Settings.QueryBoxFont)) > 0)
{
var font = new FontFamily(Settings.QueryBoxFont);
return font;
@ -604,7 +623,7 @@ namespace Flow.Launcher.ViewModel
Settings.QueryBoxFontStyle,
Settings.QueryBoxFontWeight,
Settings.QueryBoxFontStretch
));
));
return typeface;
}
set
@ -621,8 +640,8 @@ namespace Flow.Launcher.ViewModel
get
{
if (Fonts.SystemFontFamilies.Count(o =>
o.FamilyNames.Values != null &&
o.FamilyNames.Values.Contains(Settings.ResultFont)) > 0)
o.FamilyNames.Values != null &&
o.FamilyNames.Values.Contains(Settings.ResultFont)) > 0)
{
var font = new FontFamily(Settings.ResultFont);
return font;
@ -649,7 +668,7 @@ namespace Flow.Launcher.ViewModel
Settings.ResultFontStyle,
Settings.ResultFontWeight,
Settings.ResultFontStretch
));
));
return typeface;
}
set
@ -671,6 +690,65 @@ namespace Flow.Launcher.ViewModel
#endregion
#region shortcut
public CustomShortcutModel? SelectedCustomShortcut { get; set; }
public void DeleteSelectedCustomShortcut()
{
var item = SelectedCustomShortcut;
if (item == null)
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
return;
}
string deleteWarning = string.Format(
InternationalizationManager.Instance.GetTranslation("deleteCustomShortcutWarning"),
item?.Key, item?.Value);
if (MessageBox.Show(deleteWarning, InternationalizationManager.Instance.GetTranslation("delete"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
Settings.CustomShortcuts.Remove(item);
}
}
public bool EditSelectedCustomShortcut()
{
var item = SelectedCustomShortcut;
if (item == null)
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
return false;
}
var shortcutSettingWindow = new CustomShortcutSetting(item.Key, item.Value, this);
if (shortcutSettingWindow.ShowDialog() == true)
{
item.Key = shortcutSettingWindow.Key;
item.Value = shortcutSettingWindow.Value;
return true;
}
return false;
}
public void AddCustomShortcut()
{
var shortcutSettingWindow = new CustomShortcutSetting(this);
if (shortcutSettingWindow.ShowDialog() == true)
{
var shortcut = new CustomShortcutModel(shortcutSettingWindow.Key, shortcutSettingWindow.Value);
Settings.CustomShortcuts.Add(shortcut);
}
}
public bool ShortcutExists(string key)
{
return Settings.CustomShortcuts.Any(x => x.Key == key) || Settings.BuiltinShortcuts.Any(x => x.Key == key);
}
#endregion
#region about
public string Website => Constant.Website;

View file

@ -37,7 +37,7 @@ namespace Flow.Launcher.Plugin.PluginsManager
contextMenu = new ContextMenu(Context);
pluginManager = new PluginsManager(Context, Settings);
await pluginManager.UpdateManifestAsync();
_ = pluginManager.UpdateManifestAsync();
}
public List<Result> LoadContextMenus(Result selectedResult)
@ -74,4 +74,4 @@ namespace Flow.Launcher.Plugin.PluginsManager
return Context.API.GetTranslation("plugin_pluginsmanager_plugin_description");
}
}
}
}

View file

@ -43,6 +43,9 @@
<system:String x:Key="flowlauncher_plugin_program_suffixes_excutable_types">File Suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_URL_types">URL Protocols</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_URL_steam">Steam Games</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_URL_epic">Epic Games</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_URL_http">Http/Https</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_custom_urls">Custom URL Protocols</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_custom_file_types">Custom File Suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_tooltip">

View file

@ -189,9 +189,9 @@
FontSize="16"
FontWeight="SemiBold"
Text="{DynamicResource flowlauncher_plugin_program_suffixes_URL_types}" />
<CheckBox Name="steam" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[steam]}">Steam Games</CheckBox>
<CheckBox Name="epic" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[epic]}">Epic Games</CheckBox>
<CheckBox Name="http" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[http]}">Http/Https</CheckBox>
<CheckBox Name="steam" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[steam]}" Content="{DynamicResource flowlauncher_plugin_program_suffixes_URL_steam}"></CheckBox>
<CheckBox Name="epic" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[epic]}" Content="{DynamicResource flowlauncher_plugin_program_suffixes_URL_epic}"></CheckBox>
<CheckBox Name="http" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[http]}" Content="{DynamicResource flowlauncher_plugin_program_suffixes_URL_http}"></CheckBox>
<CheckBox
Name="CustomProtocol"
Margin="10,0,0,0"

View file

@ -390,7 +390,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
_ => Win32Program(x)
});
return programs;
}
@ -411,7 +410,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
ShortcutExtension => LnkProgram(x),
UrlExtension => UrlProgram(x),
_ => Win32Program(x)
}).Where(x => x.Valid);
});
return programs;
}
@ -569,7 +568,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
autoIndexPrograms = ProgramsHasher(autoIndexPrograms);
return programs.Concat(autoIndexPrograms).Distinct().ToArray();
return programs.Concat(autoIndexPrograms).Where(x => x.Valid).Distinct().ToArray();
}
#if DEBUG //This is to make developer aware of any unhandled exception and add in handling.
catch (Exception)

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using Windows.Foundation.Metadata;
namespace Flow.Launcher.Plugin.Program
{
@ -11,7 +12,10 @@ namespace Flow.Launcher.Plugin.Program
public DateTime LastIndexTime { get; set; }
public List<ProgramSource> ProgramSources { get; set; } = new List<ProgramSource>();
public List<DisabledProgramSource> DisabledProgramSources { get; set; } = new List<DisabledProgramSource>();
public string[] CustomSuffixes { get; set; } = Array.Empty<string>();
[Obsolete, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string[] ProgramSuffixes { get; set; } = null;
public string[] CustomSuffixes { get; set; } = Array.Empty<string>(); // Custom suffixes only
public string[] CustomProtocols { get; set; } = Array.Empty<string>();
public Dictionary<string, bool> BuiltinSuffixesStatus { get; set; } = new Dictionary<string, bool>{
@ -32,6 +36,7 @@ namespace Flow.Launcher.Plugin.Program
public string[] GetSuffixes()
{
RemoveRedundantSuffixes();
List<string> extensions = new List<string>();
foreach (var item in BuiltinSuffixesStatus)
{
@ -84,6 +89,24 @@ namespace Flow.Launcher.Plugin.Program
}
}
private void RemoveRedundantSuffixes()
{
// Migrate to new settings
// CustomSuffixes no longer contains custom suffixes
// users has tweaked the settings
// or this function has been executed once
if (UseCustomSuffixes == true || ProgramSuffixes == null)
return;
var suffixes = ProgramSuffixes.ToList();
foreach(var item in BuiltinSuffixesStatus)
{
suffixes.Remove(item.Key);
}
CustomSuffixes = suffixes.ToArray(); // Custom suffixes
UseCustomSuffixes = CustomSuffixes.Length != 0; // Search custom suffixes or not
ProgramSuffixes = null;
}
public bool EnableStartMenuSource { get; set; } = true;
public bool EnableDescription { get; set; } = false;
public bool HideAppsPath { get; set; } = true;