Merge branch 'dev' into ImagePreview

This commit is contained in:
VictoriousRaptor 2022-12-05 13:20:35 +08:00 committed by GitHub
commit e2a46ed8e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 4412 additions and 1239 deletions

View file

@ -183,6 +183,13 @@ namespace Flow.Launcher.Plugin
/// <param name="oldActionKeyword">The actionkeyword that is supposed to be removed</param>
void RemoveActionKeyword(string pluginId, string oldActionKeyword);
/// <summary>
/// Check whether specific ActionKeyword is assigned to any of the plugin
/// </summary>
/// <param name="actionKeyword">The actionkeyword for checking</param>
/// <returns>True if the actionkeyword is already assigned, False otherwise</returns>
bool ActionKeywordAssigned(string actionKeyword);
/// <summary>
/// Log debug message
/// Message will only be logged in Debug mode

View file

@ -7,6 +7,7 @@ using Flow.Launcher.Plugin.SharedCommands;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
@ -32,12 +33,11 @@ namespace Flow.Launcher.Test.Plugins
{
new Result
{
Title="Result 1"
Title = "Result 1"
},
new Result
{
Title="Result 2"
Title = "Result 2"
}
};
}
@ -46,15 +46,13 @@ namespace Flow.Launcher.Test.Plugins
private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false;
[SupportedOSPlatform("windows7.0")]
[TestCase("C:\\SomeFolder\\", "directory='file:C:\\SomeFolder\\'")]
public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestrictionsShouldUseDirectoryString(string path, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
// When
var folderPath = path;
var result = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(folderPath);
var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath);
// Then
Assert.IsTrue(result == expectedString,
@ -62,6 +60,7 @@ namespace Flow.Launcher.Test.Plugins
$"Actual: {result}{Environment.NewLine}");
}
[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")]
public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
@ -70,130 +69,68 @@ namespace Flow.Launcher.Test.Plugins
var queryConstructor = new QueryConstructor(new Settings());
//When
var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath);
var queryString = queryConstructor.Directory(folderPath);
// Then
Assert.IsTrue(queryString == expectedString,
Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
$"Expected string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {queryString}{Environment.NewLine}");
}
[TestCase("C:\\SomeFolder\\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 directory='file:C:\\SomeFolder' ORDER BY System.FileName")]
[SupportedOSPlatform("windows7.0")]
[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")]
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
string folderPath, string userSearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
//When
var queryString = queryConstructor.QueryForTopLevelDirectorySearch(userSearchString);
var queryString = queryConstructor.Directory(folderPath, userSearchString);
// Then
Assert.IsTrue(queryString == expectedString,
$"Expected string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {queryString}{Environment.NewLine}");
}
[TestCase("C:\\SomeFolder\\SomeApp", "(System.FileName LIKE 'SomeApp%' " +
"OR CONTAINS(System.FileName,'\"SomeApp*\"',1033))" +
" AND directory='file:C:\\SomeFolder'")]
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryWhereRestrictionsShouldUseDirectoryString(
string userSearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
//When
var queryString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(userSearchString);
// Then
Assert.IsTrue(queryString == expectedString,
$"Expected string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {queryString}{Environment.NewLine}");
Assert.AreEqual(expectedString, queryString);
}
[SupportedOSPlatform("windows7.0")]
[TestCase("scope='file:'")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString)
{
//When
var resultString = QueryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch;
const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch;
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
Assert.AreEqual(expectedString, resultString);
}
[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")]
"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")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
var baseQuery = queryConstructor.CreateBaseQuery();
var baseQuery = queryConstructor.BaseQueryHelper;
// system running this test could have different locale than the hard-coded 1033 LCID en-US.
var queryKeywordLocale = baseQuery.QueryKeywordLocale;
expectedString = expectedString.Replace("1033", queryKeywordLocale.ToString());
//When
var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);
var resultString = queryConstructor.FilesAndFolders(userSearchString);
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected query string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
Assert.AreEqual(expectedString, resultString);
}
[TestCase]
public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearchAsync()
{
// Given
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
// When
var results = await searchManager.TopLevelDirectorySearchBehaviourAsync(
MethodWindowsIndexSearchReturnsZeroResultsAsync,
MethodDirectoryInfoClassSearchReturnsTwoResults,
false,
new Query(),
"string not used",
default);
// Then
Assert.IsTrue(results.Count == 2,
$"Expected to have 2 results from DirectoryInfoClassSearch {Environment.NewLine} " +
$"Actual number of results is {results.Count} {Environment.NewLine}");
}
[TestCase]
public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearchAsync()
{
// Given
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
// When
var results = await searchManager.TopLevelDirectorySearchBehaviourAsync(
MethodWindowsIndexSearchReturnsZeroResultsAsync,
MethodDirectoryInfoClassSearchReturnsTwoResults,
true,
new Query(),
"string not used",
default);
// Then
Assert.IsTrue(results.Count == 0,
$"Expected to have 0 results because location is indexed {Environment.NewLine} " +
$"Actual number of results is {results.Count} {Environment.NewLine}");
}
[SupportedOSPlatform("windows7.0")]
[TestCase(@"some words", @"FREETEXT('some words')")]
public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString(
string querySearchString, string expectedString)
@ -202,7 +139,7 @@ namespace Flow.Launcher.Test.Plugins
var queryConstructor = new QueryConstructor(new Settings());
//When
var resultString = queryConstructor.QueryWhereRestrictionsForFileContentSearch(querySearchString);
var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString);
// Then
Assert.IsTrue(resultString == expectedString,
@ -210,8 +147,9 @@ namespace Flow.Launcher.Test.Plugins
$"Actual string was: {resultString}{Environment.NewLine}");
}
[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 System.FileName")]
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
@ -219,7 +157,7 @@ namespace Flow.Launcher.Test.Plugins
var queryConstructor = new QueryConstructor(new Settings());
//When
var resultString = queryConstructor.QueryForFileContentSearch(userSearchString);
var resultString = queryConstructor.FileContent(userSearchString);
// Then
Assert.IsTrue(resultString == expectedString,
@ -230,12 +168,15 @@ namespace Flow.Launcher.Test.Plugins
public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
{
// Given
var query = new Query { ActionKeyword = "doc:", Search = "search term" };
var query = new Query
{
ActionKeyword = "doc:", Search = "search term"
};
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
// When
var result = searchManager.IsFileContentSearch(query.ActionKeyword);
var result = SearchManager.IsFileContentSearch(query.ActionKeyword);
// Then
Assert.IsTrue(result,
@ -303,24 +244,19 @@ namespace Flow.Launcher.Test.Plugins
$"Actual path string is {returnedPath} {Environment.NewLine}");
}
[TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")]
[TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' "
+ "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND "
+ "scope='file:c:\\SomeFolder'")]
public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString)
[SupportedOSPlatform("windows7.0")]
[TestCase("c:\\SomeFolder", "scope='file:c:\\SomeFolder'")]
[TestCase("c:\\OtherFolder", "scope='file:c:\\OtherFolder'")]
public void GivenFilePath_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
//When
var resultString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path);
var resultString = QueryConstructor.RecursiveDirectoryConstraint(path);
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
Assert.AreEqual(expectedString, resultString);
}
[SupportedOSPlatform("windows7.0")]
[TestCase("c:\\somefolder\\>somefile", "*somefile*")]
[TestCase("c:\\somefolder\\somefile", "somefile*")]
[TestCase("c:\\somefolder\\", "*")]
@ -331,9 +267,7 @@ namespace Flow.Launcher.Test.Plugins
var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path);
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected criteria string: {expectedString}{Environment.NewLine} " +
$"Actual criteria string was: {resultString}{Environment.NewLine}");
Assert.AreEqual(expectedString, resultString);
}
}
}

View file

@ -8,24 +8,17 @@ using Flow.Launcher.ViewModel;
namespace Flow.Launcher
{
public partial class ActionKeywords : Window
public partial class ActionKeywords
{
private readonly PluginPair plugin;
private Settings settings;
private readonly Internationalization translater = InternationalizationManager.Instance;
private readonly PluginViewModel pluginViewModel;
public ActionKeywords(string pluginId, Settings settings, PluginViewModel pluginViewModel)
public ActionKeywords(PluginViewModel pluginViewModel)
{
InitializeComponent();
plugin = PluginManager.GetPluginForId(pluginId);
this.settings = settings;
plugin = pluginViewModel.PluginPair;
this.pluginViewModel = pluginViewModel;
if (plugin == null)
{
MessageBox.Show(translater.GetTranslation("cannotFindSpecifiedPlugin"));
Close();
}
}
private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e)

View file

@ -183,7 +183,7 @@ namespace Flow.Launcher
public void OnSecondAppStarted()
{
Current.MainWindow.Show();
_mainVM.Show();
}
}
}

View file

@ -610,7 +610,7 @@ namespace Flow.Launcher
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{
var queryWithoutActionKeyword =
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins).Search;
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search;
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
{

View file

@ -1,6 +1,5 @@
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
using System;
@ -23,14 +22,12 @@ namespace Flow.Launcher
public partial class PriorityChangeWindow : Window
{
private readonly PluginPair plugin;
private Settings settings;
private readonly Internationalization translater = InternationalizationManager.Instance;
private readonly PluginViewModel pluginViewModel;
public PriorityChangeWindow(string pluginId, Settings settings, PluginViewModel pluginViewModel)
public PriorityChangeWindow(string pluginId, PluginViewModel pluginViewModel)
{
InitializeComponent();
plugin = PluginManager.GetPluginForId(pluginId);
this.settings = settings;
this.pluginViewModel = pluginViewModel;
if (plugin == null)
{
@ -74,4 +71,4 @@ namespace Flow.Launcher
}
}
}
}
}

View file

@ -141,6 +141,8 @@ namespace Flow.Launcher
public void AddActionKeyword(string pluginId, string newActionKeyword) =>
PluginManager.AddActionKeyword(pluginId, newActionKeyword);
public bool ActionKeywordAssigned(string actionKeyword) => PluginManager.ActionKeywordRegistered(actionKeyword);
public void RemoveActionKeyword(string pluginId, string oldActionKeyword) =>
PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);

View file

@ -1143,7 +1143,7 @@
x:Name="PriorityButton"
Margin="0,0,22,0"
VerticalAlignment="Center"
Click="OnPluginPriorityClick"
Command="{Binding EditPluginPriorityCommand}"
Content="{Binding Priority, UpdateSourceTrigger=PropertyChanged}"
Cursor="Hand"
ToolTip="{DynamicResource priorityToolTip}">
@ -1221,8 +1221,8 @@
Height="34"
Margin="5,0,0,0"
HorizontalAlignment="Right"
Click="OnPluginActionKeywordsClick"
Content="{Binding ActionKeywordsText}"
Command="{Binding SetActionKeywordsCommand}"
Cursor="Hand"
DockPanel.Dock="Right"
FontWeight="Bold"
@ -1371,9 +1371,14 @@
Cursor="Hand"
FontSize="11"
Foreground="{DynamicResource PluginInfoColor}"
MouseUp="OnPluginDirecotyClick"
Text="{DynamicResource pluginDirectory}"
TextDecorations="Underline" />
TextDecorations="Underline" >
<TextBlock.InputBindings>
<MouseBinding
Command="{Binding OpenPluginDirectoryCommand}"
MouseAction="LeftClick"/>
</TextBlock.InputBindings>
</TextBlock>
</StackPanel>
</ItemsControl>

View file

@ -5,7 +5,6 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel;
using ModernWpf;
using ModernWpf.Controls;
@ -18,7 +17,6 @@ using System.Windows.Data;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Navigation;
using Button = System.Windows.Controls.Button;
using Control = System.Windows.Controls.Control;
@ -189,44 +187,11 @@ namespace Flow.Launcher
{
if (sender is Control { DataContext: PluginViewModel pluginViewModel })
{
PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(pluginViewModel.PluginPair.Metadata.ID, settings, pluginViewModel);
PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(pluginViewModel.PluginPair.Metadata.ID, pluginViewModel);
priorityChangeWindow.ShowDialog();
}
}
private void OnPluginActionKeywordsClick(object sender, RoutedEventArgs e)
{
var id = viewModel.SelectedPlugin.PluginPair.Metadata.ID;
ActionKeywords changeKeywordsWindow = new ActionKeywords(id, settings, viewModel.SelectedPlugin);
changeKeywordsWindow.ShowDialog();
}
private void OnPluginNameClick(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
var website = viewModel.SelectedPlugin.PluginPair.Metadata.Website;
if (!string.IsNullOrEmpty(website))
{
var uri = new Uri(website);
if (Uri.CheckSchemeName(uri.Scheme))
{
website.OpenInBrowserTab();
}
}
}
}
private void OnPluginDirecotyClick(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
var directory = viewModel.SelectedPlugin.PluginPair.Metadata.PluginDirectory;
if (!string.IsNullOrEmpty(directory))
PluginManager.API.OpenDirectory(directory);
}
}
#endregion
#region Proxy
@ -297,22 +262,6 @@ namespace Flow.Launcher
}
}
private static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
//get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
return parent;
else
return FindParent<T>(parentObject);
}
private void OnExternalPluginInstallClick(object sender, RoutedEventArgs e)
{
if (sender is not Button { DataContext: PluginStoreItemViewModel plugin } button)
@ -335,8 +284,6 @@ namespace Flow.Launcher
var name = viewModel.SelectedPlugin.PluginPair.Metadata.Name;
viewModel.DisplayPluginQuery($"uninstall {name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"));
}
}
private void OnExternalPluginUninstallClick(object sender, RoutedEventArgs e)

View file

@ -5,10 +5,11 @@ using Flow.Launcher.Plugin;
using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Core.Plugin;
using System.Windows.Controls;
using CommunityToolkit.Mvvm.Input;
namespace Flow.Launcher.ViewModel
{
public class PluginViewModel : BaseModel
public partial class PluginViewModel : BaseModel
{
private readonly PluginPair _pluginPair;
public PluginPair PluginPair
@ -53,6 +54,7 @@ namespace Flow.Launcher.ViewModel
set
{
_isExpanded = value;
OnPropertyChanged();
OnPropertyChanged(nameof(SettingControl));
}
@ -60,12 +62,12 @@ namespace Flow.Launcher.ViewModel
private Control _settingControl;
private bool _isExpanded;
public Control SettingControl
public Control SettingControl
=> IsExpanded
? _settingControl
??= PluginPair.Plugin is not ISettingProvider settingProvider
? new Control()
: settingProvider.CreateSettingPanel()
? new Control()
: settingProvider.CreateSettingPanel()
: null;
private ImageSource _image = ImageLoader.MissingImage;
@ -87,7 +89,29 @@ namespace Flow.Launcher.ViewModel
OnPropertyChanged(nameof(Priority));
}
[RelayCommand]
private void EditPluginPriority()
{
PriorityChangeWindow priorityChangeWindow = new PriorityChangeWindow(PluginPair.Metadata.ID, this);
priorityChangeWindow.ShowDialog();
}
[RelayCommand]
private void OpenPluginDirectory()
{
var directory = PluginPair.Metadata.PluginDirectory;
if (!string.IsNullOrEmpty(directory))
PluginManager.API.OpenDirectory(directory);
}
public static bool IsActionKeywordRegistered(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword);
[RelayCommand]
private void SetActionKeywords()
{
ActionKeywords changeKeywordsWindow = new ActionKeywords(this);
changeKeywordsWindow.ShowDialog();
}
}
}

View file

