diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs index f6e5e5879a0..ed8f94bcf25 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -44,8 +44,10 @@ internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISetting private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); - private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, - Context.CurrentPluginMetadata.Name, "Settings.json"); + private string SettingDirectory => Path.Combine(DataLocation.PluginSettingsDirectory, + Context.CurrentPluginMetadata.Name); + + private string SettingPath => Path.Combine(SettingDirectory, "Settings.json"); public abstract List LoadContextMenus(Result selectedResult); @@ -159,5 +161,13 @@ public Control CreateSettingPanel() { return Settings.CreateSettingPanel(); } + + public void DeletePluginSettingsDirectory() + { + if (Directory.Exists(SettingDirectory)) + { + Directory.Delete(SettingDirectory, true); + } + } } } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs index 50eb30998d3..2a4b22bf395 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -112,7 +112,7 @@ public void Save() public Control CreateSettingPanel() { if (Settings == null || Settings.Count == 0) - return new(); + return null; var settingWindow = new UserControl(); var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center }; diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index 305b2815034..abe563c145a 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -112,10 +112,15 @@ private void SetupJsonRPC() RPC.StartListening(); } - public virtual Task ReloadDataAsync() + public virtual async Task ReloadDataAsync() { - SetupJsonRPC(); - return Task.CompletedTask; + try + { + await RPC.InvokeAsync("reload_data", Context); + } + catch (RemoteMethodNotFoundException e) + { + } } public virtual async ValueTask DisposeAsync() diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index 0e8a4b7764b..5ee72383383 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -210,9 +210,9 @@ public static async Task InitializePluginsAsync() { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); API.ShowMsg( - InternationalizationManager.Instance.GetTranslation("failedToInitializePluginsTitle"), + API.GetTranslation("failedToInitializePluginsTitle"), string.Format( - InternationalizationManager.Instance.GetTranslation("failedToInitializePluginsMessage"), + API.GetTranslation("failedToInitializePluginsMessage"), failed ), "", @@ -281,7 +281,7 @@ public static async Task> QueryForPluginAsync(PluginPair pair, Quer return results; } - public static void UpdatePluginMetadata(List results, PluginMetadata metadata, Query query) + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) { @@ -439,7 +439,7 @@ public static bool PluginModified(string uuid) public static void UpdatePlugin(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath) { InstallPlugin(newVersion, zipFilePath, checkModified:false); - UninstallPlugin(existingVersion, removeSettings:false, checkModified:false); + UninstallPlugin(existingVersion, removePluginFromSettings:false, removePluginSettings:false, checkModified: false); _modifiedPlugins.Add(existingVersion.ID); } @@ -454,9 +454,9 @@ public static void InstallPlugin(UserPlugin plugin, string zipFilePath) /// /// Uninstall a plugin. /// - public static void UninstallPlugin(PluginMetadata plugin, bool removeSettings = true) + public static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings = true, bool removePluginSettings = false) { - UninstallPlugin(plugin, removeSettings, true); + UninstallPlugin(plugin, removePluginFromSettings, removePluginSettings, true); } #endregion @@ -521,7 +521,15 @@ internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool c FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => API.ShowMsgBox(s)); - Directory.Delete(tempFolderPluginPath, true); + try + { + if (Directory.Exists(tempFolderPluginPath)) + Directory.Delete(tempFolderPluginPath, true); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.InstallPlugin|Failed to delete temp folder {tempFolderPluginPath}", e); + } if (checkModified) { @@ -529,14 +537,62 @@ internal static void InstallPlugin(UserPlugin plugin, string zipFilePath, bool c } } - internal static void UninstallPlugin(PluginMetadata plugin, bool removeSettings, bool checkModified) + internal static void UninstallPlugin(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified) { if (checkModified && PluginModified(plugin.ID)) { throw new ArgumentException($"Plugin {plugin.Name} has been modified"); } - if (removeSettings) + if (removePluginSettings) + { + if (AllowedLanguage.IsDotNet(plugin.Language)) // for the plugin in .NET, we can use assembly loader + { + var assemblyLoader = new PluginAssemblyLoader(plugin.ExecuteFilePath); + var assembly = assemblyLoader.LoadAssemblyAndDependencies(); + var assemblyName = assembly.GetName().Name; + + // if user want to remove the plugin settings, we cannot call save method for the plugin json storage instance of this plugin + // so we need to remove it from the api instance + var method = API.GetType().GetMethod("RemovePluginSettings"); + var pluginJsonStorage = method?.Invoke(API, new object[] { assemblyName }); + + // if there exists a json storage for current plugin, we need to delete the directory path + if (pluginJsonStorage != null) + { + var deleteMethod = pluginJsonStorage.GetType().GetMethod("DeleteDirectory"); + try + { + deleteMethod?.Invoke(pluginJsonStorage, null); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"), + string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); + } + } + } + else // the plugin with json prc interface + { + var pluginPair = AllPlugins.FirstOrDefault(p => p.Metadata.ID == plugin.ID); + if (pluginPair != null && pluginPair.Plugin is JsonRPCPlugin jsonRpcPlugin) + { + try + { + jsonRpcPlugin.DeletePluginSettingsDirectory(); + } + catch (Exception e) + { + Log.Exception($"|PluginManager.UninstallPlugin|Failed to delete plugin json folder for {plugin.Name}", e); + API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"), + string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); + } + } + } + } + + if (removePluginFromSettings) { Settings.Plugins.Remove(plugin.ID); AllPlugins.RemoveAll(p => p.Metadata.ID == plugin.ID); diff --git a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs index abe3f55b5ad..bc3900da802 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -3,14 +3,17 @@ namespace Flow.Launcher.Infrastructure.Storage { - public class PluginJsonStorage :JsonStorage where T : new() + public class PluginJsonStorage : JsonStorage where T : new() { + // Use assembly name to check which plugin is using this storage + public readonly string AssemblyName; + public PluginJsonStorage() { // C# related, add python related below var dataType = typeof(T); - var assemblyName = dataType.Assembly.GetName().Name; - DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, assemblyName); + AssemblyName = dataType.Assembly.GetName().Name; + DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName, Constant.Plugins, AssemblyName); Helper.ValidateDirectory(DirectoryPath); FilePath = Path.Combine(DirectoryPath, $"{dataType.Name}{FileSuffix}"); @@ -20,6 +23,13 @@ public PluginJsonStorage(T data) : this() { Data = data; } + + public void DeleteDirectory() + { + if (Directory.Exists(DirectoryPath)) + { + Directory.Delete(DirectoryPath, true); + } + } } } - diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 8376fd07ba7..07fc378c3c4 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -17,7 +17,8 @@ namespace Flow.Launcher.Plugin public interface IPublicAPI { /// - /// Change Flow.Launcher query + /// Change Flow.Launcher query. + /// When current results are from context menu or history, it will go back to query results before changing query. /// /// query text /// @@ -299,7 +300,7 @@ public interface IPublicAPI /// /// Reloads the query. - /// This method should run when selected item is from query results. + /// When current results are from context menu or history, it will go back to query results before requerying. /// /// Choose the first result after reload if true; keep the last selected result if false. Default is true. public void ReQuery(bool reselect = true); diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index bb005752eba..9b16cc1cbf0 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -266,8 +266,9 @@ public ValueTask ExecuteAsync(ActionContext context) /// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records. /// This can be useful when your plugin will change the Title or SubTitle of the result dynamically. /// If the plugin does not specific this, FL just uses Title and SubTitle to identify this result. + /// Note: Because old data does not have this key, we should use null as the default value for consistency. /// - public string RecordKey { get; set; } = string.Empty; + public string RecordKey { get; set; } = null; /// /// Info of the preview section of a diff --git a/Flow.Launcher.Test/FilesFoldersTest.cs b/Flow.Launcher.Test/FilesFoldersTest.cs index 3dead99183e..2621fc2da1f 100644 --- a/Flow.Launcher.Test/FilesFoldersTest.cs +++ b/Flow.Launcher.Test/FilesFoldersTest.cs @@ -1,5 +1,6 @@ using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; +using NUnit.Framework.Legacy; namespace Flow.Launcher.Test { @@ -35,7 +36,7 @@ public class FilesFoldersTest [TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)] public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) { - Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path)); + ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path)); } // Equality @@ -47,7 +48,7 @@ public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(strin [TestCase(@"c:\foo", @"c:\foo\", true)] public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) { - Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult)); + ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult)); } } } diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index 8286e142e67..0241a374e41 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -49,7 +49,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs index d97ce227c1a..090719642ed 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; @@ -36,7 +37,7 @@ public List GetSearchStrings() OneOneOneOne }; - public List GetPrecisionScores() + public static List GetPrecisionScores() { var listToReturn = new List(); @@ -73,10 +74,10 @@ public void MatchTest() results = results.Where(x => x.Score > 0).OrderByDescending(x => x.Score).ToList(); - Assert.IsTrue(results.Count == 3); - Assert.IsTrue(results[0].Title == "Inste"); - Assert.IsTrue(results[1].Title == "Install Package"); - Assert.IsTrue(results[2].Title == "file open in browser-test"); + ClassicAssert.IsTrue(results.Count == 3); + ClassicAssert.IsTrue(results[0].Title == "Inste"); + ClassicAssert.IsTrue(results[1].Title == "Install Package"); + ClassicAssert.IsTrue(results[2].Title == "file open in browser-test"); } [TestCase("Chrome")] @@ -86,7 +87,7 @@ public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(st var matcher = new StringMatcher(alphabet); var scoreResult = matcher.FuzzyMatch(searchString, compareString).RawScore; - Assert.True(scoreResult == 0); + ClassicAssert.True(scoreResult == 0); } [TestCase("chr")] @@ -127,7 +128,7 @@ public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreat Debug.WriteLine("###############################################"); Debug.WriteLine(""); - Assert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); + ClassicAssert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); } } @@ -153,7 +154,7 @@ public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring( var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore; // Should - Assert.AreEqual(expectedScore, rawScore, + ClassicAssert.AreEqual(expectedScore, rawScore, $"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}"); } @@ -192,12 +193,12 @@ public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual( Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); Debug.WriteLine( - $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); + $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); // Should - Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), $"Query: {queryString}{Environment.NewLine} " + $"Compare: {compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + @@ -243,12 +244,12 @@ public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings( Debug.WriteLine("###############################################"); Debug.WriteLine($"QueryString: {queryString} CompareString: {compareString}"); Debug.WriteLine( - $"RAW SCORE: {matchResult.RawScore.ToString()}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); + $"RAW SCORE: {matchResult.RawScore}, PrecisionLevelSetAt: {expectedPrecisionScore} ({(int) expectedPrecisionScore})"); Debug.WriteLine("###############################################"); Debug.WriteLine(""); // Should - Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), $"Query:{queryString}{Environment.NewLine} " + $"Compare:{compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + @@ -279,7 +280,7 @@ public void WhenGivenAQuery_Scoring_ShouldGiveMoreWeightToStartOfNewWord( Debug.WriteLine(""); // Should - Assert.True(compareString1Result.Score > compareString2Result.Score, + ClassicAssert.True(compareString1Result.Score > compareString2Result.Score, $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -312,7 +313,7 @@ public void WhenGivenTwoStrings_Scoring_ShouldGiveMoreWeightToTheStringCloserToI Debug.WriteLine(""); // Should - Assert.True(compareString1Result.Score > compareString2Result.Score, + ClassicAssert.True(compareString1Result.Score > compareString2Result.Score, $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -338,7 +339,7 @@ public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore( var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max(); // Assert - Assert.IsTrue(firstScore > secondScore, + ClassicAssert.IsTrue(firstScore > secondScore, $"Query: \"{queryString}\"{Environment.NewLine} " + $"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -362,7 +363,7 @@ public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, { var matcher = new StringMatcher(alphabet); var score = matcher.FuzzyMatch(queryString, compareString).Score; - Assert.IsTrue(score == desiredScore, + ClassicAssert.IsTrue(score == desiredScore, $@"Query: ""{queryString}"" CompareString: ""{compareString}"" Score: {score} diff --git a/Flow.Launcher.Test/HttpTest.cs b/Flow.Launcher.Test/HttpTest.cs index e72ad7a6761..4f135978a6e 100644 --- a/Flow.Launcher.Test/HttpTest.cs +++ b/Flow.Launcher.Test/HttpTest.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using NUnit.Framework; +using NUnit.Framework.Legacy; using System; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.Http; @@ -16,16 +17,16 @@ public void GivenHttpProxy_WhenUpdated_ThenWebProxyShouldAlsoBeUpdatedToTheSame( proxy.Enabled = true; proxy.Server = "127.0.0.1"; - Assert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}")); - Assert.IsNull(Http.WebProxy.Credentials); + ClassicAssert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}")); + ClassicAssert.IsNull(Http.WebProxy.Credentials); proxy.UserName = "test"; - Assert.NotNull(Http.WebProxy.Credentials); - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName); - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, ""); + ClassicAssert.NotNull(Http.WebProxy.Credentials); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, ""); proxy.Password = "test password"; - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password); } } } diff --git a/Flow.Launcher.Test/PluginLoadTest.cs b/Flow.Launcher.Test/PluginLoadTest.cs index d6ba48f1905..2cc05f95a1e 100644 --- a/Flow.Launcher.Test/PluginLoadTest.cs +++ b/Flow.Launcher.Test/PluginLoadTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; using System.Collections.Generic; @@ -15,37 +16,37 @@ public void GivenDuplicatePluginMetadatasWhenLoadedThenShouldReturnOnlyUniqueLis // Given var duplicateList = new List { - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.1" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.2" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B4085823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "ABC0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "ABC0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" @@ -56,11 +57,11 @@ public void GivenDuplicatePluginMetadatasWhenLoadedThenShouldReturnOnlyUniqueLis (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); // Then - Assert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2"); - Assert.True(unique.Count() == 1); + ClassicAssert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2"); + ClassicAssert.True(unique.Count == 1); - Assert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855")); - Assert.True(duplicates.Count() == 6); + ClassicAssert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855")); + ClassicAssert.True(duplicates.Count == 6); } [Test] @@ -69,12 +70,12 @@ public void GivenDuplicatePluginMetadatasWithNoUniquePluginWhenLoadedThenShouldR // Given var duplicateList = new List { - new PluginMetadata + new() { ID = "CEA0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" }, - new PluginMetadata + new() { ID = "CEA0TYUC6D3B7855823D60DC76F28855", Version = "1.0.0" @@ -85,8 +86,8 @@ public void GivenDuplicatePluginMetadatasWithNoUniquePluginWhenLoadedThenShouldR (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); // Then - Assert.True(unique.Count() == 0); - Assert.True(duplicates.Count() == 2); + ClassicAssert.True(unique.Count == 0); + ClassicAssert.True(duplicates.Count == 2); } } } diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 80cb74729fc..420da266d87 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -5,12 +5,10 @@ using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; +using NUnit.Framework.Legacy; using System; -using System.Collections.Generic; using System.IO; using System.Runtime.Versioning; -using System.Threading; -using System.Threading.Tasks; using static Flow.Launcher.Plugin.Explorer.Search.SearchManager; namespace Flow.Launcher.Test.Plugins @@ -22,28 +20,6 @@ namespace Flow.Launcher.Test.Plugins [TestFixture] public class ExplorerTest { -#pragma warning disable CS1998 // async method with no await (more readable to leave it async to match the tested signature) - private async Task> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken) - { - return new List(); - } -#pragma warning restore CS1998 - - private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token) - { - return new List - { - new Result - { - Title = "Result 1" - }, - new Result - { - Title = "Result 2" - } - }; - } - private bool PreviousLocationExistsReturnsTrue(string dummyString) => true; private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false; @@ -57,7 +33,7 @@ public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestric var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath); // Then - Assert.IsTrue(result == expectedString, + ClassicAssert.IsTrue(result == expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual: {result}{Environment.NewLine}"); } @@ -74,7 +50,7 @@ public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_Then var queryString = queryConstructor.Directory(folderPath); // Then - Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), + ClassicAssert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), $"Expected string: {expectedString}{Environment.NewLine} " + $"Actual string was: {queryString}{Environment.NewLine}"); } @@ -94,7 +70,7 @@ public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificIte var queryString = queryConstructor.Directory(folderPath, userSearchString); // Then - Assert.AreEqual(expectedString, queryString); + ClassicAssert.AreEqual(expectedString, queryString); } [SupportedOSPlatform("windows7.0")] @@ -105,7 +81,7 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereR const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch; // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -128,7 +104,7 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShould var resultString = queryConstructor.FilesAndFolders(userSearchString); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } @@ -138,13 +114,13 @@ public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSe string querySearchString, string expectedString) { // Given - var queryConstructor = new QueryConstructor(new Settings()); + _ = new QueryConstructor(new Settings()); //When var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString); // Then - Assert.IsTrue(resultString == expectedString, + ClassicAssert.IsTrue(resultString == expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } @@ -162,12 +138,12 @@ public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseE var resultString = queryConstructor.FileContent(userSearchString); // Then - Assert.IsTrue(resultString == expectedString, + ClassicAssert.IsTrue(resultString == expectedString, $"Expected query string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } - public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue() + public static void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue() { // Given var query = new Query @@ -181,7 +157,7 @@ public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileConte var result = searchManager.IsFileContentSearch(query.ActionKeyword); // Then - Assert.IsTrue(result, + ClassicAssert.IsTrue(result, $"Expected True for file content search. {Environment.NewLine} " + $"Actual result was: {result}{Environment.NewLine}"); } @@ -206,7 +182,7 @@ public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString( var result = FilesFolders.IsLocationPathString(querySearchString); //Then - Assert.IsTrue(result == expectedResult, + ClassicAssert.IsTrue(result == expectedResult, $"Expected query search string check result is: {expectedResult} {Environment.NewLine} " + $"Actual check result is {result} {Environment.NewLine}"); @@ -233,7 +209,7 @@ public void GivenAPartialPath_WhenPreviousLevelDirectoryExists_ThenShouldReturnT var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path); //Then - Assert.IsTrue(previousDirectoryPath == expectedString, + ClassicAssert.IsTrue(previousDirectoryPath == expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {previousDirectoryPath} {Environment.NewLine}"); } @@ -246,7 +222,7 @@ public void WhenGivenAPath_ThenShouldReturnThePreviousDirectoryPathIfIncompleteO var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); //Then - Assert.IsTrue(returnedPath == expectedString, + ClassicAssert.IsTrue(returnedPath == expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {returnedPath} {Environment.NewLine}"); } @@ -260,7 +236,7 @@ public void GivenFilePath_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestr var resultString = QueryConstructor.RecursiveDirectoryConstraint(path); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -274,7 +250,7 @@ public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSear var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")] @@ -305,7 +281,7 @@ public void GivenFolderResult_WhenGetPath_ThenPathShouldBeExpectedString( var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")] @@ -334,7 +310,7 @@ public void GivenFileResult_WhenGetPath_ThenPathShouldBeExpectedString( var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")] @@ -366,7 +342,7 @@ public void GivenQueryWithFolderTypeResult_WhenGetAutoComplete_ThenResultShouldB var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")] @@ -398,7 +374,7 @@ public void GivenQueryWithFileTypeResult_WhenGetAutoComplete_ThenResultShouldBeE var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase(@"c:\foo", @"c:\foo", true)] @@ -420,7 +396,7 @@ public void GivenTwoPaths_WhenCompared_ThenShouldBeExpectedSameOrDifferent(strin }; // When, Then - Assert.AreEqual(expectedResult, comparator.Equals(result1, result2)); + ClassicAssert.AreEqual(expectedResult, comparator.Equals(result1, result2)); } [TestCase(@"c:\foo\", @"c:\foo\")] @@ -444,7 +420,7 @@ public void GivenTwoPaths_WhenComparedHasCode_ThenShouldBeSame(string path1, str var hash2 = comparator.GetHashCode(result2); // When, Then - Assert.IsTrue(hash1 == hash2); + ClassicAssert.IsTrue(hash1 == hash2); } [TestCase(@"%appdata%", true)] @@ -461,7 +437,7 @@ public void GivenPath_WhenHavingEnvironmentVariableOrNot_ThenShouldBeExpected(st var result = EnvironmentVariables.HasEnvironmentVar(path); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } } } diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs index 42a4630fe5c..497f874e70f 100644 --- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs @@ -1,12 +1,11 @@ -using NUnit.Framework; +using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; using System.Threading.Tasks; using System.IO; using System.Threading; using System.Text; -using System.Text.Json; -using System.Linq; using System.Collections.Generic; namespace Flow.Launcher.Test.Plugins @@ -40,13 +39,13 @@ public async Task GivenVariousJsonText_WhenVariousNamingCase_ThenExpectNotNullRe Search = resultText }, default); - Assert.IsNotNull(results); + ClassicAssert.IsNotNull(results); foreach (var result in results) { - Assert.IsNotNull(result); - Assert.IsNotNull(result.AsyncAction); - Assert.IsNotNull(result.Title); + ClassicAssert.IsNotNull(result); + ClassicAssert.IsNotNull(result.AsyncAction); + ClassicAssert.IsNotNull(result.Title); } } @@ -56,12 +55,11 @@ public async Task GivenVariousJsonText_WhenVariousNamingCase_ThenExpectNotNullRe new JsonRPCQueryResponseModel(0, new List()), new JsonRPCQueryResponseModel(0, new List { - new JsonRPCResult + new() { Title = "Test1", SubTitle = "Test2" } }) }; - } } diff --git a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs index 7ccac5bd59d..0dd1fe4895a 100644 --- a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs @@ -1,7 +1,8 @@ using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Plugin.Url; -namespace Flow.Launcher.Test +namespace Flow.Launcher.Test.Plugins { [TestFixture] public class UrlPluginTest @@ -10,23 +11,23 @@ public class UrlPluginTest public void URLMatchTest() { var plugin = new Main(); - Assert.IsTrue(plugin.IsURL("http://www.google.com")); - Assert.IsTrue(plugin.IsURL("https://www.google.com")); - Assert.IsTrue(plugin.IsURL("http://google.com")); - Assert.IsTrue(plugin.IsURL("www.google.com")); - Assert.IsTrue(plugin.IsURL("google.com")); - Assert.IsTrue(plugin.IsURL("http://localhost")); - Assert.IsTrue(plugin.IsURL("https://localhost")); - Assert.IsTrue(plugin.IsURL("http://localhost:80")); - Assert.IsTrue(plugin.IsURL("https://localhost:80")); - Assert.IsTrue(plugin.IsURL("http://110.10.10.10")); - Assert.IsTrue(plugin.IsURL("110.10.10.10")); - Assert.IsTrue(plugin.IsURL("ftp://110.10.10.10")); + ClassicAssert.IsTrue(plugin.IsURL("http://www.google.com")); + ClassicAssert.IsTrue(plugin.IsURL("https://www.google.com")); + ClassicAssert.IsTrue(plugin.IsURL("http://google.com")); + ClassicAssert.IsTrue(plugin.IsURL("www.google.com")); + ClassicAssert.IsTrue(plugin.IsURL("google.com")); + ClassicAssert.IsTrue(plugin.IsURL("http://localhost")); + ClassicAssert.IsTrue(plugin.IsURL("https://localhost")); + ClassicAssert.IsTrue(plugin.IsURL("http://localhost:80")); + ClassicAssert.IsTrue(plugin.IsURL("https://localhost:80")); + ClassicAssert.IsTrue(plugin.IsURL("http://110.10.10.10")); + ClassicAssert.IsTrue(plugin.IsURL("110.10.10.10")); + ClassicAssert.IsTrue(plugin.IsURL("ftp://110.10.10.10")); - Assert.IsFalse(plugin.IsURL("wwww")); - Assert.IsFalse(plugin.IsURL("wwww.c")); - Assert.IsFalse(plugin.IsURL("wwww.c")); + ClassicAssert.IsFalse(plugin.IsURL("wwww")); + ClassicAssert.IsFalse(plugin.IsURL("wwww.c")); + ClassicAssert.IsFalse(plugin.IsURL("wwww.c")); } } } diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index aa0c8da12b9..c8ac17748da 100644 --- a/Flow.Launcher.Test/QueryBuilderTest.cs +++ b/Flow.Launcher.Test/QueryBuilderTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using NUnit.Framework; +using NUnit.Framework.Legacy; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; @@ -17,17 +18,17 @@ public void ExclusivePluginQueryTest() Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); - Assert.AreEqual("> ping google.com -n 20 -6", q.RawQuery); - Assert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); - Assert.AreEqual(">", q.ActionKeyword); + ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery); + ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); + ClassicAssert.AreEqual(">", q.ActionKeyword); - Assert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match."); + ClassicAssert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match."); - Assert.AreEqual("ping", q.FirstSearch); - Assert.AreEqual("google.com", q.SecondSearch); - Assert.AreEqual("-n", q.ThirdSearch); + ClassicAssert.AreEqual("ping", q.FirstSearch); + ClassicAssert.AreEqual("google.com", q.SecondSearch); + ClassicAssert.AreEqual("-n", q.ThirdSearch); - Assert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); + ClassicAssert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); } [Test] @@ -40,11 +41,11 @@ public void ExclusivePluginQueryIgnoreDisabledTest() Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); - Assert.AreEqual("> ping google.com -n 20 -6", q.Search); - Assert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); - Assert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match."); - Assert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin."); - Assert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); + ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search); + ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); + ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match."); + ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin."); + ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); } [Test] @@ -52,13 +53,13 @@ public void GenericPluginQueryTest() { Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary()); - Assert.AreEqual("file.txt file2 file3", q.Search); - Assert.AreEqual("", q.ActionKeyword); + ClassicAssert.AreEqual("file.txt file2 file3", q.Search); + ClassicAssert.AreEqual("", q.ActionKeyword); - Assert.AreEqual("file.txt", q.FirstSearch); - Assert.AreEqual("file2", q.SecondSearch); - Assert.AreEqual("file3", q.ThirdSearch); - Assert.AreEqual("file2 file3", q.SecondToEndSearch); + ClassicAssert.AreEqual("file.txt", q.FirstSearch); + ClassicAssert.AreEqual("file2", q.SecondSearch); + ClassicAssert.AreEqual("file3", q.ThirdSearch); + ClassicAssert.AreEqual("file2 file3", q.SecondToEndSearch); } } } diff --git a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs index eab2705d004..af3c8f6c9f2 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs @@ -13,7 +13,6 @@ public partial class CustomQueryHotkeySetting : Window { private bool update; private CustomPluginHotkey updateCustomHotkey; - public Settings Settings { get; } public CustomQueryHotkeySetting(Settings settings) { @@ -30,13 +29,13 @@ private void btnAdd_OnClick(object sender, RoutedEventArgs e) { if (!update) { - Settings.CustomPluginHotkeys ??= new ObservableCollection(); + _settings.CustomPluginHotkeys ??= new ObservableCollection(); var pluginHotkey = new CustomPluginHotkey { Hotkey = HotkeyControl.CurrentHotkey.ToString(), ActionKeyword = tbAction.Text }; - Settings.CustomPluginHotkeys.Add(pluginHotkey); + _settings.CustomPluginHotkeys.Add(pluginHotkey); HotKeyMapper.SetCustomQueryHotkey(pluginHotkey); } @@ -56,7 +55,7 @@ private void btnAdd_OnClick(object sender, RoutedEventArgs e) public void UpdateItem(CustomPluginHotkey item) { - updateCustomHotkey = Settings.CustomPluginHotkeys.FirstOrDefault(o => + updateCustomHotkey = _settings.CustomPluginHotkeys.FirstOrDefault(o => o.ActionKeyword == item.ActionKeyword && o.Hotkey == item.Hotkey); if (updateCustomHotkey == null) { @@ -74,8 +73,7 @@ public void UpdateItem(CustomPluginHotkey item) private void BtnTestActionKeyword_OnClick(object sender, RoutedEventArgs e) { App.API.ChangeQuery(tbAction.Text); - Application.Current.MainWindow.Show(); - Application.Current.MainWindow.Opacity = 1; + App.API.ShowMainWindow(); Application.Current.MainWindow.Focus(); } diff --git a/Flow.Launcher/CustomShortcutSetting.xaml.cs b/Flow.Launcher/CustomShortcutSetting.xaml.cs index 4589b45ec72..416f8e04951 100644 --- a/Flow.Launcher/CustomShortcutSetting.xaml.cs +++ b/Flow.Launcher/CustomShortcutSetting.xaml.cs @@ -65,8 +65,7 @@ private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e) private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e) { App.API.ChangeQuery(tbExpand.Text); - Application.Current.MainWindow.Show(); - Application.Current.MainWindow.Opacity = 1; + App.API.ShowMainWindow(); Application.Current.MainWindow.Focus(); } } diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index e6f18560ed2..43c1dfd9007 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -83,6 +83,8 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index b40406e4282..79b524f502c 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -6,6 +6,9 @@ using Flow.Launcher.Core.Resource; using Flow.Launcher.ViewModel; using Flow.Launcher.Core; +using ChefKeys; +using System.Globalization; +using Flow.Launcher.Infrastructure.Logger; namespace Flow.Launcher.Helper; @@ -29,21 +32,57 @@ internal static void OnToggleHotkey(object sender, HotkeyEventArgs args) _mainViewModel.ToggleFlowLauncher(); } + internal static void OnToggleHotkeyWithChefKeys() + { + if (!_mainViewModel.ShouldIgnoreHotkeys()) + _mainViewModel.ToggleFlowLauncher(); + } + private static void SetHotkey(string hotkeyStr, EventHandler action) { var hotkey = new HotkeyModel(hotkeyStr); SetHotkey(hotkey, action); } + private static void SetWithChefKeys(string hotkeyStr) + { + try + { + ChefKeysManager.RegisterHotkey(hotkeyStr, hotkeyStr, OnToggleHotkeyWithChefKeys); + ChefKeysManager.Start(); + } + catch (Exception e) + { + Log.Error( + string.Format("|HotkeyMapper.SetWithChefKeys|Error registering hotkey: {0} \nStackTrace:{1}", + e.Message, + e.StackTrace)); + string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr); + string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle"); + MessageBoxEx.Show(errorMsg, errorMsgTitle); + } + } + internal static void SetHotkey(HotkeyModel hotkey, EventHandler action) { string hotkeyStr = hotkey.ToString(); try { + if (hotkeyStr == "LWin" || hotkeyStr == "RWin") + { + SetWithChefKeys(hotkeyStr); + return; + } + HotkeyManager.Current.AddOrReplace(hotkeyStr, hotkey.CharKey, hotkey.ModifierKeys, action); } - catch (Exception) + catch (Exception e) { + Log.Error( + string.Format("|HotkeyMapper.SetHotkey|Error registering hotkey {2}: {0} \nStackTrace:{1}", + e.Message, + e.StackTrace, + hotkeyStr)); string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr); string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle"); App.API.ShowMsgBox(errorMsg, errorMsgTitle); @@ -52,10 +91,33 @@ internal static void SetHotkey(HotkeyModel hotkey, EventHandler internal static void RemoveHotkey(string hotkeyStr) { - if (!string.IsNullOrEmpty(hotkeyStr)) + try { - HotkeyManager.Current.Remove(hotkeyStr); + if (hotkeyStr == "LWin" || hotkeyStr == "RWin") + { + RemoveWithChefKeys(hotkeyStr); + return; + } + + if (!string.IsNullOrEmpty(hotkeyStr)) + HotkeyManager.Current.Remove(hotkeyStr); } + catch (Exception e) + { + Log.Error( + string.Format("|HotkeyMapper.RemoveHotkey|Error removing hotkey: {0} \nStackTrace:{1}", + e.Message, + e.StackTrace)); + string errorMsg = string.Format(InternationalizationManager.Instance.GetTranslation("unregisterHotkeyFailed"), hotkeyStr); + string errorMsgTitle = InternationalizationManager.Instance.GetTranslation("MessageBoxTitle"); + MessageBoxEx.Show(errorMsg, errorMsgTitle); + } + } + + private static void RemoveWithChefKeys(string hotkeyStr) + { + ChefKeysManager.UnregisterHotkey(hotkeyStr); + ChefKeysManager.Stop(); } internal static void LoadCustomPluginHotkey() diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index a42bde7c9cd..e1dfc1108ef 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -154,7 +154,16 @@ private void SetHotkey(HotkeyModel keyModel, bool triggerValidate = true) { if (triggerValidate) { - bool hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture); + bool hotkeyAvailable = false; + // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157 + if (keyModel.ToString() == "LWin" || keyModel.ToString() == "RWin") + { + hotkeyAvailable = true; + } + else + { + hotkeyAvailable = CheckHotkeyAvailability(keyModel, ValidateKeyGesture); + } if (!hotkeyAvailable) { diff --git a/Flow.Launcher/HotkeyControlDialog.xaml.cs b/Flow.Launcher/HotkeyControlDialog.xaml.cs index a7b99f6704b..a4d21a7827b 100644 --- a/Flow.Launcher/HotkeyControlDialog.xaml.cs +++ b/Flow.Launcher/HotkeyControlDialog.xaml.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Windows; using System.Windows.Input; +using ChefKeys; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure.Hotkey; @@ -33,6 +34,8 @@ public enum EResultType public string ResultValue { get; private set; } = string.Empty; public static string EmptyHotkey => InternationalizationManager.Instance.GetTranslation("none"); + private static bool isOpenFlowHotkey; + public HotkeyControlDialog(string hotkey, string defaultHotkey, IHotkeySettings hotkeySettings, string windowTitle = "") { WindowTitle = windowTitle switch @@ -46,6 +49,14 @@ public HotkeyControlDialog(string hotkey, string defaultHotkey, IHotkeySettings SetKeysToDisplay(CurrentHotkey); InitializeComponent(); + + // TODO: This is a temporary way to enforce changing only the open flow hotkey to Win, and will be removed by PR #3157 + isOpenFlowHotkey = _hotkeySettings.RegisteredHotkeys + .Any(x => x.DescriptionResourceKey == "flowlauncherHotkey" + && x.Hotkey.ToString() == hotkey); + + ChefKeysManager.StartMenuEnableBlocking = true; + ChefKeysManager.Start(); } private void Reset(object sender, RoutedEventArgs routedEventArgs) @@ -61,12 +72,18 @@ private void Delete(object sender, RoutedEventArgs routedEventArgs) private void Cancel(object sender, RoutedEventArgs routedEventArgs) { + ChefKeysManager.StartMenuEnableBlocking = false; + ChefKeysManager.Stop(); + ResultType = EResultType.Cancel; Hide(); } private void Save(object sender, RoutedEventArgs routedEventArgs) { + ChefKeysManager.StartMenuEnableBlocking = false; + ChefKeysManager.Stop(); + if (KeysToDisplay.Count == 1 && KeysToDisplay[0] == EmptyHotkey) { ResultType = EResultType.Delete; @@ -85,6 +102,9 @@ private void OnPreviewKeyDown(object sender, KeyEventArgs e) //when alt is pressed, the real key should be e.SystemKey Key key = e.Key == Key.System ? e.SystemKey : e.Key; + if (ChefKeysManager.StartMenuBlocked && key.ToString() == ChefKeysManager.StartMenuSimulatedKey) + return; + SpecialKeyState specialKeyState = GlobalHotkey.CheckModifiers(); var hotkeyModel = new HotkeyModel( @@ -168,8 +188,13 @@ private void SetKeysToDisplay(HotkeyModel? hotkey) } } - private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) => - hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); + private static bool CheckHotkeyAvailability(HotkeyModel hotkey, bool validateKeyGesture) + { + if (isOpenFlowHotkey && (hotkey.ToString() == "LWin" || hotkey.ToString() == "RWin")) + return true; + + return hotkey.Validate(validateKeyGesture) && HotKeyMapper.CheckAvailability(hotkey); + } private void Overwrite(object sender, RoutedEventArgs e) { diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 4c465d61f52..c66772c83c5 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -15,6 +15,7 @@ Failed to register hotkey "{0}". The hotkey may be in use by another program. Change to a different hotkey, or exit another program. + Failed to unregister hotkey "{0}". Please try again or see log for details Flow Launcher Could not start {0} Invalid Flow Launcher plugin file format @@ -128,7 +129,8 @@ Version Website Uninstall - + Fail to remove plugin settings + Plugins: {0} - Fail to remove plugin settings files, please remove them manually Plugin Store @@ -145,8 +147,6 @@ This plugin has been updated within the last 7 days New Update is Available - - Theme Appearance @@ -196,7 +196,6 @@ This theme supports two(light/dark) modes. This theme supports Blur Transparent Background. - Hotkey Hotkeys diff --git a/Flow.Launcher/MessageBoxEx.xaml b/Flow.Launcher/MessageBoxEx.xaml index be12ca16c20..a0e5ffaf83a 100644 --- a/Flow.Launcher/MessageBoxEx.xaml +++ b/Flow.Launcher/MessageBoxEx.xaml @@ -12,6 +12,7 @@ Foreground="{DynamicResource PopupTextColor}" ResizeMode="NoResize" SizeToContent="Height" + Topmost="True" WindowStartupLocation="CenterScreen" mc:Ignorable="d"> diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 3fdd945e8d1..8bcd3e04631 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -188,6 +188,23 @@ public void LogException(string className, string message, Exception e, private readonly ConcurrentDictionary _pluginJsonStorages = new(); + public object RemovePluginSettings(string assemblyName) + { + foreach (var keyValuePair in _pluginJsonStorages) + { + var key = keyValuePair.Key; + var value = keyValuePair.Value; + var name = value.GetType().GetField("AssemblyName")?.GetValue(value)?.ToString(); + if (name == assemblyName) + { + _pluginJsonStorages.Remove(key, out var pluginJsonStorage); + return pluginJsonStorage; + } + } + + return null; + } + /// /// Save plugin settings. /// diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index d57209a14e4..650a276109d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -232,8 +232,8 @@ private void RegisterResultsUpdatedEvent() var token = e.Token == default ? _updateToken : e.Token; - // make a copy of results to avoid plugin change the result when updating view model - var resultsCopy = e.Results.ToList(); + // make a clone to avoid possible issue that plugin will also change the list and items when updating view model + var resultsCopy = DeepCloneResults(e.Results, token); PluginManager.UpdatePluginMetadata(resultsCopy, pair.Metadata, e.Query); if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, pair.Metadata, e.Query, @@ -280,10 +280,8 @@ public void ReQuery() public void ReQuery(bool reselect) { - if (SelectedIsFromQueryResults()) - { - QueryResults(isReQuery: true, reSelect: reselect); - } + BackToQueryResults(); + QueryResults(isReQuery: true, reSelect: reselect); } [RelayCommand] @@ -415,6 +413,22 @@ private async Task OpenResultAsync(string index) } } + private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default) + { + var resultsCopy = new List(); + foreach (var result in results.ToList()) + { + if (token.IsCancellationRequested) + { + break; + } + + var resultCopy = result.Clone(); + resultsCopy.Add(resultCopy); + } + return resultsCopy; + } + #endregion #region BasicCommands @@ -611,6 +625,8 @@ public void ChangeQueryText(string queryText, bool isReQuery = false) { Application.Current.Dispatcher.Invoke(() => { + BackToQueryResults(); + if (QueryText != queryText) { // re-query is done in QueryText's setter method @@ -1181,9 +1197,18 @@ async Task QueryTask(PluginPair plugin, bool reSelect = true) currentCancellationToken.ThrowIfCancellationRequested(); - results ??= _emptyResult; + IReadOnlyList resultsCopy; + if (results == null) + { + resultsCopy = _emptyResult; + } + else + { + // make a copy of results to avoid possible issue that FL changes some properties of the records, like score, etc. + resultsCopy = DeepCloneResults(results); + } - if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(results, plugin.Metadata, query, + if (!_resultsUpdateChannelWriter.TryWrite(new ResultsForUpdate(resultsCopy, plugin.Metadata, query, currentCancellationToken, reSelect))) { Log.Error("MainViewModel", "Unable to add item to Result Update Queue"); @@ -1267,7 +1292,6 @@ private Result ContextMenuTopMost(Result result) { _topMostRecord.Remove(result); App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success")); - App.API.BackToQueryResults(); App.API.ReQuery(); return false; } @@ -1285,7 +1309,6 @@ private Result ContextMenuTopMost(Result result) { _topMostRecord.AddOrUpdate(result); App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success")); - App.API.BackToQueryResults(); App.API.ReQuery(); return false; } @@ -1472,12 +1495,31 @@ public void UpdateResultView(ICollection resultsForUpdates) { result.Score = Result.MaxScore; } - else if (result.Score != Result.MaxScore) + else { var priorityScore = metaResults.Metadata.Priority * 150; - result.Score += result.AddSelectedCount ? - _userSelectedRecord.GetSelectedCount(result) + priorityScore : - priorityScore; + if (result.AddSelectedCount) + { + if ((long)result.Score + _userSelectedRecord.GetSelectedCount(result) + priorityScore > Result.MaxScore) + { + result.Score = Result.MaxScore; + } + else + { + result.Score += _userSelectedRecord.GetSelectedCount(result) + priorityScore; + } + } + else + { + if ((long)result.Score + priorityScore > Result.MaxScore) + { + result.Score = Result.MaxScore; + } + else + { + result.Score += priorityScore; + } + } } } } diff --git a/Flow.Launcher/ViewModel/PluginViewModel.cs b/Flow.Launcher/ViewModel/PluginViewModel.cs index 4ce8bd4706f..bae9292bff7 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -90,7 +90,7 @@ public bool IsExpanded private Control _bottomPart2; public Control BottomPart2 => IsExpanded ? _bottomPart2 ??= new InstalledPluginDisplayBottomData() : null; - public bool HasSettingControl => PluginPair.Plugin is ISettingProvider; + public bool HasSettingControl => PluginPair.Plugin is ISettingProvider settingProvider && settingProvider.CreateSettingPanel() != null; public Control SettingControl => IsExpanded ? _settingControl @@ -159,5 +159,4 @@ private void SetActionKeywords() changeKeywordsWindow.ShowDialog(); } } - } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs index 17e9fe2bca7..482e821dc4c 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs @@ -59,7 +59,6 @@ public List LoadContextMenus(Result selectedResult) var link = pluginManifestInfo.UrlSourceCode.StartsWith("https://github.com") ? Regex.Replace(pluginManifestInfo.UrlSourceCode, @"\/tree\/\w+$", "") + "/issues" : pluginManifestInfo.UrlSourceCode; - Context.API.OpenUrl(link); return true; } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml index de6a3a2fb4c..573ca90519e 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Languages/en.xaml @@ -15,6 +15,8 @@ Installing Plugin Download and install {0} Plugin Uninstall + Keep plugin settings + Do you want to keep the settings of the plugin for the next usage? Plugin {0} successfully installed. Restarting Flow, please wait... Unable to find the plugin.json metadata file from the extracted zip file. Error: A plugin which has the same or greater version with {0} already exists. @@ -37,13 +39,13 @@ Plugin {0} successfully updated. Restarting Flow, please wait... Installing from an unknown source You are installing this plugin from an unknown source and it may contain potential risks!{0}{0}Please ensure you understand where this plugin is from and that it is safe.{0}{0}Would you like to continue still?{0}{0}(You can switch off this warning via settings) - + Plugin {0} successfully installed. Please restart Flow. Plugin {0} successfully uninstalled. Please restart Flow. Plugin {0} successfully updated. Please restart Flow. {0} plugins successfully updated. Please restart Flow. Plugin {0} has already been modified. Please restart Flow before making any further changes. - + Plugins Manager Management of installing, uninstalling or updating Flow Launcher plugins diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index c1ed904b337..f4c8a66da31 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -733,7 +733,11 @@ private void Uninstall(PluginMetadata plugin) { try { - PluginManager.UninstallPlugin(plugin, removeSettings: true); + var removePluginSettings = Context.API.ShowMsgBox( + Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_subtitle"), + Context.API.GetTranslation("plugin_pluginsmanager_keep_plugin_settings_title"), + button: MessageBoxButton.YesNo) == MessageBoxResult.No; + PluginManager.UninstallPlugin(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings); } catch (ArgumentException e) { diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml index 7ed711e172e..790c9d2c621 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml @@ -36,6 +36,8 @@ Hides programs with common uninstaller names, such as unins000.exe Search in Program Description Flow will search program's description + Hide duplicated apps + Hide duplicated Win32 programs that are already in the UWP list Suffixes Max Depth diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 6ba7047f23e..b3763aaa69c 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -72,6 +72,8 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I private const string ExeUninstallerSuffix = ".exe"; private const string InkUninstallerSuffix = ".lnk"; + private static readonly string WindowsAppPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WindowsApps"); + static Main() { } @@ -90,11 +92,20 @@ public async Task> QueryAsync(Query query, CancellationToken token) { try { + // Collect all UWP Windows app directories + var uwpsDirectories = _settings.HideDuplicatedWindowsApp ? _uwps + .Where(uwp => !string.IsNullOrEmpty(uwp.Location)) // Exclude invalid paths + .Where(uwp => uwp.Location.StartsWith(WindowsAppPath, StringComparison.OrdinalIgnoreCase)) // Keep system apps + .Select(uwp => uwp.Location.TrimEnd('\\')) // Remove trailing slash + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray() : null; + return _win32s.Cast() .Concat(_uwps) .AsParallel() .WithCancellation(token) .Where(HideUninstallersFilter) + .Where(p => HideDuplicatedWindowsAppFilter(p, uwpsDirectories)) .Where(p => p.Enabled) .Select(p => p.Result(query.Search, Context.API)) .Where(r => r?.Score > 0) @@ -152,6 +163,23 @@ private bool HideUninstallersFilter(IProgram program) return true; } + private static bool HideDuplicatedWindowsAppFilter(IProgram program, string[] uwpsDirectories) + { + if (uwpsDirectories == null || uwpsDirectories.Length == 0) return true; + if (program is UWPApp) return true; + + var location = program.Location.TrimEnd('\\'); // Ensure trailing slash + if (string.IsNullOrEmpty(location)) + return true; // Keep if location is invalid + + if (!location.StartsWith(WindowsAppPath, StringComparison.OrdinalIgnoreCase)) + return true; // Keep if not a Windows app + + // Check if the any Win32 executable directory contains UWP Windows app location matches + return !uwpsDirectories.Any(uwpDirectory => + location.StartsWith(uwpDirectory, StringComparison.OrdinalIgnoreCase)); + } + public async Task InitAsync(PluginInitContext context) { Context = context; @@ -264,7 +292,6 @@ public List LoadContextMenus(Result selectedResult) Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"), Context.API.GetTranslation( "flowlauncher_plugin_program_disable_dlgtitle_success_message")); - Context.API.BackToQueryResults(); Context.API.ReQuery(); return false; }, diff --git a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs index fb24f64d7d6..b2aad63b325 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs @@ -121,6 +121,7 @@ private void RemoveRedundantSuffixes() public bool EnableRegistrySource { get; set; } = true; public bool EnablePathSource { get; set; } = false; public bool EnableUWP { get; set; } = true; + public bool HideDuplicatedWindowsApp { get; set; } = false; internal const char SuffixSeparator = ';'; } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml index e5ca6967e73..973ac9f60af 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml @@ -8,7 +8,7 @@ DataContext="{Binding RelativeSource={RelativeSource Self}}" mc:Ignorable="d"> - + @@ -18,40 +18,40 @@ + ToolTip="{DynamicResource flowlauncher_plugin_program_index_uwp_tooltip}" + Visibility="{Binding ShowUWPCheckbox, Converter={StaticResource BooleanToVisibilityConverter}}" /> @@ -67,21 +67,20 @@ BorderBrush="{DynamicResource Color03B}" BorderThickness="1" /> @@ -91,11 +90,15 @@ IsChecked="{Binding HideUninstallers}" ToolTip="{DynamicResource flowlauncher_plugin_program_enable_hideuninstallers_tooltip}" /> +