diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs index f6e5e5879..ed8f94bcf 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -44,8 +44,10 @@ namespace Flow.Launcher.Core.Plugin 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 @@ namespace Flow.Launcher.Core.Plugin { 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 50eb30998..2a4b22bf3 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -112,7 +112,7 @@ namespace Flow.Launcher.Core.Plugin 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 305b28150..abe563c14 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -112,10 +112,15 @@ namespace Flow.Launcher.Core.Plugin 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 0e8a4b776..5ee723833 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -210,9 +210,9 @@ namespace Flow.Launcher.Core.Plugin { 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 @@ namespace Flow.Launcher.Core.Plugin 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 @@ namespace Flow.Launcher.Core.Plugin 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 @@ namespace Flow.Launcher.Core.Plugin /// /// 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 @@ namespace Flow.Launcher.Core.Plugin 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 @@ namespace Flow.Launcher.Core.Plugin } } - 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 abe3f55b5..bc3900da8 100644 --- a/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/PluginJsonStorage.cs @@ -3,14 +3,17 @@ using Flow.Launcher.Infrastructure.UserSettings; 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 @@ namespace Flow.Launcher.Infrastructure.Storage { 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 8376fd07b..07fc378c3 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 @@ namespace Flow.Launcher.Plugin /// /// 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 bb005752e..9b16cc1cb 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -266,8 +266,9 @@ namespace Flow.Launcher.Plugin /// 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 3dead9918..2621fc2da 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 @@ namespace Flow.Launcher.Test [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 @@ namespace Flow.Launcher.Test [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 8286e142e..0241a374e 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 d97ce227c..090719642 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; 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 @@ namespace Flow.Launcher.Test OneOneOneOne }; - public List GetPrecisionScores() + public static List GetPrecisionScores() { var listToReturn = new List(); @@ -73,10 +74,10 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test Debug.WriteLine("###############################################"); Debug.WriteLine(""); - Assert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); + ClassicAssert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); } } @@ -153,7 +154,7 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test { 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 e72ad7a67..4f135978a 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 @@ namespace Flow.Launcher.Test 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 d6ba48f19..2cc05f95a 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 @@ namespace Flow.Launcher.Test // 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 @@ namespace Flow.Launcher.Test (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 @@ namespace Flow.Launcher.Test // 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 @@ namespace Flow.Launcher.Test (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 80cb74729..420da266d 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.DirectoryInfo; 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins var queryString = queryConstructor.Directory(folderPath, userSearchString); // Then - Assert.AreEqual(expectedString, queryString); + ClassicAssert.AreEqual(expectedString, queryString); } [SupportedOSPlatform("windows7.0")] @@ -105,7 +81,7 @@ namespace Flow.Launcher.Test.Plugins const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch; // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -128,7 +104,7 @@ namespace Flow.Launcher.Test.Plugins var resultString = queryConstructor.FilesAndFolders(userSearchString); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } @@ -138,13 +114,13 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins var resultString = QueryConstructor.RecursiveDirectoryConstraint(path); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -274,7 +250,7 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins }; // 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 @@ namespace Flow.Launcher.Test.Plugins var hash2 = comparator.GetHashCode(result2); // When, Then - Assert.IsTrue(hash1 == hash2); + ClassicAssert.IsTrue(hash1 == hash2); } [TestCase(@"%appdata%", true)] @@ -461,7 +437,7 @@ namespace Flow.Launcher.Test.Plugins 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 42a4630fe..497f874e7 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 @@ namespace Flow.Launcher.Test.Plugins 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 @@ namespace Flow.Launcher.Test.Plugins 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 7ccac5bd5..0dd1fe489 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 @@ namespace Flow.Launcher.Test 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 aa0c8da12..c8ac17748 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 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test 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 @@ namespace Flow.Launcher.Test { 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 eab2705d0..af3c8f6c9 100644 --- a/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs +++ b/Flow.Launcher/CustomQueryHotkeySetting.xaml.cs @@ -13,7 +13,6 @@ namespace Flow.Launcher { private bool update; private CustomPluginHotkey updateCustomHotkey; - public Settings Settings { get; } public CustomQueryHotkeySetting(Settings settings) { @@ -30,13 +29,13 @@ namespace Flow.Launcher { 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 @@ namespace Flow.Launcher 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 @@ namespace Flow.Launcher 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 4589b45ec..416f8e049 100644 --- a/Flow.Launcher/CustomShortcutSetting.xaml.cs +++ b/Flow.Launcher/CustomShortcutSetting.xaml.cs @@ -65,8 +65,7 @@ namespace Flow.Launcher 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 e6f18560e..43c1dfd90 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 b40406e42..79b524f50 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -6,6 +6,9 @@ using NHotkey.Wpf; 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 class HotKeyMapper _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 class HotKeyMapper 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 a42bde7c9..e1dfc1108 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -154,7 +154,16 @@ namespace Flow.Launcher { 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 a7b99f670..a4d21a782 100644 --- a/Flow.Launcher/HotkeyControlDialog.xaml.cs +++ b/Flow.Launcher/HotkeyControlDialog.xaml.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; 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 partial class HotkeyControlDialog : ContentDialog 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 partial class HotkeyControlDialog : ContentDialog 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 @@ public partial class HotkeyControlDialog : ContentDialog 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 @@ public partial class HotkeyControlDialog : ContentDialog //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 @@ public partial class HotkeyControlDialog : ContentDialog } } - 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 4c465d61f..c66772c83 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 be12ca16c..a0e5ffaf8 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 3fdd945e8..8bcd3e046 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -188,6 +188,23 @@ namespace Flow.Launcher 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 d57209a14..650a27610 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -232,8 +232,8 @@ namespace Flow.Launcher.ViewModel 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 @@ namespace Flow.Launcher.ViewModel public void ReQuery(bool reselect) { - if (SelectedIsFromQueryResults()) - { - QueryResults(isReQuery: true, reSelect: reselect); - } + BackToQueryResults(); + QueryResults(isReQuery: true, reSelect: reselect); } [RelayCommand] @@ -415,6 +413,22 @@ namespace Flow.Launcher.ViewModel } } + 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 @@ namespace Flow.Launcher.ViewModel { Application.Current.Dispatcher.Invoke(() => { + BackToQueryResults(); + if (QueryText != queryText) { // re-query is done in QueryText's setter method @@ -1181,9 +1197,18 @@ namespace Flow.Launcher.ViewModel 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 @@ namespace Flow.Launcher.ViewModel { _topMostRecord.Remove(result); App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success")); - App.API.BackToQueryResults(); App.API.ReQuery(); return false; } @@ -1285,7 +1309,6 @@ namespace Flow.Launcher.ViewModel { _topMostRecord.AddOrUpdate(result); App.API.ShowMsg(InternationalizationManager.Instance.GetTranslation("success")); - App.API.BackToQueryResults(); App.API.ReQuery(); return false; } @@ -1472,12 +1495,31 @@ namespace Flow.Launcher.ViewModel { 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 4ce8bd470..bae9292bf 100644 --- a/Flow.Launcher/ViewModel/PluginViewModel.cs +++ b/Flow.Launcher/ViewModel/PluginViewModel.cs @@ -90,7 +90,7 @@ namespace Flow.Launcher.ViewModel 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 @@ namespace Flow.Launcher.ViewModel changeKeywordsWindow.ShowDialog(); } } - } diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs index 17e9fe2bc..482e821dc 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/ContextMenu.cs @@ -59,7 +59,6 @@ namespace Flow.Launcher.Plugin.PluginsManager 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 de6a3a2fb..573ca9051 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 c1ed904b3..f4c8a66da 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -733,7 +733,11 @@ namespace Flow.Launcher.Plugin.PluginsManager { 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 7ed711e17..790c9d2c6 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 6ba7047f2..b3763aaa6 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -72,6 +72,8 @@ namespace Flow.Launcher.Plugin.Program 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 @@ namespace Flow.Launcher.Plugin.Program { 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 @@ namespace Flow.Launcher.Plugin.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 @@ namespace Flow.Launcher.Plugin.Program 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 fb24f64d7..b2aad63b3 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Settings.cs @@ -121,6 +121,7 @@ namespace Flow.Launcher.Plugin.Program 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 e5ca6967e..973ac9f60 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}" /> +