@ -356,6 +356,8 @@ namespace Flow.Launcher.ViewModel
await PluginsManifest.UpdateManifestAsync();
OnPropertyChanged(nameof(ExternalPlugins));
}
internal void DisplayPluginQuery(string queryToDisplay, PluginPair plugin, int actionKeywordPosition = 0)
{

View file

@ -9,6 +9,8 @@ using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Input;
using MessageBox = System.Windows.Forms.MessageBox;
using MessageBoxIcon = System.Windows.Forms.MessageBoxIcon;
using MessageBoxButton = System.Windows.Forms.MessageBoxButtons;
@ -37,25 +39,25 @@ namespace Flow.Launcher.Plugin.Explorer
var contextMenus = new List<Result>();
if (selectedResult.ContextData is SearchResult record)
{
if (record.Type == ResultType.File)
if (record.Type == ResultType.File && !string.IsNullOrEmpty(Settings.EditorPath))
contextMenus.Add(CreateOpenWithEditorResult(record));
if (record.Type == ResultType.Folder && record.WindowsIndexed)
{
contextMenus.Add(CreateAddToIndexSearchExclusionListResult(record));
contextMenus.Add(CreateOpenWithShellResult(record));
}
contextMenus.Add(CreateOpenContainingFolderResult(record));
contextMenus.Add(CreateOpenWindowsIndexingOptions());
if (record.ShowIndexState)
contextMenus.Add(new Result {Title = "From index search: " + (record.WindowsIndexed ? "Yes" : "No"),
SubTitle = "Location: " + record.FullPath,
Score = 501, IcoPath = Constants.IndexImagePath});
if (record.WindowsIndexed)
{
contextMenus.Add(CreateOpenWindowsIndexingOptions());
}
var icoPath = (record.Type == ResultType.File) ? Constants.FileImagePath : Constants.FolderImagePath;
var fileOrFolder = (record.Type == ResultType.File) ? "file" : "folder";
if (!Settings.QuickAccessLinks.Any(x => x.Path == record.FullPath))
if (Settings.QuickAccessLinks.All(x => x.Path != record.FullPath))
{
contextMenus.Add(new Result
{
@ -63,13 +65,16 @@ namespace Flow.Launcher.Plugin.Explorer
SubTitle = string.Format(Context.API.GetTranslation("plugin_explorer_add_to_quickaccess_subtitle"), fileOrFolder),
Action = (context) =>
{
Settings.QuickAccessLinks.Add(new AccessLink { Path = record.FullPath, Type = record.Type });
Settings.QuickAccessLinks.Add(new AccessLink
{
Path = record.FullPath, Type = record.Type
});
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess"),
string.Format(
Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess_detail"),
fileOrFolder),
Constants.ExplorerIconImageFullPath);
string.Format(
Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess_detail"),
fileOrFolder),
Constants.ExplorerIconImageFullPath);
ViewModel.Save();
@ -91,10 +96,10 @@ namespace Flow.Launcher.Plugin.Explorer
Settings.QuickAccessLinks.Remove(Settings.QuickAccessLinks.FirstOrDefault(x => x.Path == record.FullPath));
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess"),
string.Format(
Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess_detail"),
fileOrFolder),
Constants.ExplorerIconImageFullPath);
string.Format(
Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess_detail"),
fileOrFolder),
Constants.ExplorerIconImageFullPath);
ViewModel.Save();
@ -105,12 +110,12 @@ namespace Flow.Launcher.Plugin.Explorer
IcoPath = Constants.RemoveQuickAccessImagePath
});
}
contextMenus.Add(new Result
{
Title = Context.API.GetTranslation("plugin_explorer_copypath"),
SubTitle = $"Copy the current {fileOrFolder} path to clipboard",
Action = (context) =>
Action = _ =>
{
try
{
@ -132,11 +137,14 @@ namespace Flow.Launcher.Plugin.Explorer
{
Title = Context.API.GetTranslation("plugin_explorer_copyfilefolder") + $" {fileOrFolder}",
SubTitle = $"Copy the {fileOrFolder} to clipboard",
Action = (context) =>
Action = _ =>
{
try
{
Clipboard.SetFileDropList(new System.Collections.Specialized.StringCollection { record.FullPath });
Clipboard.SetFileDropList(new System.Collections.Specialized.StringCollection
{
record.FullPath
});
return true;
}
catch (Exception e)
@ -151,7 +159,8 @@ namespace Flow.Launcher.Plugin.Explorer
IcoPath = icoPath
});
if (record.Type == ResultType.File || record.Type == ResultType.Folder)
if (record.Type is ResultType.File or ResultType.Folder)
contextMenus.Add(new Result
{
Title = Context.API.GetTranslation("plugin_explorer_deletefilefolder") + $" {fileOrFolder}",
@ -161,10 +170,10 @@ namespace Flow.Launcher.Plugin.Explorer
try
{
if (MessageBox.Show(
string.Format(Context.API.GetTranslation("plugin_explorer_deletefilefolderconfirm"),fileOrFolder),
string.Empty,
MessageBoxButton.YesNo,
MessageBoxIcon.Warning)
string.Format(Context.API.GetTranslation("plugin_explorer_deletefilefolderconfirm"), fileOrFolder),
string.Empty,
MessageBoxButton.YesNo,
MessageBoxIcon.Warning)
== DialogResult.No)
return false;
@ -173,11 +182,11 @@ namespace Flow.Launcher.Plugin.Explorer
else
Directory.Delete(record.FullPath, true);
Task.Run(() =>
_ = Task.Run(() =>
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_deletefilefoldersuccess"),
string.Format(Context.API.GetTranslation("plugin_explorer_deletefilefoldersuccess_detail"), fileOrFolder),
Constants.ExplorerIconImageFullPath);
string.Format(Context.API.GetTranslation("plugin_explorer_deletefilefoldersuccess_detail"), fileOrFolder),
Constants.ExplorerIconImageFullPath);
});
}
catch (Exception e)
@ -193,6 +202,52 @@ namespace Flow.Launcher.Plugin.Explorer
IcoPath = Constants.DeleteFileFolderImagePath
});
if (record.Type is not ResultType.Volume)
{
contextMenus.Add(new Result()
{
Title = Context.API.GetTranslation("plugin_explorer_show_contextmenu_title"),
IcoPath = Constants.ShowContextMenuImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue700"),
Action = _ =>
{
if (record.Type is ResultType.Volume)
return false;
var screenWithMouseCursor = System.Windows.Forms.Screen.FromPoint(System.Windows.Forms.Cursor.Position);
var xOfScreenCenter = screenWithMouseCursor.WorkingArea.Left + screenWithMouseCursor.WorkingArea.Width / 2;
var yOfScreenCenter = screenWithMouseCursor.WorkingArea.Top + screenWithMouseCursor.WorkingArea.Height / 2;
var showPosition = new System.Drawing.Point(xOfScreenCenter, yOfScreenCenter);
switch (record.Type)
{
case ResultType.File:
{
var fileInfos = new FileInfo[]
{
new(record.FullPath)
};
new Peter.ShellContextMenu().ShowContextMenu(fileInfos, showPosition);
break;
}
case ResultType.Folder:
{
var directoryInfos = new DirectoryInfo[]
{
new(record.FullPath)
};
new Peter.ShellContextMenu().ShowContextMenu(directoryInfos, showPosition);
break;
}
}
return false;
},
});
}
if (record.Type == ResultType.File && CanRunAsDifferentUser(record.FullPath))
contextMenus.Add(new Result
{
@ -246,12 +301,13 @@ namespace Flow.Launcher.Plugin.Explorer
};
}
private Result CreateOpenWithEditorResult(SearchResult record)
{
string editorPath = "Notepad.exe"; // TODO add the ability to create a custom editor
string editorPath = Settings.EditorPath;
var name = Context.API.GetTranslation("plugin_explorer_openwitheditor")
+ " " + Path.GetFileNameWithoutExtension(editorPath);
var name = $"{Context.API.GetTranslation("plugin_explorer_openwitheditor")} {Path.GetFileNameWithoutExtension(editorPath)}";
return new Result
{
@ -265,7 +321,38 @@ namespace Flow.Launcher.Plugin.Explorer
}
catch (Exception e)
{
var message = $"Failed to open editor for file at {record.FullPath}";
var message = $"Failed to open editor for file at {record.FullPath} with Editor {Path.GetFileNameWithoutExtension(editorPath)} at {editorPath}";
LogException(message, e);
Context.API.ShowMsgError(message);
return false;
}
},
IcoPath = Constants.FileImagePath
};
}
private Result CreateOpenWithShellResult(SearchResult record)
{
string shellPath = Settings.ShellPath;
var name = $"{Context.API.GetTranslation("plugin_explorer_openwithshell")} {Path.GetFileNameWithoutExtension(shellPath)}";
return new Result
{
Title = name,
Action = _ =>
{
try
{
Process.Start(new ProcessStartInfo()
{
FileName = shellPath, WorkingDirectory = record.FullPath
});
return true;
}
catch (Exception e)
{
var message = $"Failed to open editor for file at {record.FullPath} with Shell {Path.GetFileNameWithoutExtension(shellPath)} at {shellPath}";
LogException(message, e);
Context.API.ShowMsgError(message);
return false;
@ -283,14 +370,17 @@ namespace Flow.Launcher.Plugin.Explorer
SubTitle = Context.API.GetTranslation("plugin_explorer_path") + " " + record.FullPath,
Action = _ =>
{
if(!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == record.FullPath))
Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink { Path = record.FullPath });
if (!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == record.FullPath))
Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink
{
Path = record.FullPath
});
Task.Run(() =>
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_excludedfromindexsearch_msg"),
Context.API.GetTranslation("plugin_explorer_path") +
" " + record.FullPath, Constants.ExplorerIconImageFullPath);
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_excludedfromindexsearch_msg"),
Context.API.GetTranslation("plugin_explorer_path") +
" " + record.FullPath, Constants.ExplorerIconImageFullPath);
// so the new path can be persisted to storage and not wait till next ViewModel save.
Context.API.SaveAppAllSettings();
@ -313,11 +403,11 @@ namespace Flow.Launcher.Plugin.Explorer
try
{
var psi = new ProcessStartInfo
{
FileName = "control.exe",
UseShellExecute = true,
Arguments = "srchadmin.dll"
};
{
FileName = "control.exe",
UseShellExecute = true,
Arguments = "srchadmin.dll"
};
Process.Start(psi);
return true;

View file

@ -0,0 +1,48 @@
#nullable enable
using System;
using System.Threading.Tasks;
using System.Windows;
using Flow.Launcher.Plugin.Explorer.Search.IProvider;
using JetBrains.Annotations;
namespace Flow.Launcher.Plugin.Explorer.Exceptions;
public class EngineNotAvailableException : Exception
{
public string EngineName { get; }
public string Resolution { get; }
public Func<ActionContext, ValueTask<bool>>? Action { get; }
public string? ErrorIcon { get; init; }
public EngineNotAvailableException(
string engineName,
string resolution,
string message,
Func<ActionContext, ValueTask<bool>> action = null) : base(message)
{
EngineName = engineName;
Resolution = resolution;
Action = action ?? (_ =>
{
Clipboard.SetDataObject(this.ToString());
return ValueTask.FromResult(true);
});
}
public EngineNotAvailableException(
string engineName,
string resolution,
string message,
Exception innerException) : base(message, innerException)
{
EngineName = engineName;
Resolution = resolution;
}
public override string ToString()
{
return $"Engine {EngineName} is not available.\n Try to {Resolution}\n {base.ToString()}";
}
}

View file

@ -0,0 +1,23 @@
using System;
namespace Flow.Launcher.Plugin.Explorer.Exceptions
{
public class SearchException : Exception
{
public string EngineName { get; }
public SearchException(string engineName, string message) : base(message)
{
EngineName = engineName;
}
public SearchException(string engineName, string message, Exception innerException) : base(message, innerException)
{
EngineName = engineName;
}
public override string ToString()
{
return $"{EngineName} Search Exception:\n {base.ToString()}";
}
}
}

View file

@ -8,6 +8,7 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Nullable>warnings</Nullable>
<ApplicationIcon />
<StartupObject />
</PropertyGroup>
@ -24,6 +25,12 @@
<None Include="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="EverythingSDK\x64\Everything.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="EverythingSDK\x86\Everything.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
@ -39,6 +46,7 @@
<ItemGroup>
<PackageReference Include="System.Data.OleDb" Version="5.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="tlbimp-Microsoft.Search.Interop" Version="1.0.0" />
</ItemGroup>
@ -47,5 +55,5 @@
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
using Flow.Launcher.Plugin.Everything.Everything;
using JetBrains.Annotations;
using System;
namespace Flow.Launcher.Plugin.Explorer.Helper;
public static class SortOptionTranslationHelper
{
[CanBeNull]
public static IPublicAPI API { get; internal set; }
public static string GetTranslatedName(this SortOption sortOption)
{
const string prefix = "flowlauncher_plugin_everything_sort_by_";
ArgumentNullException.ThrowIfNull(API);
var enumName = Enum.GetName(sortOption);
var splited = enumName.Split('_');
var name = string.Join('_', splited[..^1]);
var direction = splited[^1];
return $"{API.GetTranslation(prefix + name.ToLower())} {API.GetTranslation(prefix + direction.ToLower())}";
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -1,8 +1,8 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<!--Dialogues-->
<!-- Dialogues -->
<system:String x:Key="plugin_explorer_make_selection_warning">Please make a selection first</system:String>
<system:String x:Key="plugin_explorer_select_folder_link_warning">Please select a folder link</system:String>
<system:String x:Key="plugin_explorer_delete_folder_link">Are you sure you want to delete {0}?</system:String>
@ -17,15 +17,22 @@
<system:String x:Key="plugin_explorer_alternative_title">Explorer Alternative</system:String>
<system:String x:Key="plugin_explorer_directoryinfosearch_error">Error occurred during search: {0}</system:String>
<!--Controls-->
<!-- Controls -->
<system:String x:Key="plugin_explorer_delete">Delete</system:String>
<system:String x:Key="plugin_explorer_edit">Edit</system:String>
<system:String x:Key="plugin_explorer_add">Add</system:String>
<system:String x:Key="plugin_explorer_generalsetting_header">General Setting</system:String>
<system:String x:Key="plugin_explorer_manageactionkeywords_header">Customise Action Keywords</system:String>
<system:String x:Key="plugin_explorer_quickaccesslinks_header">Quick Access Links</system:String>
<system:String x:Key="plugin_explorer_everything_setting_header">Everything Setting</system:String>
<system:String x:Key="plugin_explorer_everything_sort_option">Sort Option:</system:String>
<system:String x:Key="plugin_explorer_everything_installed_path">Everything Path:</system:String>
<system:String x:Key="plugin_explorer_launch_hidden">Launch Hidden</system:String>
<system:String x:Key="plugin_explorer_editor_path">Editor Path</system:String>
<system:String x:Key="plugin_explorer_shell_path">Shell Path</system:String>
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">Index Search Excluded Paths</system:String>
<system:String x:Key="plugin_explorer_use_location_as_working_dir">Use search result's location as executable working directory</system:String>
<system:String x:Key="plugin_explorer_usewindowsindexfordirectorysearch">Use Index Search For Path Search</system:String>
<system:String x:Key="plugin_explorer_usewindowsindexfordirectorysearch_tooltip">Turning this on will return indexed directories/files faster, but if a directory/file is not indexed it will not show up. If a directory/file has been added to Index Search Excluded Path then it will still show up even if this option is on</system:String>
<system:String x:Key="plugin_explorer_manageindexoptions">Indexing Options</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_search">Search:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_pathsearch">Path Search:</system:String>
@ -36,12 +43,24 @@
<system:String x:Key="plugin_explorer_actionkeyword_done">Done</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_enabled">Enabled</system:String>
<system:String x:Key="plugin_explorer_actionkeyword_enabled_tooltip">When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword</system:String>
<system:String x:Key="plugin_explorer_engine_everything">Everything</system:String>
<system:String x:Key="plugin_explorer_engine_windows_index">Windows Index</system:String>
<system:String x:Key="plugin_explorer_path_enumeration_engine_none">Direct Enumeration</system:String>
<!--Plugin Infos-->
<system:String x:Key="plugin_explorer_Content_Search_Engine">Content Search Engine</system:String>
<system:String x:Key="plugin_explorer_Directory_Recursive_Search_Engine">Directory Recursive Search Engine</system:String>
<system:String x:Key="plugin_explorer_Index_Search_Engine">Index Search Engine</system:String>
<system:String x:Key="plugin_explorer_Open_Window_Index_Option">Open Window Index Option</system:String>
<!-- Plugin Infos -->
<system:String x:Key="plugin_explorer_plugin_name">Explorer</system:String>
<system:String x:Key="plugin_explorer_plugin_description">Search and manage files and folders. Explorer utilises Windows Index Search</system:String>
<!--Context menu items-->
<!-- Plugin Tooltip -->
<system:String x:Key="plugin_explorer_plugin_ToolTipOpenDirectory">Ctrl + Enter to open the directory</system:String>
<system:String x:Key="plugin_explorer_plugin_ToolTipOpenContainingFolder">Ctrl + Enter to open the containing folder</system:String>
<!-- Context menu items -->
<system:String x:Key="plugin_explorer_copypath">Copy path</system:String>
<system:String x:Key="plugin_explorer_copyfilefolder">Copy</system:String>
<system:String x:Key="plugin_explorer_deletefilefolder">Delete</system:String>
@ -52,6 +71,7 @@
<system:String x:Key="plugin_explorer_opencontainingfolder">Open containing folder</system:String>
<system:String x:Key="plugin_explorer_opencontainingfolder_subtitle">Opens the location that contains the file or folder</system:String>
<system:String x:Key="plugin_explorer_openwitheditor">Open With Editor:</system:String>
<system:String x:Key="plugin_explorer_openwithshell">Open With Shell:</system:String>
<system:String x:Key="plugin_explorer_excludefromindexsearch">Exclude current and sub-directories from Index Search</system:String>
<system:String x:Key="plugin_explorer_excludedfromindexsearch_msg">Excluded from Index Search</system:String>
<system:String x:Key="plugin_explorer_openindexingoptions">Open Windows Indexing Options</system:String>
@ -67,5 +87,36 @@
<system:String x:Key="plugin_explorer_contextmenu_remove_titletooltip">Remove from Quick Access</system:String>
<system:String x:Key="plugin_explorer_remove_from_quickaccess_title">Remove from Quick Access</system:String>
<system:String x:Key="plugin_explorer_remove_from_quickaccess_subtitle">Remove the current {0} from Quick Access</system:String>
<system:String x:Key="plugin_explorer_show_contextmenu_title">Show Windows Context Menu</system:String>
<!-- Everything -->
<system:String x:Key="flowlauncher_plugin_everything_sdk_issue">Everything SDK Loaded Fail</system:String>
<system:String x:Key="flowlauncher_plugin_everything_is_not_running">Warning: Everything service is not running</system:String>
<system:String x:Key="flowlauncher_plugin_everything_query_error">Error while querying Everything</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by">Sort By</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_name">Name</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_path">Path</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_size">Size</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_extension">Extension</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_type_name">Type Name</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_date_created">Date Created</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_date_modified">Date Modified</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_attributes">Attributes</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_file_list_filename">File List FileName</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_run_count">Run Count</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_date_recently_changed">Date Recently Changed</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_date_accessed">Date Accessed</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_date_run">Date Run</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_ascending">&#x2191;</system:String>
<system:String x:Key="flowlauncher_plugin_everything_sort_by_descending">&#x2193;</system:String>
<system:String x:Key="flowlauncher_plugin_everything_nonfastsort_warning">Warning: This is not a Fast Sort option, searches may be slow</system:String>
<system:String x:Key="flowlauncher_plugin_everything_click_to_launch_or_install">Click to Launch or Install Everything</system:String>
<system:String x:Key="flowlauncher_plugin_everything_installing_title">Everything Installation</system:String>
<system:String x:Key="flowlauncher_plugin_everything_installing_subtitle">Installing Everything service. Please wait...</system:String>
<system:String x:Key="flowlauncher_plugin_everything_installationsuccess_subtitle">Successfully installed Everything service</system:String>
<system:String x:Key="flowlauncher_plugin_everything_installationfailed_subtitle">Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com</system:String>
<system:String x:Key="flowlauncher_plugin_everything_run_service">Click here to start it</system:String>
<system:String x:Key="flowlauncher_plugin_everything_installing_select">Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you</system:String>
</ResourceDictionary>

View file

@ -1,19 +1,22 @@
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using Flow.Launcher.Plugin.Explorer.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using Flow.Launcher.Plugin.Explorer.Exceptions;
namespace Flow.Launcher.Plugin.Explorer
{
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n
{
internal PluginInitContext Context { get; set; }
internal static PluginInitContext Context { get; set; }
internal Settings Settings;
@ -31,23 +34,19 @@ namespace Flow.Launcher.Plugin.Explorer
public Task InitAsync(PluginInitContext context)
{
Context = context;
Settings = context.API.LoadSettingJsonStorage<Settings>();
viewModel = new SettingsViewModel(context, Settings);
// as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards.
if (Settings.QuickFolderAccessLinks.Any())
{
Settings.QuickAccessLinks = Settings.QuickFolderAccessLinks;
Settings.QuickFolderAccessLinks = new List<AccessLink>();
}
contextMenu = new ContextMenu(Context, Settings, viewModel);
searchManager = new SearchManager(Settings, Context);
ResultManager.Init(Context, Settings);
SortOptionTranslationHelper.API = context.API;
EverythingApiDllImport.Load(Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "EverythingSDK",
Environment.Is64BitProcess ? "x64" : "x86"));
return Task.CompletedTask;
}
@ -58,7 +57,34 @@ namespace Flow.Launcher.Plugin.Explorer
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
return await searchManager.SearchAsync(query, token);
try
{
return await searchManager.SearchAsync(query, token);
}
catch (Exception e) when (e is SearchException or EngineNotAvailableException)
{
return new List<Result>
{
new()
{
Title = e.Message,
SubTitle = e is EngineNotAvailableException { Resolution: { } resolution }
? resolution
: "Enter to copy the message to clipboard",
Score = 501,
IcoPath = e is EngineNotAvailableException { ErrorIcon: { } iconPath }
? iconPath
: Constants.GeneralSearchErrorImagePath,
AsyncAction = e is EngineNotAvailableException {Action: { } action}
? action
: _ =>
{
Clipboard.SetDataObject(e.ToString());
return new ValueTask<bool>(true);
}
}
};
}
}
public string GetTranslatedPluginTitle()

View file

@ -17,6 +17,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal const string IndexingOptionsIconImagePath = "Images\\windowsindexingoptions.png";
internal const string QuickAccessImagePath = "Images\\quickaccess.png";
internal const string RemoveQuickAccessImagePath = "Images\\removequickaccess.png";
internal const string ShowContextMenuImagePath = "Images\\context_menu.png";
internal const string EverythingErrorImagePath = "Images\\everything_error.png";
internal const string IndexSearchWarningImagePath = "Images\\index_error.png";
internal const string WindowsIndexErrorImagePath = "Images\\index_error2.png";
internal const string GeneralSearchErrorImagePath = "Images\\robot_error.png";
internal const string ToolTipOpenDirectory = "Ctrl + Enter to open the directory";

View file

@ -10,7 +10,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{
public static class DirectoryInfoSearch
{
internal static List<Result> TopLevelDirectorySearch(Query query, string search, CancellationToken token)
internal static IEnumerable<SearchResult> TopLevelDirectorySearch(Query query, string search, CancellationToken token)
{
var criteria = ConstructSearchCriteria(search);
@ -19,9 +19,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
return DirectorySearch(new EnumerationOptions
{
RecurseSubdirectories = true
}, query, search, criteria, token);
}, search, criteria, token);
return DirectorySearch(new EnumerationOptions(), query, search, criteria,
return DirectorySearch(new EnumerationOptions(), search, criteria,
token); // null will be passed as default
}
@ -33,10 +33,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{
var indexOfSeparator = search.LastIndexOf(Constants.DirectorySeperator);
incompleteName = search.Substring(indexOfSeparator + 1).ToLower();
incompleteName = search[(indexOfSeparator + 1)..].ToLower();
if (incompleteName.StartsWith(Constants.AllFilesFolderSearchWildcard))
incompleteName = "*" + incompleteName.Substring(1);
incompleteName = string.Concat("*", incompleteName.AsSpan(1));
}
incompleteName += "*";
@ -44,54 +44,45 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
return incompleteName;
}
private static List<Result> DirectorySearch(EnumerationOptions enumerationOption, Query query, string search,
private static IEnumerable<SearchResult> DirectorySearch(EnumerationOptions enumerationOption, string search,
string searchCriteria, CancellationToken token)
{
var results = new List<Result>();
var results = new List<SearchResult>();
var path = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(search);
var folderList = new List<Result>();
var fileList = new List<Result>();
try
{
var directoryInfo = new System.IO.DirectoryInfo(path);
foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, enumerationOption))
{
if (fileSystemInfo is System.IO.DirectoryInfo)
results.Add(new SearchResult
{
folderList.Add(ResultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName,
fileSystemInfo.FullName, query, 0, true, false));
}
else
{
fileList.Add(ResultManager.CreateFileResult(fileSystemInfo.FullName, query, 0, true, false));
}
FullPath = fileSystemInfo.FullName,
Type = fileSystemInfo switch
{
System.IO.DirectoryInfo {Parent: null} => ResultType.Volume,
System.IO.DirectoryInfo => ResultType.Folder,
FileInfo => ResultType.File,
_ => throw new ArgumentOutOfRangeException(nameof(fileSystemInfo))
},
WindowsIndexed = false
});
token.ThrowIfCancellationRequested();
if (token.IsCancellationRequested)
return results;
}
}
catch (Exception e)
{
Log.Exception(nameof(DirectoryInfoSearch), "Error occured while searching path", e);
results.Add(
new Result
{
Title = string.Format(SearchManager.Context.API.GetTranslation(
"plugin_explorer_directoryinfosearch_error"),
e.Message),
Score = 501,
IcoPath = Constants.ExplorerIconImagePath
});
return results;
throw;
}
// Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList();
return results.OrderBy(r=>r.Type).ThenBy(r=>r.FullPath);
}
}
}
}

View file

@ -0,0 +1,216 @@
using Flow.Launcher.Plugin.Everything.Everything;
using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms.Design;
using Flow.Launcher.Plugin.Explorer.Exceptions;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything
{
public static class EverythingApi
{
private const int BufferSize = 4096;
private static SemaphoreSlim _semaphore = new(1, 1);
// cached buffer to remove redundant allocations.
private static readonly StringBuilder buffer = new(BufferSize);
public enum StateCode
{
OK,
MemoryError,
IPCError,
RegisterClassExError,
CreateWindowError,
CreateThreadError,
InvalidIndexError,
InvalidCallError
}
/// <summary>
/// Gets or sets a value indicating whether [match path].
/// </summary>
/// <value><c>true</c> if [match path]; otherwise, <c>false</c>.</value>
public static bool MatchPath
{
get => EverythingApiDllImport.Everything_GetMatchPath();
set => EverythingApiDllImport.Everything_SetMatchPath(value);
}
/// <summary>
/// Gets or sets a value indicating whether [match case].
/// </summary>
/// <value><c>true</c> if [match case]; otherwise, <c>false</c>.</value>
public static bool MatchCase
{
get => EverythingApiDllImport.Everything_GetMatchCase();
set => EverythingApiDllImport.Everything_SetMatchCase(value);
}
/// <summary>
/// Gets or sets a value indicating whether [match whole word].
/// </summary>
/// <value><c>true</c> if [match whole word]; otherwise, <c>false</c>.</value>
public static bool MatchWholeWord
{
get => EverythingApiDllImport.Everything_GetMatchWholeWord();
set => EverythingApiDllImport.Everything_SetMatchWholeWord(value);
}
/// <summary>
/// Gets or sets a value indicating whether [enable regex].
/// </summary>
/// <value><c>true</c> if [enable regex]; otherwise, <c>false</c>.</value>
public static bool EnableRegex
{
get => EverythingApiDllImport.Everything_GetRegex();
set => EverythingApiDllImport.Everything_SetRegex(value);
}
/// <summary>
/// Checks whether the sort option is Fast Sort.
/// </summary>
public static bool IsFastSortOption(SortOption sortOption)
{
var fastSortOptionEnabled = EverythingApiDllImport.Everything_IsFastSort(sortOption);
// If the Everything service is not running, then this call will incorrectly report
// the state as false. This checks for errors thrown by the api and up to the caller to handle.
CheckAndThrowExceptionOnError();
return fastSortOptionEnabled;
}
public static async ValueTask<bool> IsEverythingRunningAsync(CancellationToken token = default)
{
await _semaphore.WaitAsync(token);
try
{
EverythingApiDllImport.Everything_GetMajorVersion();
var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError;
return result;
}
finally
{
_semaphore.Release();
}
}
/// <summary>
/// Searches the specified key word and reset the everything API afterwards
/// </summary>
/// <param name="option">Search Criteria</param>
/// <param name="token">when cancelled the current search will stop and exit (and would not reset)</param>
/// <returns>An IAsyncEnumerable that will enumerate all results searched by the specific query and option</returns>
public static async IAsyncEnumerable<SearchResult> SearchAsync(EverythingSearchOption option,
[EnumeratorCancellation] CancellationToken token)
{
if (option.Offset < 0)
throw new ArgumentOutOfRangeException(nameof(option.Offset), option.Offset, "Offset must be greater than or equal to 0");
if (option.MaxCount < 0)
throw new ArgumentOutOfRangeException(nameof(option.MaxCount), option.MaxCount, "MaxCount must be greater than or equal to 0");
await _semaphore.WaitAsync(token);
try
{
if (token.IsCancellationRequested)
yield break;
if (option.Keyword.StartsWith("@"))
{
EverythingApiDllImport.Everything_SetRegex(true);
option.Keyword = option.Keyword[1..];
}
var builder = new StringBuilder();
builder.Append(option.Keyword);
if (!string.IsNullOrWhiteSpace(option.ParentPath))
{
builder.Append($" {(option.IsRecursive ? "" : "parent:")}\"{option.ParentPath}\"");
}
if (option.IsContentSearch)
{
builder.Append($" content:\"{option.ContentSearchKeyword}\"");
}
EverythingApiDllImport.Everything_SetSearchW(builder.ToString());
EverythingApiDllImport.Everything_SetOffset(option.Offset);
EverythingApiDllImport.Everything_SetMax(option.MaxCount);
EverythingApiDllImport.Everything_SetSort(option.SortOption);
if (token.IsCancellationRequested) yield break;
if (!EverythingApiDllImport.Everything_QueryW(true))
{
CheckAndThrowExceptionOnError();
yield break;
}
for (var idx = 0; idx < EverythingApiDllImport.Everything_GetNumResults(); ++idx)
{
if (token.IsCancellationRequested)
{
yield break;
}
EverythingApiDllImport.Everything_GetResultFullPathNameW(idx, buffer, BufferSize);
var result = new SearchResult
{
FullPath = buffer.ToString(),
Type = EverythingApiDllImport.Everything_IsFolderResult(idx) ? ResultType.Folder :
EverythingApiDllImport.Everything_IsFileResult(idx) ? ResultType.File :
ResultType.Volume
};
yield return result;
}
}
finally
{
EverythingApiDllImport.Everything_Reset();
_semaphore.Release();
}
}
private static void CheckAndThrowExceptionOnError()
{
switch (EverythingApiDllImport.Everything_GetLastError())
{
case StateCode.CreateThreadError:
throw new CreateThreadException();
case StateCode.CreateWindowError:
throw new CreateWindowException();
case StateCode.InvalidCallError:
throw new InvalidCallException();
case StateCode.InvalidIndexError:
throw new InvalidIndexException();
case StateCode.IPCError:
throw new IPCErrorException();
case StateCode.MemoryError:
throw new MemoryErrorException();
case StateCode.RegisterClassExError:
throw new RegisterClassExException();
case StateCode.OK:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View file

@ -0,0 +1,163 @@
using Flow.Launcher.Plugin.Everything.Everything;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything
{
public static class EverythingApiDllImport
{
public static void Load(string directory)
{
var path = Path.Combine(directory, DLL);
int code = LoadLibrary(path);
if (code == 0)
{
int err = Marshal.GetLastPInvokeError();
Marshal.ThrowExceptionForHR(err);
}
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int LoadLibrary(string name);
private const string DLL = "Everything.dll";
[DllImport(DLL, CharSet = CharSet.Unicode)]
internal static extern int Everything_SetSearchW(string lpSearchString);
[DllImport(DLL)]
internal static extern void Everything_SetMatchPath(bool bEnable);
[DllImport(DLL)]
internal static extern void Everything_SetMatchCase(bool bEnable);
[DllImport(DLL)]
internal static extern void Everything_SetMatchWholeWord(bool bEnable);
[DllImport(DLL)]
internal static extern void Everything_SetRegex(bool bEnable);
[DllImport(DLL)]
internal static extern void Everything_SetMax(int dwMax);
[DllImport(DLL)]
internal static extern void Everything_SetOffset(int dwOffset);
[DllImport(DLL)]
internal static extern bool Everything_GetMatchPath();
[DllImport(DLL)]
internal static extern bool Everything_GetMatchCase();
[DllImport(DLL)]
internal static extern bool Everything_GetMatchWholeWord();
[DllImport(DLL)]
internal static extern bool Everything_GetRegex();
[DllImport(DLL)]
internal static extern uint Everything_GetMax();
[DllImport(DLL)]
internal static extern uint Everything_GetOffset();
[DllImport(DLL, CharSet = CharSet.Unicode)]
internal static extern string Everything_GetSearchW();
[DllImport(DLL)]
internal static extern EverythingApi.StateCode Everything_GetLastError();
[DllImport(DLL, CharSet = CharSet.Unicode)]
internal static extern bool Everything_QueryW(bool bWait);
[DllImport(DLL)]
internal static extern void Everything_SortResultsByPath();
[DllImport(DLL)]
internal static extern int Everything_GetNumFileResults();
[DllImport(DLL)]
internal static extern int Everything_GetMajorVersion();
[DllImport(DLL)]
internal static extern int Everything_GetNumFolderResults();
[DllImport(DLL)]
internal static extern int Everything_GetNumResults();
[DllImport(DLL)]
internal static extern int Everything_GetTotFileResults();
[DllImport(DLL)]
internal static extern int Everything_GetTotFolderResults();
[DllImport(DLL)]
internal static extern int Everything_GetTotResults();
[DllImport(DLL)]
internal static extern bool Everything_IsVolumeResult(int nIndex);
[DllImport(DLL)]
internal static extern bool Everything_IsFolderResult(int nIndex);
[DllImport(DLL)]
internal static extern bool Everything_IsFileResult(int nIndex);
[DllImport(DLL, CharSet = CharSet.Unicode)]
internal static extern void Everything_GetResultFullPathNameW(int nIndex, StringBuilder lpString, int nMaxCount);
[DllImport(DLL)]
internal static extern void Everything_Reset();
// Everything 1.4
[DllImport(DLL)]
public static extern void Everything_SetSort(SortOption dwSortType);
[DllImport(DLL)]
public static extern bool Everything_IsFastSort(SortOption dwSortType);
[DllImport(DLL)]
public static extern SortOption Everything_GetSort();
[DllImport(DLL)]
public static extern uint Everything_GetResultListSort();
[DllImport(DLL)]
public static extern void Everything_SetRequestFlags(uint dwRequestFlags);
[DllImport(DLL)]
public static extern uint Everything_GetRequestFlags();
[DllImport(DLL)]
public static extern uint Everything_GetResultListRequestFlags();
[DllImport("Everything64.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr Everything_GetResultExtension(uint nIndex);
[DllImport(DLL)]
public static extern bool Everything_GetResultSize(uint nIndex, out long lpFileSize);
[DllImport(DLL)]
public static extern bool Everything_GetResultDateCreated(uint nIndex, out long lpFileTime);
[DllImport(DLL)]
public static extern bool Everything_GetResultDateModified(uint nIndex, out long lpFileTime);
[DllImport(DLL)]
public static extern bool Everything_GetResultDateAccessed(uint nIndex, out long lpFileTime);
[DllImport(DLL)]
public static extern uint Everything_GetResultAttributes(uint nIndex);
[DllImport(DLL, CharSet = CharSet.Unicode)]
public static extern IntPtr Everything_GetResultFileListFileName(uint nIndex);
[DllImport(DLL)]
public static extern uint Everything_GetResultRunCount(uint nIndex);
[DllImport(DLL)]
public static extern bool Everything_GetResultDateRun(uint nIndex, out long lpFileTime);
[DllImport(DLL)]
public static extern bool Everything_GetResultDateRecentlyChanged(uint nIndex, out long lpFileTime);
[DllImport(DLL, CharSet = CharSet.Unicode)]
public static extern IntPtr Everything_GetResultHighlightedFileName(uint nIndex);
[DllImport(DLL, CharSet = CharSet.Unicode)]
public static extern IntPtr Everything_GetResultHighlightedPath(uint nIndex);
[DllImport(DLL, CharSet = CharSet.Unicode)]
public static extern IntPtr Everything_GetResultHighlightedFullPathAndFileName(uint nIndex);
[DllImport(DLL)]
public static extern uint Everything_GetRunCountFromFileName(string lpFileName);
[DllImport(DLL)]
public static extern bool Everything_SetRunCountFromFileName(string lpFileName, uint dwRunCount);
[DllImport(DLL)]
public static extern uint Everything_IncRunCountFromFileName(string lpFileName);
}
}

View file

@ -0,0 +1,88 @@
using Droplex;
using Flow.Launcher.Plugin.SharedCommands;
using Microsoft.Win32;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything;
public static class EverythingDownloadHelper
{
public static async Task<string> PromptDownloadIfNotInstallAsync(string installedLocation, IPublicAPI api)
{
if (!string.IsNullOrEmpty(installedLocation) && installedLocation.FileExists())
return installedLocation;
installedLocation = GetInstalledPath();
if (string.IsNullOrEmpty(installedLocation))
{
if (System.Windows.Forms.MessageBox.Show(
string.Format(api.GetTranslation("flowlauncher_plugin_everything_installing_select"), Environment.NewLine),
api.GetTranslation("flowlauncher_plugin_everything_installing_title"),
System.Windows.Forms.MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes)
{
var dlg = new System.Windows.Forms.OpenFileDialog
{
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var result = dlg.ShowDialog();
if (result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrEmpty(dlg.FileName))
installedLocation = dlg.FileName;
}
}
if (!string.IsNullOrEmpty(installedLocation))
{
return installedLocation;
}
api.ShowMsg(api.GetTranslation("flowlauncher_plugin_everything_installing_title"),
api.GetTranslation("flowlauncher_plugin_everything_installing_subtitle"), "", useMainWindowAsOwner: false);
await DroplexPackage.Drop(App.Everything1_4_1_1009).ConfigureAwait(false);
api.ShowMsg(api.GetTranslation("flowlauncher_plugin_everything_installing_title"),
api.GetTranslation("flowlauncher_plugin_everything_installationsuccess_subtitle"), "", useMainWindowAsOwner: false);
installedLocation = "C:\\Program Files\\Everything\\Everything.exe";
FilesFolders.OpenPath(installedLocation);
return installedLocation;
}
internal static string GetInstalledPath()
{
using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall");
if (key is not null)
{
foreach (var subKey in key.GetSubKeyNames().Select(keyName => key.OpenSubKey(keyName)))
{
if (subKey?.GetValue("DisplayName") is not string displayName || !displayName.Contains("Everything"))
{
continue;
}
if (subKey.GetValue("UninstallString") is not string uninstallString)
{
continue;
}
if (Path.GetDirectoryName(uninstallString) is not { } uninstallDirectory)
{
continue;
}
return Path.Combine(uninstallDirectory, "Everything.exe");
}
}
var scoopInstalledPath = Environment.ExpandEnvironmentVariables(@"%userprofile%\scoop\apps\everything\current\Everything.exe");
return File.Exists(scoopInstalledPath) ? scoopInstalledPath : string.Empty;
}
}

View file

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Plugin.Explorer.Exceptions;
using Flow.Launcher.Plugin.Explorer.Search.IProvider;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything
{
public class EverythingSearchManager : IIndexProvider, IContentIndexProvider, IPathIndexProvider
{
private Settings Settings { get; }
public EverythingSearchManager(Settings settings)
{
Settings = settings;
}
private async ValueTask ThrowIfEverythingNotAvailableAsync(CancellationToken token = default)
{
try
{
if (!await EverythingApi.IsEverythingRunningAsync(token))
throw new EngineNotAvailableException(
Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_click_to_launch_or_install"),
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_is_not_running"),
ClickToInstallEverythingAsync)
{
ErrorIcon = Constants.EverythingErrorImagePath
};
}
catch (DllNotFoundException)
{
throw new EngineNotAvailableException(
Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
"Please check whether your system is x86 or x64",
Main.Context.API.GetTranslation("flowlauncher_plugin_everything_sdk_issue"))
{
ErrorIcon = Constants.GeneralSearchErrorImagePath
};
}
}
private async ValueTask<bool> ClickToInstallEverythingAsync(ActionContext _)
{
var installedPath = await EverythingDownloadHelper.PromptDownloadIfNotInstallAsync(Settings.EverythingInstalledPath, Main.Context.API);
if (installedPath == null)
{
Main.Context.API.ShowMsgError("Unable to find Everything.exe");
return false;
}
Settings.EverythingInstalledPath = installedPath;
Process.Start(installedPath, "-startup");
return true;
}
public async IAsyncEnumerable<SearchResult> SearchAsync(string search, [EnumeratorCancellation] CancellationToken token)
{
await ThrowIfEverythingNotAvailableAsync(token);
if (token.IsCancellationRequested)
yield break;
var option = new EverythingSearchOption(search, Settings.SortOption);
await foreach (var result in EverythingApi.SearchAsync(option, token))
yield return result;
}
public async IAsyncEnumerable<SearchResult> ContentSearchAsync(string plainSearch,
string contentSearch, [EnumeratorCancellation] CancellationToken token)
{
await ThrowIfEverythingNotAvailableAsync(token);
if (!Settings.EnableEverythingContentSearch)
{
throw new EngineNotAvailableException(Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
"Click to Enable Everything Content Search (only applicable to Everything 1.5+ with indexed content)",
"Everything Content Search is not enabled.",
_ =>
{
Settings.EnableEverythingContentSearch = true;
return ValueTask.FromResult(true);
})
{
ErrorIcon = Constants.EverythingErrorImagePath
};
}
if (token.IsCancellationRequested)
yield break;
var option = new EverythingSearchOption(plainSearch,
Settings.SortOption,
true,
contentSearch);
await foreach (var result in EverythingApi.SearchAsync(option, token))
{
yield return result;
}
}
public async IAsyncEnumerable<SearchResult> EnumerateAsync(string path, string search, bool recursive, [EnumeratorCancellation] CancellationToken token)
{
await ThrowIfEverythingNotAvailableAsync(token);
if (token.IsCancellationRequested)
yield break;
var option = new EverythingSearchOption(search,
Settings.SortOption,
ParentPath: path,
IsRecursive: recursive);
await foreach (var result in EverythingApi.SearchAsync(option, token))
{
yield return result;
}
}
}
}

View file

@ -0,0 +1,14 @@
using System;
using Flow.Launcher.Plugin.Everything.Everything;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything
{
public record struct EverythingSearchOption(string Keyword,
SortOption SortOption,
bool IsContentSearch = false,
string ContentSearchKeyword = default,
string ParentPath = default,
bool IsRecursive = true,
int Offset = 0,
int MaxCount = 100);
}

View file

@ -0,0 +1,11 @@
using System;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
{
/// <summary>
///
/// </summary>
public class CreateThreadException : ApplicationException
{
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
{
/// <summary>
///
/// </summary>
public class CreateWindowException : ApplicationException
{
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
{
/// <summary>
///
/// </summary>
public class IPCErrorException : ApplicationException
{
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
{
/// <summary>
///
/// </summary>
public class InvalidCallException : ApplicationException
{
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
{
/// <summary>
///
/// </summary>
public class InvalidIndexException : ApplicationException
{
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
{
/// <summary>
///
/// </summary>
public class MemoryErrorException : ApplicationException
{
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
{
/// <summary>
///
/// </summary>
public class RegisterClassExException : ApplicationException
{
}
}

View file

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Flow.Launcher.Plugin.Everything.Everything
{
public enum SortOption : uint
{
NAME_ASCENDING = 1u,
NAME_DESCENDING = 2u,
PATH_ASCENDING = 3u,
PATH_DESCENDING = 4u,
SIZE_ASCENDING = 5u,
SIZE_DESCENDING = 6u,
EXTENSION_ASCENDING = 7u,
EXTENSION_DESCENDING = 8u,
TYPE_NAME_ASCENDING = 9u,
TYPE_NAME_DESCENDING = 10u,
DATE_CREATED_ASCENDING = 11u,
DATE_CREATED_DESCENDING = 12u,
DATE_MODIFIED_ASCENDING = 13u,
DATE_MODIFIED_DESCENDING = 14u,
ATTRIBUTES_ASCENDING = 15u,
ATTRIBUTES_DESCENDING = 16u,
FILE_LIST_FILENAME_ASCENDING = 17u,
FILE_LIST_FILENAME_DESCENDING = 18u,
RUN_COUNT_ASCENDING = 19u,
RUN_COUNT_DESCENDING = 20u,
DATE_RECENTLY_CHANGED_ASCENDING = 21u,
DATE_RECENTLY_CHANGED_DESCENDING = 22u,
DATE_ACCESSED_ASCENDING = 23u,
DATE_ACCESSED_DESCENDING = 24u,
DATE_RUN_ASCENDING = 25u,
DATE_RUN_DESCENDING = 26u
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace Flow.Launcher.Plugin.Explorer.Search.IProvider
{
public interface IContentIndexProvider
{
public IAsyncEnumerable<SearchResult> ContentSearchAsync(string plainSearch, string contentSearch, CancellationToken token = default);
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace Flow.Launcher.Plugin.Explorer.Search.IProvider
{
public interface IIndexProvider
{
public IAsyncEnumerable<SearchResult> SearchAsync(string search, CancellationToken token);
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
namespace Flow.Launcher.Plugin.Explorer.Search.IProvider
{
public interface IPathIndexProvider
{
public IAsyncEnumerable<SearchResult> EnumerateAsync(string path, string search, bool recursive, CancellationToken token);
}
}

View file

@ -8,7 +8,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
{
private const int quickAccessResultScore = 100;
internal static List<Result> AccessLinkListMatched(Query query, List<AccessLink> accessLinks)
internal static List<Result> AccessLinkListMatched(Query query, IEnumerable<AccessLink> accessLinks)
{
if (string.IsNullOrEmpty(query.Search))
return new List<Result>();
@ -17,7 +17,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
var queriedAccessLinks =
accessLinks
.Where(x => x.Name.Contains(search, StringComparison.OrdinalIgnoreCase))
.Where(x => x.Name.Contains(search, StringComparison.OrdinalIgnoreCase) || x.Path.Contains(search, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.Type)
.ThenBy(x => x.Name);
@ -29,7 +29,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks
}).ToList();
}
internal static List<Result> AccessLinkListAll(Query query, List<AccessLink> accessLinks)
internal static List<Result> AccessLinkListAll(Query query, IEnumerable<AccessLink> accessLinks)
=> accessLinks
.OrderBy(x => x.Type)
.ThenBy(x => x.Name)

View file

@ -1,7 +1,9 @@
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -35,13 +37,25 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return $"{keyword}{formatted_path}";
}
internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool showIndexState = false, bool windowsIndexed = false)
public static Result CreateResult(Query query, SearchResult result)
{
return result.Type switch
{
ResultType.Folder or ResultType.Volume => CreateFolderResult(Path.GetFileName(result.FullPath),
result.FullPath, result.FullPath, query, 0, result.WindowsIndexed),
ResultType.File => CreateFileResult(
result.FullPath, query, 0, result.WindowsIndexed),
_ => throw new ArgumentOutOfRangeException()
};
}
internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool windowsIndexed = false)
{
return new Result
{
Title = title,
IcoPath = path,
SubTitle = subtitle,
SubTitle = Path.GetDirectoryName(path),
AutoCompleteText = GetPathWithActionKeyword(path, ResultType.Folder),
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
Action = c =>
@ -61,17 +75,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search
}
Context.API.ChangeQuery(GetPathWithActionKeyword(path, ResultType.Folder));
return false;
},
Score = score,
TitleToolTip = Constants.ToolTipOpenDirectory,
TitleToolTip = InternationalizationManager.Instance.GetTranslation("plugin_explorer_plugin_ToolTipOpenDirectory"),
SubTitleToolTip = path,
ContextData = new SearchResult
{
Type = ResultType.Folder,
FullPath = path,
ShowIndexState = showIndexState,
WindowsIndexed = windowsIndexed
}
};
@ -80,15 +93,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal static Result CreateDriveSpaceDisplayResult(string path, bool windowsIndexed = false)
{
var progressBarColor = "#26a0da";
int progressValue = 0;
var title = string.Empty; // hide title when use progress bar,
var driveLetter = path.Substring(0, 1).ToUpper();
var driveLetter = path[..1].ToUpper();
var driveName = driveLetter + ":\\";
DriveInfo drv = new DriveInfo(driveLetter);
var subtitle = toReadableSize(drv.AvailableFreeSpace, 2) + " free of " + toReadableSize(drv.TotalSize, 2);
double UsingSize = (Convert.ToDouble(drv.TotalSize) - Convert.ToDouble(drv.AvailableFreeSpace)) / Convert.ToDouble(drv.TotalSize) * 100;
var subtitle = ToReadableSize(drv.AvailableFreeSpace, 2) + " free of " + ToReadableSize(drv.TotalSize, 2);
double usingSize = (Convert.ToDouble(drv.TotalSize) - Convert.ToDouble(drv.AvailableFreeSpace)) / Convert.ToDouble(drv.TotalSize) * 100;
progressValue = Convert.ToInt32(UsingSize);
int? progressValue = Convert.ToInt32(usingSize);
if (progressValue >= 90)
progressBarColor = "#da2626";
@ -111,15 +123,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search
SubTitleToolTip = path,
ContextData = new SearchResult
{
Type = ResultType.Folder,
Type = ResultType.Volume,
FullPath = path,
ShowIndexState = true,
WindowsIndexed = windowsIndexed
}
};
}
private static string toReadableSize(long pDrvSize, int pi)
private static string ToReadableSize(long pDrvSize, int pi)
{
int mok = 0;
double drvSize = pDrvSize;
@ -140,24 +151,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search
else if (mok == 4)
Space = " TB";
var returnStr = string.Format("{0}{1}", Convert.ToInt32(drvSize), Space);
var returnStr = $"{Convert.ToInt32(drvSize)}{Space}";
if (mok != 0)
{
switch (pi)
returnStr = pi switch
{
case 1:
returnStr = string.Format("{0:F1}{1}", drvSize, Space);
break;
case 2:
returnStr = string.Format("{0:F2}{1}", drvSize, Space);
break;
case 3:
returnStr = string.Format("{0:F3}{1}", drvSize, Space);
break;
default:
returnStr = string.Format("{0}{1}", Convert.ToInt32(drvSize), Space);
break;
}
1 => $"{drvSize:F1}{Space}",
2 => $"{drvSize:F2}{Space}",
3 => $"{drvSize:F3}{Space}",
_ => $"{Convert.ToInt32(drvSize)}{Space}"
};
}
return returnStr;
@ -165,25 +168,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search
internal static Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false)
{
var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
var folderName = retrievedDirectoryPath.TrimEnd(Constants.DirectorySeperator).Split(new[]
var folderName = path.TrimEnd(Constants.DirectorySeperator).Split(new[]
{
Path.DirectorySeparatorChar
}, StringSplitOptions.None).Last();
if (retrievedDirectoryPath.EndsWith(":\\"))
{
var driveLetter = path.Substring(0, 1).ToUpper();
folderName = driveLetter + " drive";
}
var title = "Open current directory";
if (retrievedDirectoryPath != path)
title = "Open " + folderName;
var title = $"Open {folderName}";
var subtitleFolderName = folderName;
// ie. max characters can be displayed without subtitle cutting off: "Program Files (x86)"
@ -195,32 +186,29 @@ namespace Flow.Launcher.Plugin.Explorer.Search
Title = title,
SubTitle = $"Use > to search within {subtitleFolderName}, " +
$"* to search for file extensions or >* to combine both searches.",
AutoCompleteText = GetPathWithActionKeyword(retrievedDirectoryPath, ResultType.Folder),
IcoPath = retrievedDirectoryPath,
AutoCompleteText = GetPathWithActionKeyword(path, ResultType.Folder),
IcoPath = path,
Score = 500,
Action = c =>
Action = _ =>
{
Context.API.OpenDirectory(retrievedDirectoryPath);
Context.API.OpenDirectory(path);
return true;
},
TitleToolTip = retrievedDirectoryPath,
SubTitleToolTip = retrievedDirectoryPath,
ContextData = new SearchResult
{
Type = ResultType.Folder,
FullPath = retrievedDirectoryPath,
ShowIndexState = true,
FullPath = path,
WindowsIndexed = windowsIndexed
}
};
}
internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool showIndexState = false, bool windowsIndexed = false)
internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false)
{
var result = new Result
{
Title = Path.GetFileName(filePath),
SubTitle = filePath,
SubTitle = Path.GetDirectoryName(filePath),
IcoPath = filePath,
AutoCompleteText = GetPathWithActionKeyword(filePath, ResultType.File),
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, Path.GetFileName(filePath)).MatchData,
@ -231,7 +219,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search
{
if (File.Exists(filePath) && c.SpecialKeyState.CtrlPressed && c.SpecialKeyState.ShiftPressed)
{
Task.Run(() =>
_ = Task.Run(() =>
{
try
{
@ -264,13 +252,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return true;
},
TitleToolTip = Constants.ToolTipOpenContainingFolder,
TitleToolTip = InternationalizationManager.Instance.GetTranslation("plugin_explorer_plugin_ToolTipOpenContainingFolder"),
SubTitleToolTip = filePath,
ContextData = new SearchResult
{
Type = ResultType.File,
FullPath = filePath,
ShowIndexState = showIndexState,
WindowsIndexed = windowsIndexed
}
};
@ -278,16 +265,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search
}
}
internal class SearchResult
{
public string FullPath { get; set; }
public ResultType Type { get; set; }
public bool WindowsIndexed { get; set; }
public bool ShowIndexState { get; set; }
}
public enum ResultType
{
Volume,

View file

@ -1,12 +1,13 @@
using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Plugin.Explorer.Exceptions;
namespace Flow.Launcher.Plugin.Explorer.Search
{
@ -29,52 +30,81 @@ namespace Flow.Launcher.Plugin.Explorer.Search
public bool Equals(Result x, Result y)
{
return x.SubTitle == y.SubTitle;
return x.Title == y.Title && x.SubTitle == y.SubTitle;
}
public int GetHashCode(Result obj)
{
return obj.SubTitle.GetHashCode();
return HashCode.Combine(obj.Title.GetHashCode(), obj.SubTitle?.GetHashCode() ?? 0);
}
}
internal async Task<List<Result>> SearchAsync(Query query, CancellationToken token)
{
var querySearch = query.Search;
var results = new HashSet<Result>(PathEqualityComparator.Instance);
// This allows the user to type the below action keywords and see/search the list of quick folder links
if (ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)
|| ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword)
|| ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword))
|| ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword)
|| ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword)
|| ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword))
{
if (string.IsNullOrEmpty(query.Search))
if (string.IsNullOrEmpty(query.Search) && ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword))
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
var quickAccessLinks = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
results.UnionWith(quickaccessLinks);
results.UnionWith(quickAccessLinks);
}
if (IsFileContentSearch(query.ActionKeyword))
return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false);
if (ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword) ||
ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword))
else
{
results.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false));
return new List<Result>();
}
if ((ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) ||
ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)) &&
querySearch.Length > 0 &&
!querySearch.IsLocationPathString())
IAsyncEnumerable<SearchResult> searchResults = null;
bool isPathSearch = query.Search.IsLocationPathString();
switch (isPathSearch)
{
results.UnionWith(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token)
.ConfigureAwait(false));
case true
when ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword)
|| ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword):
results.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false));
return results.ToList();
case false
when ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword):
// Intentionally require enabling of Everything's content search due to its slowness
if (Settings.ContentIndexProvider is EverythingSearchManager && !Settings.EnableEverythingContentSearch)
return EverythingContentSearchResult(query);
searchResults = Settings.ContentIndexProvider.ContentSearchAsync("", query.Search, token);
break;
case false
when ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword)
|| ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword):
searchResults = Settings.IndexProvider.SearchAsync(query.Search, token);
break;
}
if (searchResults == null)
return results.ToList();
await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false))
results.Add(ResultManager.CreateResult(query, search));
results.RemoveWhere(r => Settings.IndexSearchExcludedSubdirectoryPaths.Any(
excludedPath => r.SubTitle.StartsWith(excludedPath.Path, StringComparison.OrdinalIgnoreCase)));
return results.ToList();
}
@ -93,12 +123,31 @@ namespace Flow.Launcher.Plugin.Explorer.Search
Settings.ActionKeyword.IndexSearchActionKeyword => Settings.IndexSearchKeywordEnabled &&
keyword == Settings.IndexSearchActionKeyword,
Settings.ActionKeyword.QuickAccessActionKeyword => Settings.QuickAccessKeywordEnabled &&
keyword == Settings.QuickAccessActionKeyword,
_ => throw new NotImplementedException()
keyword == Settings.QuickAccessActionKeyword,
_ => throw new ArgumentOutOfRangeException(nameof(allowedActionKeyword), allowedActionKeyword, "actionKeyword out of range")
};
}
public async Task<List<Result>> PathSearchAsync(Query query, CancellationToken token = default)
private static List<Result> EverythingContentSearchResult(Query query)
{
return new List<Result>()
{
new()
{
Title = "Do you want to enable content search for Everything?",
SubTitle = "It can be very slow without index (which is only supported in Everything v1.5+)",
IcoPath = "Images/index_error.png",
Action = c =>
{
Settings.EnableEverythingContentSearch = true;
Context.API.ChangeQuery(query.RawQuery, true);
return false;
}
}
};
}
private async Task<List<Result>> PathSearchAsync(Query query, CancellationToken token = default)
{
var querySearch = query.Search;
@ -118,118 +167,68 @@ namespace Flow.Launcher.Plugin.Explorer.Search
locationPath = EnvironmentVariables.TranslateEnvironmentVariablePath(locationPath);
// Check that actual location exists, otherwise directory search will throw directory not found exception
if (!FilesFolders.LocationExists(FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath)))
if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath).LocationExists())
return results.ToList();
var useIndexSearch = UseWindowsIndexForDirectorySearch(locationPath);
var useIndexSearch = Settings.IndexSearchEngine is Settings.IndexSearchEngineOption.WindowsIndex
&& UseWindowsIndexForDirectorySearch(locationPath);
if (locationPath.EndsWith(":\\"))
var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath);
results.Add(retrievedDirectoryPath.EndsWith(":\\")
? ResultManager.CreateDriveSpaceDisplayResult(retrievedDirectoryPath, useIndexSearch)
: ResultManager.CreateOpenCurrentFolderResult(retrievedDirectoryPath, useIndexSearch));
if (token.IsCancellationRequested)
return new List<Result>();
IEnumerable<SearchResult> directoryResult;
var recursiveIndicatorIndex = query.Search.IndexOf('>');
if (recursiveIndicatorIndex > 0 && Settings.PathEnumerationEngine != Settings.PathEnumerationEngineOption.DirectEnumeration)
{
results.Add(ResultManager.CreateDriveSpaceDisplayResult(locationPath, useIndexSearch));
directoryResult =
await Settings.PathEnumerator.EnumerateAsync(
query.Search[..recursiveIndicatorIndex],
query.Search[(recursiveIndicatorIndex + 1)..],
true,
token)
.ToListAsync(cancellationToken: token)
.ConfigureAwait(false);
}
else
{
results.Add(ResultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch));
try
{
directoryResult = DirectoryInfoSearch.TopLevelDirectorySearch(query, query.Search, token);
}
catch (Exception e)
{
throw new SearchException("DirectoryInfoSearch", e.Message, e);
}
}
token.ThrowIfCancellationRequested();
var directoryResult = await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync,
DirectoryInfoClassSearch,
useIndexSearch,
query,
locationPath,
token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
results.UnionWith(directoryResult);
results.UnionWith(directoryResult.Select(searchResult => ResultManager.CreateResult(query, searchResult)));
return results.ToList();
}
private async Task<List<Result>> WindowsIndexFileContentSearchAsync(Query query, string querySearchString,
CancellationToken token)
{
var queryConstructor = new QueryConstructor(Settings);
public static bool IsFileContentSearch(string actionKeyword) => actionKeyword == Settings.FileContentSearchActionKeyword;
if (string.IsNullOrEmpty(querySearchString))
return new List<Result>();
return await IndexSearch.WindowsIndexSearchAsync(
querySearchString,
queryConstructor.CreateQueryHelper,
queryConstructor.QueryForFileContentSearch,
Settings.IndexSearchExcludedSubdirectoryPaths,
query,
token).ConfigureAwait(false);
}
public bool IsFileContentSearch(string actionKeyword)
{
return actionKeyword == Settings.FileContentSearchActionKeyword;
}
private List<Result> DirectoryInfoClassSearch(Query query, string querySearch, CancellationToken token)
{
return DirectoryInfoSearch.TopLevelDirectorySearch(query, querySearch, token);
}
public async Task<List<Result>> TopLevelDirectorySearchBehaviourAsync(
Func<Query, string, CancellationToken, Task<List<Result>>> windowsIndexSearch,
Func<Query, string, CancellationToken, List<Result>> directoryInfoClassSearch,
bool useIndexSearch,
Query query,
string querySearchString,
CancellationToken token)
{
if (!useIndexSearch)
return directoryInfoClassSearch(query, querySearchString, token);
return await windowsIndexSearch(query, querySearchString, token);
}
private async Task<List<Result>> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString,
CancellationToken token)
{
var queryConstructor = new QueryConstructor(Settings);
return await IndexSearch.WindowsIndexSearchAsync(
querySearchString,
queryConstructor.CreateQueryHelper,
queryConstructor.QueryForAllFilesAndFolders,
Settings.IndexSearchExcludedSubdirectoryPaths,
query,
token).ConfigureAwait(false);
}
private async Task<List<Result>> WindowsIndexTopLevelFolderSearchAsync(Query query, string path,
CancellationToken token)
{
var queryConstructor = new QueryConstructor(Settings);
return await IndexSearch.WindowsIndexSearchAsync(
path,
queryConstructor.CreateQueryHelper,
queryConstructor.QueryForTopLevelDirectorySearch,
Settings.IndexSearchExcludedSubdirectoryPaths,
query,
token).ConfigureAwait(false);
}
private bool UseWindowsIndexForDirectorySearch(string locationPath)
{
var pathToDirectory = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath);
if (!Settings.UseWindowsIndexForDirectorySearch)
return false;
if (Settings.IndexSearchExcludedSubdirectoryPaths
.Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory)
.StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)))
return false;
return IndexSearch.PathIsIndexed(pathToDirectory);
return !Settings.IndexSearchExcludedSubdirectoryPaths.Any(
x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory).StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))
&& WindowsIndex.WindowsIndex.PathIsIndexed(pathToDirectory);
}
}
}

View file

@ -0,0 +1,13 @@
using System;
namespace Flow.Launcher.Plugin.Explorer.Search
{
public record struct SearchResult
{
public string FullPath { get; init; }
public ResultType Type { get; init; }
public int Score { get; init; }
public bool WindowsIndexed { get; init; }
}
}

View file

@ -1,227 +0,0 @@
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Microsoft.Search.Interop;
using System;
using System.Collections.Generic;
using System.Data.OleDb;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
internal static class IndexSearch
{
// Reserved keywords in oleDB
private const string reservedStringPattern = @"^[`\@\\#\\\^,\&\\/\\\$\%_;\[\]]+$";
internal static async Task<List<Result>> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token)
{
var results = new List<Result>();
var fileResults = new List<Result>();
try
{
await using var conn = new OleDbConnection(connectionString);
await conn.OpenAsync(token);
token.ThrowIfCancellationRequested();
await using var command = new OleDbCommand(indexQueryString, conn);
// Results return as an OleDbDataReader.
await using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader;
token.ThrowIfCancellationRequested();
if (dataReaderResults.HasRows)
{
while (await dataReaderResults.ReadAsync(token))
{
token.ThrowIfCancellationRequested();
if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value)
{
// # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path
var encodedFragmentPath = dataReaderResults
.GetString(1)
.Replace("#", "%23", StringComparison.OrdinalIgnoreCase);
var path = new Uri(encodedFragmentPath).LocalPath;
if (dataReaderResults.GetString(2) == "Directory")
{
results.Add(ResultManager.CreateFolderResult(
dataReaderResults.GetString(0),
path,
path,
query, 0, true, true));
}
else
{
fileResults.Add(ResultManager.CreateFileResult(path, query, 0, true, true));
}
}
}
}
}
catch (OperationCanceledException)
{
// return empty result when cancelled
return results;
}
catch (InvalidOperationException e)
{
// Internal error from ExecuteReader(): Connection closed.
LogException("Internal error from ExecuteReader()", e);
}
catch (Exception e)
{
LogException("General error from performing index search", e);
}
results.AddRange(fileResults);
// Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
return results;
}
internal async static Task<List<Result>> WindowsIndexSearchAsync(
string searchString,
Func<CSearchQueryHelper> createQueryHelper,
Func<string, string> constructQuery,
List<AccessLink> exclusionList,
Query query,
CancellationToken token)
{
var regexMatch = Regex.Match(searchString, reservedStringPattern);
if (regexMatch.Success)
return new List<Result>();
try
{
var constructedQuery = constructQuery(searchString);
return RemoveResultsInExclusionList(
await ExecuteWindowsIndexSearchAsync(constructedQuery, createQueryHelper().ConnectionString, query, token).ConfigureAwait(false),
exclusionList,
token);
}
catch (COMException)
{
// Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
if (!SearchManager.Settings.WarnWindowsSearchServiceOff)
return new List<Result>();
return ResultForWindexSearchOff(query.RawQuery);
}
}
private static List<Result> RemoveResultsInExclusionList(List<Result> results, List<AccessLink> exclusionList, CancellationToken token)
{
var indexExclusionListCount = exclusionList.Count;
if (indexExclusionListCount == 0)
return results;
var filteredResults = new List<Result>();
for (var index = 0; index < results.Count; index++)
{
token.ThrowIfCancellationRequested();
var excludeResult = false;
for (var i = 0; i < indexExclusionListCount; i++)
{
token.ThrowIfCancellationRequested();
if (results[index].SubTitle.StartsWith(exclusionList[i].Path, StringComparison.OrdinalIgnoreCase))
{
excludeResult = true;
break;
}
}
if (!excludeResult)
filteredResults.Add(results[index]);
}
return filteredResults;
}
internal static bool PathIsIndexed(string path)
{
try
{
var csm = new CSearchManager();
var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
return indexManager.IncludedInCrawlScope(path) > 0;
}
catch(COMException)
{
// Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
return false;
}
}
private static List<Result> ResultForWindexSearchOff(string rawQuery)
{
var api = SearchManager.Context.API;
return new List<Result>
{
new Result
{
Title = api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
SubTitle = api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
Action = c =>
{
SearchManager.Settings.WarnWindowsSearchServiceOff = false;
var pluginsManagerPlugin= api.GetAllPlugins().FirstOrDefault(x => x.Metadata.ID == "9f8f9b14-2518-4907-b211-35ab6290dee7");
var actionKeywordCount = pluginsManagerPlugin.Metadata.ActionKeywords.Count;
if (actionKeywordCount > 1)
LogException("PluginsManager's action keyword has increased to more than 1, this does not allow for determining the " +
"right action keyword. Explorer's code for managing Windows Search service not running exception needs to be updated",
new InvalidOperationException());
if (MessageBox.Show(string.Format(api.GetTranslation("plugin_explorer_alternative"), Environment.NewLine),
api.GetTranslation("plugin_explorer_alternative_title"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes
&& actionKeywordCount == 1)
{
api.ChangeQuery(string.Format("{0} install everything", pluginsManagerPlugin.Metadata.ActionKeywords[0]));
}
else
{
// Clears the warning message because same query string will not alter the displayed result list
api.ChangeQuery(string.Empty);
api.ChangeQuery(rawQuery);
}
var mainWindow = Application.Current.MainWindow;
mainWindow.Show();
mainWindow.Focus();
return false;
},
IcoPath = Constants.ExplorerIconImagePath
}
};
}
private static void LogException(string message, Exception e)
{
#if DEBUG // Please investigate and handle error from index search
throw e;
#else
Log.Exception($"|Flow.Launcher.Plugin.Explorer.IndexSearch|{message}", e);
#endif
}
}
}

View file

@ -1,24 +1,30 @@
using System;
using System.Buffers;
using Microsoft.Search.Interop;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
public class QueryConstructor
{
private readonly Settings settings;
private Settings Settings { get; }
private const string SystemIndex = "SystemIndex";
public CSearchQueryHelper BaseQueryHelper { get; }
public QueryConstructor(Settings settings)
{
this.settings = settings;
Settings = settings;
BaseQueryHelper = CreateBaseQuery();
}
public CSearchQueryHelper CreateBaseQuery()
private CSearchQueryHelper CreateBaseQuery()
{
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";
@ -32,7 +38,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
return baseQuery;
}
internal CSearchQueryHelper CreateQueryHelper()
internal static CSearchQueryHelper CreateQueryHelper()
{
// This uses the Microsoft.Search.Interop assembly
var manager = new CSearchManager();
@ -42,98 +48,67 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
// Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer
var queryHelper = catalogManager.GetQueryHelper();
return queryHelper;
}
///<summary>
/// Set the required WHERE clause restriction to search on the first level of a specified directory.
///</summary>
public string QueryWhereRestrictionsForTopLevelDirectorySearch(string path)
{
var searchDepth = $"directory='file:";
return QueryWhereRestrictionsFromLocationPath(path, searchDepth);
}
///<summary>
/// Set the required WHERE clause restriction to search all files and subfolders of a specified directory.
///</summary>
public string QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(string path)
{
var searchDepth = $"scope='file:";
return QueryWhereRestrictionsFromLocationPath(path, searchDepth);
}
private string QueryWhereRestrictionsFromLocationPath(string path, string searchDepth)
{
if (path.EndsWith(Constants.DirectorySeperator))
return searchDepth + $"{path}'";
var indexOfSeparator = path.LastIndexOf(Constants.DirectorySeperator);
var itemName = path.Substring(indexOfSeparator + 1);
if (itemName.StartsWith(Constants.AllFilesFolderSearchWildcard))
itemName = itemName.Substring(1);
var previousLevelDirectory = path.Substring(0, indexOfSeparator);
if (string.IsNullOrEmpty(itemName))
return $"{searchDepth}{previousLevelDirectory}'";
return $"(System.FileName LIKE '{itemName}%' OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND {searchDepth}{previousLevelDirectory}'";
}
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 QueryForTopLevelDirectorySearch(string path)
public string Directory(ReadOnlySpan<char> path, ReadOnlySpan<char> searchString = default, bool recursive = false)
{
string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE ";
var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND ({FileName} LIKE '{searchString}%' OR CONTAINS({FileName},'\"{searchString}*\"'))";
if (path.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > path.LastIndexOf(Constants.DirectorySeperator))
return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path) + QueryOrderByFileNameRestriction;
var scopeConstraint = recursive
? RecursiveDirectoryConstraint(path)
: TopLevelDirectoryConstraint(path);
return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path) + QueryOrderByFileNameRestriction;
var query = $"SELECT TOP {Settings.MaxResult} {BaseQueryHelper.QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {FileName}";
return query;
}
///<summary>
/// Search will be performed on all folders and files based on user's search keywords.
///</summary>
public string QueryForAllFilesAndFolders(string userSearchString)
public string FilesAndFolders(ReadOnlySpan<char> userSearchString)
{
if (userSearchString.IsWhiteSpace())
userSearchString = "*";
// Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause
return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch
+ QueryOrderByFileNameRestriction;
return $"{BaseQueryHelper.GenerateSQLFromUserQuery(userSearchString.ToString())} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
}
///<summary>
/// Set the required WHERE clause restriction to search for all files and folders.
///</summary>
public const string QueryWhereRestrictionsForAllFilesAndFoldersSearch = "scope='file:'";
public const string RestrictionsForAllFilesAndFoldersSearch = "scope='file:'";
public const string QueryOrderByFileNameRestriction = " ORDER BY System.FileName";
/// <summary>
/// Order identifier: file name
/// </summary>
public const string FileName = "System.FileName";
///<summary>
/// Search will be performed on all indexed file contents for the specified search keywords.
///</summary>
public string QueryForFileContentSearch(string userSearchString)
public string FileContent(ReadOnlySpan<char> userSearchString)
{
string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE ";
string query =
$"SELECT TOP {Settings.MaxResult} {BaseQueryHelper.QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
return query + QueryWhereRestrictionsForFileContentSearch(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch
+ QueryOrderByFileNameRestriction;
return query;
}
///<summary>
/// Set the required WHERE clause restriction to search within file content.
///</summary>
public string QueryWhereRestrictionsForFileContentSearch(string searchQuery)
{
return $"FREETEXT('{searchQuery}')";
}
public static string RestrictionsForFileContentSearch(ReadOnlySpan<char> searchQuery) => $"FREETEXT('{searchQuery}')";
}
}

View file

@ -0,0 +1,132 @@
using Flow.Launcher.Infrastructure.Logger;
using Microsoft.Search.Interop;
using System;
using System.Collections.Generic;
using System.Data.OleDb;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Plugin.Explorer.Exceptions;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
internal static class WindowsIndex
{
// Reserved keywords in oleDB
private static Regex _reservedPatternMatcher = new(@"^[`\@\\#\\\^,\&\\/\\\$\%_;\[\]]+$", RegexOptions.Compiled);
private static async IAsyncEnumerable<SearchResult> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, [EnumeratorCancellation] CancellationToken token)
{
await using var conn = new OleDbConnection(connectionString);
await conn.OpenAsync(token);
token.ThrowIfCancellationRequested();
await using var command = new OleDbCommand(indexQueryString, conn);
// Results return as an OleDbDataReader.
OleDbDataReader dataReaderAttempt;
try
{
dataReaderAttempt = await command.ExecuteReaderAsync(token) as OleDbDataReader;
}
catch (OleDbException e)
{
Log.Exception($"|WindowsIndex.ExecuteWindowsIndexSearchAsync|Failed to execute windows index search query: {indexQueryString}", e);
yield break;
}
await using var dataReader = dataReaderAttempt;
token.ThrowIfCancellationRequested();
if (dataReader is not { HasRows: true })
{
yield break;
}
while (await dataReader.ReadAsync(token))
{
token.ThrowIfCancellationRequested();
if (dataReader.GetValue(0) == DBNull.Value || dataReader.GetValue(1) == DBNull.Value)
{
continue;
}
// # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path
var encodedFragmentPath = dataReader
.GetString(1)
.Replace("#", "%23", StringComparison.OrdinalIgnoreCase);
var path = new Uri(encodedFragmentPath).LocalPath;
yield return new SearchResult
{
FullPath = path,
Type = dataReader.GetString(2) == "Directory" ? ResultType.Folder : ResultType.File,
WindowsIndexed = true
};
}
// Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
}
internal static IAsyncEnumerable<SearchResult> WindowsIndexSearchAsync(
string connectionString,
string search,
CancellationToken token)
{
try
{
return _reservedPatternMatcher.IsMatch(search)
? AsyncEnumerable.Empty<SearchResult>()
: ExecuteWindowsIndexSearchAsync(search, connectionString, token);
}
catch (InvalidOperationException e)
{
throw new SearchException("Windows Index", e.Message, e);
}
catch (COMException)
{
// Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
if (!SearchManager.Settings.WarnWindowsSearchServiceOff)
return AsyncEnumerable.Empty<SearchResult>();
var api = SearchManager.Context.API;
throw new EngineNotAvailableException(
"Windows Index",
api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
c =>
{
SearchManager.Settings.WarnWindowsSearchServiceOff = false;
// Clears the warning message so user is not mistaken that it has not worked
api.ChangeQuery(string.Empty);
return ValueTask.FromResult(false);
})
{
ErrorIcon = Constants.WindowsIndexErrorImagePath
};
}
}
internal static bool PathIsIndexed(string path)
{
try
{
var csm = new CSearchManager();
var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
return indexManager.IncludedInCrawlScope(path) > 0;
}
catch (COMException)
{
// Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
return false;
}
}
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Plugin.Explorer.Search.IProvider;
using Microsoft.Search.Interop;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
public class WindowsIndexSearchManager : IIndexProvider, IContentIndexProvider, IPathIndexProvider
{
private Settings Settings { get; }
private QueryConstructor QueryConstructor { get; }
private CSearchQueryHelper QueryHelper { get; }
public WindowsIndexSearchManager(Settings settings)
{
Settings = settings;
QueryConstructor = new QueryConstructor(Settings);
QueryHelper = QueryConstructor.CreateQueryHelper();
}
private IAsyncEnumerable<SearchResult> WindowsIndexFileContentSearchAsync(
ReadOnlySpan<char> querySearchString,
CancellationToken token)
{
if (querySearchString.IsEmpty)
return AsyncEnumerable.Empty<SearchResult>();
return WindowsIndex.WindowsIndexSearchAsync(
QueryHelper.ConnectionString,
QueryConstructor.FileContent(querySearchString),
token);
}
private IAsyncEnumerable<SearchResult> WindowsIndexFilesAndFoldersSearchAsync(
ReadOnlySpan<char> querySearchString,
CancellationToken token = default)
{
return WindowsIndex.WindowsIndexSearchAsync(
QueryHelper.ConnectionString,
QueryConstructor.FilesAndFolders(querySearchString),
token);
}
private IAsyncEnumerable<SearchResult> WindowsIndexTopLevelFolderSearchAsync(
ReadOnlySpan<char> search,
ReadOnlySpan<char> path,
bool recursive,
CancellationToken token)
{
var queryConstructor = new QueryConstructor(Settings);
return WindowsIndex.WindowsIndexSearchAsync(
QueryConstructor.CreateQueryHelper().ConnectionString,
queryConstructor.Directory(path, search, recursive),
token);
}
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);
}
}
}

View file

@ -1,7 +1,15 @@
using Flow.Launcher.Plugin.Everything.Everything;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin.Explorer.Search.IProvider;
namespace Flow.Launcher.Plugin.Explorer
{
@ -9,14 +17,19 @@ namespace Flow.Launcher.Plugin.Explorer
{
public int MaxResult { get; set; } = 100;
public List<AccessLink> QuickAccessLinks { get; set; } = new List<AccessLink>();
public ObservableCollection<AccessLink> QuickAccessLinks { get; set; } = new();
// as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards.
public List<AccessLink> QuickFolderAccessLinks { get; set; } = new List<AccessLink>();
public ObservableCollection<AccessLink> IndexSearchExcludedSubdirectoryPaths { get; set; } = new ObservableCollection<AccessLink>();
public bool UseWindowsIndexForDirectorySearch { get; set; } = false;
public string EditorPath { get; set; } = "";
public string ShellPath { get; set; } = "cmd";
public bool UseLocationAsWorkingDir { get; set; } = false;
public bool ShowWindowsContextMenu { get; set; } = true;
public List<AccessLink> IndexSearchExcludedSubdirectoryPaths { get; set; } = new List<AccessLink>();
public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
@ -38,8 +51,92 @@ namespace Flow.Launcher.Plugin.Explorer
public bool QuickAccessKeywordEnabled { get; set; }
public bool WarnWindowsSearchServiceOff { get; set; } = true;
private EverythingSearchManager _everythingManagerInstance;
private WindowsIndexSearchManager _windowsIndexSearchManager;
#region SearchEngine
private EverythingSearchManager EverythingManagerInstance => _everythingManagerInstance ??= new EverythingSearchManager(this);
private WindowsIndexSearchManager WindowsIndexSearchManager => _windowsIndexSearchManager ??= new WindowsIndexSearchManager(this);
public IndexSearchEngineOption IndexSearchEngine { get; set; } = IndexSearchEngineOption.WindowsIndex;
[JsonIgnore]
public IIndexProvider IndexProvider => IndexSearchEngine switch
{
IndexSearchEngineOption.Everything => EverythingManagerInstance,
IndexSearchEngineOption.WindowsIndex => WindowsIndexSearchManager,
_ => throw new ArgumentOutOfRangeException(nameof(IndexSearchEngine))
};
public PathEnumerationEngineOption PathEnumerationEngine { get; set; } = PathEnumerationEngineOption.WindowsIndex;
[JsonIgnore]
public IPathIndexProvider PathEnumerator => PathEnumerationEngine switch
{
PathEnumerationEngineOption.Everything => EverythingManagerInstance,
PathEnumerationEngineOption.WindowsIndex => WindowsIndexSearchManager,
_ => throw new ArgumentOutOfRangeException(nameof(PathEnumerationEngine))
};
public ContentIndexSearchEngineOption ContentSearchEngine { get; set; } = ContentIndexSearchEngineOption.WindowsIndex;
[JsonIgnore]
public IContentIndexProvider ContentIndexProvider => ContentSearchEngine switch
{
ContentIndexSearchEngineOption.Everything => EverythingManagerInstance,
ContentIndexSearchEngineOption.WindowsIndex => WindowsIndexSearchManager,
_ => throw new ArgumentOutOfRangeException(nameof(ContentSearchEngine))
};
public enum PathEnumerationEngineOption
{
[Description("plugin_explorer_engine_windows_index")]
WindowsIndex,
[Description("plugin_explorer_engine_everything")]
Everything,
[Description("plugin_explorer_path_enumeration_engine_none")]
DirectEnumeration
}
public enum IndexSearchEngineOption
{
[Description("plugin_explorer_engine_windows_index")]
WindowsIndex,
[Description("plugin_explorer_engine_everything")]
Everything,
}
public enum ContentIndexSearchEngineOption
{
[Description("plugin_explorer_engine_windows_index")]
WindowsIndex,
[Description("plugin_explorer_engine_everything")]
Everything,
}
#endregion
#region Everything Settings
public string EverythingInstalledPath { get; set; }
[JsonIgnore]
public SortOption[] SortOptions { get; set; } = Enum.GetValues<SortOption>();
public SortOption SortOption { get; set; } = SortOption.NAME_ASCENDING;
public bool EnableEverythingContentSearch { get; set; } = false;
public bool EverythingEnabled => IndexSearchEngine == IndexSearchEngineOption.Everything ||
PathEnumerationEngine == PathEnumerationEngineOption.Everything ||
ContentSearchEngine == ContentIndexSearchEngineOption.Everything;
#endregion
internal enum ActionKeyword
{
SearchActionKeyword,
@ -89,4 +186,4 @@ namespace Flow.Launcher.Plugin.Explorer
_ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "ActionKeyword enabled status not defined")
};
}
}
}

View file

@ -0,0 +1,57 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Flow.Launcher.Plugin.Explorer.Views
{
public class ActionKeywordModel : INotifyPropertyChanged
{
private static Settings _settings;
public event PropertyChangedEventHandler PropertyChanged;
public static void Init(Settings settings)
{
_settings = settings;
}
internal ActionKeywordModel(Settings.ActionKeyword actionKeyword, string description)
{
KeywordProperty = actionKeyword;
Description = description;
}
public string Description { get; private init; }
internal Settings.ActionKeyword KeywordProperty { get; }
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string? keyword;
public string Keyword
{
get => keyword ??= _settings.GetActionKeyword(KeywordProperty);
set
{
keyword = value;
_settings.SetActionKeyword(KeywordProperty, value);
OnPropertyChanged();
}
}
private bool? enabled;
public bool Enabled
{
get => enabled ??= _settings.GetActionKeywordEnabled(KeywordProperty);
set
{
enabled = value;
_settings.SetActionKeywordEnabled(KeywordProperty, value);
OnPropertyChanged();
}
}
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Flow.Launcher.Plugin.Explorer.ViewModels;
public class EnumBindingModel<T> where T : struct, Enum
{
public static IReadOnlyList<EnumBindingModel<T>> CreateList()
{
return Enum.GetValues<T>()
.Select(value => new EnumBindingModel<T>
{
Value = value, LocalizationKey = GetDescriptionAttr(value)
})
.ToArray();
}
public EnumBindingModel<T> From(T value)
{
var name = value.ToString();
var description = GetDescriptionAttr(value);
return new EnumBindingModel<T>
{
Name = name,
LocalizationKey = description,
Value = value
};
}
private static string GetDescriptionAttr(T source)
{
var fi = source.GetType().GetField(source.ToString());
var attributes = (DescriptionAttribute[])fi?.GetCustomAttributes(
typeof(DescriptionAttribute), false);
return attributes is { Length: > 0 } ? attributes[0].Description : source.ToString();
}
public string Name { get; set; }
private string LocalizationKey { get; set; }
public string Description => Main.Context.API.GetTranslation(LocalizationKey);
public T Value { get; set; }
}

View file

@ -0,0 +1,27 @@
using System;
using System.Windows.Input;
namespace Flow.Launcher.Plugin.Explorer.ViewModels
{
internal class RelayCommand : ICommand
{
private Action<object> _action;
public RelayCommand(Action<object> action)
{
_action = action;
}
public virtual bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public virtual void Execute(object parameter)
{
_action?.Invoke(parameter);
}
}
}

View file

@ -1,22 +1,40 @@
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.Storage;
#nullable enable
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Views;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using MessageBox = System.Windows.Forms.MessageBox;
namespace Flow.Launcher.Plugin.Explorer.ViewModels
{
public class SettingsViewModel
public class SettingsViewModel : BaseModel
{
internal Settings Settings { get; set; }
public Settings Settings { get; set; }
internal PluginInitContext Context { get; set; }
public IReadOnlyList<EnumBindingModel<Settings.IndexSearchEngineOption>> IndexSearchEngines { get; set; }
public IReadOnlyList<EnumBindingModel<Settings.ContentIndexSearchEngineOption>> ContentIndexSearchEngines { get; set; }
public IReadOnlyList<EnumBindingModel<Settings.PathEnumerationEngineOption>> PathEnumerationEngines { get; set; }
public SettingsViewModel(PluginInitContext context, Settings settings)
{
Context = context;
Settings = settings;
InitializeEngineSelection();
InitializeActionKeywordModels();
}
@ -25,11 +43,267 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
Context.API.SaveSettingJsonStorage<Settings>();
}
internal void RemoveLinkFromQuickAccess(AccessLink selectedRow) => Settings.QuickAccessLinks.Remove(selectedRow);
#region Engine Selection
internal void RemoveAccessLinkFromExcludedIndexPaths(AccessLink selectedRow) => Settings.IndexSearchExcludedSubdirectoryPaths.Remove(selectedRow);
private EnumBindingModel<Settings.IndexSearchEngineOption> _selectedIndexSearchEngine;
private EnumBindingModel<Settings.ContentIndexSearchEngineOption> _selectedContentSearchEngine;
private EnumBindingModel<Settings.PathEnumerationEngineOption> _selectedPathEnumerationEngine;
internal void OpenWindowsIndexingOptions()
public EnumBindingModel<Settings.IndexSearchEngineOption> SelectedIndexSearchEngine
{
get => _selectedIndexSearchEngine;
set
{
_selectedIndexSearchEngine = value;
Settings.IndexSearchEngine = value.Value;
OnPropertyChanged();
}
}
public EnumBindingModel<Settings.ContentIndexSearchEngineOption> SelectedContentSearchEngine
{
get => _selectedContentSearchEngine;
set
{
_selectedContentSearchEngine = value;
Settings.ContentSearchEngine = value.Value;
OnPropertyChanged();
}
}
public EnumBindingModel<Settings.PathEnumerationEngineOption> SelectedPathEnumerationEngine
{
get => _selectedPathEnumerationEngine;
set
{
_selectedPathEnumerationEngine = value;
Settings.PathEnumerationEngine = value.Value;
OnPropertyChanged();
}
}
[MemberNotNull(nameof(IndexSearchEngines),
nameof(ContentIndexSearchEngines),
nameof(PathEnumerationEngines),
nameof(_selectedIndexSearchEngine),
nameof(_selectedContentSearchEngine),
nameof(_selectedPathEnumerationEngine))]
private void InitializeEngineSelection()
{
IndexSearchEngines = EnumBindingModel<Settings.IndexSearchEngineOption>.CreateList();
ContentIndexSearchEngines = EnumBindingModel<Settings.ContentIndexSearchEngineOption>.CreateList();
PathEnumerationEngines = EnumBindingModel<Settings.PathEnumerationEngineOption>.CreateList();
_selectedIndexSearchEngine = IndexSearchEngines.First(x => x.Value == Settings.IndexSearchEngine);
_selectedContentSearchEngine = ContentIndexSearchEngines.First(x => x.Value == Settings.ContentSearchEngine);
_selectedPathEnumerationEngine = PathEnumerationEngines.First(x => x.Value == Settings.PathEnumerationEngine);
}
#endregion
#region ActionKeyword
[MemberNotNull(nameof(ActionKeywordsModels))]
private void InitializeActionKeywordModels()
{
ActionKeywordsModels = new List<ActionKeywordModel>
{
new(Settings.ActionKeyword.SearchActionKeyword,
Context.API.GetTranslation("plugin_explorer_actionkeywordview_search")),
new(Settings.ActionKeyword.FileContentSearchActionKeyword,
Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch")),
new(Settings.ActionKeyword.PathSearchActionKeyword,
Context.API.GetTranslation("plugin_explorer_actionkeywordview_pathsearch")),
new(Settings.ActionKeyword.IndexSearchActionKeyword,
Context.API.GetTranslation("plugin_explorer_actionkeywordview_indexsearch")),
new(Settings.ActionKeyword.QuickAccessActionKeyword,
Context.API.GetTranslation("plugin_explorer_actionkeywordview_quickaccess"))
};
}
public IReadOnlyList<ActionKeywordModel> ActionKeywordsModels { get; set; }
public ActionKeywordModel? SelectedActionKeyword { get; set; }
public ICommand EditActionKeywordCommand => new RelayCommand(EditActionKeyword);
private void EditActionKeyword(object obj)
{
if (SelectedActionKeyword is not { } actionKeyword)
{
ShowUnselectedMessage();
return;
}
var actionKeywordWindow = new ActionKeywordSetting(actionKeyword, Context.API);
if (!(actionKeywordWindow.ShowDialog() ?? false))
{
return;
}
switch (actionKeyword.Enabled, actionKeywordWindow.KeywordEnabled)
{
case (true, false):
Context.API.RemoveActionKeyword(Context.CurrentPluginMetadata.ID, actionKeyword.Keyword);
break;
case (true, true):
// same keyword will have dialog result false
Context.API.RemoveActionKeyword(Context.CurrentPluginMetadata.ID, actionKeyword.Keyword);
Context.API.AddActionKeyword(Context.CurrentPluginMetadata.ID, actionKeywordWindow.ActionKeyword);
break;
case (false, true):
Context.API.AddActionKeyword(Context.CurrentPluginMetadata.ID, actionKeywordWindow.ActionKeyword);
break;
case (false, false):
throw new ArgumentException(
$"Both false in {nameof(actionKeyword)}.{nameof(actionKeyword.Enabled)} and {nameof(actionKeywordWindow)}.{nameof(actionKeywordWindow.KeywordEnabled)} should suggest that the ShowDialog() result is false");
}
(actionKeyword.Keyword, actionKeyword.Enabled) = (actionKeywordWindow.ActionKeyword, actionKeywordWindow.KeywordEnabled);
}
#endregion
#region AccessLinks
public AccessLink? SelectedQuickAccessLink { get; set; }
public AccessLink? SelectedIndexSearchExcludedPath { get; set; }
public ICommand RemoveLinkCommand => new RelayCommand(RemoveLink);
public ICommand EditLinkCommand => new RelayCommand(EditLink);
public ICommand AddLinkCommand => new RelayCommand(AddLink);
public void AppendLink(string containerName, AccessLink link)
{
var container = containerName switch
{
"QuickAccessLink" => Settings.QuickAccessLinks,
"IndexSearchExcludedPaths" => Settings.IndexSearchExcludedSubdirectoryPaths,
_ => throw new ArgumentException($"Unknown container name: {containerName}")
};
container.Add(link);
}
private void EditLink(object commandParameter)
{
var (selectedLink, collection) = commandParameter switch
{
"QuickAccessLink" => (SelectedQuickAccessLink, Settings.QuickAccessLinks),
"IndexSearchExcludedPaths" => (SelectedIndexSearchExcludedPath, Settings.IndexSearchExcludedSubdirectoryPaths),
_ => throw new ArgumentOutOfRangeException(nameof(commandParameter))
};
if (selectedLink is null)
{
ShowUnselectedMessage();
return;
}
var path = PromptUserSelectPath(selectedLink.Type,
selectedLink.Type == ResultType.Folder
? selectedLink.Path
: Path.GetDirectoryName(selectedLink.Path));
if (path is null)
return;
collection.Remove(selectedLink);
collection.Add(new AccessLink
{
Path = path, Type = selectedLink.Type,
});
}
private void ShowUnselectedMessage()
{
var warning = Context.API.GetTranslation("plugin_explorer_make_selection_warning");
MessageBox.Show(warning);
}
private void AddLink(object commandParameter)
{
var container = commandParameter switch
{
"QuickAccessLink" => Settings.QuickAccessLinks,
"IndexSearchExcludedPaths" => Settings.IndexSearchExcludedSubdirectoryPaths,
_ => throw new ArgumentOutOfRangeException(nameof(commandParameter))
};
ArgumentNullException.ThrowIfNull(container);
var folderBrowserDialog = new FolderBrowserDialog();
if (folderBrowserDialog.ShowDialog() != DialogResult.OK)
return;
var newAccessLink = new AccessLink
{
Path = folderBrowserDialog.SelectedPath
};
container.Add(newAccessLink);
}
private void RemoveLink(object obj)
{
if (obj is not string container) return;
switch (container)
{
case "QuickAccessLink":
if (SelectedQuickAccessLink == null) return;
Settings.QuickAccessLinks.Remove(SelectedQuickAccessLink);
break;
case "IndexSearchExcludedPaths":
if (SelectedIndexSearchExcludedPath == null) return;
Settings.IndexSearchExcludedSubdirectoryPaths.Remove(SelectedIndexSearchExcludedPath);
break;
}
Save();
}
#endregion
private string? PromptUserSelectPath(ResultType type, string? initialDirectory = null)
{
string? path = null;
if (type is ResultType.Folder)
{
var folderBrowserDialog = new FolderBrowserDialog();
if (initialDirectory is not null)
folderBrowserDialog.InitialDirectory = initialDirectory;
if (folderBrowserDialog.ShowDialog() != DialogResult.OK)
return path;
path = folderBrowserDialog.SelectedPath;
}
else if (type is ResultType.File)
{
var openFileDialog = new OpenFileDialog();
if (initialDirectory is not null)
openFileDialog.InitialDirectory = initialDirectory;
if (openFileDialog.ShowDialog() != DialogResult.OK)
return path;
path = openFileDialog.FileName;
}
return path;
}
internal static void OpenWindowsIndexingOptions()
{
var psi = new ProcessStartInfo
{
@ -41,27 +315,106 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
Process.Start(psi);
}
internal void UpdateActionKeyword(Settings.ActionKeyword modifiedActionKeyword, string newActionKeyword, string oldActionKeyword)
private ICommand? _openEditorPathCommand;
public ICommand OpenEditorPath => _openEditorPathCommand ??= new RelayCommand(_ =>
{
PluginManager.ReplaceActionKeyword(Context.CurrentPluginMetadata.ID, oldActionKeyword, newActionKeyword);
}
var path = PromptUserSelectPath(ResultType.File, Settings.EditorPath != null ? Path.GetDirectoryName(Settings.EditorPath) : null);
if (path is null)
return;
internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword)
EditorPath = path;
});
private ICommand? _openShellPathCommand;
public ICommand OpenShellPath => _openShellPathCommand ??= new RelayCommand(_ =>
{
return PluginManager.ActionKeywordRegistered(newActionKeyword);
}
var path = PromptUserSelectPath(ResultType.File, Settings.EditorPath != null ? Path.GetDirectoryName(Settings.EditorPath) : null);
if (path is null)
return;
internal bool IsNewActionKeywordGlobal(string newActionKeyword) => newActionKeyword == Query.GlobalPluginWildcardSign;
ShellPath = path;
});
public bool UseWindowsIndexForDirectorySearch {
get
{
return Settings.UseWindowsIndexForDirectorySearch;
}
public string EditorPath
{
get => Settings.EditorPath;
set
{
Settings.UseWindowsIndexForDirectorySearch = value;
Settings.EditorPath = value;
OnPropertyChanged();
}
}
public string ShellPath
{
get => Settings.ShellPath;
set
{
Settings.ShellPath = value;
OnPropertyChanged();
}
}
#region Everything FastSortWarning
public Visibility FastSortWarningVisibility
{
get
{
try
{
return EverythingApi.IsFastSortOption(Settings.SortOption) ? Visibility.Collapsed : Visibility.Visible;
}
catch (IPCErrorException)
{
// this error occurs if the Everything service is not running, in this instance show the warning and
// update the message to let user know in the settings panel.
return Visibility.Visible;
}
catch (DllNotFoundException)
{
return Visibility.Collapsed;
}
}
}
public string SortOptionWarningMessage
{
get
{
try
{
// this method is used to determine if Everything service is running because as at Everything v1.4.1
// the sdk does not provide a dedicated interface to determine if it is running.
return EverythingApi.IsFastSortOption(Settings.SortOption) ? string.Empty
: Context.API.GetTranslation("flowlauncher_plugin_everything_nonfastsort_warning");
}
catch (IPCErrorException)
{
return Context.API.GetTranslation("flowlauncher_plugin_everything_is_not_running");
}
catch (DllNotFoundException)
{
return Context.API.GetTranslation("flowlauncher_plugin_everything_sdk_issue");
}
}
}
public string EverythingInstalledPath
{
get => Settings.EverythingInstalledPath;
set
{
Settings.EverythingInstalledPath = value;
OnPropertyChanged();
}
}
#endregion
}
}

View file

@ -77,8 +77,6 @@
Text="{DynamicResource plugin_explorer_actionkeyword_current}" />
<TextBox
Name="TxtCurrentActionKeyword"
Grid.Row="0"
Grid.Column="1"
Width="135"
HorizontalAlignment="Left"
VerticalAlignment="Center"
@ -97,7 +95,7 @@
Name="ChkActionKeywordEnabled"
Width="auto"
VerticalAlignment="Center"
IsChecked="{Binding Enabled}"
IsChecked="{Binding KeywordEnabled, Mode=TwoWay}"
ToolTip="{DynamicResource plugin_explorer_actionkeyword_enabled_tooltip}" />
</StackPanel>
</StackPanel>

View file

@ -2,7 +2,9 @@ using Flow.Launcher.Plugin.Explorer.ViewModels;
using ICSharpCode.SharpZipLib.Zip;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
@ -11,11 +13,9 @@ namespace Flow.Launcher.Plugin.Explorer.Views
/// <summary>
/// Interaction logic for ActionKeywordSetting.xaml
/// </summary>
public partial class ActionKeywordSetting : Window
public partial class ActionKeywordSetting : INotifyPropertyChanged
{
private SettingsViewModel settingsViewModel;
public ActionKeywordView CurrentActionKeyword { get; set; }
private ActionKeywordModel CurrentActionKeyword { get; }
public string ActionKeyword
{
@ -23,24 +23,27 @@ namespace Flow.Launcher.Plugin.Explorer.Views
set
{
// Set Enable to be true if user change ActionKeyword
Enabled = true;
actionKeyword = value;
KeywordEnabled = true;
_ = SetField(ref actionKeyword, value);
}
}
public bool Enabled { get; set; }
public bool KeywordEnabled
{
get => _keywordEnabled;
set => SetField(ref _keywordEnabled, value);
}
private string actionKeyword;
private readonly IPublicAPI api;
private bool _keywordEnabled;
public ActionKeywordSetting(SettingsViewModel settingsViewModel,
ActionKeywordView selectedActionKeyword)
public ActionKeywordSetting(ActionKeywordModel selectedActionKeyword, IPublicAPI api)
{
this.settingsViewModel = settingsViewModel;
CurrentActionKeyword = selectedActionKeyword;
this.api = api;
ActionKeyword = selectedActionKeyword.Keyword;
Enabled = selectedActionKeyword.Enabled;
KeywordEnabled = selectedActionKeyword.Enabled;
InitializeComponent();
@ -52,56 +55,38 @@ namespace Flow.Launcher.Plugin.Explorer.Views
if (string.IsNullOrEmpty(ActionKeyword))
ActionKeyword = Query.GlobalPluginWildcardSign;
if (CurrentActionKeyword.Keyword == ActionKeyword && CurrentActionKeyword.Enabled == Enabled)
if (CurrentActionKeyword.Keyword == ActionKeyword && CurrentActionKeyword.Enabled == KeywordEnabled)
{
DialogResult = false;
Close();
return;
}
if (ActionKeyword == Query.GlobalPluginWildcardSign)
switch (CurrentActionKeyword.KeywordProperty)
switch (CurrentActionKeyword.KeywordProperty, KeywordEnabled)
{
case Settings.ActionKeyword.FileContentSearchActionKeyword:
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
case (Settings.ActionKeyword.FileContentSearchActionKeyword, true):
MessageBox.Show(api.GetTranslation("plugin_explorer_globalActionKeywordInvalid"));
return;
case Settings.ActionKeyword.QuickAccessActionKeyword:
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("plugin_explorer_quickaccess_globalActionKeywordInvalid"));
case (Settings.ActionKeyword.QuickAccessActionKeyword, true):
MessageBox.Show(api.GetTranslation("plugin_explorer_quickaccess_globalActionKeywordInvalid"));
return;
}
var oldActionKeyword = CurrentActionKeyword.Keyword;
if (!Enabled || !settingsViewModel.IsActionKeywordAlreadyAssigned(ActionKeyword))
if (!KeywordEnabled || !api.ActionKeywordAssigned(ActionKeyword))
{
// Update View Data
CurrentActionKeyword.Keyword = Enabled == true ? ActionKeyword : Query.GlobalPluginWildcardSign;
CurrentActionKeyword.Enabled = Enabled;
switch (Enabled)
{
// reset to global so it does not take up an action keyword when disabled
// not for null Enable plugin
case false when oldActionKeyword != Query.GlobalPluginWildcardSign:
settingsViewModel.UpdateActionKeyword(CurrentActionKeyword.KeywordProperty,
Query.GlobalPluginWildcardSign, oldActionKeyword);
break;
default:
settingsViewModel.UpdateActionKeyword(CurrentActionKeyword.KeywordProperty,
CurrentActionKeyword.Keyword, oldActionKeyword);
break;
}
DialogResult = true;
Close();
return;
}
// The keyword is not valid, so show message
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
MessageBox.Show(api.GetTranslation("newActionKeywordsHasBeenAssigned"));
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void TxtCurrentActionKeyword_OnKeyDown(object sender, KeyEventArgs e)
@ -113,5 +98,18 @@ namespace Flow.Launcher.Plugin.Explorer.Views
e.Handled = true;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}
}

View file

@ -0,0 +1,20 @@
using Flow.Launcher.Plugin.Everything.Everything;
using Flow.Launcher.Plugin.Explorer.Helper;
using System;
using System.Globalization;
using System.Windows.Data;
namespace Flow.Launcher.Plugin.Explorer.Views.Converters;
public class EnumNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is SortOption option ? option.GetTranslatedName() : value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View file

@ -2,20 +2,111 @@
x:Class="Flow.Launcher.Plugin.Explorer.Views.ExplorerSettings"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Flow.Launcher.Plugin.Explorer.Views.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:qa="clr-namespace:Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:viewModels="clr-namespace:Flow.Launcher.Plugin.Explorer.ViewModels"
xmlns:views="clr-namespace:Flow.Launcher.Plugin.Explorer.Views"
d:DataContext="{d:DesignInstance viewModels:SettingsViewModel}"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Resources>
<DataTemplate x:Key="ListViewTemplateAccessLinks">
<Style x:Key="ExplorerTabItem" TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Border
x:Name="LayoutRoot"
Margin="0,0,0,0"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<Grid>
<Border
x:Name="TabSeparator"
Width="0"
Margin="{DynamicResource TabViewItemSeparatorMargin}"
HorizontalAlignment="Right"
BorderBrush="{DynamicResource TabViewItemSeparator}"
BorderThickness="1" />
<Border
x:Name="TabContainer"
Grid.Column="1"
Padding="{DynamicResource TabViewItemHeaderPadding}"
Background="{DynamicResource TabViewItemHeaderBackground}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="1"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentSource="Header"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
TextElement.FontSize="{DynamicResource TabViewItemHeaderFontSize}"
TextElement.FontWeight="{TemplateBinding FontWeight}"
TextElement.Foreground="{DynamicResource Color05B}" />
</Grid>
</Border>
</Grid>
</Border>
<ControlTemplate.Triggers>
<!-- PointerOver -->
<Trigger Property="IsMouseOver" Value="False">
<Setter TargetName="TabContainer" Property="Background" Value="{DynamicResource Color06B}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource Color08B}" />
</Trigger>
<!-- PointerOver -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="TabContainer" Property="Background" Value="{DynamicResource Color06B}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource TabViewItemHeaderForegroundPointerOver}" />
</Trigger>
<!-- Selected -->
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="TabContainer" Property="Background" Value="{DynamicResource Color00B}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
<Setter Property="Panel.ZIndex" Value="1" />
<Setter TargetName="ContentPresenter" Property="TextElement.FontWeight" Value="SemiBold" />
</Trigger>
<!-- PointerOverSelected -->
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsSelected" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="TabContainer" Property="Background" Value="{DynamicResource Color00B}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
<Setter Property="Panel.ZIndex" Value="1" />
<Setter TargetName="ContentPresenter" Property="TextElement.FontWeight" Value="SemiBold" />
</MultiTrigger>
<!-- Disabled -->
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource TabViewItemHeaderForegroundDisabled}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="ListViewTemplateAccessLinks" DataType="qa:AccessLink">
<TextBlock Margin="0,5,0,5" Text="{Binding Path, Mode=OneTime}" />
</DataTemplate>
<DataTemplate x:Key="ListViewTemplateExcludedPaths">
<TextBlock Margin="0,5,0,5" Text="{Binding Path, Mode=OneTime}" />
</DataTemplate>
<DataTemplate x:Key="ListViewActionKeywords" DataType="views:ActionKeywordView">
<DataTemplate x:Key="ListViewActionKeywords" DataType="{x:Type views:ActionKeywordModel}">
<Grid>
<TextBlock
Margin="0,5,0,0"
@ -37,7 +128,7 @@
<TextBlock
Margin="250,5,0,0"
IsEnabled="{Binding Enabled}"
Text="{Binding Keyword, Mode=OneTime}">
Text="{Binding Keyword}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
@ -53,102 +144,367 @@
</TextBlock>
</Grid>
</DataTemplate>
<converters:EnumNameConverter x:Key="EnumNameConverter" />
</UserControl.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<ScrollViewer
Grid.Row="0"
Margin="20,35,0,0"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<Expander
Name="expActionKeywords"
Collapsed="expActionKeywords_Collapsed"
Expanded="expActionKeywords_Click"
Header="{DynamicResource plugin_explorer_manageactionkeywords_header}">
<ListView x:Name="lbxActionKeywords" ItemTemplate="{StaticResource ListViewActionKeywords}" />
</Expander>
<Expander
Name="expAccessLinks"
Margin="0,10,0,0"
Collapsed="expAccessLinks_Collapsed"
Expanded="expAccessLinks_Click"
Header="{DynamicResource plugin_explorer_quickaccesslinks_header}">
<ListView
x:Name="lbxAccessLinks"
AllowDrop="True"
DragEnter="lbxAccessLinks_DragEnter"
Drop="lbxAccessLinks_Drop"
ItemTemplate="{StaticResource ListViewTemplateAccessLinks}" />
</Expander>
<Expander
x:Name="expExcludedPaths"
Margin="0,10,0,0"
Collapsed="expExcludedPaths_Collapsed"
Expanded="expExcludedPaths_Click"
Header="{DynamicResource plugin_explorer_indexsearchexcludedpaths_header}">
<Grid Margin="0">
<TabControl
x:Name="TabView"
MinHeight="0"
Background="{DynamicResource Color00B}"
BorderThickness="0"
SelectedIndex="0"
TabStripPlacement="Top">
<TabItem
Width="Auto"
Header="{DynamicResource plugin_explorer_generalsetting_header}"
Style="{DynamicResource ExplorerTabItem}">
<StackPanel Grid.Row="0" Margin="30,10,0,0">
<StackPanel>
<CheckBox
Name="UseWindowsIndexForDirectorySearch"
Margin="12,10,0,0"
Content="{DynamicResource plugin_explorer_usewindowsindexfordirectorysearch}"
IsChecked="{Binding UseWindowsIndexForDirectorySearch}"
ToolTip="{DynamicResource plugin_explorer_usewindowsindexfordirectorysearch_tooltip}" />
<ListView
x:Name="lbxExcludedPaths"
AllowDrop="True"
DragEnter="lbxAccessLinks_DragEnter"
Drop="lbxAccessLinks_Drop"
ItemTemplate="{StaticResource ListViewTemplateExcludedPaths}" />
<StackPanel Orientation="Vertical">
<CheckBox
Margin="0,10,0,0"
HorizontalAlignment="Left"
Content="{DynamicResource plugin_explorer_use_location_as_working_dir}"
IsChecked="{Binding Settings.UseLocationAsWorkingDir}" />
<StackPanel Margin="0,10,0,10" Orientation="Horizontal">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="A" />
<ColumnDefinition Width="*" SharedSizeGroup="B" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="0,6,16,6"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{DynamicResource plugin_explorer_editor_path}"
TextBlock.Foreground="{DynamicResource Color05B}" />
<StackPanel
Grid.Row="0"
Grid.Column="1"
Margin="0,6,6,6"
Orientation="Horizontal">
<TextBox
Width="250"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding EditorPath}"
TextWrapping="NoWrap" />
<Button
MinWidth="50"
Margin="5,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Command="{Binding OpenEditorPath}"
Content="..." />
</StackPanel>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="0,16,6,6"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{DynamicResource plugin_explorer_shell_path}"
TextBlock.Foreground="{DynamicResource Color05B}" />
<StackPanel
Grid.Row="1"
Grid.Column="1"
Orientation="Horizontal">
<TextBox
Width="250"
Margin="0,10,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding ShellPath}"
TextWrapping="NoWrap" />
<Button
MinWidth="50"
Margin="5,10,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Command="{Binding OpenShellPath}"
Content="..." />
</StackPanel>
</Grid>
</StackPanel>
<StackPanel Margin="0,0,0,5" Orientation="Horizontal">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="C" />
<ColumnDefinition Width="*" SharedSizeGroup="D" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="{DynamicResource plugin_explorer_Index_Search_Engine}"
TextBlock.Foreground="{DynamicResource Color05B}" />
<ComboBox
Grid.Row="0"
Grid.Column="1"
Width="250"
Margin="10,15,10,10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ItemsSource="{Binding IndexSearchEngines}"
SelectedItem="{Binding SelectedIndexSearchEngine}"
DisplayMemberPath="Description"/>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="{DynamicResource plugin_explorer_Content_Search_Engine}"
TextBlock.Foreground="{DynamicResource Color05B}" />
<ComboBox
Grid.Row="1"
Grid.Column="1"
Width="250"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ItemsSource="{Binding ContentIndexSearchEngines}"
SelectedItem="{Binding SelectedContentSearchEngine}"
DisplayMemberPath="Description"/>
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="{DynamicResource plugin_explorer_Directory_Recursive_Search_Engine}"
TextBlock.Foreground="{DynamicResource Color05B}" />
<ComboBox
Grid.Row="2"
Grid.Column="1"
Width="250"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ItemsSource="{Binding PathEnumerationEngines}"
DisplayMemberPath="Description"
SelectedItem="{Binding SelectedPathEnumerationEngine}" />
</Grid>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button
Margin="0,10,10,20"
Padding="20,10,20,10"
Click="btnOpenIndexingOptions_Click"
Content="{DynamicResource plugin_explorer_Open_Window_Index_Option}" />
</StackPanel>
</StackPanel>
</StackPanel>
</Expander>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="450" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Left"
Orientation="Horizontal">
<Button
x:Name="btnIndexingOptions"
MinWidth="130"
Margin="10"
Click="btnOpenIndexingOptions_Click"
Content="{DynamicResource plugin_explorer_manageindexoptions}" />
</StackPanel>
<StackPanel
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
x:Name="btnDelete"
MinWidth="100"
Margin="10"
Click="btnDelete_Click"
Content="{DynamicResource plugin_explorer_delete}" />
<Button
x:Name="btnEdit"
MinWidth="100"
Margin="10"
Click="btnEdit_Click"
Content="{DynamicResource plugin_explorer_edit}" />
<Button
x:Name="btnAdd"
MinWidth="100"
Margin="10"
Click="btnAdd_Click"
Content="{DynamicResource plugin_explorer_add}" />
</StackPanel>
</Grid>
</StackPanel>
</TabItem>
<TabItem
Width="Auto"
Header="{DynamicResource plugin_explorer_everything_setting_header}"
Style="{DynamicResource ExplorerTabItem}">
<StackPanel Margin="10" Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Grid Margin="20,10,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="F" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="0,0,20,0"
VerticalAlignment="Center"
Text="{DynamicResource plugin_explorer_everything_sort_option}"
TextBlock.Foreground="{DynamicResource Color05B}" />
<ComboBox
Grid.Row="1"
Grid.Column="1"
Width="200"
VerticalAlignment="Center"
ItemsSource="{Binding Settings.SortOptions, Mode=OneWay}"
SelectedItem="{Binding Settings.SortOption}"
SelectionChanged="EverythingSortOptionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Converter={StaticResource EnumNameConverter}}" />
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="0,15,20,0"
VerticalAlignment="Center"
Text="{DynamicResource plugin_explorer_everything_installed_path}"
TextBlock.Foreground="{DynamicResource Color05B}" />
<TextBox
Grid.Row="2"
Grid.Column="1"
Width="250"
Margin="0,15,0,0"
Text="{Binding EverythingInstalledPath}" />
</Grid>
</StackPanel>
<StackPanel>
<TextBlock
Name="tbFastSortWarning"
Margin="20,10,10,10"
VerticalAlignment="Center"
DockPanel.Dock="Right"
Foreground="Orange"
Text="{Binding SortOptionWarningMessage, Mode=OneTime}"
TextAlignment="Left"
TextWrapping="Wrap"
Visibility="{Binding FastSortWarningVisibility, Mode=OneTime}" />
</StackPanel>
</StackPanel>
</TabItem>
<TabItem
Width="Auto"
Header="{DynamicResource plugin_explorer_manageactionkeywords_header}"
Style="{DynamicResource ExplorerTabItem}">
<DockPanel HorizontalAlignment="Stretch">
<Border
Margin="10,10,10,5"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="1"
DockPanel.Dock="Top">
<ListView
ItemTemplate="{StaticResource ListViewActionKeywords}"
ItemsSource="{Binding ActionKeywordsModels}"
SelectedItem="{Binding SelectedActionKeyword}" />
</Border>
<StackPanel
HorizontalAlignment="Right"
DockPanel.Dock="Bottom"
Orientation="Horizontal">
<Button
MinWidth="100"
Margin="10"
Command="{Binding EditActionKeywordCommand}"
Content="{DynamicResource plugin_explorer_edit}" />
</StackPanel>
</DockPanel>
</TabItem>
<TabItem
Name="expAccessLinks"
Width="Auto"
Header="{DynamicResource plugin_explorer_quickaccesslinks_header}"
Style="{DynamicResource ExplorerTabItem}">
<ScrollViewer>
<DockPanel HorizontalAlignment="Stretch">
<Border
Margin="10,10,10,5"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="1"
DockPanel.Dock="Top">
<ListView
x:Name="lbxAccessLinks"
Height="200"
AllowDrop="True"
BorderThickness="1"
DragEnter="lbxAccessLinks_DragEnter"
Drop="LbxAccessLinks_OnDrop"
ItemTemplate="{StaticResource ListViewTemplateAccessLinks}"
ItemsSource="{Binding Settings.QuickAccessLinks}"
SelectedItem="{Binding SelectedQuickAccessLink}" />
</Border>
<StackPanel
HorizontalAlignment="Right"
DockPanel.Dock="Bottom"
Orientation="Horizontal">
<Button
MinWidth="100"
Margin="10"
Command="{Binding RemoveLinkCommand}"
CommandParameter="QuickAccessLink"
Content="{DynamicResource plugin_explorer_delete}" />
<Button
MinWidth="100"
Margin="10"
Command="{Binding EditLinkCommand}"
CommandParameter="QuickAccessLink"
Content="{DynamicResource plugin_explorer_edit}" />
<Button
MinWidth="100"
Margin="10"
Command="{Binding AddLinkCommand}"
CommandParameter="QuickAccessLink"
Content="{DynamicResource plugin_explorer_add}" />
</StackPanel>
</DockPanel>
</ScrollViewer>
</TabItem>
<TabItem
Width="Auto"
Header="{DynamicResource plugin_explorer_indexsearchexcludedpaths_header}"
Style="{DynamicResource ExplorerTabItem}">
<ScrollViewer>
<DockPanel HorizontalAlignment="Stretch">
<Border
Margin="10,10,10,5"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="1"
DockPanel.Dock="Top">
<ListView
Name="lbxExcludedPaths"
Height="200"
AllowDrop="True"
DragEnter="lbxAccessLinks_DragEnter"
Drop="LbxExcludedPaths_OnDrop"
ItemTemplate="{StaticResource ListViewTemplateAccessLinks}"
ItemsSource="{Binding Settings.IndexSearchExcludedSubdirectoryPaths}"
SelectedItem="{Binding SelectedIndexSearchExcludedPath}" />
</Border>
<StackPanel
HorizontalAlignment="Right"
DockPanel.Dock="Bottom"
Orientation="Horizontal">
<Button
MinWidth="100"
Margin="10"
Command="{Binding RemoveLinkCommand}"
CommandParameter="IndexSearchExcludedPaths"
Content="{DynamicResource plugin_explorer_delete}" />
<Button
MinWidth="100"
Margin="10"
Command="{Binding EditLinkCommand}"
CommandParameter="IndexSearchExcludedPaths"
Content="{DynamicResource plugin_explorer_edit}" />
<Button
MinWidth="100"
Margin="10"
Command="{Binding AddLinkCommand}"
CommandParameter="IndexSearchExcludedPaths"
Content="{DynamicResource plugin_explorer_add}" />
</StackPanel>
</DockPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</UserControl>
</UserControl>

View file

@ -1,16 +1,14 @@
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Controls;
using DataFormats = System.Windows.DataFormats;
using DragDropEffects = System.Windows.DragDropEffects;
using DragEventArgs = System.Windows.DragEventArgs;
using MessageBox = System.Windows.MessageBox;
namespace Flow.Launcher.Plugin.Explorer.Views
{
@ -21,280 +19,46 @@ namespace Flow.Launcher.Plugin.Explorer.Views
{
private readonly SettingsViewModel viewModel;
private List<ActionKeywordView> actionKeywordsListView;
private List<ActionKeywordModel> actionKeywordsListView;
public ExplorerSettings(SettingsViewModel viewModel)
{
DataContext = viewModel;
InitializeComponent();
this.viewModel = viewModel;
DataContext = viewModel;
lbxAccessLinks.ItemsSource = this.viewModel.Settings.QuickAccessLinks;
ActionKeywordModel.Init(viewModel.Settings);
lbxExcludedPaths.ItemsSource = this.viewModel.Settings.IndexSearchExcludedSubdirectoryPaths;
actionKeywordsListView = new List<ActionKeywordView>
{
new(Settings.ActionKeyword.SearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search")),
new(Settings.ActionKeyword.FileContentSearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch")),
new(Settings.ActionKeyword.PathSearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_pathsearch")),
new(Settings.ActionKeyword.IndexSearchActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_indexsearch")),
new(Settings.ActionKeyword.QuickAccessActionKeyword,
viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_quickaccess"))
};
lbxActionKeywords.ItemsSource = actionKeywordsListView;
ActionKeywordView.Init(viewModel.Settings);
RefreshView();
}
public void RefreshView()
{
lbxAccessLinks.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
lbxExcludedPaths.Items.SortDescriptions.Add(new SortDescription("Path", ListSortDirection.Ascending));
}
SetButtonVisibilityToHidden();
if (expAccessLinks.IsExpanded || expExcludedPaths.IsExpanded || expActionKeywords.IsExpanded)
private void AccessLinkDragDrop(string containerName, DragEventArgs e)
{
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files == null || !files.Any())
{
if (!expActionKeywords.IsExpanded)
btnAdd.Visibility = Visibility.Visible;
if (expActionKeywords.IsExpanded
&& btnEdit.Visibility == Visibility.Hidden)
btnEdit.Visibility = Visibility.Visible;
if (lbxAccessLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0
&& btnDelete.Visibility == Visibility.Visible
&& btnEdit.Visibility == Visibility.Visible)
{
btnDelete.Visibility = Visibility.Hidden;
btnEdit.Visibility = Visibility.Hidden;
}
if (expAccessLinks.IsExpanded
&& lbxAccessLinks.Items.Count > 0
&& btnDelete.Visibility == Visibility.Hidden
&& btnEdit.Visibility == Visibility.Hidden)
{
btnDelete.Visibility = Visibility.Visible;
btnEdit.Visibility = Visibility.Visible;
}
if (expExcludedPaths.IsExpanded
&& lbxExcludedPaths.Items.Count > 0
&& btnDelete.Visibility == Visibility.Hidden
&& btnEdit.Visibility == Visibility.Hidden)
{
btnDelete.Visibility = Visibility.Visible;
btnEdit.Visibility = Visibility.Visible;
}
return;
}
lbxAccessLinks.Items.Refresh();
lbxExcludedPaths.Items.Refresh();
lbxActionKeywords.Items.Refresh();
}
private void expActionKeywords_Click(object sender, RoutedEventArgs e)
{
if (expActionKeywords.IsExpanded)
expActionKeywords.Height = 205;
if (expExcludedPaths.IsExpanded)
expExcludedPaths.IsExpanded = false;
if (expAccessLinks.IsExpanded)
expAccessLinks.IsExpanded = false;
RefreshView();
}
private void expActionKeywords_Collapsed(object sender, RoutedEventArgs e)
{
expActionKeywords.Height = double.NaN;
SetButtonVisibilityToHidden();
}
private void expAccessLinks_Click(object sender, RoutedEventArgs e)
{
if (expAccessLinks.IsExpanded)
expAccessLinks.Height = 205;
if (expExcludedPaths.IsExpanded)
expExcludedPaths.IsExpanded = false;
if (expActionKeywords.IsExpanded)
expActionKeywords.IsExpanded = false;
RefreshView();
}
private void expAccessLinks_Collapsed(object sender, RoutedEventArgs e)
{
expAccessLinks.Height = double.NaN;
SetButtonVisibilityToHidden();
}
private void expExcludedPaths_Click(object sender, RoutedEventArgs e)
{
if (expExcludedPaths.IsExpanded)
expAccessLinks.Height = double.NaN;
if (expAccessLinks.IsExpanded)
expAccessLinks.IsExpanded = false;
if (expActionKeywords.IsExpanded)
expActionKeywords.IsExpanded = false;
RefreshView();
}
private void expExcludedPaths_Collapsed(object sender, RoutedEventArgs e)
{
SetButtonVisibilityToHidden();
}
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ?? lbxExcludedPaths.SelectedItem as AccessLink;
if (selectedRow != null)
foreach (var s in files)
{
string msg = string.Format(viewModel.Context.API.GetTranslation("plugin_explorer_delete_folder_link"),
selectedRow.Path);
if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
if (Directory.Exists(s))
{
if (expAccessLinks.IsExpanded)
viewModel.RemoveLinkFromQuickAccess(selectedRow);
if (expExcludedPaths.IsExpanded)
viewModel.RemoveAccessLinkFromExcludedIndexPaths(selectedRow);
RefreshView();
}
}
else
{
string warning = viewModel.Context.API.GetTranslation("plugin_explorer_select_folder_link_warning");
MessageBox.Show(warning);
}
}
private void btnEdit_Click(object sender, RoutedEventArgs e)
{
if (lbxActionKeywords.SelectedItem is ActionKeywordView)
{
var selectedActionKeyword = lbxActionKeywords.SelectedItem as ActionKeywordView;
var actionKeywordWindow = new ActionKeywordSetting(viewModel,
selectedActionKeyword);
actionKeywordWindow.ShowDialog();
RefreshView();
}
else
{
var selectedRow = lbxAccessLinks.SelectedItem as AccessLink ??
lbxExcludedPaths.SelectedItem as AccessLink;
if (selectedRow != null)
{
var folderBrowserDialog = new FolderBrowserDialog();
folderBrowserDialog.SelectedPath = selectedRow.Path;
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
var newFolderLink = new AccessLink
{
if (expAccessLinks.IsExpanded)
{
var link = viewModel.Settings.QuickAccessLinks.First(x => x.Path == selectedRow.Path);
link.Path = folderBrowserDialog.SelectedPath;
}
if (expExcludedPaths.IsExpanded)
{
var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x =>
x.Path == selectedRow.Path);
link.Path = folderBrowserDialog.SelectedPath;
}
}
RefreshView();
Path = s
};
viewModel.AppendLink(containerName, newFolderLink);
}
else
{
string warning = viewModel.Context.API.GetTranslation("plugin_explorer_make_selection_warning");
MessageBox.Show(warning);
}
}
}
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
var folderBrowserDialog = new FolderBrowserDialog();
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
var newAccessLink = new AccessLink {Path = folderBrowserDialog.SelectedPath};
AddAccessLink(newAccessLink);
}
RefreshView();
}
private void lbxAccessLinks_Drop(object sender, DragEventArgs e)
{
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files != null && files.Count() > 0)
{
if (expAccessLinks.IsExpanded && viewModel.Settings.QuickAccessLinks == null)
viewModel.Settings.QuickAccessLinks = new List<AccessLink>();
foreach (string s in files)
{
if (Directory.Exists(s))
{
var newFolderLink = new AccessLink {Path = s};
AddAccessLink(newFolderLink);
}
RefreshView();
}
}
}
private void AddAccessLink(AccessLink newAccessLink)
{
if (expAccessLinks.IsExpanded
&& !viewModel.Settings.QuickAccessLinks.Any(x => x.Path == newAccessLink.Path))
{
if (viewModel.Settings.QuickAccessLinks == null)
viewModel.Settings.QuickAccessLinks = new List<AccessLink>();
viewModel.Settings.QuickAccessLinks.Add(newAccessLink);
}
if (expExcludedPaths.IsExpanded
&& !viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == newAccessLink.Path))
{
if (viewModel.Settings.IndexSearchExcludedSubdirectoryPaths == null)
viewModel.Settings.IndexSearchExcludedSubdirectoryPaths = new List<AccessLink>();
viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.Add(newAccessLink);
}
}
@ -312,46 +76,23 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void btnOpenIndexingOptions_Click(object sender, RoutedEventArgs e)
{
viewModel.OpenWindowsIndexingOptions();
SettingsViewModel.OpenWindowsIndexingOptions();
}
public void SetButtonVisibilityToHidden()
private void EverythingSortOptionChanged(object sender, SelectionChangedEventArgs e)
{
btnDelete.Visibility = Visibility.Hidden;
btnEdit.Visibility = Visibility.Hidden;
btnAdd.Visibility = Visibility.Hidden;
if (tbFastSortWarning is not null)
{
tbFastSortWarning.Visibility = viewModel.FastSortWarningVisibility;
tbFastSortWarning.Text = viewModel.SortOptionWarningMessage;
}
}
private void LbxAccessLinks_OnDrop(object sender, DragEventArgs e)
{
AccessLinkDragDrop("QuickAccessLink", e);
}
private void LbxExcludedPaths_OnDrop(object sender, DragEventArgs e)
{
AccessLinkDragDrop("IndexSearchExcludedPath", e);
}
}
public class ActionKeywordView
{
private static Settings _settings;
public static void Init(Settings settings)
{
_settings = settings;
}
internal ActionKeywordView(Settings.ActionKeyword actionKeyword, string description)
{
KeywordProperty = actionKeyword;
Description = description;
}
public string Description { get; private init; }
internal Settings.ActionKeyword KeywordProperty { get; }
public string Keyword
{
get => _settings.GetActionKeyword(KeywordProperty);
set => _settings.SetActionKeyword(KeywordProperty, value);
}
public bool Enabled
{
get => _settings.GetActionKeywordEnabled(KeywordProperty);
set => _settings.SetActionKeywordEnabled(KeywordProperty, value);
}
}
}
}

View file

@ -8,6 +8,7 @@
<RootNamespace>Flow.Launcher.Plugin.Shell</RootNamespace>
<AssemblyName>Flow.Launcher.Plugin.Shell</AssemblyName>
<UseWindowsForms>true</UseWindowsForms>
<UseWpf>true</UseWpf>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
@ -54,10 +55,6 @@
<Content Include="Images\*.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Page Include="ShellSetting.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>

View file

@ -8,6 +8,7 @@
<RootNamespace>Flow.Launcher.Plugin.Sys</RootNamespace>
<AssemblyName>Flow.Launcher.Plugin.Sys</AssemblyName>
<UseWindowsForms>true</UseWindowsForms>
<UseWpf>true</UseWpf>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
@ -48,10 +49,6 @@
<Content Include="Images\*.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Page Include="SysSettings.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>