diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index a36a6af3e..670a7a799 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -4,5 +4,4 @@ ssh ubuntu runcount Firefox -Português -Português (Brasil) +workaround diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 5b3419041..0fea6d9ab 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -103,3 +103,4 @@ Reloadable metadatas WMP VSTHRD +CJK diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt index f308ec599..eb8534c49 100644 --- a/.github/actions/spelling/patterns.txt +++ b/.github/actions/spelling/patterns.txt @@ -134,3 +134,12 @@ \bčeština\b \bPortuguês\b \bIoc\b +\bXiao\s*He\b +\bZi\s*Ran\s*Ma\b +\bWei\s*Ruan\b +\bZhi\s*Neng\s*ABC\b +\bZi\s*Guang\s*Pin\s*Yin\b +\bPin\s*Yin\s*Jia\s*Jia\b +\bXing\s*Kong\s*Jian\s*Dao\b +\bDa\s*Niu\b +\bXiao\s*Lang\b diff --git a/.github/workflows/default_plugins.yml b/.github/workflows/default_plugins.yml index ec8dfcd4e..59cedc1e8 100644 --- a/.github/workflows/default_plugins.yml +++ b/.github/workflows/default_plugins.yml @@ -14,7 +14,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.0.x + dotnet-version: 9.0.x - name: Update Plugins To Production Version run: | @@ -42,7 +42,7 @@ jobs: - name: Build BrowserBookmark run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark" + dotnet publish 'Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.BrowserBookmark" 7z a -tzip "Flow.Launcher.Plugin.BrowserBookmark.zip" "./Flow.Launcher.Plugin.BrowserBookmark/*" rm -r "Flow.Launcher.Plugin.BrowserBookmark" @@ -66,7 +66,7 @@ jobs: - name: Build Calculator run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator" + dotnet publish 'Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Calculator" 7z a -tzip "Flow.Launcher.Plugin.Calculator.zip" "./Flow.Launcher.Plugin.Calculator/*" rm -r "Flow.Launcher.Plugin.Calculator" @@ -90,7 +90,7 @@ jobs: - name: Build Explorer run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer" + dotnet publish 'Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Explorer" 7z a -tzip "Flow.Launcher.Plugin.Explorer.zip" "./Flow.Launcher.Plugin.Explorer/*" rm -r "Flow.Launcher.Plugin.Explorer" @@ -114,7 +114,7 @@ jobs: - name: Build PluginIndicator run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator" + dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.PluginIndicator" 7z a -tzip "Flow.Launcher.Plugin.PluginIndicator.zip" "./Flow.Launcher.Plugin.PluginIndicator/*" rm -r "Flow.Launcher.Plugin.PluginIndicator" @@ -138,7 +138,7 @@ jobs: - name: Build PluginsManager run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager" + dotnet publish 'Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.PluginsManager" 7z a -tzip "Flow.Launcher.Plugin.PluginsManager.zip" "./Flow.Launcher.Plugin.PluginsManager/*" rm -r "Flow.Launcher.Plugin.PluginsManager" @@ -162,7 +162,7 @@ jobs: - name: Build ProcessKiller run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller" + dotnet publish 'Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.ProcessKiller" 7z a -tzip "Flow.Launcher.Plugin.ProcessKiller.zip" "./Flow.Launcher.Plugin.ProcessKiller/*" rm -r "Flow.Launcher.Plugin.ProcessKiller" @@ -186,7 +186,7 @@ jobs: - name: Build Program run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net7.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program" + dotnet publish 'Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj' --framework net9.0-windows10.0.19041.0 -c Release -o "Flow.Launcher.Plugin.Program" 7z a -tzip "Flow.Launcher.Plugin.Program.zip" "./Flow.Launcher.Plugin.Program/*" rm -r "Flow.Launcher.Plugin.Program" @@ -210,7 +210,7 @@ jobs: - name: Build Shell run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Shell" + dotnet publish 'Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Shell" 7z a -tzip "Flow.Launcher.Plugin.Shell.zip" "./Flow.Launcher.Plugin.Shell/*" rm -r "Flow.Launcher.Plugin.Shell" @@ -234,7 +234,7 @@ jobs: - name: Build Sys run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Sys" + dotnet publish 'Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Sys" 7z a -tzip "Flow.Launcher.Plugin.Sys.zip" "./Flow.Launcher.Plugin.Sys/*" rm -r "Flow.Launcher.Plugin.Sys" @@ -258,7 +258,7 @@ jobs: - name: Build Url run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.Url" + dotnet publish 'Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.Url" 7z a -tzip "Flow.Launcher.Plugin.Url.zip" "./Flow.Launcher.Plugin.Url/*" rm -r "Flow.Launcher.Plugin.Url" @@ -282,7 +282,7 @@ jobs: - name: Build WebSearch run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch" + dotnet publish 'Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.WebSearch" 7z a -tzip "Flow.Launcher.Plugin.WebSearch.zip" "./Flow.Launcher.Plugin.WebSearch/*" rm -r "Flow.Launcher.Plugin.WebSearch" @@ -306,7 +306,7 @@ jobs: - name: Build WindowsSettings run: | - dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net7.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings" + dotnet publish 'Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj' --framework net9.0-windows -c Release -o "Flow.Launcher.Plugin.WindowsSettings" 7z a -tzip "Flow.Launcher.Plugin.WindowsSettings.zip" "./Flow.Launcher.Plugin.WindowsSettings/*" rm -r "Flow.Launcher.Plugin.WindowsSettings" diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 7498262de..988548bee 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: runs-on: windows-latest env: - FlowVersion: 1.19.5 + FlowVersion: 1.20.2 NUGET_CERT_REVOCATION_MODE: offline BUILD_NUMBER: ${{ github.run_number }} steps: @@ -31,7 +31,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.0.x + dotnet-version: 9.0.x # cache: true # cache-dependency-path: | # Flow.Launcher/packages.lock.json diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 47bd66107..904392bf0 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -72,7 +72,7 @@ jobs: steps: - name: check-spelling id: spelling - uses: check-spelling/check-spelling@prerelease + uses: check-spelling/check-spelling@v0.0.25 with: suppress_push_for_open_pull_request: 1 checkout: true @@ -128,7 +128,7 @@ jobs: if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request') steps: - name: comment - uses: check-spelling/check-spelling@prerelease + uses: check-spelling/check-spelling@v0.0.25 with: checkout: true spell_check_this: check-spelling/spell-check-this@main diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs index 7f02cef09..721e14dca 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -45,8 +45,7 @@ namespace Flow.Launcher.Core.Configuration #endif IndicateDeletion(DataLocation.PortableDataPath); - API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " + - "after the restart your portable data profile will be deleted and roaming data profile kept"); + API.ShowMsgBox(API.GetTranslation("restartToDisablePortableMode")); UpdateManager.RestartApp(Constant.ApplicationFileName); } @@ -69,8 +68,7 @@ namespace Flow.Launcher.Core.Configuration #endif IndicateDeletion(DataLocation.RoamingDataPath); - API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " + - "after the restart your roaming data profile will be deleted and portable data profile kept"); + API.ShowMsgBox(API.GetTranslation("restartToEnablePortableMode")); UpdateManager.RestartApp(Constant.ApplicationFileName); } @@ -154,9 +152,8 @@ namespace Flow.Launcher.Core.Configuration { FilesFolders.RemoveFolderIfExists(roamingDataDir, (s) => API.ShowMsgBox(s)); - if (API.ShowMsgBox("Flow Launcher has detected you enabled portable mode, " + - "would you like to move it to a different location?", string.Empty, - MessageBoxButton.YesNo) == MessageBoxResult.Yes) + if (API.ShowMsgBox(API.GetTranslation("moveToDifferentLocation"), + string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes) { FilesFolders.OpenPath(Constant.RootDirectory, (s) => API.ShowMsgBox(s)); @@ -169,8 +166,7 @@ namespace Flow.Launcher.Core.Configuration { FilesFolders.RemoveFolderIfExists(portableDataDir, (s) => API.ShowMsgBox(s)); - API.ShowMsgBox("Flow Launcher has detected you disabled portable mode, " + - "the relevant shortcuts and uninstaller entry have been created"); + API.ShowMsgBox(API.GetTranslation("shortcutsUninstallerCreated")); } } @@ -181,9 +177,8 @@ namespace Flow.Launcher.Core.Configuration if (roamingLocationExists && portableLocationExists) { - API.ShowMsgBox(string.Format("Flow Launcher detected your user data exists both in {0} and " + - "{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.", - DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); + API.ShowMsgBox(string.Format(API.GetTranslation("userDataDuplicated"), + DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); return false; } diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index 7ca91eaec..e58b299f6 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -22,6 +22,10 @@ namespace Flow.Launcher.Core.ExternalPlugins private static DateTime lastFetchedAt = DateTime.MinValue; private static readonly TimeSpan fetchTimeout = TimeSpan.FromMinutes(2); + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + public static List UserPlugins { get; private set; } public static async Task UpdateManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) @@ -46,7 +50,7 @@ namespace Flow.Launcher.Core.ExternalPlugins } catch (Exception e) { - Ioc.Default.GetRequiredService().LogException(ClassName, "Http request failed", e); + API.LogException(ClassName, "Http request failed", e); } finally { diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index e9f199d00..b31793450 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -1,7 +1,7 @@ - net7.0-windows + net9.0-windows true true Library @@ -12,6 +12,7 @@ false false en + true @@ -57,6 +58,7 @@ + diff --git a/Flow.Launcher.Core/Plugin/PluginInstaller.cs b/Flow.Launcher.Core/Plugin/PluginInstaller.cs index 33963c01a..22aeef224 100644 --- a/Flow.Launcher.Core/Plugin/PluginInstaller.cs +++ b/Flow.Launcher.Core/Plugin/PluginInstaller.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; @@ -277,6 +278,122 @@ public static class PluginInstaller } } + /// + /// Updates the plugin to the latest version available from its source. + /// + /// Action to execute when the user chooses to update all plugins. + /// If true, do not show any messages when there is no update available. + /// If true, only use the primary URL for updates. + /// Cancellation token to cancel the update operation. + /// + public static async Task CheckForPluginUpdatesAsync(Action> updateAllPlugins, bool silentUpdate = true, bool usePrimaryUrlOnly = false, CancellationToken token = default) + { + // Update the plugin manifest + await API.UpdatePluginManifestAsync(usePrimaryUrlOnly, token); + + // Get all plugins that can be updated + var resultsForUpdate = ( + from existingPlugin in API.GetAllPlugins() + join pluginUpdateSource in API.GetPluginManifest() + on existingPlugin.Metadata.ID equals pluginUpdateSource.ID + where string.Compare(existingPlugin.Metadata.Version, pluginUpdateSource.Version, + StringComparison.InvariantCulture) < + 0 // if current version precedes version of the plugin from update source (e.g. PluginsManifest) + && !API.PluginModified(existingPlugin.Metadata.ID) + select + new PluginUpdateInfo() + { + ID = existingPlugin.Metadata.ID, + Name = existingPlugin.Metadata.Name, + Author = existingPlugin.Metadata.Author, + CurrentVersion = existingPlugin.Metadata.Version, + NewVersion = pluginUpdateSource.Version, + IcoPath = existingPlugin.Metadata.IcoPath, + PluginExistingMetadata = existingPlugin.Metadata, + PluginNewUserPlugin = pluginUpdateSource + }).ToList(); + + // No updates + if (!resultsForUpdate.Any()) + { + if (!silentUpdate) + { + API.ShowMsg(API.GetTranslation("updateNoResultTitle"), API.GetTranslation("updateNoResultSubtitle")); + } + return; + } + + // If all plugins are modified, just return + if (resultsForUpdate.All(x => API.PluginModified(x.ID))) + { + return; + } + + // Show message box with button to update all plugins + API.ShowMsgWithButton( + API.GetTranslation("updateAllPluginsTitle"), + API.GetTranslation("updateAllPluginsButtonContent"), + () => + { + updateAllPlugins(resultsForUpdate); + }, + string.Join(", ", resultsForUpdate.Select(x => x.PluginExistingMetadata.Name))); + } + + /// + /// Updates all plugins that have available updates. + /// + /// + /// + public static async Task UpdateAllPluginsAsync(IEnumerable resultsForUpdate, bool restart) + { + var anyPluginSuccess = false; + await Task.WhenAll(resultsForUpdate.Select(async plugin => + { + var downloadToFilePath = Path.Combine(Path.GetTempPath(), $"{plugin.Name}-{plugin.NewVersion}.zip"); + + try + { + using var cts = new CancellationTokenSource(); + + await DownloadFileAsync( + $"{API.GetTranslation("DownloadingPlugin")} {plugin.PluginNewUserPlugin.Name}", + plugin.PluginNewUserPlugin.UrlDownload, downloadToFilePath, cts); + + // check if user cancelled download before installing plugin + if (cts.IsCancellationRequested) + { + return; + } + + if (!await API.UpdatePluginAsync(plugin.PluginExistingMetadata, plugin.PluginNewUserPlugin, downloadToFilePath)) + { + return; + } + + anyPluginSuccess = true; + } + catch (Exception e) + { + API.LogException(ClassName, "Failed to update plugin", e); + API.ShowMsgError(API.GetTranslation("ErrorUpdatingPlugin")); + } + })); + + if (!anyPluginSuccess) return; + + if (restart) + { + API.RestartApp(); + } + else + { + API.ShowMsg( + API.GetTranslation("updatebtn"), + API.GetTranslation("PluginsUpdateSuccessNoRestart")); + } + } + /// /// Downloads a file from a URL to a local path, optionally showing a progress box and handling cancellation. /// @@ -351,3 +468,15 @@ public static class PluginInstaller ); } } + +public record PluginUpdateInfo +{ + public string ID { get; init; } + public string Name { get; init; } + public string Author { get; init; } + public string CurrentVersion { get; init; } + public string NewVersion { get; init; } + public string IcoPath { get; init; } + public PluginMetadata PluginExistingMetadata { get; init; } + public UserPlugin PluginNewUserPlugin { get; init; } +} diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index d88f2f050..a4ab8de08 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -9,6 +9,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.ExternalPlugins; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -40,10 +41,13 @@ namespace Flow.Launcher.Core.Plugin private static IEnumerable _resultUpdatePlugin; private static IEnumerable _translationPlugins; + private static readonly List _dialogJumpExplorerPlugins = new(); + private static readonly List _dialogJumpDialogPlugins = new(); + /// /// Directories that will hold Flow Launcher plugin directory /// - private static readonly string[] Directories = + public static readonly string[] Directories = { Constant.PreinstalledDirectory, DataLocation.PluginsDirectory }; @@ -186,6 +190,24 @@ namespace Flow.Launcher.Core.Plugin _homePlugins = GetPluginsForInterface(); _resultUpdatePlugin = GetPluginsForInterface(); _translationPlugins = GetPluginsForInterface(); + + // Initialize Dialog Jump plugin pairs + foreach (var pair in GetPluginsForInterface()) + { + _dialogJumpExplorerPlugins.Add(new DialogJumpExplorerPair + { + Plugin = (IDialogJumpExplorer)pair.Plugin, + Metadata = pair.Metadata + }); + } + foreach (var pair in GetPluginsForInterface()) + { + _dialogJumpDialogPlugins.Add(new DialogJumpDialogPair + { + Plugin = (IDialogJumpDialog)pair.Plugin, + Metadata = pair.Metadata + }); + } } private static void UpdatePluginDirectory(List metadatas) @@ -288,20 +310,24 @@ namespace Flow.Launcher.Core.Plugin } } - public static ICollection ValidPluginsForQuery(Query query) + public static ICollection ValidPluginsForQuery(Query query, bool dialogJump) { if (query is null) return Array.Empty(); if (!NonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin)) { - return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + if (dialogJump) + return GlobalPlugins.Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID)).ToList(); + else + return GlobalPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); } - if (API.PluginModified(plugin.Metadata.ID)) - { + if (dialogJump && plugin.Plugin is not IAsyncDialogJump) + return Array.Empty(); + + if (API.PluginModified(plugin.Metadata.ID)) return Array.Empty(); - } return new List { @@ -388,6 +414,36 @@ namespace Flow.Launcher.Core.Plugin return results; } + public static async Task> QueryDialogJumpForPluginAsync(PluginPair pair, Query query, CancellationToken token) + { + var results = new List(); + var metadata = pair.Metadata; + + try + { + var milliseconds = await API.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}", + async () => results = await ((IAsyncDialogJump)pair.Plugin).QueryDialogJumpAsync(query, token).ConfigureAwait(false)); + + token.ThrowIfCancellationRequested(); + if (results == null) + return null; + UpdatePluginMetadata(results, metadata, query); + + token.ThrowIfCancellationRequested(); + } + catch (OperationCanceledException) + { + // null will be fine since the results will only be added into queue if the token hasn't been cancelled + return null; + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e); + return null; + } + return results; + } + public static void UpdatePluginMetadata(IReadOnlyList results, PluginMetadata metadata, Query query) { foreach (var r in results) @@ -463,6 +519,16 @@ namespace Flow.Launcher.Core.Plugin return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id); } + public static IList GetDialogJumpExplorers() + { + return _dialogJumpExplorerPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + } + + public static IList GetDialogJumpDialogs() + { + return _dialogJumpDialogPlugins.Where(p => !PluginModified(p.Metadata.ID)).ToList(); + } + public static bool ActionKeywordRegistered(string actionKeyword) { // this method is only checking for action keywords (defined as not '*') registration @@ -719,7 +785,7 @@ namespace Flow.Launcher.Core.Plugin catch (Exception e) { API.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e); - API.ShowMsg(API.GetTranslation("failedToRemovePluginSettingsTitle"), + API.ShowMsgError(API.GetTranslation("failedToRemovePluginSettingsTitle"), string.Format(API.GetTranslation("failedToRemovePluginSettingsMessage"), plugin.Name)); } } @@ -735,7 +801,7 @@ namespace Flow.Launcher.Core.Plugin catch (Exception e) { API.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e); - API.ShowMsg(API.GetTranslation("failedToRemovePluginCacheTitle"), + API.ShowMsgError(API.GetTranslation("failedToRemovePluginCacheTitle"), string.Format(API.GetTranslation("failedToRemovePluginCacheMessage"), plugin.Name)); } Settings.RemovePluginSettings(plugin.ID); diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 256c36065..9d511297e 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -120,15 +120,15 @@ namespace Flow.Launcher.Core.Plugin { var errorPluginString = string.Join(Environment.NewLine, erroredPlugins); - var errorMessage = "The following " - + (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ") - + "errored and cannot be loaded:"; + var errorMessage = erroredPlugins.Count > 1 ? + API.GetTranslation("pluginsHaveErrored") : + API.GetTranslation("pluginHasErrored"); _ = Task.Run(() => { - Ioc.Default.GetRequiredService().ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + + API.ShowMsgBox($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" + $"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" + - $"Please refer to the logs for more information", "", + API.GetTranslation("referToLogs"), string.Empty, MessageBoxButton.OK, MessageBoxImage.Warning); }); } diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index 7b7d6eef6..d2ab2d028 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -28,22 +27,20 @@ namespace Flow.Launcher.Core.Resource private const string DefaultFile = "en.xaml"; private const string Extension = ".xaml"; private readonly Settings _settings; - private readonly List _languageDirectories = new(); - private readonly List _oldResources = new(); + private readonly List _languageDirectories = []; + private readonly List _oldResources = []; private static string SystemLanguageCode; public Internationalization(Settings settings) { _settings = settings; - AddFlowLauncherLanguageDirectory(); } - private void AddFlowLauncherLanguageDirectory() - { - var directory = Path.Combine(Constant.ProgramDirectory, Folder); - _languageDirectories.Add(directory); - } + #region Initialization + /// + /// Initialize the system language code based on the current culture. + /// public static void InitSystemLanguageCode() { var availableLanguages = AvailableLanguages.GetAvailableLanguages(); @@ -72,35 +69,6 @@ namespace Flow.Launcher.Core.Resource SystemLanguageCode = DefaultLanguageCode; } - private void AddPluginLanguageDirectories() - { - foreach (var plugin in PluginManager.GetTranslationPlugins()) - { - var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location; - var dir = Path.GetDirectoryName(location); - if (dir != null) - { - var pluginThemeDirectory = Path.Combine(dir, Folder); - _languageDirectories.Add(pluginThemeDirectory); - } - else - { - API.LogError(ClassName, $"Can't find plugin path <{location}> for <{plugin.Metadata.Name}>"); - } - } - - LoadDefaultLanguage(); - } - - private void LoadDefaultLanguage() - { - // Removes language files loaded before any plugins were loaded. - // Prevents the language Flow started in from overwriting English if the user switches back to English - RemoveOldLanguageFiles(); - LoadLanguage(AvailableLanguages.English); - _oldResources.Clear(); - } - /// /// Initialize language. Will change app language and plugin language based on settings. /// @@ -116,13 +84,64 @@ namespace Flow.Launcher.Core.Resource // Get language by language code and change language var language = GetLanguageByLanguageCode(languageCode); + // Add Flow Launcher language directory + AddFlowLauncherLanguageDirectory(); + // Add plugin language directories first so that we can load language files from plugins AddPluginLanguageDirectories(); + // Load default language resources + LoadDefaultLanguage(); + // Change language - await ChangeLanguageAsync(language); + await ChangeLanguageAsync(language, false); } + private void AddFlowLauncherLanguageDirectory() + { + // Check if Flow Launcher language directory exists + var directory = Path.Combine(Constant.ProgramDirectory, Folder); + if (!Directory.Exists(directory)) + { + API.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>"); + return; + } + + _languageDirectories.Add(directory); + } + + private void AddPluginLanguageDirectories() + { + foreach (var pluginsDir in PluginManager.Directories) + { + if (!Directory.Exists(pluginsDir)) continue; + + // Enumerate all top directories in the plugin directory + foreach (var dir in Directory.GetDirectories(pluginsDir)) + { + // Check if the directory contains a language folder + var pluginLanguageDir = Path.Combine(dir, Folder); + if (!Directory.Exists(pluginLanguageDir)) continue; + + // Check if the language directory contains default language file since it will be checked later + _languageDirectories.Add(pluginLanguageDir); + } + } + } + + private void LoadDefaultLanguage() + { + // Removes language files loaded before any plugins were loaded. + // Prevents the language Flow started in from overwriting English if the user switches back to English + RemoveOldLanguageFiles(); + LoadLanguage(AvailableLanguages.English); + _oldResources.Clear(); + } + + #endregion + + #region Change Language + /// /// Change language during runtime. Will change app language and plugin language & save settings. /// @@ -151,8 +170,8 @@ namespace Flow.Launcher.Core.Resource private static Language GetLanguageByLanguageCode(string languageCode) { - var lowercase = languageCode.ToLower(); - var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); + var language = AvailableLanguages.GetAvailableLanguages(). + FirstOrDefault(o => o.LanguageCode.Equals(languageCode, StringComparison.OrdinalIgnoreCase)); if (language == null) { API.LogError(ClassName, $"Language code can't be found <{languageCode}>"); @@ -164,7 +183,7 @@ namespace Flow.Launcher.Core.Resource } } - private async Task ChangeLanguageAsync(Language language) + private async Task ChangeLanguageAsync(Language language, bool updateMetadata = true) { // Remove old language files and load language RemoveOldLanguageFiles(); @@ -176,8 +195,11 @@ namespace Flow.Launcher.Core.Resource // Change culture info ChangeCultureInfo(language.LanguageCode); - // Raise event for plugins after culture is set - await Task.Run(UpdatePluginMetadataTranslations); + if (updateMetadata) + { + // Raise event for plugins after culture is set + await Task.Run(UpdatePluginMetadataTranslations); + } } public static void ChangeCultureInfo(string languageCode) @@ -200,6 +222,10 @@ namespace Flow.Launcher.Core.Resource thread.CurrentUICulture = currentCulture; } + #endregion + + #region Prompt Pinyin + public bool PromptShouldUsePinyin(string languageCodeToSet) { var languageToSet = GetLanguageByLanguageCode(languageCodeToSet); @@ -212,14 +238,18 @@ namespace Flow.Launcher.Core.Resource // No other languages should show the following text so just make it hard-coded // "Do you want to search with pinyin?" - string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ; + string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?"; - if (Ioc.Default.GetRequiredService().ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) + if (API.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) return false; return true; } + #endregion + + #region Language Resources Management + private void RemoveOldLanguageFiles() { var dicts = Application.Current.Resources.MergedDictionaries; @@ -255,46 +285,6 @@ namespace Flow.Launcher.Core.Resource } } - public List LoadAvailableLanguages() - { - var list = AvailableLanguages.GetAvailableLanguages(); - list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode))); - return list; - } - - public static string GetTranslation(string key) - { - var translation = Application.Current.TryFindResource(key); - if (translation is string) - { - return translation.ToString(); - } - else - { - API.LogError(ClassName, $"No Translation for key {key}"); - return $"No Translation for key {key}"; - } - } - - private void UpdatePluginMetadataTranslations() - { - // Update plugin metadata name & description - foreach (var p in PluginManager.GetTranslationPlugins()) - { - if (p.Plugin is not IPluginI18n pluginI18N) return; - try - { - p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); - p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription(); - pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture); - } - catch (Exception e) - { - API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); - } - } - } - private static string LanguageFile(string folder, string language) { if (Directory.Exists(folder)) @@ -324,5 +314,59 @@ namespace Flow.Launcher.Core.Resource return string.Empty; } } + + #endregion + + #region Available Languages + + public List LoadAvailableLanguages() + { + var list = AvailableLanguages.GetAvailableLanguages(); + list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode))); + return list; + } + + #endregion + + #region Get Translations + + public static string GetTranslation(string key) + { + var translation = Application.Current.TryFindResource(key); + if (translation is string) + { + return translation.ToString(); + } + else + { + API.LogError(ClassName, $"No Translation for key {key}"); + return $"No Translation for key {key}"; + } + } + + #endregion + + #region Update Metadata + + public static void UpdatePluginMetadataTranslations() + { + // Update plugin metadata name & description + foreach (var p in PluginManager.GetTranslationPlugins()) + { + if (p.Plugin is not IPluginI18n pluginI18N) return; + try + { + p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); + p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription(); + pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture); + } + catch (Exception e) + { + API.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); + } + } + } + + #endregion } } diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index bc3655f69..45275696c 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -49,8 +49,9 @@ namespace Flow.Launcher.Core // UpdateApp CheckForUpdate will return value only if the app is squirrel installed var newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false); - var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); - var currentVersion = Version.Parse(Constant.Version); + var newReleaseVersion = + SemanticVersioning.Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); + var currentVersion = SemanticVersioning.Version.Parse(Constant.Version); _api.LogInfo(ClassName, $"Future Release <{Formatted(newUpdateInfo.FutureReleaseEntry)}>"); @@ -71,10 +72,13 @@ namespace Flow.Launcher.Core if (DataLocation.PortableDataLocationInUse()) { - var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}"; + var targetDestination = updateManager.RootAppDirectory + + $"\\app-{newReleaseVersion}\\{DataLocation.PortableFolderName}"; FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s)); - if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, (s) => _api.ShowMsgBox(s))) - _api.ShowMsgBox(string.Format(_api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), + if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination, + (s) => _api.ShowMsgBox(s))) + _api.ShowMsgBox(string.Format( + _api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), DataLocation.PortableDataPath, targetDestination)); } @@ -87,24 +91,27 @@ namespace Flow.Launcher.Core _api.LogInfo(ClassName, $"Update success:{newVersionTips}"); - if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) + if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) { UpdateManager.RestartApp(Constant.ApplicationFileName); } } catch (Exception e) { - if (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException) + if (e is HttpRequestException or WebException or SocketException || + e.InnerException is TimeoutException) { - _api.LogException(ClassName, $"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); + _api.LogException(ClassName, + $"Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); } else { _api.LogException(ClassName, $"Error Occurred", e); } - + if (!silentUpdate) - _api.ShowMsg(_api.GetTranslation("update_flowlauncher_fail"), + _api.ShowMsgError(_api.GetTranslation("update_flowlauncher_fail"), _api.GetTranslation("update_flowlauncher_check_connection")); } finally @@ -116,14 +123,11 @@ namespace Flow.Launcher.Core [UsedImplicitly] private class GithubRelease { - [JsonPropertyName("prerelease")] - public bool Prerelease { get; [UsedImplicitly] set; } + [JsonPropertyName("prerelease")] public bool Prerelease { get; [UsedImplicitly] set; } - [JsonPropertyName("published_at")] - public DateTime PublishedAt { get; [UsedImplicitly] set; } + [JsonPropertyName("published_at")] public DateTime PublishedAt { get; [UsedImplicitly] set; } - [JsonPropertyName("html_url")] - public string HtmlUrl { get; [UsedImplicitly] set; } + [JsonPropertyName("html_url")] public string HtmlUrl { get; [UsedImplicitly] set; } } // https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs @@ -138,10 +142,7 @@ namespace Flow.Launcher.Core var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First(); var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/"); - var client = new WebClient - { - Proxy = Http.WebProxy - }; + var client = new WebClient { Proxy = Http.WebProxy }; var downloader = new FileDownloader(client); var manager = new UpdateManager(latestUrl, urlDownloader: downloader); @@ -158,10 +159,7 @@ namespace Flow.Launcher.Core private static string Formatted(T t) { - var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions - { - WriteIndented = true - }); + var formatted = JsonSerializer.Serialize(t, new JsonSerializerOptions { WriteIndented = true }); return formatted; } diff --git a/Flow.Launcher.Core/packages.lock.json b/Flow.Launcher.Core/packages.lock.json new file mode 100644 index 000000000..0c513951b --- /dev/null +++ b/Flow.Launcher.Core/packages.lock.json @@ -0,0 +1,238 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "Droplex": { + "type": "Direct", + "requested": "[1.7.0, )", + "resolved": "1.7.0", + "contentHash": "wutfIus/Ufw/9TDsp86R1ycnIH+wWrj4UhcmrzAHWjsdyC2iM07WEQ9+APTB7pQynsDnYH1r2i58XgAJ3lxUXA==", + "dependencies": { + "YamlDotNet": "9.1.0" + } + }, + "FSharp.Core": { + "type": "Direct", + "requested": "[9.0.101, )", + "resolved": "9.0.101", + "contentHash": "3/YR1SDWFA+Ojx9HiBwND+0UR8ZWoeZfkhD0DWAPCDdr/YI+CyFkArmMGzGSyPXeYtjG0sy0emzfyNwjt7zhig==" + }, + "Meziantou.Framework.Win32.Jobs": { + "type": "Direct", + "requested": "[3.4.0, )", + "resolved": "3.4.0", + "contentHash": "5GGLckfpwoC1jznInEYfK2INrHyD7K1RtwZJ98kNPKBU6jeu24i4zfgDGHHfb+eK3J+eFPAxo0aYcbUxNXIbNw==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Direct", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, + "squirrel.windows": { + "type": "Direct", + "requested": "[1.5.2, )", + "resolved": "1.5.2", + "contentHash": "89Y/CFxWm7SEOjvuV2stVa8p+SNM9GOLk4tUNm2nUF792nfkimAgwRA/umVsdyd/OXBH8byXSh4V1qck88ZAyQ==", + "dependencies": { + "DeltaCompressionDotNet": "[1.0.0, 2.0.0)", + "Mono.Cecil": "0.9.6.1", + "Splat": "1.6.2" + } + }, + "StreamJsonRpc": { + "type": "Direct", + "requested": "[2.20.20, )", + "resolved": "2.20.20", + "contentHash": "gwG7KViLbSWS7EI0kYevinVmIga9wZNrpSY/FnWyC6DbdjKJ1xlv/FV1L9b0rLkVP8cGxfIMexdvo/+2W5eq6Q==", + "dependencies": { + "MessagePack": "2.5.187", + "Microsoft.VisualStudio.Threading": "17.10.48", + "Microsoft.VisualStudio.Threading.Analyzers": "17.10.48", + "Microsoft.VisualStudio.Validation": "17.8.8", + "Nerdbank.Streams": "2.11.74", + "Newtonsoft.Json": "13.0.1", + "System.IO.Pipelines": "8.0.0" + } + }, + "Ben.Demystifier": { + "type": "Transitive", + "resolved": "0.4.1", + "contentHash": "axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==", + "dependencies": { + "System.Reflection.Metadata": "5.0.0" + } + }, + "BitFaster.Caching": { + "type": "Transitive", + "resolved": "2.5.3", + "contentHash": "Vo/39qcam5Xe+DbyfH0JZyqPswdOoa7jv4PGtRJ6Wj8AU+aZ+TuJRlJcIe+MQjRTJwliI8k8VSQpN8sEoBIv2g==" + }, + "CommunityToolkit.Mvvm": { + "type": "Transitive", + "resolved": "8.4.0", + "contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw==" + }, + "DeltaCompressionDotNet": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "nwbZAYd+DblXAIzlnwDSnl0CiCm8jWLfHSYnoN4wYhtIav6AegB3+T/vKzLbU2IZlPB8Bvl8U3NXpx3eaz+N5w==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" + }, + "MemoryPack": { + "type": "Transitive", + "resolved": "1.21.3", + "contentHash": "cwCtED8y400vMWx/Vp0QCSeEpVFjDU4JwF52VX9WTaqVERUvNqjG9n6osFlmFuytegyXnHvYEu1qRJ8rv/rkbg==", + "dependencies": { + "MemoryPack.Core": "1.21.3", + "MemoryPack.Generator": "1.21.3" + } + }, + "MemoryPack.Core": { + "type": "Transitive", + "resolved": "1.21.3", + "contentHash": "ajrYoBWT2aKeH4tlY8q/1C9qK1R/NK+7FkuVOX58ebOSxkABoFTqCR7W+Zk2rakUHZiEgNdRqO67hiRZPq6fLA==" + }, + "MemoryPack.Generator": { + "type": "Transitive", + "resolved": "1.21.3", + "contentHash": "hYU0TAIarDKnbkNIWvb7P4zBUL+CTahkuNkczsKvycSMR5kiwQ4IfLexywNKX3s05Izp4gzDSPbueepNWZRpWA==" + }, + "MessagePack": { + "type": "Transitive", + "resolved": "2.5.187", + "contentHash": "uW4j8m4Nc+2Mk5n6arOChavJ9bLjkis0qWASOj2h2OwmfINuzYv+mjCHUymrYhmyyKTu3N+ObtTXAY4uQ7jIhg==", + "dependencies": { + "MessagePack.Annotations": "2.5.187", + "Microsoft.NET.StringTools": "17.6.3" + } + }, + "MessagePack.Annotations": { + "type": "Transitive", + "resolved": "2.5.187", + "contentHash": "/IvvMMS8opvlHjEJ/fR2Cal4Co726Kj77Z8KiohFhuHfLHHmb9uUxW5+tSCL4ToKFfkQlrS3HD638mRq83ySqA==" + }, + "Microsoft.NET.StringTools": { + "type": "Transitive", + "resolved": "17.6.3", + "contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA==" + }, + "Microsoft.VisualStudio.Threading": { + "type": "Transitive", + "resolved": "17.12.19", + "contentHash": "eLiGMkMYyaSguqHs3lsrFxy3tAWSLuPEL2pIWRcADMDVAs2xqm3dr1d9QYjiEusTgiClF9KD6OB2NdZP72Oy0Q==", + "dependencies": { + "Microsoft.VisualStudio.Threading.Analyzers": "17.12.19", + "Microsoft.VisualStudio.Validation": "17.8.8" + } + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Transitive", + "resolved": "17.12.19", + "contentHash": "v3IYeedjoktvZ+GqYmLudxZJngmf/YWIxNT2Uy6QMMN19cvw+nkWoip1Gr1RtnFkUo1MPUVMis4C8Kj8d8DpSQ==" + }, + "Microsoft.VisualStudio.Validation": { + "type": "Transitive", + "resolved": "17.8.8", + "contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g==" + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "9.0.2", + "contentHash": "5BkGZ6mHp2dHydR29sb0fDfAuqkv30AHtTih8wMzvPZysOmBFvHfnkR2w3tsc0pSiIg8ZoKyefJXWy9r3pBh0w==" + }, + "Mono.Cecil": { + "type": "Transitive", + "resolved": "0.9.6.1", + "contentHash": "yMsurNaOxxKIjyW9pEB+tRrR1S3DFnN1+iBgKvYvXG8kW0Y6yknJeMAe/tl3+P78/2C6304TgF7aVqpqXgEQ9Q==" + }, + "Nerdbank.Streams": { + "type": "Transitive", + "resolved": "2.11.74", + "contentHash": "r4G7uHHfoo8LCilPOdtf2C+Q5ymHOAXtciT4ZtB2xRlAvv4gPkWBYNAijFblStv3+uidp81j5DP11jMZl4BfJw==", + "dependencies": { + "Microsoft.VisualStudio.Threading": "17.10.48", + "Microsoft.VisualStudio.Validation": "17.8.8", + "System.IO.Pipelines": "8.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "NLog": { + "type": "Transitive", + "resolved": "4.7.10", + "contentHash": "rcegW7kYOCjl7wX0SzsqpPBqnJ51JKi1WkYb6QBVX0Wc5IgH19Pv4t/co+T0s06OS0Ne44xgkY/mHg0PdrmJow==" + }, + "PropertyChanged.Fody": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "IAZyq0uolKo2WYm4mjx+q7A8fSGFT0x2e1s3y+ODn4JI0kqTDoo9GF2tdaypUzRFJZfdMxfC5HZW9QzdJLtOnA==", + "dependencies": { + "Fody": "6.5.1" + } + }, + "Splat": { + "type": "Transitive", + "resolved": "1.6.2", + "contentHash": "DeH0MxPU+D4JchkIDPYG4vUT+hsWs9S41cFle0/4K5EJMXWurx5DzAkj2366DfK14/XKNhsu6tCl4dZXJ3CD4w==" + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "9.0.2", + "contentHash": "JU947wzf8JbBS16Y5EIZzAlyQU+k68D7LRx6y03s2wlhlvLqkt/8uPBrjv2hJnnaJKbdb0GhQ3JZsfYXhrRjyg==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "9.0.2" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "ToolGood.Words.Pinyin": { + "type": "Transitive", + "resolved": "3.0.1.4", + "contentHash": "uQo97618y9yzLDxrnehPN+/tuiOlk5BqieEdwctHZOAS9miMXnHKgMFYVw8CSGXRglyTYXlrW7qtUlU7Fje5Ew==" + }, + "YamlDotNet": { + "type": "Transitive", + "resolved": "9.1.0", + "contentHash": "fuvGXU4Ec5HrsmEc+BiFTNPCRf1cGBI2kh/3RzMWgddM2M4ALhbSPoI3X3mhXZUD1qqQd9oSkFAtWjpz8z9eRg==" + }, + "flow.launcher.infrastructure": { + "type": "Project", + "dependencies": { + "Ben.Demystifier": "[0.4.1, )", + "BitFaster.Caching": "[2.5.3, )", + "CommunityToolkit.Mvvm": "[8.4.0, )", + "Flow.Launcher.Plugin": "[4.4.0, )", + "MemoryPack": "[1.21.3, )", + "Microsoft.VisualStudio.Threading": "[17.12.19, )", + "NLog": "[4.7.10, )", + "PropertyChanged.Fody": "[3.4.0, )", + "System.Drawing.Common": "[9.0.2, )", + "ToolGood.Words.Pinyin": "[3.0.1.4, )" + } + }, + "flow.launcher.plugin": { + "type": "Project", + "dependencies": { + "JetBrains.Annotations": "[2024.3.0, )", + "PropertyChanged.Fody": "[3.4.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs new file mode 100644 index 000000000..65652878f --- /dev/null +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs @@ -0,0 +1,1079 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Threading; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.DialogJump.Models; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; +using NHotkey; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Accessibility; + +namespace Flow.Launcher.Infrastructure.DialogJump +{ + public static class DialogJump + { + #region Public Properties + + public static Func ShowDialogJumpWindowAsync { get; set; } = null; + + public static Action UpdateDialogJumpWindow { get; set; } = null; + + public static Action ResetDialogJumpWindow { get; set; } = null; + + public static Action HideDialogJumpWindow { get; set; } = null; + + public static DialogJumpWindowPositions DialogJumpWindowPosition { get; private set; } + + public static DialogJumpExplorerPair WindowsDialogJumpExplorer { get; } = new() + { + Metadata = new() + { + ID = "298b197c08a24e90ab66ac060ee2b6b8", // ID is for calculating the hash id of the Dialog Jump pairs + Disabled = false // Disabled is for enabling the Windows DialogJump explorers & dialogs + }, + Plugin = new WindowsExplorer() + }; + + public static DialogJumpDialogPair WindowsDialogJumpDialog { get; } = new() + { + Metadata = new() + { + ID = "a4a113dc51094077ab4abb391e866c7b", // ID is for calculating the hash id of the Dialog Jump pairs + Disabled = false // Disabled is for enabling the Windows DialogJump explorers & dialogs + }, + Plugin = new WindowsDialog() + }; + + #endregion + + #region Private Fields + + private static readonly string ClassName = nameof(DialogJump); + + private static readonly Settings _settings = Ioc.Default.GetRequiredService(); + + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + + private static HWND _mainWindowHandle = HWND.Null; + + private static readonly Dictionary _dialogJumpExplorers = new(); + + private static DialogJumpExplorerPair _lastExplorer = null; + private static readonly object _lastExplorerLock = new(); + + private static readonly Dictionary _dialogJumpDialogs = new(); + + private static IDialogJumpDialogWindow _dialogWindow = null; + private static readonly object _dialogWindowLock = new(); + + private static HWINEVENTHOOK _foregroundChangeHook = HWINEVENTHOOK.Null; + private static HWINEVENTHOOK _locationChangeHook = HWINEVENTHOOK.Null; + private static HWINEVENTHOOK _destroyChangeHook = HWINEVENTHOOK.Null; + private static HWINEVENTHOOK _hideChangeHook = HWINEVENTHOOK.Null; + private static HWINEVENTHOOK _dialogEndChangeHook = HWINEVENTHOOK.Null; + + private static readonly WINEVENTPROC _fgProc = ForegroundChangeCallback; + private static readonly WINEVENTPROC _locProc = LocationChangeCallback; + private static readonly WINEVENTPROC _desProc = DestroyChangeCallback; + private static readonly WINEVENTPROC _hideProc = HideChangeCallback; + private static readonly WINEVENTPROC _dialogEndProc = DialogEndChangeCallback; + + private static DispatcherTimer _dragMoveTimer = null; + + // A list of all file dialog windows that are auto switched already + private static readonly List _autoSwitchedDialogs = new(); + private static readonly object _autoSwitchedDialogsLock = new(); + + private static HWINEVENTHOOK _moveSizeHook = HWINEVENTHOOK.Null; + private static readonly WINEVENTPROC _moveProc = MoveSizeCallBack; + + private static readonly SemaphoreSlim _foregroundChangeLock = new(1, 1); + private static readonly SemaphoreSlim _navigationLock = new(1, 1); + + private static bool _initialized = false; + private static bool _enabled = false; + + #endregion + + #region Initialize & Setup + + public static void InitializeDialogJump(IList dialogJumpExplorers, + IList dialogJumpDialogs) + { + if (_initialized) return; + + // Initialize Dialog Jump explorers & dialogs + _dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null); + foreach (var explorer in dialogJumpExplorers) + { + _dialogJumpExplorers.Add(explorer, null); + } + _dialogJumpDialogs.Add(WindowsDialogJumpDialog, null); + foreach (var dialog in dialogJumpDialogs) + { + _dialogJumpDialogs.Add(dialog, null); + } + + // Initialize main window handle + _mainWindowHandle = Win32Helper.GetMainWindowHandle(); + + // Initialize timer + _dragMoveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) }; + _dragMoveTimer.Tick += (s, e) => InvokeUpdateDialogJumpWindow(); + + // Initialize Dialog Jump window position + DialogJumpWindowPosition = _settings.DialogJumpWindowPosition; + + _initialized = true; + } + + public static void SetupDialogJump(bool enabled) + { + if (enabled == _enabled) return; + + if (enabled) + { + // Check if there are explorer windows and get the topmost one + try + { + if (RefreshLastExplorer()) + { + Log.Debug(ClassName, $"Explorer window found"); + } + } + catch (System.Exception) + { + // Ignored + } + + // Unhook events + if (!_foregroundChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_foregroundChangeHook); + _foregroundChangeHook = HWINEVENTHOOK.Null; + } + if (!_locationChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_locationChangeHook); + _locationChangeHook = HWINEVENTHOOK.Null; + } + if (!_destroyChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_destroyChangeHook); + _destroyChangeHook = HWINEVENTHOOK.Null; + } + if (!_hideChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_hideChangeHook); + _hideChangeHook = HWINEVENTHOOK.Null; + } + if (!_dialogEndChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_dialogEndChangeHook); + _dialogEndChangeHook = HWINEVENTHOOK.Null; + } + + // Hook events + _foregroundChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_FOREGROUND, + PInvoke.EVENT_SYSTEM_FOREGROUND, + PInvoke.GetModuleHandle((PCWSTR)null), + _fgProc, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + _locationChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_LOCATIONCHANGE, + PInvoke.EVENT_OBJECT_LOCATIONCHANGE, + PInvoke.GetModuleHandle((PCWSTR)null), + _locProc, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + _destroyChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_DESTROY, + PInvoke.EVENT_OBJECT_DESTROY, + PInvoke.GetModuleHandle((PCWSTR)null), + _desProc, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + _hideChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_OBJECT_HIDE, + PInvoke.EVENT_OBJECT_HIDE, + PInvoke.GetModuleHandle((PCWSTR)null), + _hideProc, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + _dialogEndChangeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_DIALOGEND, + PInvoke.EVENT_SYSTEM_DIALOGEND, + PInvoke.GetModuleHandle((PCWSTR)null), + _dialogEndProc, + 0, + 0, + PInvoke.WINEVENT_OUTOFCONTEXT); + + if (_foregroundChangeHook.IsNull || + _locationChangeHook.IsNull || + _destroyChangeHook.IsNull || + _hideChangeHook.IsNull || + _dialogEndChangeHook.IsNull) + { + Log.Error(ClassName, "Failed to enable DialogJump"); + return; + } + } + else + { + // Remove explorer windows + foreach (var explorer in _dialogJumpExplorers.Keys) + { + _dialogJumpExplorers[explorer] = null; + } + + // Remove dialog windows + foreach (var dialog in _dialogJumpDialogs.Keys) + { + _dialogJumpDialogs[dialog] = null; + } + + // Remove dialog window handle + var dialogWindowExists = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null) + { + _dialogWindow = null; + dialogWindowExists = true; + } + } + + // Remove auto switched dialogs + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Clear(); + } + + // Unhook events + if (!_foregroundChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_foregroundChangeHook); + _foregroundChangeHook = HWINEVENTHOOK.Null; + } + if (!_locationChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_locationChangeHook); + _locationChangeHook = HWINEVENTHOOK.Null; + } + if (!_destroyChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_destroyChangeHook); + _destroyChangeHook = HWINEVENTHOOK.Null; + } + if (!_hideChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_hideChangeHook); + _hideChangeHook = HWINEVENTHOOK.Null; + } + if (!_dialogEndChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_dialogEndChangeHook); + _dialogEndChangeHook = HWINEVENTHOOK.Null; + } + + // Stop drag move timer + _dragMoveTimer?.Stop(); + + // Reset Dialog Jump window + if (dialogWindowExists) + { + InvokeResetDialogJumpWindow(); + } + } + + _enabled = enabled; + } + + private static bool RefreshLastExplorer() + { + var found = false; + + lock (_lastExplorerLock) + { + // Enum windows from the top to the bottom + PInvoke.EnumWindows((hWnd, _) => + { + foreach (var explorer in _dialogJumpExplorers.Keys) + { + if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified + explorer.Metadata.Disabled) continue; // Plugin is disabled + + var explorerWindow = explorer.Plugin.CheckExplorerWindow(hWnd); + if (explorerWindow != null) + { + _dialogJumpExplorers[explorer] = explorerWindow; + _lastExplorer = explorer; + found = true; + return false; + } + } + + // If we reach here, it means that the window is not a file explorer + return true; + }, IntPtr.Zero); + } + + return found; + } + + #endregion + + #region Active Explorer + + public static string GetActiveExplorerPath() + { + return RefreshLastExplorer() ? _dialogJumpExplorers[_lastExplorer].GetExplorerPath() : string.Empty; + } + + #endregion + + #region Events + + #region Invoke Property Events + + private static async Task InvokeShowDialogJumpWindowAsync(bool dialogWindowChanged) + { + // Show Dialog Jump window + if (_settings.ShowDialogJumpWindow) + { + // Save Dialog Jump window position for one file dialog + if (dialogWindowChanged) + { + DialogJumpWindowPosition = _settings.DialogJumpWindowPosition; + } + + // Call show Dialog Jump window + IDialogJumpDialogWindow dialogWindow; + lock (_dialogWindowLock) + { + dialogWindow = _dialogWindow; + } + if (dialogWindow != null && ShowDialogJumpWindowAsync != null) + { + await ShowDialogJumpWindowAsync.Invoke(dialogWindow.Handle); + } + + // Hook move size event if Dialog Jump window is under dialog & dialog window changed + if (DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) + { + if (dialogWindowChanged) + { + HWND dialogWindowHandle = HWND.Null; + lock (_dialogWindowLock) + { + if (_dialogWindow != null) + { + dialogWindowHandle = new(_dialogWindow.Handle); + } + } + + if (dialogWindowHandle == HWND.Null) return; + + if (!_moveSizeHook.IsNull) + { + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; + } + + // Call _moveProc when the window is moved or resized + SetMoveProc(dialogWindowHandle); + } + } + } + + static unsafe void SetMoveProc(HWND handle) + { + uint processId; + var threadId = PInvoke.GetWindowThreadProcessId(handle, &processId); + _moveSizeHook = PInvoke.SetWinEventHook( + PInvoke.EVENT_SYSTEM_MOVESIZESTART, + PInvoke.EVENT_SYSTEM_MOVESIZEEND, + PInvoke.GetModuleHandle((PCWSTR)null), + _moveProc, + processId, + threadId, + PInvoke.WINEVENT_OUTOFCONTEXT); + } + } + + private static void InvokeUpdateDialogJumpWindow() + { + UpdateDialogJumpWindow?.Invoke(); + } + + private static void InvokeResetDialogJumpWindow() + { + lock (_dialogWindowLock) + { + _dialogWindow = null; + } + + // Reset Dialog Jump window + ResetDialogJumpWindow?.Invoke(); + + // Stop drag move timer + _dragMoveTimer?.Stop(); + + // Unhook move size event + if (!_moveSizeHook.IsNull) + { + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; + } + } + + private static void InvokeHideDialogJumpWindow() + { + // Hide Dialog Jump window + HideDialogJumpWindow?.Invoke(); + + // Stop drag move timer + _dragMoveTimer?.Stop(); + } + + #endregion + + #region Hotkey + + public static void OnToggleHotkey(object sender, HotkeyEventArgs args) + { + _ = Task.Run(async () => + { + try + { + await NavigateDialogPathAsync(PInvoke.GetForegroundWindow()); + } + catch (System.Exception ex) + { + Log.Exception(ClassName, "Failed to navigate dialog path", ex); + } + }); + } + + #endregion + + #region Windows Events + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] + private static async void ForegroundChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + await _foregroundChangeLock.WaitAsync(); + try + { + // Check if it is a file dialog window + var isDialogWindow = false; + var dialogWindowChanged = false; + foreach (var dialog in _dialogJumpDialogs.Keys) + { + if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + dialog.Metadata.Disabled) continue; // Plugin is disabled + + IDialogJumpDialogWindow dialogWindow; + var existingDialogWindow = _dialogJumpDialogs[dialog]; + if (existingDialogWindow != null && existingDialogWindow.Handle == hwnd) + { + // If the dialog window is already in the list, no need to check again + dialogWindow = existingDialogWindow; + } + else + { + dialogWindow = dialog.Plugin.CheckDialogWindow(hwnd); + } + + // If the dialog window is found, set it + if (dialogWindow != null) + { + lock (_dialogWindowLock) + { + dialogWindowChanged = _dialogWindow == null || _dialogWindow.Handle != hwnd; + _dialogWindow = dialogWindow; + } + + isDialogWindow = true; + break; + } + } + + // Handle window based on its type + if (isDialogWindow) + { + Log.Debug(ClassName, $"Dialog Window: {hwnd}"); + // Navigate to path + if (_settings.AutoDialogJump) + { + // Check if we have already switched for this dialog + bool alreadySwitched; + lock (_autoSwitchedDialogsLock) + { + alreadySwitched = _autoSwitchedDialogs.Contains(hwnd); + } + + // Just show Dialog Jump window + if (alreadySwitched) + { + await InvokeShowDialogJumpWindowAsync(dialogWindowChanged); + } + // Show Dialog Jump window after navigating the path + else + { + if (!await Task.Run(async () => + { + try + { + return await NavigateDialogPathAsync(hwnd, true); + } + catch (System.Exception ex) + { + Log.Exception(ClassName, "Failed to navigate dialog path", ex); + return false; + } + })) + { + await InvokeShowDialogJumpWindowAsync(dialogWindowChanged); + } + } + } + else + { + await InvokeShowDialogJumpWindowAsync(dialogWindowChanged); + } + } + // Dialog jump window + else if (hwnd == _mainWindowHandle) + { + Log.Debug(ClassName, $"Main Window: {hwnd}"); + } + // Other window + else + { + Log.Debug(ClassName, $"Other Window: {hwnd}"); + var dialogWindowExist = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null) + { + dialogWindowExist = true; + } + } + if (dialogWindowExist) // Neither Dialog Jump window nor file dialog window is foreground + { + // Hide Dialog Jump window until the file dialog window is brought to the foreground + InvokeHideDialogJumpWindow(); + } + + // Check if there are foreground explorer windows + try + { + lock (_lastExplorerLock) + { + foreach (var explorer in _dialogJumpExplorers.Keys) + { + if (API.PluginModified(explorer.Metadata.ID) || // Plugin is modified + explorer.Metadata.Disabled) continue; // Plugin is disabled + + var explorerWindow = explorer.Plugin.CheckExplorerWindow(hwnd); + if (explorerWindow != null) + { + Log.Debug(ClassName, $"Explorer window: {hwnd}"); + _dialogJumpExplorers[explorer] = explorerWindow; + _lastExplorer = explorer; + break; + } + } + } + } + catch (System.Exception ex) + { + Log.Exception(ClassName, "An error occurred while checking foreground explorer windows", ex); + } + } + } + catch (System.Exception ex) + { + Log.Exception(ClassName, "Failed to invoke ForegroundChangeCallback", ex); + } + finally + { + _foregroundChangeLock.Release(); + } + } + + private static void LocationChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is moved, update the Dialog Jump window position + var dialogWindowExist = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + dialogWindowExist = true; + } + } + if (dialogWindowExist) + { + InvokeUpdateDialogJumpWindow(); + } + } + + private static void MoveSizeCallBack( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is moved or resized, update the Dialog Jump window position + if (_dragMoveTimer != null) + { + switch (eventType) + { + case PInvoke.EVENT_SYSTEM_MOVESIZESTART: + _dragMoveTimer.Start(); // Start dragging position + break; + case PInvoke.EVENT_SYSTEM_MOVESIZEEND: + _dragMoveTimer.Stop(); // Stop dragging + break; + } + } + } + + private static void DestroyChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is destroyed, set _dialogWindowHandle to null + var dialogWindowExist = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + Log.Debug(ClassName, $"Destory dialog: {hwnd}"); + _dialogWindow = null; + dialogWindowExist = true; + } + } + if (dialogWindowExist) + { + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Remove(hwnd); + } + InvokeResetDialogJumpWindow(); + } + } + + private static void HideChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is hidden, set _dialogWindowHandle to null + var dialogWindowExist = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + Log.Debug(ClassName, $"Hide dialog: {hwnd}"); + _dialogWindow = null; + dialogWindowExist = true; + } + } + if (dialogWindowExist) + { + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Remove(hwnd); + } + InvokeResetDialogJumpWindow(); + } + } + + private static void DialogEndChangeCallback( + HWINEVENTHOOK hWinEventHook, + uint eventType, + HWND hwnd, + int idObject, + int idChild, + uint dwEventThread, + uint dwmsEventTime + ) + { + // If the dialog window is ended, set _dialogWindowHandle to null + var dialogWindowExist = false; + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + Log.Debug(ClassName, $"End dialog: {hwnd}"); + _dialogWindow = null; + dialogWindowExist = true; + } + } + if (dialogWindowExist) + { + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Remove(hwnd); + } + InvokeResetDialogJumpWindow(); + } + } + + #endregion + + #endregion + + #region Path Navigation + + // Edited from: https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump + + public static async Task JumpToPathAsync(nint hwnd, string path) + { + // Check handle + if (hwnd == nint.Zero) return false; + + // Check path null or empty + if (string.IsNullOrEmpty(path)) return false; + + // Check path + if (!CheckPath(path, out var isFile)) return false; + + // Get dialog tab + var dialogWindowTab = GetDialogWindowTab(new(hwnd)); + if (dialogWindowTab == null) return false; + + return await JumpToPathAsync(dialogWindowTab, path, isFile, false); + } + + private static async Task NavigateDialogPathAsync(HWND hwnd, bool auto = false) + { + // Check handle + if (hwnd == HWND.Null) return false; + + // Get explorer path + string path; + lock (_lastExplorerLock) + { + path = _dialogJumpExplorers[_lastExplorer]?.GetExplorerPath(); + } + + // Check path null or empty + if (string.IsNullOrEmpty(path)) return false; + + // Check path + if (!CheckPath(path, out var isFile)) return false; + + // Get dialog tab + var dialogWindowTab = GetDialogWindowTab(hwnd); + if (dialogWindowTab == null) return false; + + // Jump to path + return await JumpToPathAsync(dialogWindowTab, path, isFile, auto); + } + + private static bool CheckPath(string path, out bool file) + { + file = false; + try + { + // shell: and shell::: paths + if (path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + // file: URI paths + var localPath = path.StartsWith("file:", StringComparison.OrdinalIgnoreCase) + ? new Uri(path).LocalPath + : path; + // Is folder? + var isFolder = Directory.Exists(localPath); + // Is file? + var isFile = File.Exists(localPath); + file = isFile; + return isFolder || isFile; + } + catch (System.Exception e) + { + Log.Exception(ClassName, "Failed to check path", e); + return false; + } + } + + private static IDialogJumpDialogWindowTab GetDialogWindowTab(HWND hwnd) + { + var dialogWindow = GetDialogWindow(hwnd); + if (dialogWindow == null) return null; + var dialogWindowTab = dialogWindow.GetCurrentTab(); + return dialogWindowTab; + } + + private static IDialogJumpDialogWindow GetDialogWindow(HWND hwnd) + { + // First check dialog window + lock (_dialogWindowLock) + { + if (_dialogWindow != null && _dialogWindow.Handle == hwnd) + { + return _dialogWindow; + } + } + + // Then check all dialog windows + foreach (var dialog in _dialogJumpDialogs.Keys) + { + if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + dialog.Metadata.Disabled) continue; // Plugin is disabled + + var dialogWindow = _dialogJumpDialogs[dialog]; + if (dialogWindow != null && dialogWindow.Handle == hwnd) + { + return dialogWindow; + } + } + + // Finally search for the dialog window again + foreach (var dialog in _dialogJumpDialogs.Keys) + { + if (API.PluginModified(dialog.Metadata.ID) || // Plugin is modified + dialog.Metadata.Disabled) continue; // Plugin is disabled + + IDialogJumpDialogWindow dialogWindow; + var existingDialogWindow = _dialogJumpDialogs[dialog]; + if (existingDialogWindow != null && existingDialogWindow.Handle == hwnd) + { + // If the dialog window is already in the list, no need to check again + dialogWindow = existingDialogWindow; + } + else + { + dialogWindow = dialog.Plugin.CheckDialogWindow(hwnd); + } + + // Update dialog window if found + if (dialogWindow != null) + { + _dialogJumpDialogs[dialog] = dialogWindow; + return dialogWindow; + } + } + + return null; + } + + private static async Task JumpToPathAsync(IDialogJumpDialogWindowTab dialog, string path, bool isFile, bool auto = false) + { + // Jump after flow launcher window vanished (after JumpAction returned true) + // and the dialog had been in the foreground. + var dialogHandle = dialog.Handle; + var timeOut = !SpinWait.SpinUntil(() => Win32Helper.IsForegroundWindow(dialogHandle), 1000); + if (timeOut) return false; + + // Assume that the dialog is in the foreground now + await _navigationLock.WaitAsync(); + try + { + bool result; + if (isFile) + { + switch (_settings.DialogJumpFileResultBehaviour) + { + case DialogJumpFileResultBehaviours.FullPath: + Log.Debug(ClassName, $"File Jump FullPath: {path}"); + result = FileJump(path, dialog); + break; + case DialogJumpFileResultBehaviours.FullPathOpen: + Log.Debug(ClassName, $"File Jump FullPathOpen: {path}"); + result = FileJump(path, dialog, openFile: true); + break; + case DialogJumpFileResultBehaviours.Directory: + Log.Debug(ClassName, $"File Jump Directory (Auto: {auto}): {path}"); + result = DirJump(Path.GetDirectoryName(path), dialog, auto); + break; + default: + return false; + } + } + else + { + Log.Debug(ClassName, $"Dir Jump: {path}"); + result = DirJump(path, dialog, auto); + } + + if (result) + { + lock (_autoSwitchedDialogsLock) + { + _autoSwitchedDialogs.Add(new(dialogHandle)); + } + } + + return result; + } + catch (System.Exception e) + { + Log.Exception(ClassName, "Failed to jump to path", e); + return false; + } + finally + { + _navigationLock.Release(); + } + } + + private static bool FileJump(string filePath, IDialogJumpDialogWindowTab dialog, bool openFile = false) + { + if (!dialog.JumpFile(filePath)) + { + Log.Error(ClassName, "Failed to jump file"); + return false; + } + + if (openFile && !dialog.Open()) + { + Log.Error(ClassName, "Failed to open file"); + return false; + } + + return true; + } + + private static bool DirJump(string dirPath, IDialogJumpDialogWindowTab dialog, bool auto = false) + { + if (!dialog.JumpFolder(dirPath, auto)) + { + Log.Error(ClassName, "Failed to jump folder"); + return false; + } + + return true; + } + + #endregion + + #region Dispose + + public static void Dispose() + { + // Reset flags + _enabled = false; + _initialized = false; + + // Unhook events + if (!_foregroundChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_foregroundChangeHook); + _foregroundChangeHook = HWINEVENTHOOK.Null; + } + if (!_locationChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_locationChangeHook); + _locationChangeHook = HWINEVENTHOOK.Null; + } + if (!_moveSizeHook.IsNull) + { + PInvoke.UnhookWinEvent(_moveSizeHook); + _moveSizeHook = HWINEVENTHOOK.Null; + } + if (!_destroyChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_destroyChangeHook); + _destroyChangeHook = HWINEVENTHOOK.Null; + } + if (!_hideChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_hideChangeHook); + _hideChangeHook = HWINEVENTHOOK.Null; + } + if (!_dialogEndChangeHook.IsNull) + { + PInvoke.UnhookWinEvent(_dialogEndChangeHook); + _dialogEndChangeHook = HWINEVENTHOOK.Null; + } + + // Dispose explorers + foreach (var explorer in _dialogJumpExplorers.Keys) + { + _dialogJumpExplorers[explorer]?.Dispose(); + } + _dialogJumpExplorers.Clear(); + lock (_lastExplorerLock) + { + _lastExplorer = null; + } + + // Dispose dialogs + foreach (var dialog in _dialogJumpDialogs.Keys) + { + _dialogJumpDialogs[dialog]?.Dispose(); + } + _dialogJumpDialogs.Clear(); + lock (_dialogWindowLock) + { + _dialogWindow = null; + } + + // Dispose locks + _foregroundChangeLock.Dispose(); + _navigationLock.Dispose(); + + // Stop drag move timer + if (_dragMoveTimer != null) + { + _dragMoveTimer.Stop(); + _dragMoveTimer = null; + } + } + + #endregion + } +} diff --git a/Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs b/Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs new file mode 100644 index 000000000..d1248eac1 --- /dev/null +++ b/Flow.Launcher.Infrastructure/DialogJump/DialogJumpPair.cs @@ -0,0 +1,63 @@ +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Infrastructure.DialogJump; + +public class DialogJumpExplorerPair +{ + public IDialogJumpExplorer Plugin { get; init; } + + public PluginMetadata Metadata { get; init; } + + public override string ToString() + { + return Metadata.Name; + } + + public override bool Equals(object obj) + { + if (obj is DialogJumpExplorerPair r) + { + return string.Equals(r.Metadata.ID, Metadata.ID); + } + else + { + return false; + } + } + + public override int GetHashCode() + { + var hashcode = Metadata.ID?.GetHashCode() ?? 0; + return hashcode; + } +} + +public class DialogJumpDialogPair +{ + public IDialogJumpDialog Plugin { get; init; } + + public PluginMetadata Metadata { get; init; } + + public override string ToString() + { + return Metadata.Name; + } + + public override bool Equals(object obj) + { + if (obj is DialogJumpDialogPair r) + { + return string.Equals(r.Metadata.ID, Metadata.ID); + } + else + { + return false; + } + } + + public override int GetHashCode() + { + var hashcode = Metadata.ID?.GetHashCode() ?? 0; + return hashcode; + } +} diff --git a/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs new file mode 100644 index 000000000..ee4e03433 --- /dev/null +++ b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsDialog.cs @@ -0,0 +1,345 @@ +using System; +using System.Threading; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Plugin; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; +using WindowsInput; +using WindowsInput.Native; + +namespace Flow.Launcher.Infrastructure.DialogJump.Models +{ + /// + /// Class for handling Windows File Dialog instances in DialogJump. + /// + public class WindowsDialog : IDialogJumpDialog + { + private const string WindowsDialogClassName = "#32770"; + + public IDialogJumpDialogWindow CheckDialogWindow(IntPtr hwnd) + { + // Is it a Win32 dialog box? + if (GetClassName(new(hwnd)) == WindowsDialogClassName) + { + // Is it a windows file dialog? + var dialogType = GetFileDialogType(new(hwnd)); + if (dialogType != DialogType.Others) + { + return new WindowsDialogWindow(hwnd, dialogType); + } + } + + return null; + } + + public void Dispose() + { + + } + + #region Help Methods + + private static unsafe string GetClassName(HWND handle) + { + fixed (char* buf = new char[256]) + { + return PInvoke.GetClassName(handle, buf, 256) switch + { + 0 => string.Empty, + _ => new string(buf), + }; + } + } + + private static DialogType GetFileDialogType(HWND handle) + { + // Is it a Windows Open file dialog? + var fileEditor = PInvoke.GetDlgItem(handle, 0x047C); + if (fileEditor != HWND.Null && GetClassName(fileEditor) == "ComboBoxEx32") return DialogType.Open; + + // Is it a Windows Save or Save As file dialog? + fileEditor = PInvoke.GetDlgItem(handle, 0x0000); + if (fileEditor != HWND.Null && GetClassName(fileEditor) == "DUIViewWndClassName") return DialogType.SaveOrSaveAs; + + return DialogType.Others; + } + + #endregion + } + + public class WindowsDialogWindow : IDialogJumpDialogWindow + { + public IntPtr Handle { get; private set; } = IntPtr.Zero; + + // After jumping folder, file editor handle of Save / SaveAs file dialogs cannot be found anymore + // So we need to cache the current tab and use the original handle + private IDialogJumpDialogWindowTab _currentTab { get; set; } = null; + + private readonly DialogType _dialogType; + + internal WindowsDialogWindow(IntPtr handle, DialogType dialogType) + { + Handle = handle; + _dialogType = dialogType; + } + + public IDialogJumpDialogWindowTab GetCurrentTab() + { + return _currentTab ??= new WindowsDialogTab(Handle, _dialogType); + } + + public void Dispose() + { + + } + } + + public class WindowsDialogTab : IDialogJumpDialogWindowTab + { + #region Public Properties + + public IntPtr Handle { get; private set; } = IntPtr.Zero; + + #endregion + + #region Private Fields + + private static readonly string ClassName = nameof(WindowsDialogTab); + + private static readonly InputSimulator _inputSimulator = new(); + + private readonly DialogType _dialogType; + + private bool _legacy { get; set; } = false; + private HWND _pathControl { get; set; } = HWND.Null; + private HWND _pathEditor { get; set; } = HWND.Null; + private HWND _fileEditor { get; set; } = HWND.Null; + private HWND _openButton { get; set; } = HWND.Null; + + #endregion + + #region Constructor + + internal WindowsDialogTab(IntPtr handle, DialogType dialogType) + { + Handle = handle; + _dialogType = dialogType; + Log.Debug(ClassName, $"File dialog type: {dialogType}"); + } + + #endregion + + #region Public Methods + + public string GetCurrentFolder() + { + if (_pathEditor.IsNull && !GetPathControlEditor()) return string.Empty; + return GetWindowText(_pathEditor); + } + + public string GetCurrentFile() + { + if (_fileEditor.IsNull && !GetFileEditor()) return string.Empty; + return GetWindowText(_fileEditor); + } + + public bool JumpFolder(string path, bool auto) + { + if (auto) + { + // Use legacy jump folder method for auto Dialog Jump because file editor is default value. + // After setting path using file editor, we do not need to revert its value. + return JumpFolderWithFileEditor(path, false); + } + + // Alt-D or Ctrl-L to focus on the path input box + // "ComboBoxEx32" is not visible when the path editor is not with the keyboard focus + _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LMENU, VirtualKeyCode.VK_D); + // _inputSimulator.Keyboard.ModifiedKeyStroke(VirtualKeyCode.LCONTROL, VirtualKeyCode.VK_L); + + if (_pathControl.IsNull && !GetPathControlEditor()) + { + // https://github.com/idkidknow/Flow.Launcher.Plugin.DirQuickJump/issues/1 + // The dialog is a legacy one, so we can only edit file editor directly. + Log.Debug(ClassName, "Legacy dialog, using legacy jump folder method"); + return JumpFolderWithFileEditor(path, true); + } + + var timeOut = !SpinWait.SpinUntil(() => + { + var style = PInvoke.GetWindowLongPtr(_pathControl, WINDOW_LONG_PTR_INDEX.GWL_STYLE); + return (style & (int)WINDOW_STYLE.WS_VISIBLE) != 0; + }, 1000); + if (timeOut) + { + // Path control is not visible, so we can only edit file editor directly. + Log.Debug(ClassName, "Path control is not visible, using legacy jump folder method"); + return JumpFolderWithFileEditor(path, true); + } + + if (_pathEditor.IsNull) + { + // Path editor cannot be found, so we can only edit file editor directly. + Log.Debug(ClassName, "Path editor cannot be found, using legacy jump folder method"); + return JumpFolderWithFileEditor(path, true); + } + SetWindowText(_pathEditor, path); + + _inputSimulator.Keyboard.KeyPress(VirtualKeyCode.RETURN); + + return true; + } + + public bool JumpFile(string path) + { + if (_fileEditor.IsNull && !GetFileEditor()) return false; + SetWindowText(_fileEditor, path); + + return true; + } + + public bool Open() + { + if (_openButton.IsNull && !GetOpenButton()) return false; + PInvoke.PostMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + + return true; + } + + public void Dispose() + { + + } + + #endregion + + #region Helper Methods + + #region Get Handles + + private bool GetPathControlEditor() + { + // Get the handle of the path editor + // Must use PInvoke.FindWindowEx because PInvoke.GetDlgItem(Handle, 0x0000) will get another control + _pathControl = PInvoke.FindWindowEx(new(Handle), HWND.Null, "WorkerW", null); // 0x0000 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ReBarWindow32", null); // 0xA005 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "Address Band Root", null); // 0xA205 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "msctls_progress32", null); // 0x0000 + _pathControl = PInvoke.FindWindowEx(_pathControl, HWND.Null, "ComboBoxEx32", null); // 0xA205 + if (_pathControl == HWND.Null) + { + _pathEditor = HWND.Null; + _legacy = true; + Log.Info(ClassName, "Legacy dialog"); + } + else + { + _pathEditor = PInvoke.GetDlgItem(_pathControl, 0xA205); // ComboBox + _pathEditor = PInvoke.GetDlgItem(_pathEditor, 0xA205); // Edit + if (_pathEditor == HWND.Null) + { + _legacy = true; + Log.Error(ClassName, "Failed to find path editor handle"); + } + } + + return !_legacy; + } + + private bool GetFileEditor() + { + if (_dialogType == DialogType.Open) + { + // Get the handle of the file name editor of Open file dialog + _fileEditor = PInvoke.GetDlgItem(new(Handle), 0x047C); // ComboBoxEx32 + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // ComboBox + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x047C); // Edit + } + else + { + // Get the handle of the file name editor of Save / SaveAs file dialog + _fileEditor = PInvoke.GetDlgItem(new(Handle), 0x0000); // DUIViewWndClassName + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // DirectUIHWND + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // FloatNotifySink + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x0000); // ComboBox + _fileEditor = PInvoke.GetDlgItem(_fileEditor, 0x03E9); // Edit + } + + if (_fileEditor == HWND.Null) + { + Log.Error(ClassName, "Failed to find file name editor handle"); + return false; + } + + return true; + } + + private bool GetOpenButton() + { + // Get the handle of the open button + _openButton = PInvoke.GetDlgItem(new(Handle), 0x0001); // Open/Save/SaveAs Button + if (_openButton == HWND.Null) + { + Log.Error(ClassName, "Failed to find open button handle"); + return false; + } + + return true; + } + + #endregion + + #region Windows Text + + private static unsafe string GetWindowText(HWND handle) + { + int length; + Span buffer = stackalloc char[1000]; + fixed (char* pBuffer = buffer) + { + // If the control has no title bar or text, or if the control handle is invalid, the return value is zero. + length = (int)PInvoke.SendMessage(handle, PInvoke.WM_GETTEXT, 1000, (nint)pBuffer); + } + + return buffer[..length].ToString(); + } + + private static unsafe nint SetWindowText(HWND handle, string text) + { + fixed (char* textPtr = text + '\0') + { + return PInvoke.SendMessage(handle, PInvoke.WM_SETTEXT, 0, (nint)textPtr).Value; + } + } + + #endregion + + #region Legacy Jump Folder + + private bool JumpFolderWithFileEditor(string path, bool resetFocus) + { + // For Save / Save As dialog, the default value in file editor is not null and it can cause strange behaviors. + if (resetFocus && _dialogType == DialogType.SaveOrSaveAs) return false; + + if (_fileEditor.IsNull && !GetFileEditor()) return false; + SetWindowText(_fileEditor, path); + + if (_openButton.IsNull && !GetOpenButton()) return false; + PInvoke.SendMessage(_openButton, PInvoke.BM_CLICK, 0, 0); + + return true; + } + + #endregion + + #endregion + } + + internal enum DialogType + { + Others, + Open, + SaveOrSaveAs + } +} diff --git a/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs new file mode 100644 index 000000000..e9ed9dae7 --- /dev/null +++ b/Flow.Launcher.Infrastructure/DialogJump/Models/WindowsExplorer.cs @@ -0,0 +1,260 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Flow.Launcher.Plugin; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; + +namespace Flow.Launcher.Infrastructure.DialogJump.Models +{ + /// + /// Class for handling Windows Explorer instances in DialogJump. + /// + public class WindowsExplorer : IDialogJumpExplorer + { + public IDialogJumpExplorerWindow CheckExplorerWindow(IntPtr hwnd) + { + IDialogJumpExplorerWindow explorerWindow = null; + + // Is it from Explorer? + var processName = Win32Helper.GetProcessNameFromHwnd(new(hwnd)); + if (processName.Equals("explorer.exe", StringComparison.OrdinalIgnoreCase)) + { + EnumerateShellWindows((shellWindow) => + { + try + { + if (shellWindow is not IWebBrowser2 explorer) return true; + + if (explorer.HWND != hwnd) return true; + + explorerWindow = new WindowsExplorerWindow(hwnd); + return false; + } + catch + { + // Ignored + } + + return true; + }); + } + return explorerWindow; + } + + internal static unsafe void EnumerateShellWindows(Func action) + { + // Create an instance of ShellWindows + var clsidShellWindows = new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); // ShellWindowsClass + var iidIShellWindows = typeof(IShellWindows).GUID; // IShellWindows + + var result = PInvoke.CoCreateInstance( + &clsidShellWindows, + null, + CLSCTX.CLSCTX_ALL, + &iidIShellWindows, + out var shellWindowsObj); + + if (result.Failed) return; + + var shellWindows = (IShellWindows)shellWindowsObj; + + // Enumerate the shell windows + var count = shellWindows.Count; + for (var i = 0; i < count; i++) + { + if (!action(shellWindows.Item(i))) + { + return; + } + } + } + + public void Dispose() + { + + } + } + + public class WindowsExplorerWindow : IDialogJumpExplorerWindow + { + public IntPtr Handle { get; } + + private static Guid _shellBrowserGuid = typeof(IShellBrowser).GUID; + + internal WindowsExplorerWindow(IntPtr handle) + { + Handle = handle; + } + + public string GetExplorerPath() + { + if (Handle == IntPtr.Zero) return null; + + var activeTabHandle = GetActiveTabHandle(new(Handle)); + if (activeTabHandle.IsNull) return null; + + var window = GetExplorerByTabHandle(activeTabHandle); + if (window == null) return null; + + var path = GetLocation(window); + return path; + } + + public void Dispose() + { + + } + + #region Helper Methods + + // Inspired by: https://github.com/w4po/ExplorerTabUtility + + private static HWND GetActiveTabHandle(HWND windowHandle) + { + // Active tab always at the top of the z-index, so it is the first child of the ShellTabWindowClass. + var activeTab = PInvoke.FindWindowEx(windowHandle, HWND.Null, "ShellTabWindowClass", null); + return activeTab; + } + + private static IWebBrowser2 GetExplorerByTabHandle(HWND tabHandle) + { + if (tabHandle.IsNull) return null; + + IWebBrowser2 window = null; + WindowsExplorer.EnumerateShellWindows((shellWindow) => + { + try + { + return StartSTAThread(() => + { + if (shellWindow is not IWebBrowser2 explorer) return true; + + if (explorer is not IServiceProvider sp) return true; + + sp.QueryService(ref _shellBrowserGuid, ref _shellBrowserGuid, out var shellBrowser); + if (shellBrowser == null) return true; + + try + { + shellBrowser.GetWindow(out var hWnd); // Must execute in STA thread to get this hWnd + + if (hWnd == tabHandle) + { + window = explorer; + return false; + } + } + catch + { + // Ignored + } + finally + { + Marshal.ReleaseComObject(shellBrowser); + } + + return true; + }) ?? true; + } + catch + { + // Ignored + } + + return true; + }); + + return window; + } + + private static bool? StartSTAThread(Func action) + { + bool? result = null; + var thread = new Thread(() => + { + result = action(); + }) + { + IsBackground = true + }; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + return result; + } + + private static string GetLocation(IWebBrowser2 window) + { + var path = window.LocationURL.ToString(); + if (!string.IsNullOrWhiteSpace(path)) return NormalizeLocation(path); + + // Recycle Bin, This PC, etc + if (window.Document is not IShellFolderViewDual folderView) return null; + + // Attempt to get the path from the folder view + try + { + // CSWin32 Folder does not have Self, so we need to use dynamic type here + // Use dynamic to bypass static typing + dynamic folder = folderView.Folder; + + // Access the Self property via dynamic binding + dynamic folderItem = folder.Self; + + // Get path from the folder item + path = folderItem.Path; + } + catch + { + return null; + } + + return NormalizeLocation(path); + } + + private static string NormalizeLocation(string location) + { + if (location.IndexOf('%') > -1) + location = Environment.ExpandEnvironmentVariables(location); + + if (location.StartsWith("::", StringComparison.Ordinal)) + location = $"shell:{location}"; + + else if (location.StartsWith("{", StringComparison.Ordinal)) + location = $"shell:::{location}"; + + location = location.Trim(' ', '/', '\\', '\n', '\'', '"'); + + return location.Replace('/', '\\'); + } + + #endregion + } + + #region COM Interfaces + + // Inspired by: https://github.com/w4po/ExplorerTabUtility + + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("6d5140c1-7436-11ce-8034-00aa006009fa")] + [ComImport] + public interface IServiceProvider + { + [PreserveSig] + int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellBrowser ppvObject); + } + + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("000214E2-0000-0000-C000-000000000046")] + [ComImport] + public interface IShellBrowser + { + [PreserveSig] + int GetWindow(out nint handle); + } + + #endregion +} diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 31547200b..390de341d 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -1,7 +1,7 @@ - + - net7.0-windows + net9.0-windows {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3} Library true @@ -12,6 +12,7 @@ false false true + true @@ -59,12 +60,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive + all @@ -76,5 +79,4 @@ - \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs index 22eb065f5..8afab419b 100644 --- a/Flow.Launcher.Infrastructure/Http/Http.cs +++ b/Flow.Launcher.Infrastructure/Http/Http.cs @@ -20,6 +20,10 @@ namespace Flow.Launcher.Infrastructure.Http private static readonly HttpClient client = new(); + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + static Http() { // need to be added so it would work on a win10 machine @@ -78,7 +82,7 @@ namespace Flow.Launcher.Infrastructure.Http } catch (UriFormatException e) { - Ioc.Default.GetRequiredService().ShowMsg("Please try again", "Unable to parse Http Proxy"); + API.ShowMsgError(API.GetTranslation("pleaseTryAgain"), API.GetTranslation("parseProxyFailed")); Log.Exception(ClassName, "Unable to parse Uri", e); } } diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index edc71feef..965ab6caa 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -66,3 +66,27 @@ LOCALE_TRANSIENT_KEYBOARD4 SHParseDisplayName SHOpenFolderAndSelectItems CoTaskMemFree + +SetWinEventHook +UnhookWinEvent +SendMessage +EVENT_SYSTEM_FOREGROUND +WINEVENT_OUTOFCONTEXT +WM_SETTEXT +IShellFolderViewDual2 +CoCreateInstance +CLSCTX +IShellWindows +IWebBrowser2 +EVENT_OBJECT_DESTROY +EVENT_OBJECT_LOCATIONCHANGE +EVENT_SYSTEM_MOVESIZESTART +EVENT_SYSTEM_MOVESIZEEND +GetDlgItem +PostMessage +BM_CLICK +WM_GETTEXT +OpenProcess +QueryFullProcessImageName +EVENT_OBJECT_HIDE +EVENT_SYSTEM_DIALOGEND diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index b5344c7e9..1c0cc6872 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -14,11 +14,8 @@ namespace Flow.Launcher.Infrastructure { public class PinyinAlphabet : IAlphabet { - private ConcurrentDictionary _pinyinCache = - new(); - + private readonly ConcurrentDictionary _pinyinCache = new(); private readonly Settings _settings; - private ReadOnlyDictionary currentDoublePinyinTable; public PinyinAlphabet() @@ -28,10 +25,21 @@ namespace Flow.Launcher.Infrastructure _settings.PropertyChanged += (sender, e) => { - if (e.PropertyName == nameof(Settings.UseDoublePinyin) || - e.PropertyName == nameof(Settings.DoublePinyinSchema)) + switch (e.PropertyName) { - Reload(); + case nameof (Settings.ShouldUsePinyin): + if (_settings.ShouldUsePinyin) + { + Reload(); + } + break; + case nameof(Settings.UseDoublePinyin): + case nameof(Settings.DoublePinyinSchema): + if (_settings.UseDoublePinyin) + { + Reload(); + } + break; } }; } @@ -44,105 +52,142 @@ namespace Flow.Launcher.Infrastructure private void CreateDoublePinyinTableFromStream(Stream jsonStream) { - Dictionary> table = JsonSerializer.Deserialize>>(jsonStream); - string schemaKey = _settings.DoublePinyinSchema.ToString(); // Convert enum to string - if (!table.TryGetValue(schemaKey, out var value)) + var table = JsonSerializer.Deserialize>>(jsonStream) ?? + throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null"); + + var schemaKey = _settings.DoublePinyinSchema.ToString(); + if (!table.TryGetValue(schemaKey, out var schemaDict)) { - throw new ArgumentException("DoublePinyinSchema is invalid or double pinyin table is broken."); + throw new ArgumentException($"DoublePinyinSchema '{schemaKey}' is invalid or double pinyin table is broken."); } - currentDoublePinyinTable = new ReadOnlyDictionary(value); + + currentDoublePinyinTable = new ReadOnlyDictionary(schemaDict); } private void LoadDoublePinyinTable() { - if (_settings.UseDoublePinyin) + if (!_settings.UseDoublePinyin) { - var tablePath = Path.Join(AppContext.BaseDirectory, "Resources", "double_pinyin.json"); - try - { - using var fs = File.OpenRead(tablePath); - CreateDoublePinyinTableFromStream(fs); - } - catch (System.Exception e) - { - Log.Exception(nameof(PinyinAlphabet), "Failed to load double pinyin table from file: " + tablePath, e); - currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); - } + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + return; } - else + + var tablePath = Path.Combine(AppContext.BaseDirectory, "Resources", "double_pinyin.json"); + try { + using var fs = File.OpenRead(tablePath); + CreateDoublePinyinTableFromStream(fs); + } + catch (FileNotFoundException e) + { + Log.Exception(nameof(PinyinAlphabet), $"Double pinyin table file not found: {tablePath}", e); + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + } + catch (DirectoryNotFoundException e) + { + Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e); + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + } + catch (UnauthorizedAccessException e) + { + Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e); + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + } + catch (System.Exception e) + { + Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e); currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); } } public bool ShouldTranslate(string stringToTranslate) { - // If a string has Chinese characters, we don't need to translate it to pinyin. - return _settings.ShouldUsePinyin && !WordsHelper.HasChinese(stringToTranslate); + // If the query (stringToTranslate) does NOT contain Chinese characters, + // we should translate the target string to pinyin for matching + return _settings.ShouldUsePinyin && !ContainsChinese(stringToTranslate); } public (string translation, TranslationMapping map) Translate(string content) { - if (!_settings.ShouldUsePinyin || !WordsHelper.HasChinese(content)) + if (!_settings.ShouldUsePinyin || !ContainsChinese(content)) return (content, null); - return _pinyinCache.TryGetValue(content, out var value) - ? value - : BuildCacheFromContent(content); + return _pinyinCache.TryGetValue(content, out var cached) ? cached : BuildCacheFromContent(content); } private (string translation, TranslationMapping map) BuildCacheFromContent(string content) { var resultList = WordsHelper.GetPinyinList(content); - - var resultBuilder = new StringBuilder(); + var resultBuilder = new StringBuilder(_settings.UseDoublePinyin ? 3 : 4); // Pre-allocate with estimated capacity var map = new TranslationMapping(); var previousIsChinese = false; for (var i = 0; i < resultList.Length; i++) { - if (content[i] >= 0x3400 && content[i] <= 0x9FD5) + if (IsChineseCharacter(content[i])) { - string translated = _settings.UseDoublePinyin ? ToDoublePin(resultList[i]) : resultList[i]; + var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i]; + if (i > 0) { resultBuilder.Append(' '); } + map.AddNewIndex(resultBuilder.Length, translated.Length); resultBuilder.Append(translated); previousIsChinese = true; } else { + // Add space after Chinese characters before non-Chinese characters if (previousIsChinese) { previousIsChinese = false; resultBuilder.Append(' '); } + map.AddNewIndex(resultBuilder.Length, resultList[i].Length); resultBuilder.Append(resultList[i]); } } - map.endConstruct(); + map.EndConstruct(); - var key = resultBuilder.ToString(); - - return _pinyinCache[content] = (key, map); + var translation = resultBuilder.ToString(); + var result = (translation, map); + + return _pinyinCache[content] = result; } - #region Double Pinyin - - private string ToDoublePin(string fullPinyin) + /// + /// Optimized Chinese character detection using the comprehensive CJK Unicode ranges + /// + private static bool ContainsChinese(ReadOnlySpan text) { - if (currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)) + foreach (var c in text) { - return doublePinyinValue; + if (IsChineseCharacter(c)) + return true; } - return fullPinyin; + return false; } - #endregion + /// + /// Check if a character is a Chinese character using comprehensive Unicode ranges + /// Covers CJK Unified Ideographs, Extension A + /// + private static bool IsChineseCharacter(char c) + { + return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs + (c >= 0x3400 && c <= 0x4DBF); // CJK Extension A + } + + private string ToDoublePinyin(string fullPinyin) + { + return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue) + ? doublePinyinValue + : fullPinyin; + } } } diff --git a/Flow.Launcher.Infrastructure/TranslationMapping.cs b/Flow.Launcher.Infrastructure/TranslationMapping.cs index 5b02ae666..b4c6764df 100644 --- a/Flow.Launcher.Infrastructure/TranslationMapping.cs +++ b/Flow.Launcher.Infrastructure/TranslationMapping.cs @@ -5,31 +5,30 @@ namespace Flow.Launcher.Infrastructure { public class TranslationMapping { - private bool constructed; + private bool _isConstructed; - // Assuming one original item maps to multi translated items - // list[i] is the last translated index + 1 of original index i - private readonly List originalToTranslated = new(); + // Assuming one original item maps to multi translated items + // list[i] is the last translated index + 1 of original index i + private readonly List _originalToTranslated = new(); public void AddNewIndex(int translatedIndex, int length) { - if (constructed) - throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); - - originalToTranslated.Add(translatedIndex + length); + if (_isConstructed) + throw new InvalidOperationException("Mapping shouldn't be changed after construction"); + _originalToTranslated.Add(translatedIndex + length); } public int MapToOriginalIndex(int translatedIndex) { - int loc = originalToTranslated.BinarySearch(translatedIndex); - return loc >= 0 ? loc : ~loc; + var searchResult = _originalToTranslated.BinarySearch(translatedIndex); + return searchResult >= 0 ? searchResult : ~searchResult; } - public void endConstruct() + public void EndConstruct() { - if (constructed) + if (_isConstructed) throw new InvalidOperationException("Mapping has already been constructed"); - constructed = true; + _isConstructed = true; } } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 6b10d693d..23f9047fe 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -86,6 +86,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public string OpenHistoryHotkey { get; set; } = $"Ctrl+H"; public string CycleHistoryUpHotkey { get; set; } = $"{KeyConstant.Alt} + Up"; public string CycleHistoryDownHotkey { get; set; } = $"{KeyConstant.Alt} + Down"; + public string DialogJumpHotkey { get; set; } = $"{KeyConstant.Alt} + G"; private string _language = Constant.SystemLanguageCode; public string Language @@ -233,6 +234,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings public bool AutoRestartAfterChanging { get; set; } = false; public bool ShowUnknownSourceWarning { get; set; } = true; + public bool AutoUpdatePlugins { get; set; } = true; public int CustomExplorerIndex { get; set; } = 0; @@ -322,13 +324,40 @@ namespace Flow.Launcher.Infrastructure.UserSettings } }; + public bool EnableDialogJump { get; set; } = true; + + public bool AutoDialogJump { get; set; } = false; + + public bool ShowDialogJumpWindow { get; set; } = false; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public DialogJumpWindowPositions DialogJumpWindowPosition { get; set; } = DialogJumpWindowPositions.UnderDialog; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public DialogJumpResultBehaviours DialogJumpResultBehaviour { get; set; } = DialogJumpResultBehaviours.LeftClick; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public DialogJumpFileResultBehaviours DialogJumpFileResultBehaviour { get; set; } = DialogJumpFileResultBehaviours.FullPath; + [JsonConverter(typeof(JsonStringEnumConverter))] public LOGLEVEL LogLevel { get; set; } = LOGLEVEL.INFO; /// /// when false Alphabet static service will always return empty results /// - public bool ShouldUsePinyin { get; set; } = false; + private bool _useAlphabet = true; + public bool ShouldUsePinyin + { + get => _useAlphabet; + set + { + if (_useAlphabet != value) + { + _useAlphabet = value; + OnPropertyChanged(); + } + } + } private bool _useDoublePinyin = false; public bool UseDoublePinyin @@ -502,7 +531,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings { var list = FixedHotkeys(); - // Customizeable hotkeys + // Customizable hotkeys if (!string.IsNullOrEmpty(Hotkey)) list.Add(new(Hotkey, "flowlauncherHotkey", () => Hotkey = "")); if (!string.IsNullOrEmpty(PreviewHotkey)) @@ -533,6 +562,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings list.Add(new(CycleHistoryUpHotkey, "CycleHistoryUpHotkey", () => CycleHistoryUpHotkey = "")); if (!string.IsNullOrEmpty(CycleHistoryDownHotkey)) list.Add(new(CycleHistoryDownHotkey, "CycleHistoryDownHotkey", () => CycleHistoryDownHotkey = "")); + if (!string.IsNullOrEmpty(DialogJumpHotkey)) + list.Add(new(DialogJumpHotkey, "dialogJumpHotkey", () => DialogJumpHotkey = "")); // Custom Query Hotkeys foreach (var customPluginHotkey in CustomPluginHotkeys) @@ -646,4 +677,23 @@ namespace Flow.Launcher.Infrastructure.UserSettings DaNiu, XiaoLang } + + public enum DialogJumpWindowPositions + { + UnderDialog, + FollowDefault + } + + public enum DialogJumpResultBehaviours + { + LeftClick, + RightClick + } + + public enum DialogJumpFileResultBehaviours + { + FullPath, + FullPathOpen, + Directory + } } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 32ed31137..bb1996c3b 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -14,9 +14,11 @@ using System.Windows.Markup; using System.Windows.Media; using Flow.Launcher.Infrastructure.UserSettings; using Microsoft.Win32; +using Microsoft.Win32.SafeHandles; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dwm; +using Windows.Win32.System.Threading; using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.Shell.Common; using Windows.Win32.UI.WindowsAndMessaging; @@ -138,6 +140,11 @@ namespace Flow.Launcher.Infrastructure return IsForegroundWindow(GetWindowHandle(window)); } + public static bool IsForegroundWindow(nint handle) + { + return IsForegroundWindow(new HWND(handle)); + } + internal static bool IsForegroundWindow(HWND handle) { return handle.Equals(PInvoke.GetForegroundWindow()); @@ -344,6 +351,16 @@ namespace Flow.Launcher.Infrastructure return new(windowHelper.Handle); } + internal static HWND GetMainWindowHandle() + { + // When application is exiting, the Application.Current will be null + if (Application.Current == null) return HWND.Null; + + // Get the FL main window + var hwnd = GetWindowHandle(Application.Current.MainWindow, true); + return hwnd; + } + #endregion #region STA Thread @@ -761,6 +778,65 @@ namespace Flow.Launcher.Infrastructure #endregion + #region Window Rect + + public static unsafe bool GetWindowRect(nint handle, out Rect outRect) + { + var rect = new RECT(); + var result = PInvoke.GetWindowRect(new(handle), &rect); + if (!result) + { + outRect = new Rect(); + return false; + } + + // Convert RECT to Rect + outRect = new Rect( + rect.left, + rect.top, + rect.right - rect.left, + rect.bottom - rect.top + ); + return true; + } + + #endregion + + #region Window Process + + internal static unsafe string GetProcessNameFromHwnd(HWND hWnd) + { + return Path.GetFileName(GetProcessPathFromHwnd(hWnd)); + } + + internal static unsafe string GetProcessPathFromHwnd(HWND hWnd) + { + uint pid; + var threadId = PInvoke.GetWindowThreadProcessId(hWnd, &pid); + if (threadId == 0) return string.Empty; + + var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION, false, pid); + if (process.Value != IntPtr.Zero) + { + using var safeHandle = new SafeProcessHandle(process.Value, true); + uint capacity = 2000; + Span buffer = new char[capacity]; + fixed (char* pBuffer = buffer) + { + if (!PInvoke.QueryFullProcessImageName(safeHandle, PROCESS_NAME_FORMAT.PROCESS_NAME_WIN32, (PWSTR)pBuffer, ref capacity)) + { + return string.Empty; + } + + return buffer[..(int)capacity].ToString(); + } + } + + return string.Empty; + } + + #endregion + #region Explorer // https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems diff --git a/Flow.Launcher.Infrastructure/packages.lock.json b/Flow.Launcher.Infrastructure/packages.lock.json new file mode 100644 index 000000000..f38f91ef9 --- /dev/null +++ b/Flow.Launcher.Infrastructure/packages.lock.json @@ -0,0 +1,155 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "Ben.Demystifier": { + "type": "Direct", + "requested": "[0.4.1, )", + "resolved": "0.4.1", + "contentHash": "axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==", + "dependencies": { + "System.Reflection.Metadata": "5.0.0" + } + }, + "BitFaster.Caching": { + "type": "Direct", + "requested": "[2.5.3, )", + "resolved": "2.5.3", + "contentHash": "Vo/39qcam5Xe+DbyfH0JZyqPswdOoa7jv4PGtRJ6Wj8AU+aZ+TuJRlJcIe+MQjRTJwliI8k8VSQpN8sEoBIv2g==" + }, + "CommunityToolkit.Mvvm": { + "type": "Direct", + "requested": "[8.4.0, )", + "resolved": "8.4.0", + "contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw==" + }, + "Fody": { + "type": "Direct", + "requested": "[6.5.5, )", + "resolved": "6.5.5", + "contentHash": "Krca41L/PDva1VsmDec5n52cQZxQAQp/bsHdzsNi8iLLI0lqKL94fNIkNaC8tVolUkCyWsbzvxfxJCeD2789fA==" + }, + "MemoryPack": { + "type": "Direct", + "requested": "[1.21.3, )", + "resolved": "1.21.3", + "contentHash": "cwCtED8y400vMWx/Vp0QCSeEpVFjDU4JwF52VX9WTaqVERUvNqjG9n6osFlmFuytegyXnHvYEu1qRJ8rv/rkbg==", + "dependencies": { + "MemoryPack.Core": "1.21.3", + "MemoryPack.Generator": "1.21.3" + } + }, + "Microsoft.VisualStudio.Threading": { + "type": "Direct", + "requested": "[17.12.19, )", + "resolved": "17.12.19", + "contentHash": "eLiGMkMYyaSguqHs3lsrFxy3tAWSLuPEL2pIWRcADMDVAs2xqm3dr1d9QYjiEusTgiClF9KD6OB2NdZP72Oy0Q==", + "dependencies": { + "Microsoft.VisualStudio.Threading.Analyzers": "17.12.19", + "Microsoft.VisualStudio.Validation": "17.8.8" + } + }, + "Microsoft.Windows.CsWin32": { + "type": "Direct", + "requested": "[0.3.106, )", + "resolved": "0.3.106", + "contentHash": "Mx5fK7uN6fwLR4wUghs6//HonAnwPBNmC2oonyJVhCUlHS/r6SUS3NkBc3+gaQiv+0/9bqdj1oSCKQFkNI+21Q==", + "dependencies": { + "Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha", + "Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview", + "Microsoft.Windows.WDK.Win32Metadata": "0.11.4-experimental" + } + }, + "NLog": { + "type": "Direct", + "requested": "[4.7.10, )", + "resolved": "4.7.10", + "contentHash": "rcegW7kYOCjl7wX0SzsqpPBqnJ51JKi1WkYb6QBVX0Wc5IgH19Pv4t/co+T0s06OS0Ne44xgkY/mHg0PdrmJow==" + }, + "PropertyChanged.Fody": { + "type": "Direct", + "requested": "[3.4.0, )", + "resolved": "3.4.0", + "contentHash": "IAZyq0uolKo2WYm4mjx+q7A8fSGFT0x2e1s3y+ODn4JI0kqTDoo9GF2tdaypUzRFJZfdMxfC5HZW9QzdJLtOnA==", + "dependencies": { + "Fody": "6.5.1" + } + }, + "System.Drawing.Common": { + "type": "Direct", + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "JU947wzf8JbBS16Y5EIZzAlyQU+k68D7LRx6y03s2wlhlvLqkt/8uPBrjv2hJnnaJKbdb0GhQ3JZsfYXhrRjyg==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "9.0.2" + } + }, + "ToolGood.Words.Pinyin": { + "type": "Direct", + "requested": "[3.0.1.4, )", + "resolved": "3.0.1.4", + "contentHash": "uQo97618y9yzLDxrnehPN+/tuiOlk5BqieEdwctHZOAS9miMXnHKgMFYVw8CSGXRglyTYXlrW7qtUlU7Fje5Ew==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" + }, + "MemoryPack.Core": { + "type": "Transitive", + "resolved": "1.21.3", + "contentHash": "ajrYoBWT2aKeH4tlY8q/1C9qK1R/NK+7FkuVOX58ebOSxkABoFTqCR7W+Zk2rakUHZiEgNdRqO67hiRZPq6fLA==" + }, + "MemoryPack.Generator": { + "type": "Transitive", + "resolved": "1.21.3", + "contentHash": "hYU0TAIarDKnbkNIWvb7P4zBUL+CTahkuNkczsKvycSMR5kiwQ4IfLexywNKX3s05Izp4gzDSPbueepNWZRpWA==" + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Transitive", + "resolved": "17.12.19", + "contentHash": "v3IYeedjoktvZ+GqYmLudxZJngmf/YWIxNT2Uy6QMMN19cvw+nkWoip1Gr1RtnFkUo1MPUVMis4C8Kj8d8DpSQ==" + }, + "Microsoft.VisualStudio.Validation": { + "type": "Transitive", + "resolved": "17.8.8", + "contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g==" + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "9.0.2", + "contentHash": "5BkGZ6mHp2dHydR29sb0fDfAuqkv30AHtTih8wMzvPZysOmBFvHfnkR2w3tsc0pSiIg8ZoKyefJXWy9r3pBh0w==" + }, + "Microsoft.Windows.SDK.Win32Docs": { + "type": "Transitive", + "resolved": "0.1.42-alpha", + "contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA==" + }, + "Microsoft.Windows.SDK.Win32Metadata": { + "type": "Transitive", + "resolved": "60.0.34-preview", + "contentHash": "TA3DUNi4CTeo+ItTXBnGZFt2159XOGSl0UOlG5vjDj4WHqZjhwYyyUnzOtrbCERiSaP2Hzg7otJNWwOSZgutyA==" + }, + "Microsoft.Windows.WDK.Win32Metadata": { + "type": "Transitive", + "resolved": "0.11.4-experimental", + "contentHash": "bf5MCmUyZf0gBlYQjx9UpRAZWBkRndyt9XicR+UNLvAUAFTZQbu6YaX/sNKZlR98Grn0gydfh/yT4I3vc0AIQA==", + "dependencies": { + "Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "flow.launcher.plugin": { + "type": "Project", + "dependencies": { + "JetBrains.Annotations": "[2024.3.0, )", + "PropertyChanged.Fody": "[3.4.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin/DialogJumpResult.cs b/Flow.Launcher.Plugin/DialogJumpResult.cs new file mode 100644 index 000000000..2c9f0c139 --- /dev/null +++ b/Flow.Launcher.Plugin/DialogJumpResult.cs @@ -0,0 +1,92 @@ +namespace Flow.Launcher.Plugin +{ + /// + /// Describes a result of a executed by a plugin in Dialog Jump window + /// + public class DialogJumpResult : Result + { + /// + /// This holds the path which can be provided by plugin to be navigated to the + /// file dialog when records in Dialog Jump window is right clicked on a result. + /// + public required string DialogJumpPath { get; init; } + + /// + /// Clones the current Dialog Jump result + /// + public new DialogJumpResult Clone() + { + return new DialogJumpResult + { + Title = Title, + SubTitle = SubTitle, + ActionKeywordAssigned = ActionKeywordAssigned, + CopyText = CopyText, + AutoCompleteText = AutoCompleteText, + IcoPath = IcoPath, + BadgeIcoPath = BadgeIcoPath, + RoundedIcon = RoundedIcon, + Icon = Icon, + BadgeIcon = BadgeIcon, + Glyph = Glyph, + Action = Action, + AsyncAction = AsyncAction, + Score = Score, + TitleHighlightData = TitleHighlightData, + OriginQuery = OriginQuery, + PluginDirectory = PluginDirectory, + ContextData = ContextData, + PluginID = PluginID, + TitleToolTip = TitleToolTip, + SubTitleToolTip = SubTitleToolTip, + PreviewPanel = PreviewPanel, + ProgressBar = ProgressBar, + ProgressBarColor = ProgressBarColor, + Preview = Preview, + AddSelectedCount = AddSelectedCount, + RecordKey = RecordKey, + ShowBadge = ShowBadge, + DialogJumpPath = DialogJumpPath + }; + } + + /// + /// Convert to . + /// + public static DialogJumpResult From(Result result, string dialogJumpPath) + { + return new DialogJumpResult + { + Title = result.Title, + SubTitle = result.SubTitle, + ActionKeywordAssigned = result.ActionKeywordAssigned, + CopyText = result.CopyText, + AutoCompleteText = result.AutoCompleteText, + IcoPath = result.IcoPath, + BadgeIcoPath = result.BadgeIcoPath, + RoundedIcon = result.RoundedIcon, + Icon = result.Icon, + BadgeIcon = result.BadgeIcon, + Glyph = result.Glyph, + Action = result.Action, + AsyncAction = result.AsyncAction, + Score = result.Score, + TitleHighlightData = result.TitleHighlightData, + OriginQuery = result.OriginQuery, + PluginDirectory = result.PluginDirectory, + ContextData = result.ContextData, + PluginID = result.PluginID, + TitleToolTip = result.TitleToolTip, + SubTitleToolTip = result.SubTitleToolTip, + PreviewPanel = result.PreviewPanel, + ProgressBar = result.ProgressBar, + ProgressBarColor = result.ProgressBarColor, + Preview = result.Preview, + AddSelectedCount = result.AddSelectedCount, + RecordKey = result.RecordKey, + ShowBadge = result.ShowBadge, + DialogJumpPath = dialogJumpPath + }; + } + } +} diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 1831bf46f..a4ef39ac3 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -1,7 +1,7 @@ - net7.0-windows + net9.0-windows {8451ECDD-2EA4-4966-BB0A-7BBC40138E80} true Library @@ -11,6 +11,7 @@ false false false + true diff --git a/Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs b/Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs new file mode 100644 index 000000000..e028ebb12 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IAsyncDialogJump.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; + +namespace Flow.Launcher.Plugin +{ + /// + /// Asynchronous Dialog Jump Model + /// + public interface IAsyncDialogJump : IFeatures + { + /// + /// Asynchronous querying for Dialog Jump window + /// + /// + /// If the Querying method requires high IO transmission + /// or performing CPU intense jobs (performing better with cancellation), please use this IAsyncDialogJump interface + /// + /// Query to search + /// Cancel when querying job is obsolete + /// + Task> QueryDialogJumpAsync(Query query, CancellationToken token); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs b/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs new file mode 100644 index 000000000..d81b2bd19 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IDialogJump.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin +{ + /// + /// Synchronous Dialog Jump Model + /// + /// If the Querying method requires high IO transmission + /// or performing CPU intense jobs (performing better with cancellation), please try the IAsyncDialogJump interface + /// + /// + public interface IDialogJump : IAsyncDialogJump + { + /// + /// Querying for Dialog Jump window + /// + /// This method will be called within a Task.Run, + /// so please avoid synchrously wait for long. + /// + /// + /// Query to search + /// + List QueryDialogJump(Query query); + + Task> IAsyncDialogJump.QueryDialogJumpAsync(Query query, CancellationToken token) => Task.Run(() => QueryDialogJump(query), token); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs b/Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs new file mode 100644 index 000000000..33ad9ae73 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IDialogJumpDialog.cs @@ -0,0 +1,96 @@ +using System; + +#nullable enable + +namespace Flow.Launcher.Plugin +{ + /// + /// Interface for handling file dialog instances in DialogJump. + /// + public interface IDialogJumpDialog : IFeatures, IDisposable + { + /// + /// Check if the foreground window is a file dialog instance. + /// + /// + /// The handle of the foreground window to check. + /// + /// + /// The window if the foreground window is a file dialog instance. Null if it is not. + /// + IDialogJumpDialogWindow? CheckDialogWindow(IntPtr hwnd); + } + + /// + /// Interface for handling a specific file dialog window in DialogJump. + /// + public interface IDialogJumpDialogWindow : IDisposable + { + /// + /// The handle of the dialog window. + /// + IntPtr Handle { get; } + + /// + /// Get the current tab of the dialog window. + /// + /// + IDialogJumpDialogWindowTab GetCurrentTab(); + } + + /// + /// Interface for handling a specific tab in a file dialog window in DialogJump. + /// + public interface IDialogJumpDialogWindowTab : IDisposable + { + /// + /// The handle of the dialog tab. + /// + IntPtr Handle { get; } + + /// + /// Get the current folder path of the dialog tab. + /// + /// + string GetCurrentFolder(); + + /// + /// Get the current file of the dialog tab. + /// + /// + string GetCurrentFile(); + + /// + /// Jump to a folder in the dialog tab. + /// + /// + /// The path to the folder to jump to. + /// + /// + /// Whether folder jump is under automatical mode. + /// + /// + /// True if the jump was successful, false otherwise. + /// + bool JumpFolder(string path, bool auto); + + /// + /// Jump to a file in the dialog tab. + /// + /// + /// The path to the file to jump to. + /// + /// + /// True if the jump was successful, false otherwise. + /// + bool JumpFile(string path); + + /// + /// Open the file in the dialog tab. + /// + /// + /// True if the file was opened successfully, false otherwise. + /// + bool Open(); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs b/Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs new file mode 100644 index 000000000..9a2b879d0 --- /dev/null +++ b/Flow.Launcher.Plugin/Interfaces/IDialogJumpExplorer.cs @@ -0,0 +1,40 @@ +using System; + +#nullable enable + +namespace Flow.Launcher.Plugin +{ + /// + /// Interface for handling file explorer instances in DialogJump. + /// + public interface IDialogJumpExplorer : IFeatures, IDisposable + { + /// + /// Check if the foreground window is a Windows Explorer instance. + /// + /// + /// The handle of the foreground window to check. + /// + /// + /// The window if the foreground window is a file explorer instance. Null if it is not. + /// + IDialogJumpExplorerWindow? CheckExplorerWindow(IntPtr hwnd); + } + + /// + /// Interface for handling a specific file explorer window in DialogJump. + /// + public interface IDialogJumpExplorerWindow : IDisposable + { + /// + /// The handle of the explorer window. + /// + IntPtr Handle { get; } + + /// + /// Get the current folder path of the explorer window. + /// + /// + string? GetExplorerPath(); + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IPlugin.cs b/Flow.Launcher.Plugin/Interfaces/IPlugin.cs index bac93d090..cf5a8a582 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPlugin.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPlugin.cs @@ -32,6 +32,6 @@ namespace Flow.Launcher.Plugin Task IAsyncPlugin.InitAsync(PluginInitContext context) => Task.Run(() => Init(context)); - Task> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query)); + Task> IAsyncPlugin.QueryAsync(Query query, CancellationToken token) => Task.Run(() => Query(query), token); } } diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index cfa813d3f..dcccaebeb 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -613,5 +613,17 @@ namespace Flow.Launcher.Plugin /// Invoked when the actual theme of the application has changed. Currently, the plugin will continue to be subscribed even if it is turned off. /// event ActualApplicationThemeChangedEventHandler ActualApplicationThemeChanged; + + /// + /// Get the user data directory of Flow Launcher. + /// + /// + string GetDataDirectory(); + + /// + /// Get the log directory of Flow Launcher. + /// + /// + string GetLogDirectory(); } } diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index f0fcd48ff..a459e9ee6 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -307,7 +307,7 @@ namespace Flow.Launcher.Plugin Preview = Preview, AddSelectedCount = AddSelectedCount, RecordKey = RecordKey, - ShowBadge = ShowBadge, + ShowBadge = ShowBadge }; } diff --git a/Flow.Launcher.Plugin/packages.lock.json b/Flow.Launcher.Plugin/packages.lock.json new file mode 100644 index 000000000..6cdf96e07 --- /dev/null +++ b/Flow.Launcher.Plugin/packages.lock.json @@ -0,0 +1,77 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "Fody": { + "type": "Direct", + "requested": "[6.5.4, )", + "resolved": "6.5.4", + "contentHash": "GXZuti428IZctfby10xkMbWLCibcb6s29I/psLbBoO2vHJI5eTNVybnlV/Wi1tlIu9GG0bgW/PQwMH+MCldHxw==" + }, + "JetBrains.Annotations": { + "type": "Direct", + "requested": "[2024.3.0, )", + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" + }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[1.1.1, )", + "resolved": "1.1.1", + "contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.1", + "Microsoft.SourceLink.Common": "1.1.1" + } + }, + "Microsoft.Windows.CsWin32": { + "type": "Direct", + "requested": "[0.3.106, )", + "resolved": "0.3.106", + "contentHash": "Mx5fK7uN6fwLR4wUghs6//HonAnwPBNmC2oonyJVhCUlHS/r6SUS3NkBc3+gaQiv+0/9bqdj1oSCKQFkNI+21Q==", + "dependencies": { + "Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha", + "Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview", + "Microsoft.Windows.WDK.Win32Metadata": "0.11.4-experimental" + } + }, + "PropertyChanged.Fody": { + "type": "Direct", + "requested": "[3.4.0, )", + "resolved": "3.4.0", + "contentHash": "IAZyq0uolKo2WYm4mjx+q7A8fSGFT0x2e1s3y+ODn4JI0kqTDoo9GF2tdaypUzRFJZfdMxfC5HZW9QzdJLtOnA==", + "dependencies": { + "Fody": "6.5.1" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==" + }, + "Microsoft.Windows.SDK.Win32Docs": { + "type": "Transitive", + "resolved": "0.1.42-alpha", + "contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA==" + }, + "Microsoft.Windows.SDK.Win32Metadata": { + "type": "Transitive", + "resolved": "60.0.34-preview", + "contentHash": "TA3DUNi4CTeo+ItTXBnGZFt2159XOGSl0UOlG5vjDj4WHqZjhwYyyUnzOtrbCERiSaP2Hzg7otJNWwOSZgutyA==" + }, + "Microsoft.Windows.WDK.Win32Metadata": { + "type": "Transitive", + "resolved": "0.11.4-experimental", + "contentHash": "bf5MCmUyZf0gBlYQjx9UpRAZWBkRndyt9XicR+UNLvAUAFTZQbu6YaX/sNKZlR98Grn0gydfh/yT4I3vc0AIQA==", + "dependencies": { + "Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview" + } + } + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs b/Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs new file mode 100644 index 000000000..1747f2b4a --- /dev/null +++ b/Flow.Launcher.Test/ChineseDetectionPerformanceTest.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using Flow.Launcher.Infrastructure; +using ToolGood.Words.Pinyin; + +namespace Flow.Launcher.Test +{ + /// + /// Performance test comparing ContainsChinese() vs WordsHelper.HasChinese() + /// + /// This test verifies: + /// 1. Both methods produce identical results (correctness) + /// 2. Performance characteristics of both implementations + /// 3. Memory allocation patterns + /// + /// The ContainsChinese() method uses optimized Unicode range checking with ReadOnlySpan + /// while WordsHelper.HasChinese() uses the ToolGood.Words library implementation. + /// + [TestFixture] + public class ChineseDetectionPerformanceTest + { + private readonly List _testStrings = new() + { + // Pure English - should return false + "Hello World", + "Visual Studio Code", + "Microsoft Office 2023", + "Adobe Photoshop Creative Suite", + "Google Chrome Browser Application", + + // Pure Chinese - should return true + "你好世界", + "微软办公软件", + "谷歌浏览器", + "北京大学计算机科学与技术学院", + "中华人民共和国国家发展和改革委员会", + + // Mixed content - should return true + "Hello 世界", + "Visual Studio 代码编辑器", + "QQ音乐 Music Player", + "Windows 10 操作系统", + "GitHub 代码仓库管理平台", + + // Edge cases + "", + " ", + "123456", + "!@#$%^&*()", + "café résumé naïve", // Accented characters (not Chinese) + + // Long strings for performance testing + "This is a very long English string that contains no Chinese characters but is designed to test performance with longer text content that might appear in file names or application descriptions", + "这是一个非常长的中文字符串,包含了很多汉字,用来测试在处理较长中文文本时的性能表现,比如可能出现在文件名或应用程序描述中的文本内容", + "This is a mixed 混合内容的字符串 that contains both English and Chinese characters 中英文混合 to test performance with 复杂的文本内容 in real-world scenarios 真实场景中的应用" + }; + + [Test] + public void ContainsChinese_CorrectnessTest() + { + // Verify ContainsChinese works correctly for known cases + ClassicAssert.IsFalse(ContainsChinese("Hello World"), "Pure English should return false"); + ClassicAssert.IsTrue(ContainsChinese("你好世界"), "Pure Chinese should return true"); + ClassicAssert.IsTrue(ContainsChinese("Hello 世界"), "Mixed content should return true"); + ClassicAssert.IsFalse(ContainsChinese(""), "Empty string should return false"); + ClassicAssert.IsFalse(ContainsChinese("123456"), "Numbers should return false"); + ClassicAssert.IsFalse(ContainsChinese("café résumé"), "Accented characters should return false"); + } + + [Test] + public void WordsHelper_CorrectnessTest() + { + // Verify WordsHelper.HasChinese works correctly for known cases + ClassicAssert.IsFalse(WordsHelper.HasChinese("Hello World"), "Pure English should return false"); + ClassicAssert.IsTrue(WordsHelper.HasChinese("你好世界"), "Pure Chinese should return true"); + ClassicAssert.IsTrue(WordsHelper.HasChinese("Hello 世界"), "Mixed content should return true"); + ClassicAssert.IsFalse(WordsHelper.HasChinese(""), "Empty string should return false"); + ClassicAssert.IsFalse(WordsHelper.HasChinese("123456"), "Numbers should return false"); + ClassicAssert.IsFalse(WordsHelper.HasChinese("café résumé"), "Accented characters should return false"); + } + + [Test] + public void BothMethods_ShouldProduceSameResults() + { + // Critical test: verify both methods produce identical results for all test cases + foreach (var testString in _testStrings) + { + var wordsHelperResult = WordsHelper.HasChinese(testString); + var containsChineseResult = ContainsChinese(testString); + + ClassicAssert.AreEqual(wordsHelperResult, containsChineseResult, + $"Results differ for string: '{testString}'. WordsHelper: {wordsHelperResult}, ContainsChinese: {containsChineseResult}"); + } + + Console.WriteLine($"✓ Both methods produce identical results for all {_testStrings.Count} test cases"); + } + + [Test] + public void PerformanceComparison_BasicBenchmark() + { + const int iterations = 1000000; + + Console.WriteLine("=== CHINESE CHARACTER DETECTION PERFORMANCE TEST ==="); + Console.WriteLine($"Test iterations: {iterations:N0}"); + Console.WriteLine($"Test strings: {_testStrings.Count}"); + Console.WriteLine($"Total operations: {iterations * _testStrings.Count:N0}"); + Console.WriteLine(); + + // Warmup to ensure JIT compilation + Console.WriteLine("Warming up..."); + for (int i = 0; i < 1000; i++) + { + foreach (var testString in _testStrings) + { + _ = ContainsChinese(testString); + _ = WordsHelper.HasChinese(testString); + } + } + + // Benchmark ContainsChinese method + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + var sw1 = System.Diagnostics.Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) + { + foreach (var testString in _testStrings) + { + _ = ContainsChinese(testString); + } + } + sw1.Stop(); + + // Benchmark WordsHelper.HasChinese method + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + var sw2 = System.Diagnostics.Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) + { + foreach (var testString in _testStrings) + { + _ = WordsHelper.HasChinese(testString); + } + } + sw2.Stop(); + + // Calculate and display results + var containsChineseMs = sw1.Elapsed.TotalMilliseconds; + var wordsHelperMs = sw2.Elapsed.TotalMilliseconds; + var speedRatio = wordsHelperMs / containsChineseMs; + var timeDifference = wordsHelperMs - containsChineseMs; + + Console.WriteLine("RESULTS:"); + Console.WriteLine($"ContainsChinese(): {containsChineseMs:F3} ms"); + Console.WriteLine($"WordsHelper.HasChinese(): {wordsHelperMs:F3} ms"); + Console.WriteLine($"Time difference: {timeDifference:F3} ms"); + Console.WriteLine($"Speed improvement: {speedRatio:F2}x"); + Console.WriteLine($"Performance gain: {((speedRatio - 1) * 100):F1}%"); + Console.WriteLine(); + + if (speedRatio > 1.0) + { + Console.WriteLine($"✓ ContainsChinese() is {speedRatio:F2}x faster than WordsHelper.HasChinese()"); + } + else + { + Console.WriteLine($"⚠ WordsHelper.HasChinese() is {(1/speedRatio):F2}x faster than ContainsChinese()"); + } + + // Test always passes - this is a measurement test + ClassicAssert.IsTrue(true); + } + + [Test] + public void PerformanceComparison_ByStringType() + { + Console.WriteLine("=== PERFORMANCE BY STRING TYPE ==="); + + var categories = new Dictionary> + { + ["Pure English"] = _testStrings.Where(s => !ContainsChinese(s) && s.All(c => c <= 127)).ToList(), + ["Pure Chinese"] = _testStrings.Where(s => ContainsChinese(s) && s.All(c => IsChineseCharacter(c) || char.IsWhiteSpace(c))).ToList(), + ["Mixed Content"] = _testStrings.Where(s => ContainsChinese(s) && s.Any(c => c <= 127 && char.IsLetter(c))).ToList(), + ["Edge Cases"] = _testStrings.Where(s => string.IsNullOrWhiteSpace(s) || s.All(c => !char.IsLetter(c))).ToList() + }; + + foreach (var category in categories) + { + if (category.Value.Count == 0) continue; + + Console.WriteLine($"\n{category.Key} ({category.Value.Count} strings):"); + + var sample = category.Value.First(); + var displayText = sample.Length > 40 ? sample.Substring(0, 40) + "..." : sample; + Console.WriteLine($" Sample: '{displayText}'"); + + const int categoryIterations = 5000; + + // Test each method + var sw1 = System.Diagnostics.Stopwatch.StartNew(); + for (int i = 0; i < categoryIterations; i++) + { + foreach (var str in category.Value) + { + _ = ContainsChinese(str); + } + } + sw1.Stop(); + + var sw2 = System.Diagnostics.Stopwatch.StartNew(); + for (int i = 0; i < categoryIterations; i++) + { + foreach (var str in category.Value) + { + _ = WordsHelper.HasChinese(str); + } + } + sw2.Stop(); + + var ratio = (double)sw2.ElapsedTicks / sw1.ElapsedTicks; + Console.WriteLine($" Performance: ContainsChinese is {ratio:F2}x faster"); + } + + ClassicAssert.IsTrue(true); + } + + /// + /// Optimized Chinese character detection using comprehensive CJK Unicode ranges + /// This method uses ReadOnlySpan for better performance and covers all CJK character ranges + /// + private static bool ContainsChinese(ReadOnlySpan text) + { + foreach (var c in text) + { + if (IsChineseCharacter(c)) + return true; + } + return false; + } + + /// + /// Check if a character is a Chinese character using comprehensive Unicode ranges + /// Covers CJK Unified Ideographs and all extension blocks + /// + private static bool IsChineseCharacter(char c) + { + return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs (most common Chinese characters) + (c >= 0x3400 && c <= 0x4DBF) || // CJK Extension A + (c >= 0x20000 && c <= 0x2A6DF) || // CJK Extension B + (c >= 0x2A700 && c <= 0x2B73F) || // CJK Extension C + (c >= 0x2B740 && c <= 0x2B81F) || // CJK Extension D + (c >= 0x2B820 && c <= 0x2CEAF) || // CJK Extension E + (c >= 0x2CEB0 && c <= 0x2EBEF) || // CJK Extension F + (c >= 0xF900 && c <= 0xFAFF) || // CJK Compatibility Ideographs + (c >= 0x2F800 && c <= 0x2FA1F); // CJK Compatibility Supplement + } + } +} diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index 0241a374e..f04a9dcc9 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -1,7 +1,7 @@ - net7.0-windows10.0.19041.0 + net9.0-windows10.0.19041.0 {FF742965-9A80-41A5-B042-D6C7D3A21708} Library Properties diff --git a/Flow.Launcher.Test/TranslationMappingTest.cs b/Flow.Launcher.Test/TranslationMappingTest.cs index 10d765f5a..bd3636f0a 100644 --- a/Flow.Launcher.Test/TranslationMappingTest.cs +++ b/Flow.Launcher.Test/TranslationMappingTest.cs @@ -1,4 +1,6 @@ -using Flow.Launcher.Infrastructure; +using System.Collections.Generic; +using System.Reflection; +using Flow.Launcher.Infrastructure; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -34,22 +36,21 @@ namespace Flow.Launcher.Test mapping.AddNewIndex(2, 2); mapping.AddNewIndex(5, 3); - var result = mapping.MapToOriginalIndex(translatedIndex); ClassicAssert.AreEqual(expectedOriginalIndex, result); } - private int GetOriginalToTranslatedCount(TranslationMapping mapping) + private static int GetOriginalToTranslatedCount(TranslationMapping mapping) { - var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var list = (System.Collections.Generic.List)field.GetValue(mapping); + var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); + var list = (List)field.GetValue(mapping); return list.Count; } - private int GetOriginalToTranslatedAt(TranslationMapping mapping, int index) + private static int GetOriginalToTranslatedAt(TranslationMapping mapping, int index) { - var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var list = (System.Collections.Generic.List)field.GetValue(mapping); + var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); + var list = (List)field.GetValue(mapping); return list[index]; } } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 7b82748fc..6e053db29 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -16,6 +16,7 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -191,6 +192,9 @@ namespace Flow.Launcher // Enable Win32 dark mode if the system is in dark mode before creating all windows Win32Helper.EnableWin32DarkMode(_settings.ColorScheme); + // Initialize language before portable clean up since it needs translations + await Ioc.Default.GetRequiredService().InitializeLanguageAsync(); + Ioc.Default.GetRequiredService().PreStartCleanUpAfterPortabilityUpdate(); API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------"); @@ -216,8 +220,8 @@ namespace Flow.Launcher await PluginManager.InitializePluginsAsync(); - // Change language after all plugins are initialized because we need to update plugin title based on their api - await Ioc.Default.GetRequiredService().InitializeLanguageAsync(); + // Update plugin titles after plugins are initialized with their api instances + Internationalization.UpdatePluginMetadataTranslations(); await imageLoadertask; @@ -233,12 +237,16 @@ namespace Flow.Launcher // Initialize theme for main window Ioc.Default.GetRequiredService().ChangeTheme(); + DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs()); + DialogJump.SetupDialogJump(_settings.EnableDialogJump); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); RegisterExitEvents(); AutoStartup(); AutoUpdates(); + AutoPluginUpdates(); API.SaveAppAllSettings(); API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------"); @@ -251,7 +259,7 @@ namespace Flow.Launcher /// Check startup only for Release /// [Conditional("RELEASE")] - private void AutoStartup() + private static void AutoStartup() { // we try to enable auto-startup on first launch, or reenable if it was removed // but the user still has the setting set @@ -266,13 +274,13 @@ namespace Flow.Launcher // but if it fails (permissions, etc) then don't keep retrying // this also gives the user a visual indication in the Settings widget _settings.StartFlowLauncherOnSystemStartup = false; - API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message); + API.ShowMsgError(API.GetTranslation("setAutoStartFailed"), e.Message); } } } [Conditional("RELEASE")] - private void AutoUpdates() + private static void AutoUpdates() { _ = Task.Run(async () => { @@ -289,6 +297,37 @@ namespace Flow.Launcher }); } + private static void AutoPluginUpdates() + { + _ = Task.Run(async () => + { + if (_settings.AutoUpdatePlugins) + { + // check plugin updates every 5 hour + var timer = new PeriodicTimer(TimeSpan.FromHours(5)); + await PluginInstaller.CheckForPluginUpdatesAsync((plugins) => + { + Current.Dispatcher.Invoke(() => + { + var pluginUpdateWindow = new PluginUpdateWindow(plugins); + pluginUpdateWindow.ShowDialog(); + }); + }); + + while (await timer.WaitForNextTickAsync()) + // check updates on startup + await PluginInstaller.CheckForPluginUpdatesAsync((plugins) => + { + Current.Dispatcher.Invoke(() => + { + var pluginUpdateWindow = new PluginUpdateWindow(plugins); + pluginUpdateWindow.ShowDialog(); + }); + }); + } + }); + } + #endregion #region Register Events @@ -380,6 +419,7 @@ namespace Flow.Launcher // since some resources owned by the thread need to be disposed. _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); _mainVM?.Dispose(); + DialogJump.Dispose(); } API.LogInfo(ClassName, "End Flow Launcher dispose ----------------------------------------------------"); diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 37e1f6bcf..67939af14 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -2,7 +2,7 @@ WinExe - net7.0-windows10.0.19041.0 + net9.0-windows10.0.19041.0 true false Flow.Launcher.App @@ -12,6 +12,7 @@ false false en + true @@ -77,7 +78,7 @@ Designer PreserveNewest - + PreserveNewest @@ -89,7 +90,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -102,7 +102,6 @@ - all diff --git a/Flow.Launcher/Helper/HotKeyMapper.cs b/Flow.Launcher/Helper/HotKeyMapper.cs index e5fabb3a8..86a68475e 100644 --- a/Flow.Launcher/Helper/HotKeyMapper.cs +++ b/Flow.Launcher/Helper/HotKeyMapper.cs @@ -1,11 +1,12 @@ -using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Infrastructure.UserSettings; -using System; -using NHotkey; -using NHotkey.Wpf; -using Flow.Launcher.ViewModel; +using System; using ChefKeys; using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.DialogJump; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.ViewModel; +using NHotkey; +using NHotkey.Wpf; namespace Flow.Launcher.Helper; @@ -22,6 +23,10 @@ internal static class HotKeyMapper _settings = Ioc.Default.GetService(); SetHotkey(_settings.Hotkey, OnToggleHotkey); + if (_settings.EnableDialogJump) + { + SetHotkey(_settings.DialogJumpHotkey, DialogJump.OnToggleHotkey); + } LoadCustomPluginHotkey(); } diff --git a/Flow.Launcher/HotkeyControl.xaml.cs b/Flow.Launcher/HotkeyControl.xaml.cs index e8961058c..89bfde349 100644 --- a/Flow.Launcher/HotkeyControl.xaml.cs +++ b/Flow.Launcher/HotkeyControl.xaml.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -110,7 +110,8 @@ namespace Flow.Launcher SelectPrevItemHotkey, SelectPrevItemHotkey2, SelectNextItemHotkey, - SelectNextItemHotkey2 + SelectNextItemHotkey2, + DialogJumpHotkey, } // We can initialize settings in static field because it has been constructed in App constuctor @@ -142,6 +143,7 @@ namespace Flow.Launcher HotkeyType.SelectPrevItemHotkey2 => _settings.SelectPrevItemHotkey2, HotkeyType.SelectNextItemHotkey => _settings.SelectNextItemHotkey, HotkeyType.SelectNextItemHotkey2 => _settings.SelectNextItemHotkey2, + HotkeyType.DialogJumpHotkey => _settings.DialogJumpHotkey, _ => throw new System.NotImplementedException("Hotkey type not set") }; } @@ -201,6 +203,9 @@ namespace Flow.Launcher case HotkeyType.SelectNextItemHotkey2: _settings.SelectNextItemHotkey2 = value; break; + case HotkeyType.DialogJumpHotkey: + _settings.DialogJumpHotkey = value; + break; default: throw new System.NotImplementedException("Hotkey type not set"); } diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index 2fca06605..f37568419 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -18,6 +18,22 @@ Fail to Init Plugins Plugins: {0} - fail to load and would be disabled, please contact plugin creator for help + + Flow Launcher needs to restart to finish disabling portable mode, after the restart your portable data profile will be deleted and roaming data profile kept + Flow Launcher needs to restart to finish enabling portable mode, after the restart your roaming data profile will be deleted and portable data profile kept + Flow Launcher has detected you enabled portable mode, would you like to move it to a different location? + Flow Launcher has detected you disabled portable mode, the relevant shortcuts and uninstaller entry have been created + Flow Launcher detected your user data exists both in {0} and {1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred. + + + The following plugin has errored and cannot be loaded: + The following plugins have errored and cannot be loaded: + Please refer to the logs for more information + + + Please try again + Unable to parse Http Proxy + 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 @@ -93,6 +109,7 @@ Always Start Typing in English Mode Temporarily change your input method to English mode when activating Flow. Auto Update + Automatically check and update the app when available Select Hide Flow Launcher on startup Flow Launcher search window is hidden in the tray after starting up. @@ -106,7 +123,7 @@ Search with Pinyin Allows using Pinyin to search. Pinyin is the standard system of romanized spelling for translating Chinese. Use Double Pinyin - Allows using Double Pinyin to search. Double Pinyin is a variation of Pinyin that uses two characters. + Use Double Pinyin instead of Full Pinyin to search. Double Pinyin Schema Xiao He Zi Ran Ma @@ -117,7 +134,7 @@ Xing Kong Jian Dao Da Niu Xiao Lang - + Always Preview Always open preview panel when Flow activates. Press {0} to toggle preview. Shadow effect is not allowed while current theme has blur effect enabled @@ -139,6 +156,8 @@ Open Use Previous Korean IME You can change the Previous Korean IME settings directly from here + Failed to change Korean IME setting + Please check your system registry access or contact support. Home Page Show home page results when query text is empty. Show History Results in Home Page @@ -150,6 +169,8 @@ Restart Flow Launcher automatically after installing/uninstalling/updating plugin via Plugin Store Show unknown source warning Show warning when installing plugins from unknown sources + Auto update plugins + Automatically check plugin updates and notify if there are any updates available Search Plugin @@ -231,6 +252,12 @@ Zip files Please select zip file Install plugin from local path + No update available + All plugins are up to date + Plugin updates available + Update plugins + Check plugin updates + Plugins are successfully updated. Please restart Flow. Theme @@ -358,6 +385,28 @@ Show Result Badges For supported plugins, badges are displayed to help distinguish them more easily. Show Result Badges for Global Query Only + Show badges for global query results only + Dialog Jump + Enter shortcut to quickly navigate the Open/Save As dialog window to the path of the current file manager. + Dialog Jump + When Open/Save As dialog window opens, quickly navigate to the current path of the file manager. + Dialog Jump Automatically + When Open/Save As dialog window is displayed, automatically navigate to the path of the current file manager. (Experimental) + Show Dialog Jump Window + Display Dialog Jump search window when the open/save dialog window is shown to quickly navigate to file/folder locations. + Dialog Jump Window Position + Select position for the Dialog Jump search window + Fixed under the Open/Save As dialog window. Displayed on open and stays until the window is closed + Default search window position. Displayed when triggered by search window hotkey + Dialog Jump Result Navigation Behaviour + Behaviour to navigate Open/Save As dialog window to the selected result path + Left click or Enter key + Right click + Dialog Jump File Navigation Behaviour + Behaviour to navigate Open/Save As dialog window when the result is a file path + Fill full path in file name box + Fill full path in file name box and open + Fill directory in path box HTTP Proxy @@ -555,6 +604,11 @@ Update files Update description + + Restart Flow Launcher after updating plugins + {0}: Update from v{1} to v{2} + No plugin selected + Skip Welcome to Flow Launcher diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 0c8fb4d02..2ddce8190 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Media; @@ -19,6 +19,7 @@ using Flow.Launcher.Core.Resource; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; +using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -119,7 +120,7 @@ namespace Flow.Launcher Win32Helper.DisableControlBox(this); } - private void OnLoaded(object sender, RoutedEventArgs _) + private void OnLoaded(object sender, RoutedEventArgs e) { // Check first launch if (_settings.FirstLaunch) @@ -168,10 +169,12 @@ namespace Flow.Launcher if (_settings.HideOnStartup) { _viewModel.Hide(); + _viewModel.InitializeVisibilityStatus(false); } else { _viewModel.Show(); + _viewModel.InitializeVisibilityStatus(true); // When HideOnStartup is off and UseAnimation is on, // there was a bug where the clock would not appear at all on the initial launch // So we need to forcibly trigger animation here to ensure the clock is visible @@ -214,6 +217,9 @@ namespace Flow.Launcher // Without this part, when shown for the first time, switching the context menu does not move the cursor to the end. _viewModel.QueryTextCursorMovedToEnd = false; + // Register Dialog Jump events + InitializeDialogJump(); + // View model property changed event _viewModel.PropertyChanged += (o, e) => { @@ -226,7 +232,7 @@ namespace Flow.Launcher if (_viewModel.MainWindowVisibilityStatus) { // Play sound effect before activing the window - if (_settings.UseSound) + if (_settings.UseSound && !_viewModel.IsDialogJumpWindowUnderDialog()) { SoundPlay(); } @@ -249,7 +255,7 @@ namespace Flow.Launcher QueryTextBox.Focus(); // Play window animation - if (_settings.UseAnimation) + if (_settings.UseAnimation && !_viewModel.IsDialogJumpWindowUnderDialog()) { WindowAnimation(); } @@ -379,6 +385,11 @@ namespace Flow.Launcher private void OnLocationChanged(object sender, EventArgs e) { + if (_viewModel.IsDialogJumpWindowUnderDialog()) + { + return; + } + if (IsLoaded) { _settings.WindowLeft = Left; @@ -388,6 +399,11 @@ namespace Flow.Launcher private async void OnDeactivated(object sender, EventArgs e) { + if (_viewModel.IsDialogJumpWindowUnderDialog()) + { + return; + } + _settings.WindowLeft = Left; _settings.WindowTop = Top; @@ -577,11 +593,23 @@ namespace Flow.Launcher switch (msg) { case Win32Helper.WM_ENTERSIZEMOVE: + // Do do handle size move event for dialog jump window + if (_viewModel.IsDialogJumpWindowUnderDialog()) + { + return IntPtr.Zero; + } + _initialWidth = (int)Width; _initialHeight = (int)Height; handled = true; break; case Win32Helper.WM_EXITSIZEMOVE: + // Do do handle size move event for Dialog Jump window + if (_viewModel.IsDialogJumpWindowUnderDialog()) + { + return IntPtr.Zero; + } + //Prevent updating the number of results when the window height is below the height of a single result item. //This situation occurs not only when the user manually resizes the window, but also when the window is released from a side snap, as the OS automatically adjusts the window height. //(Without this check, releasing from a snap can cause the window height to hit the minimum, resulting in only 2 results being shown.) @@ -792,11 +820,19 @@ namespace Flow.Launcher #region Window Position - private void UpdatePosition() + public void UpdatePosition() { // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePosition(); - InitializePosition(); + if (_viewModel.IsDialogJumpWindowUnderDialog()) + { + InitializeDialogJumpPosition(); + InitializeDialogJumpPosition(); + } + else + { + InitializePosition(); + InitializePosition(); + } } private async Task PositionResetAsync() @@ -1354,6 +1390,46 @@ namespace Flow.Launcher #endregion + #region Dialog Jump + + private void InitializeDialogJump() + { + DialogJump.ShowDialogJumpWindowAsync = _viewModel.SetupDialogJumpAsync; + DialogJump.UpdateDialogJumpWindow = InitializeDialogJumpPosition; + DialogJump.ResetDialogJumpWindow = _viewModel.ResetDialogJump; + DialogJump.HideDialogJumpWindow = _viewModel.HideDialogJump; + } + + private void InitializeDialogJumpPosition() + { + if (_viewModel.DialogWindowHandle == nint.Zero || !_viewModel.MainWindowVisibilityStatus) return; + if (!_viewModel.IsDialogJumpWindowUnderDialog()) return; + + // Get dialog window rect + var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window); + if (!result) return; + + // Move window below the bottom of the dialog and keep it center + Top = VerticalBottom(window); + Left = HorizonCenter(window); + } + + private double HorizonCenter(Rect window) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, window.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, window.Width, 0); + var left = (dip2.X - ActualWidth) / 2 + dip1.X; + return left; + } + + private double VerticalBottom(Rect window) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, window.Bottom); + return dip1.Y; + } + + #endregion + #region IDisposable protected virtual void Dispose(bool disposing) diff --git a/Flow.Launcher/MessageBoxEx.xaml.cs b/Flow.Launcher/MessageBoxEx.xaml.cs index 7296ff4ca..907bfb926 100644 --- a/Flow.Launcher/MessageBoxEx.xaml.cs +++ b/Flow.Launcher/MessageBoxEx.xaml.cs @@ -37,8 +37,9 @@ namespace Flow.Launcher try { msgBox = new MessageBoxEx(button); - if (caption == string.Empty && button == MessageBoxButton.OK && icon == MessageBoxImage.None) + if (caption == string.Empty && icon == MessageBoxImage.None) { + // If there is no caption and no icon, use DescOnlyTextBlock for vertically centered text msgBox.Title = messageBoxText; msgBox.DescOnlyTextBlock.Visibility = Visibility.Visible; msgBox.DescOnlyTextBlock.Text = messageBoxText; diff --git a/Flow.Launcher/PluginUpdateWindow.xaml b/Flow.Launcher/PluginUpdateWindow.xaml new file mode 100644 index 000000000..04cd1f7bc --- /dev/null +++ b/Flow.Launcher/PluginUpdateWindow.xaml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _emptyResult = new List(); + private readonly IReadOnlyList _emptyDialogJumpResult = new List(); private readonly PluginMetadata _historyMetadata = new() { @@ -215,7 +217,8 @@ namespace Flow.Launcher.ViewModel var resultUpdateChannel = Channel.CreateUnbounded(); _resultsUpdateChannelWriter = resultUpdateChannel.Writer; _resultsViewUpdateTask = - Task.Run(UpdateActionAsync).ContinueWith(continueAction, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); + Task.Run(UpdateActionAsync).ContinueWith(continueAction, + CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); async Task UpdateActionAsync() { @@ -285,8 +288,16 @@ namespace Flow.Launcher.ViewModel var token = e.Token == default ? _updateToken : e.Token; - // 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); + IReadOnlyList resultsCopy; + if (e.Results == null) + { + resultsCopy = _emptyResult; + } + else + { + // make a clone to avoid possible issue that plugin will also change the list and items when updating view model + resultsCopy = DeepCloneResults(e.Results, false, token); + } foreach (var result in resultsCopy) { @@ -394,12 +405,30 @@ namespace Flow.Launcher.ViewModel [RelayCommand] private void LoadContextMenu() { + // For Dialog Jump and right click mode, we need to navigate to the path + if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.RightClick) + { + if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) + { + var result = SelectedResults.SelectedItem.Result; + if (result is DialogJumpResult dialogJumpResult) + { + Win32Helper.SetForegroundWindow(DialogWindowHandle); + _ = Task.Run(() => DialogJump.JumpToPathAsync(DialogWindowHandle, dialogJumpResult.DialogJumpPath)); + } + } + return; + } + + // For query mode, we load context menu if (QueryResultsSelected()) { // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing if (SelectedResults.SelectedItem != null) + { SelectedResults = ContextMenu; + } } else { @@ -469,12 +498,34 @@ namespace Flow.Launcher.ViewModel return; } - var hideWindow = await result.ExecuteAsync(new ActionContext + // For Dialog Jump and left click mode, we need to navigate to the path + if (_isDialogJump && Settings.DialogJumpResultBehaviour == DialogJumpResultBehaviours.LeftClick) { - // not null means pressing modifier key + number, should ignore the modifier key - SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() - }) - .ConfigureAwait(false); + Hide(); + + if (SelectedResults.SelectedItem != null && DialogWindowHandle != nint.Zero) + { + if (result is DialogJumpResult dialogJumpResult) + { + Win32Helper.SetForegroundWindow(DialogWindowHandle); + _ = Task.Run(() => DialogJump.JumpToPathAsync(DialogWindowHandle, dialogJumpResult.DialogJumpPath)); + } + } + } + // For query mode, we execute the result + else + { + var hideWindow = await result.ExecuteAsync(new ActionContext + { + // not null means pressing modifier key + number, should ignore the modifier key + SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers() + }).ConfigureAwait(false); + + if (hideWindow) + { + Hide(); + } + } if (QueryResultsSelected()) { @@ -482,26 +533,33 @@ namespace Flow.Launcher.ViewModel _history.Add(result.OriginQuery.RawQuery); lastHistoryIndex = 1; } - - if (hideWindow) - { - Hide(); - } } - private static IReadOnlyList DeepCloneResults(IReadOnlyList results, CancellationToken token = default) + private static IReadOnlyList DeepCloneResults(IReadOnlyList results, bool isDialogJump, CancellationToken token = default) { var resultsCopy = new List(); - foreach (var result in results.ToList()) - { - if (token.IsCancellationRequested) - { - break; - } - var resultCopy = result.Clone(); - resultsCopy.Add(resultCopy); + if (isDialogJump) + { + foreach (var result in results.ToList()) + { + if (token.IsCancellationRequested) break; + + var resultCopy = ((DialogJumpResult)result).Clone(); + resultsCopy.Add(resultCopy); + } } + else + { + foreach (var result in results.ToList()) + { + if (token.IsCancellationRequested) break; + + var resultCopy = result.Clone(); + resultsCopy.Add(resultCopy); + } + } + return resultsCopy; } @@ -1279,25 +1337,21 @@ namespace Flow.Launcher.ViewModel if (query == null) // shortcut expanded { - App.API.LogDebug(ClassName, $"Clear query results"); - - // Hide and clear results again because running query may show and add some results - Results.Visibility = Visibility.Collapsed; - Results.Clear(); - - // Reset plugin icon - PluginIconPath = null; - PluginIconSource = null; - SearchIconVisibility = Visibility.Visible; - - // Hide progress bar again because running query may set this to visible - ProgressBarVisibility = Visibility.Hidden; + ClearResults(); return; } App.API.LogDebug(ClassName, $"Start query with ActionKeyword <{query.ActionKeyword}> and RawQuery <{query.RawQuery}>"); var currentIsHomeQuery = query.IsHomeQuery; + var currentIsDialogJump = _isDialogJump; + + // Do not show home page for Dialog Jump window + if (currentIsHomeQuery && currentIsDialogJump) + { + ClearResults(); + return; + } _updateSource?.Dispose(); @@ -1331,7 +1385,7 @@ namespace Flow.Launcher.ViewModel } else { - plugins = PluginManager.ValidPluginsForQuery(query); + plugins = PluginManager.ValidPluginsForQuery(query, currentIsDialogJump); if (plugins.Count == 1) { @@ -1425,6 +1479,23 @@ namespace Flow.Launcher.ViewModel } // Local function + void ClearResults() + { + App.API.LogDebug(ClassName, $"Clear query results"); + + // Hide and clear results again because running query may show and add some results + Results.Visibility = Visibility.Collapsed; + Results.Clear(); + + // Reset plugin icon + PluginIconPath = null; + PluginIconSource = null; + SearchIconVisibility = Visibility.Visible; + + // Hide progress bar again because running query may set this to visible + ProgressBarVisibility = Visibility.Hidden; + } + async Task QueryTaskAsync(PluginPair plugin, CancellationToken token) { App.API.LogDebug(ClassName, $"Wait for querying plugin <{plugin.Metadata.Name}>"); @@ -1442,21 +1513,23 @@ namespace Flow.Launcher.ViewModel // Task.Yield will force it to run in ThreadPool await Task.Yield(); - var results = currentIsHomeQuery ? - await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : - await PluginManager.QueryForPluginAsync(plugin, query, token); + IReadOnlyList results = currentIsDialogJump ? + await PluginManager.QueryDialogJumpForPluginAsync(plugin, query, token) : + currentIsHomeQuery ? + await PluginManager.QueryHomeForPluginAsync(plugin, query, token) : + await PluginManager.QueryForPluginAsync(plugin, query, token); if (token.IsCancellationRequested) return; IReadOnlyList resultsCopy; if (results == null) { - resultsCopy = _emptyResult; + resultsCopy = currentIsDialogJump ? _emptyDialogJumpResult : _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, token); + resultsCopy = DeepCloneResults(results, currentIsDialogJump, token); } foreach (var result in resultsCopy) @@ -1751,6 +1824,208 @@ namespace Flow.Launcher.ViewModel #endregion + #region Dialog Jump + + public nint DialogWindowHandle { get; private set; } = nint.Zero; + + private bool _isDialogJump = false; + + private bool _previousMainWindowVisibilityStatus; + + private CancellationTokenSource _dialogJumpSource; + + public void InitializeVisibilityStatus(bool visibilityStatus) + { + _previousMainWindowVisibilityStatus = visibilityStatus; + } + + public bool IsDialogJumpWindowUnderDialog() + { + return _isDialogJump && DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog; + } + + public async Task SetupDialogJumpAsync(nint handle) + { + if (handle == nint.Zero) return; + + // Only set flag & reset window once for one file dialog + var dialogWindowHandleChanged = false; + if (DialogWindowHandle != handle) + { + DialogWindowHandle = handle; + _previousMainWindowVisibilityStatus = MainWindowVisibilityStatus; + _isDialogJump = true; + + dialogWindowHandleChanged = true; + + // If don't give a time, Positioning will be weird + await Task.Delay(300); + } + + // If handle is cleared, which means the dialog is closed, clear Dialog Jump state + if (DialogWindowHandle == nint.Zero) + { + _isDialogJump = false; + return; + } + + // Initialize Dialog Jump window + if (MainWindowVisibilityStatus) + { + if (dialogWindowHandleChanged) + { + // Only update the position + Application.Current?.Dispatcher.Invoke(() => + { + (Application.Current?.MainWindow as MainWindow)?.UpdatePosition(); + }); + + _ = ResetWindowAsync(); + } + } + else + { + if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) + { + // We wait for window to be reset before showing it because if window has results, + // showing it before resetting will cause flickering when results are clearing + if (dialogWindowHandleChanged) + { + await ResetWindowAsync(); + } + + Show(); + } + else + { + if (dialogWindowHandleChanged) + { + _ = ResetWindowAsync(); + } + } + } + + if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) + { + // Cancel the previous Dialog Jump task + _dialogJumpSource?.Cancel(); + + // Create a new cancellation token source + _dialogJumpSource = new CancellationTokenSource(); + + _ = Task.Run(() => + { + try + { + // Check task cancellation + if (_dialogJumpSource.Token.IsCancellationRequested) return; + + // Check dialog handle + if (DialogWindowHandle == nint.Zero) return; + + // Wait 150ms to check if Dialog Jump window gets the focus + var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150); + if (timeOut) return; + + // Bring focus back to the dialog + Win32Helper.SetForegroundWindow(DialogWindowHandle); + } + catch (Exception e) + { + App.API.LogException(ClassName, "Failed to focus on dialog window", e); + } + }); + } + } + +#pragma warning disable VSTHRD100 // Avoid async void methods + + public async void ResetDialogJump() + { + // Cache original dialog window handle + var dialogWindowHandle = DialogWindowHandle; + + // Reset the Dialog Jump state + DialogWindowHandle = nint.Zero; + _isDialogJump = false; + + // If dialog window handle is not set, we should not reset the main window visibility + if (dialogWindowHandle == nint.Zero) return; + + if (_previousMainWindowVisibilityStatus != MainWindowVisibilityStatus) + { + // We wait for window to be reset before showing it because if window has results, + // showing it before resetting will cause flickering when results are clearing + await ResetWindowAsync(); + + // Show or hide to change visibility + if (_previousMainWindowVisibilityStatus) + { + Show(); + } + else + { + Hide(false); + } + } + else + { + if (_previousMainWindowVisibilityStatus) + { + // Only update the position + Application.Current?.Dispatcher.Invoke(() => + { + (Application.Current?.MainWindow as MainWindow)?.UpdatePosition(); + }); + + _ = ResetWindowAsync(); + } + else + { + _ = ResetWindowAsync(); + } + } + } + +#pragma warning restore VSTHRD100 // Avoid async void methods + + public void HideDialogJump() + { + if (DialogWindowHandle != nint.Zero) + { + if (DialogJump.DialogJumpWindowPosition == DialogJumpWindowPositions.UnderDialog) + { + // Warning: Main window is already in foreground + // This is because if you click popup menus in other applications to hide Dialog Jump window, + // they can steal focus before showing main window + if (MainWindowVisibilityStatus) + { + Hide(); + } + } + } + } + + // Reset index & preview & selected results & query text + private async Task ResetWindowAsync() + { + lastHistoryIndex = 1; + + if (ExternalPreviewVisible) + { + await CloseExternalPreviewAsync(); + } + + if (!QueryResultsSelected()) + { + SelectedResults = Results; + } + + await ChangeQueryTextAsync(string.Empty, true); + } + + #endregion + #region Public Methods #pragma warning disable VSTHRD100 // Avoid async void methods @@ -1770,7 +2045,7 @@ namespace Flow.Launcher.ViewModel Win32Helper.DWMSetCloakForWindow(mainWindow, false); // Set clock and search icon opacity - var opacity = Settings.UseAnimation ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !_isDialogJump) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -1799,37 +2074,40 @@ namespace Flow.Launcher.ViewModel } } - public async void Hide() + public async void Hide(bool reset = true) { - lastHistoryIndex = 1; - - if (ExternalPreviewVisible) + if (reset) { - await CloseExternalPreviewAsync(); - } + lastHistoryIndex = 1; - BackToQueryResults(); + if (ExternalPreviewVisible) + { + await CloseExternalPreviewAsync(); + } - switch (Settings.LastQueryMode) - { - case LastQueryMode.Empty: - await ChangeQueryTextAsync(string.Empty); - break; - case LastQueryMode.Preserved: - case LastQueryMode.Selected: - LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved; - break; - case LastQueryMode.ActionKeywordPreserved: - case LastQueryMode.ActionKeywordSelected: - var newQuery = _lastQuery?.ActionKeyword; + BackToQueryResults(); - if (!string.IsNullOrEmpty(newQuery)) - newQuery += " "; - await ChangeQueryTextAsync(newQuery); + switch (Settings.LastQueryMode) + { + case LastQueryMode.Empty: + await ChangeQueryTextAsync(string.Empty); + break; + case LastQueryMode.Preserved: + case LastQueryMode.Selected: + LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved; + break; + case LastQueryMode.ActionKeywordPreserved: + case LastQueryMode.ActionKeywordSelected: + var newQuery = _lastQuery.ActionKeyword; - if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected) - LastQuerySelected = false; - break; + if (!string.IsNullOrEmpty(newQuery)) + newQuery += " "; + await ChangeQueryTextAsync(newQuery); + + if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected) + LastQuerySelected = false; + break; + } } // When application is exiting, the Application.Current will be null @@ -1839,7 +2117,7 @@ namespace Flow.Launcher.ViewModel if (Application.Current?.MainWindow is MainWindow mainWindow) { // Set clock and search icon opacity - var opacity = Settings.UseAnimation ? 0.0 : 1.0; + var opacity = (Settings.UseAnimation && !_isDialogJump) ? 0.0 : 1.0; ClockPanelOpacity = opacity; SearchIconOpacity = opacity; @@ -1984,6 +2262,7 @@ namespace Flow.Launcher.ViewModel if (disposing) { _updateSource?.Dispose(); + _dialogJumpSource?.Dispose(); _resultsUpdateChannelWriter?.Complete(); if (_resultsViewUpdateTask?.IsCompleted == true) { diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json new file mode 100644 index 000000000..017065044 --- /dev/null +++ b/Flow.Launcher/packages.lock.json @@ -0,0 +1,661 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows10.0.19041": { + "ChefKeys": { + "type": "Direct", + "requested": "[0.1.2, )", + "resolved": "0.1.2", + "contentHash": "hnayWejg57tg8+lZ1Q/zPR8tj9ezUtB1sY8aCv9jiZ+3wcqK0eGL+Skt9OzT9mjSsBIg4o9Jv1HdQdzjd1lkQw==" + }, + "CommunityToolkit.Mvvm": { + "type": "Direct", + "requested": "[8.4.0, )", + "resolved": "8.4.0", + "contentHash": "tqVU8yc/ADO9oiTRyTnwhFN68hCwvkliMierptWOudIAvWY1mWCh5VFh+guwHJmpMwfg0J0rY+yyd5Oy7ty9Uw==" + }, + "Fody": { + "type": "Direct", + "requested": "[6.5.4, )", + "resolved": "6.5.4", + "contentHash": "GXZuti428IZctfby10xkMbWLCibcb6s29I/psLbBoO2vHJI5eTNVybnlV/Wi1tlIu9GG0bgW/PQwMH+MCldHxw==" + }, + "InputSimulator": { + "type": "Direct", + "requested": "[1.0.4, )", + "resolved": "1.0.4", + "contentHash": "D0LvRCPQMX6/FJHBjng+RO+wRDuHTJrfo7IAc7rmkPvRqchdVGJWg3y70peOtDy3OLNK+HSOwVkH4GiuLnkKgA==" + }, + "Jack251970.TaskScheduler": { + "type": "Direct", + "requested": "[2.12.1, )", + "resolved": "2.12.1", + "contentHash": "+epAtsLMugiznJCNRYCYB6eBcr+bx+CVlwPWMprO5CbnNkWu9mlSV8XN5BQJrGYwmlAtlGfZA3p3PcFFlrgR6A==", + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0", + "System.Diagnostics.EventLog": "8.0.0", + "System.Security.AccessControl": "6.0.1" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "elNeOmkeX3eDVG6pYVeV82p29hr+UKDaBhrZyWvWLw/EVZSYEkZlQdkp0V39k/Xehs2Qa0mvoCvkVj3eQxNQ1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0" + } + }, + "Microsoft.Extensions.Hosting": { + "type": "Direct", + "requested": "[7.0.1, )", + "resolved": "7.0.1", + "contentHash": "aoeMou6XSW84wiqd895OdaGyO9PfH6nohQJ0XBcshRDafbdIU6PQIVl8TpOCssPYq3ciRseP5064hbFyCR9J9w==", + "dependencies": { + "Microsoft.Extensions.Configuration": "7.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.Configuration.Binder": "7.0.3", + "Microsoft.Extensions.Configuration.CommandLine": "7.0.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "7.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "7.0.0", + "Microsoft.Extensions.Configuration.Json": "7.0.0", + "Microsoft.Extensions.Configuration.UserSecrets": "7.0.0", + "Microsoft.Extensions.DependencyInjection": "7.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "7.0.0", + "Microsoft.Extensions.FileProviders.Physical": "7.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging": "7.0.0", + "Microsoft.Extensions.Logging.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging.Configuration": "7.0.0", + "Microsoft.Extensions.Logging.Console": "7.0.0", + "Microsoft.Extensions.Logging.Debug": "7.0.0", + "Microsoft.Extensions.Logging.EventLog": "7.0.0", + "Microsoft.Extensions.Logging.EventSource": "7.0.0", + "Microsoft.Extensions.Options": "7.0.1", + "System.Diagnostics.DiagnosticSource": "7.0.1" + } + }, + "Microsoft.Toolkit.Uwp.Notifications": { + "type": "Direct", + "requested": "[7.1.3, )", + "resolved": "7.1.3", + "contentHash": "A1dglAzb24gjehmb7DwGd07mfyZ1gacAK7ObE0KwDlRc3mayH2QW7cSOy3TkkyELjLg19OQBuhPOj4SpXET9lg==", + "dependencies": { + "Microsoft.Win32.Registry": "4.7.0", + "System.Drawing.Common": "4.7.0", + "System.Reflection.Emit": "4.7.0", + "System.ValueTuple": "4.5.0" + } + }, + "Microsoft.Windows.CsWin32": { + "type": "Direct", + "requested": "[0.3.106, )", + "resolved": "0.3.106", + "contentHash": "Mx5fK7uN6fwLR4wUghs6//HonAnwPBNmC2oonyJVhCUlHS/r6SUS3NkBc3+gaQiv+0/9bqdj1oSCKQFkNI+21Q==", + "dependencies": { + "Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha", + "Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview", + "Microsoft.Windows.WDK.Win32Metadata": "0.11.4-experimental" + } + }, + "ModernWpfUI": { + "type": "Direct", + "requested": "[0.9.4, )", + "resolved": "0.9.4", + "contentHash": "HJ07Be9KOiGKGcMLz/AwY+84h3yGHRPuYpYXCE6h1yPtaFwGMWfanZ70jX7W5XWx8+Qk1vGox+WGKgxxsy6EHw==" + }, + "NHotkey.Wpf": { + "type": "Direct", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "BIUKlhTG5KtFf9OQzWvkmVmktt5/FFj6AOEgag8Uf0R2YdZt5ajUzs3sVskcJcT2TztWlEHKQr1jFj3KQ0D9Nw==", + "dependencies": { + "NHotkey": "3.0.0" + } + }, + "PropertyChanged.Fody": { + "type": "Direct", + "requested": "[3.4.0, )", + "resolved": "3.4.0", + "contentHash": "IAZyq0uolKo2WYm4mjx+q7A8fSGFT0x2e1s3y+ODn4JI0kqTDoo9GF2tdaypUzRFJZfdMxfC5HZW9QzdJLtOnA==", + "dependencies": { + "Fody": "6.5.1" + } + }, + "SemanticVersioning": { + "type": "Direct", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "RR+8GbPQ/gjDqov/1QN1OPoUlbUruNwcL3WjWCeLw+MY7+od/ENhnkYxCfAC6rQLIu3QifaJt3kPYyP3RumqMQ==" + }, + "VirtualizingWrapPanel": { + "type": "Direct", + "requested": "[2.1.1, )", + "resolved": "2.1.1", + "contentHash": "Fc/yjU8jqC3qpIsNxeO5RjK2lPU7xnJtBLMSQ6L9egA2PyJLQeVeXpG8WBb5N1kN15rlJEYG8dHWJ5qUGgaNrg==" + }, + "Ben.Demystifier": { + "type": "Transitive", + "resolved": "0.4.1", + "contentHash": "axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==", + "dependencies": { + "System.Reflection.Metadata": "5.0.0" + } + }, + "BitFaster.Caching": { + "type": "Transitive", + "resolved": "2.5.3", + "contentHash": "Vo/39qcam5Xe+DbyfH0JZyqPswdOoa7jv4PGtRJ6Wj8AU+aZ+TuJRlJcIe+MQjRTJwliI8k8VSQpN8sEoBIv2g==" + }, + "DeltaCompressionDotNet": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "nwbZAYd+DblXAIzlnwDSnl0CiCm8jWLfHSYnoN4wYhtIav6AegB3+T/vKzLbU2IZlPB8Bvl8U3NXpx3eaz+N5w==" + }, + "Droplex": { + "type": "Transitive", + "resolved": "1.7.0", + "contentHash": "wutfIus/Ufw/9TDsp86R1ycnIH+wWrj4UhcmrzAHWjsdyC2iM07WEQ9+APTB7pQynsDnYH1r2i58XgAJ3lxUXA==", + "dependencies": { + "YamlDotNet": "9.1.0" + } + }, + "FSharp.Core": { + "type": "Transitive", + "resolved": "9.0.101", + "contentHash": "3/YR1SDWFA+Ojx9HiBwND+0UR8ZWoeZfkhD0DWAPCDdr/YI+CyFkArmMGzGSyPXeYtjG0sy0emzfyNwjt7zhig==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" + }, + "MemoryPack": { + "type": "Transitive", + "resolved": "1.21.3", + "contentHash": "cwCtED8y400vMWx/Vp0QCSeEpVFjDU4JwF52VX9WTaqVERUvNqjG9n6osFlmFuytegyXnHvYEu1qRJ8rv/rkbg==", + "dependencies": { + "MemoryPack.Core": "1.21.3", + "MemoryPack.Generator": "1.21.3" + } + }, + "MemoryPack.Core": { + "type": "Transitive", + "resolved": "1.21.3", + "contentHash": "ajrYoBWT2aKeH4tlY8q/1C9qK1R/NK+7FkuVOX58ebOSxkABoFTqCR7W+Zk2rakUHZiEgNdRqO67hiRZPq6fLA==" + }, + "MemoryPack.Generator": { + "type": "Transitive", + "resolved": "1.21.3", + "contentHash": "hYU0TAIarDKnbkNIWvb7P4zBUL+CTahkuNkczsKvycSMR5kiwQ4IfLexywNKX3s05Izp4gzDSPbueepNWZRpWA==" + }, + "MessagePack": { + "type": "Transitive", + "resolved": "2.5.187", + "contentHash": "uW4j8m4Nc+2Mk5n6arOChavJ9bLjkis0qWASOj2h2OwmfINuzYv+mjCHUymrYhmyyKTu3N+ObtTXAY4uQ7jIhg==", + "dependencies": { + "MessagePack.Annotations": "2.5.187", + "Microsoft.NET.StringTools": "17.6.3" + } + }, + "MessagePack.Annotations": { + "type": "Transitive", + "resolved": "2.5.187", + "contentHash": "/IvvMMS8opvlHjEJ/fR2Cal4Co726Kj77Z8KiohFhuHfLHHmb9uUxW5+tSCL4ToKFfkQlrS3HD638mRq83ySqA==" + }, + "Meziantou.Framework.Win32.Jobs": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "5GGLckfpwoC1jznInEYfK2INrHyD7K1RtwZJ98kNPKBU6jeu24i4zfgDGHHfb+eK3J+eFPAxo0aYcbUxNXIbNw==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "tldQUBWt/xeH2K7/hMPPo5g8zuLc3Ro9I5d4o/XrxvxOCA2EZBtW7bCHHTc49fcBtvB8tLAb/Qsmfrq+2SJ4vA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "f34u2eaqIjNO9YLHBz8rozVZ+TcFiFs0F3r7nUJd7FRkVSxk8u4OpoK226mi49MwexHOR2ibP9MFvRUaLilcQQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "7.0.3", + "contentHash": "1eRFwJBrkkncTpvh6mivB8zg4uBVm6+Y6stEJERrVEqZZc8Hvf+N1iIgj2ySYDUQko4J1Gw1rLf1M8bG83F0eA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "a8Iq8SCw5m8W5pZJcPCgBpBO4E89+NaObPng+ApIhrGSv9X4JPrcFAaGM4sDgR0X83uhLgsNJq8VnGP/wqhr8A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "7.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "RIkfqCkvrAogirjsqSrG1E1FxgrLsOZU2nhRbl07lrajnxzSU2isj2lwQah0CtCbLWo/pOIukQzM1GfneBUnxA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "7.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "xk2lRJ1RDuqe57BmgvRPyCt6zyePKUmvT6iuXqiHR+/OIIgWVR8Ff5k2p6DwmqY8a17hx/OnrekEhziEIeQP6Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "7.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "7.0.0", + "Microsoft.Extensions.FileProviders.Physical": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "LDNYe3uw76W35Jci+be4LDf2lkQZe0A7EEYQVChFbc509CpZ4Iupod8li4PUXPBhEUOFI/rlQNf5xkzJRQGvtA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "7.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "7.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "7.0.0", + "System.Text.Json": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "33HPW1PmB2RS0ietBQyvOxjp4O3wlt+4tIs8KPyMn1kqp04goiZGa7+3mc69NRLv6bphkLDy0YR7Uw3aZyf8Zw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.Configuration.Json": "7.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "7.0.0", + "Microsoft.Extensions.FileProviders.Physical": "7.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "NyawiW9ZT/liQb34k9YqBSNPLuuPkrjMgQZ24Y/xXX1RoiBkLUdPMaQTmxhZ5TYu8ZKZ9qayzil75JX95vGQUg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "K8D2MTR+EtzkbZ8z80LrG7Ur64R7ZZdRLt1J5cgpc/pUWl0C6IkAUapPuK28oionHueCPELUqq0oYEvZfalNdg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "7.0.0", + "Microsoft.Extensions.FileSystemGlobbing": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "2jONjKHiF+E92ynz2ZFcr9OvxIw+rTGMPEH+UZGeHTEComVav93jQUWGkso8yWwVBcEJGcNcZAaqY01FFJcj7w==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "43n9Je09z0p/7ViPxfRqs5BUItRLNVh5b6JH40F2Agkh2NBsY/jpNYTtbCcxrHCsA3oRmbR6RJBzUutB4VZvNQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "7.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "Nw2muoNrOG5U5qa2ZekXwudUn2BJcD41e65zwmDHb1fQegTX66UokLWZkJRpqSSHXDOWZ5V0iqhbxOEky91atA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "7.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging.Abstractions": "7.0.0", + "Microsoft.Extensions.Options": "7.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "kmn78+LPVMOWeITUjIlfxUPDsI0R6G0RkeAMBmQxAJ7vBJn4q2dTva7pWi65ceN5vPGjJ9q/Uae2WKgvfktJAw==" + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "FLDA0HcffKA8ycoDQLJuCNGIE42cLWPxgdQGRBaSzZrYTkMBjnf9zrr8pGT06psLq9Q+RKWmmZczQ9bCrXEBcA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "7.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.Configuration.Binder": "7.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging": "7.0.0", + "Microsoft.Extensions.Logging.Abstractions": "7.0.0", + "Microsoft.Extensions.Options": "7.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "7.0.0" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qt5n8bHLZPUfuRnFxJKW5q9ZwOTncdh96rtWzWpX3Y/064MlxzCSw2ELF5Jlwdo+Y4wK3I47NmUTFsV7Sg8rqg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging": "7.0.0", + "Microsoft.Extensions.Logging.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging.Configuration": "7.0.0", + "Microsoft.Extensions.Options": "7.0.0", + "System.Text.Json": "7.0.0" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "tFGGyPDpJ8ZdQdeckCArP7nZuoY3am9zJWuvp4OD1bHq65S0epW9BNHzAWeaIO4eYwWnGm1jRNt3vRciH8H6MA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging": "7.0.0", + "Microsoft.Extensions.Logging.Abstractions": "7.0.0" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "Rp7cYL9xQRVTgjMl77H5YDxszAaO+mlA+KT0BnLSVhuCoKQQOOs1sSK2/x8BK2dZ/lKeAC/CVF+20Ef2dpKXwg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging": "7.0.0", + "Microsoft.Extensions.Logging.Abstractions": "7.0.0", + "Microsoft.Extensions.Options": "7.0.0", + "System.Diagnostics.EventLog": "7.0.0" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "MxQXndQFviIyOPqyMeLNshXnmqcfzEHE2wWcr7BF1unSisJgouZ3tItnq+aJLGPojrW8OZSC/ZdRoR6wAq+c7w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging": "7.0.0", + "Microsoft.Extensions.Logging.Abstractions": "7.0.0", + "Microsoft.Extensions.Options": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0", + "System.Text.Json": "7.0.0" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "7.0.1", + "contentHash": "pZRDYdN1FpepOIfHU62QoBQ6zdAoTvnjxFfqAzEd9Jhb2dfhA5i6jeTdgGgcgTWFRC7oT0+3XrbQu4LjvgX1Nw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "95UnxZkkFdXxF6vSrtJsMHCzkDeSMuUWGs2hDT54cX+U5eVajrCJ3qLyQRW+CtpTt5OJ8bmTvpQVHu1DLhH+cA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.Configuration.Binder": "7.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Options": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "um1KU5kxcRp3CNuI8o/GrZtD4AIOXDk+RLsytjZ9QPok3ttLUelLKpilVPuaFT3TFjOhSibUAso0odbOaCDj3Q==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Transitive", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, + "Microsoft.NET.StringTools": { + "type": "Transitive", + "resolved": "17.6.3", + "contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA==" + }, + "Microsoft.VisualStudio.Threading": { + "type": "Transitive", + "resolved": "17.12.19", + "contentHash": "eLiGMkMYyaSguqHs3lsrFxy3tAWSLuPEL2pIWRcADMDVAs2xqm3dr1d9QYjiEusTgiClF9KD6OB2NdZP72Oy0Q==", + "dependencies": { + "Microsoft.VisualStudio.Threading.Analyzers": "17.12.19", + "Microsoft.VisualStudio.Validation": "17.8.8" + } + }, + "Microsoft.VisualStudio.Threading.Analyzers": { + "type": "Transitive", + "resolved": "17.12.19", + "contentHash": "v3IYeedjoktvZ+GqYmLudxZJngmf/YWIxNT2Uy6QMMN19cvw+nkWoip1Gr1RtnFkUo1MPUVMis4C8Kj8d8DpSQ==" + }, + "Microsoft.VisualStudio.Validation": { + "type": "Transitive", + "resolved": "17.8.8", + "contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "9.0.2", + "contentHash": "5BkGZ6mHp2dHydR29sb0fDfAuqkv30AHtTih8wMzvPZysOmBFvHfnkR2w3tsc0pSiIg8ZoKyefJXWy9r3pBh0w==" + }, + "Microsoft.Windows.SDK.Win32Docs": { + "type": "Transitive", + "resolved": "0.1.42-alpha", + "contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA==" + }, + "Microsoft.Windows.SDK.Win32Metadata": { + "type": "Transitive", + "resolved": "60.0.34-preview", + "contentHash": "TA3DUNi4CTeo+ItTXBnGZFt2159XOGSl0UOlG5vjDj4WHqZjhwYyyUnzOtrbCERiSaP2Hzg7otJNWwOSZgutyA==" + }, + "Microsoft.Windows.WDK.Win32Metadata": { + "type": "Transitive", + "resolved": "0.11.4-experimental", + "contentHash": "bf5MCmUyZf0gBlYQjx9UpRAZWBkRndyt9XicR+UNLvAUAFTZQbu6YaX/sNKZlR98Grn0gydfh/yT4I3vc0AIQA==", + "dependencies": { + "Microsoft.Windows.SDK.Win32Metadata": "60.0.34-preview" + } + }, + "Mono.Cecil": { + "type": "Transitive", + "resolved": "0.9.6.1", + "contentHash": "yMsurNaOxxKIjyW9pEB+tRrR1S3DFnN1+iBgKvYvXG8kW0Y6yknJeMAe/tl3+P78/2C6304TgF7aVqpqXgEQ9Q==" + }, + "Nerdbank.Streams": { + "type": "Transitive", + "resolved": "2.11.74", + "contentHash": "r4G7uHHfoo8LCilPOdtf2C+Q5ymHOAXtciT4ZtB2xRlAvv4gPkWBYNAijFblStv3+uidp81j5DP11jMZl4BfJw==", + "dependencies": { + "Microsoft.VisualStudio.Threading": "17.10.48", + "Microsoft.VisualStudio.Validation": "17.8.8", + "System.IO.Pipelines": "8.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "NHotkey": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "IEghs0QqWsQYH0uUmvIl0Ye6RaebWRh38eB6ToOkDnQucTYRGFOgtig0gSxlwCszTilYFz3n1ZuY762x+kDR3A==" + }, + "NLog": { + "type": "Transitive", + "resolved": "4.7.10", + "contentHash": "rcegW7kYOCjl7wX0SzsqpPBqnJ51JKi1WkYb6QBVX0Wc5IgH19Pv4t/co+T0s06OS0Ne44xgkY/mHg0PdrmJow==" + }, + "Splat": { + "type": "Transitive", + "resolved": "1.6.2", + "contentHash": "DeH0MxPU+D4JchkIDPYG4vUT+hsWs9S41cFle0/4K5EJMXWurx5DzAkj2366DfK14/XKNhsu6tCl4dZXJ3CD4w==" + }, + "squirrel.windows": { + "type": "Transitive", + "resolved": "1.5.2", + "contentHash": "89Y/CFxWm7SEOjvuV2stVa8p+SNM9GOLk4tUNm2nUF792nfkimAgwRA/umVsdyd/OXBH8byXSh4V1qck88ZAyQ==", + "dependencies": { + "DeltaCompressionDotNet": "[1.0.0, 2.0.0)", + "Mono.Cecil": "0.9.6.1", + "Splat": "1.6.2" + } + }, + "StreamJsonRpc": { + "type": "Transitive", + "resolved": "2.20.20", + "contentHash": "gwG7KViLbSWS7EI0kYevinVmIga9wZNrpSY/FnWyC6DbdjKJ1xlv/FV1L9b0rLkVP8cGxfIMexdvo/+2W5eq6Q==", + "dependencies": { + "MessagePack": "2.5.187", + "Microsoft.VisualStudio.Threading": "17.10.48", + "Microsoft.VisualStudio.Threading.Analyzers": "17.10.48", + "Microsoft.VisualStudio.Validation": "17.8.8", + "Nerdbank.Streams": "2.11.74", + "Newtonsoft.Json": "13.0.1", + "System.IO.Pipelines": "8.0.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "7.0.1", + "contentHash": "T9SLFxzDp0SreCffRDXSAS5G+lq6E8qP4knHS2IBjwCdx2KEvGnGZsq7gFpselYOda7l6gXsJMD93TQsFj/URA==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "9.0.2", + "contentHash": "JU947wzf8JbBS16Y5EIZzAlyQU+k68D7LRx6y03s2wlhlvLqkt/8uPBrjv2hJnnaJKbdb0GhQ3JZsfYXhrRjyg==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "9.0.2" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA==" + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "DaGSsVqKsn/ia6RG8frjwmJonfos0srquhw09TlT8KRw5I43E+4gs+/bZj4K0vShJ5H9imCuXupb4RmS+dBy3w==", + "dependencies": { + "System.Text.Encodings.Web": "7.0.0" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "ToolGood.Words.Pinyin": { + "type": "Transitive", + "resolved": "3.0.1.4", + "contentHash": "uQo97618y9yzLDxrnehPN+/tuiOlk5BqieEdwctHZOAS9miMXnHKgMFYVw8CSGXRglyTYXlrW7qtUlU7Fje5Ew==" + }, + "YamlDotNet": { + "type": "Transitive", + "resolved": "9.1.0", + "contentHash": "fuvGXU4Ec5HrsmEc+BiFTNPCRf1cGBI2kh/3RzMWgddM2M4ALhbSPoI3X3mhXZUD1qqQd9oSkFAtWjpz8z9eRg==" + }, + "flow.launcher.core": { + "type": "Project", + "dependencies": { + "Droplex": "[1.7.0, )", + "FSharp.Core": "[9.0.101, )", + "Flow.Launcher.Infrastructure": "[1.0.0, )", + "Flow.Launcher.Plugin": "[4.4.0, )", + "Meziantou.Framework.Win32.Jobs": "[3.4.0, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", + "StreamJsonRpc": "[2.20.20, )", + "squirrel.windows": "[1.5.2, )" + } + }, + "flow.launcher.infrastructure": { + "type": "Project", + "dependencies": { + "Ben.Demystifier": "[0.4.1, )", + "BitFaster.Caching": "[2.5.3, )", + "CommunityToolkit.Mvvm": "[8.4.0, )", + "Flow.Launcher.Plugin": "[4.4.0, )", + "MemoryPack": "[1.21.3, )", + "Microsoft.VisualStudio.Threading": "[17.12.19, )", + "NLog": "[4.7.10, )", + "PropertyChanged.Fody": "[3.4.0, )", + "System.Drawing.Common": "[9.0.2, )", + "ToolGood.Words.Pinyin": "[3.0.1.4, )" + } + }, + "flow.launcher.plugin": { + "type": "Project", + "dependencies": { + "JetBrains.Annotations": "[2024.3.0, )", + "PropertyChanged.Fody": "[3.4.0, )" + } + } + } + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj index 7b2fb47f8..90d356caa 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows + net9.0-windows true {9B130CC5-14FB-41FF-B310-0A95B6894C37} Properties diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml index 22830e7c8..564714173 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml @@ -7,6 +7,9 @@ Browser Bookmarks Search your browser bookmarks + + Failed to set url in clipboard + Bookmark Data Open bookmarks in: diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index 91ade206b..b1600862e 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -223,11 +223,8 @@ public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContex } catch (Exception e) { - var message = "Failed to set url in clipboard"; - _context.API.LogException(ClassName, message, e); - - _context.API.ShowMsg(message); - + _context.API.LogException(ClassName, "Failed to set url in clipboard", e); + _context.API.ShowMsgError(_context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copy_failed")); return false; } }, diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj index 9cdef365d..e1aca4f5e 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Flow.Launcher.Plugin.Calculator.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows + net9.0-windows {59BD9891-3837-438A-958D-ADC7F91F6F7E} Properties Flow.Launcher.Plugin.Calculator diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml index 0e0911a70..e646bab0e 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Languages/en.xaml @@ -1,6 +1,7 @@ - + Calculator Allows to do mathematical calculations.(Try 5*3-2 in Flow Launcher) @@ -13,4 +14,5 @@ Comma (,) Dot (.) Max. decimal places + Copy failed, please try later \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index eb3c808e7..3d06c4ce0 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -100,7 +100,7 @@ namespace Flow.Launcher.Plugin.Calculator } catch (ExternalException) { - Context.API.ShowMsgBox("Copy failed, please try later"); + Context.API.ShowMsgBox(Context.API.GetTranslation("flowlauncher_plugin_calculator_failed_to_copy")); return false; } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs index c331c4985..c18abb3a2 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs @@ -132,9 +132,8 @@ namespace Flow.Launcher.Plugin.Explorer } catch (Exception e) { - var message = "Fail to set text in clipboard"; - LogException(message, e); - Context.API.ShowMsg(message); + LogException("Fail to set text in clipboard", e); + Context.API.ShowMsgError(Context.API.GetTranslation("plugin_explorer_fail_to_set_text")); return false; } }, @@ -155,9 +154,8 @@ namespace Flow.Launcher.Plugin.Explorer } catch (Exception e) { - var message = "Fail to set text in clipboard"; - LogException(message, e); - Context.API.ShowMsg(message); + LogException("Fail to set text in clipboard", e); + Context.API.ShowMsgError(Context.API.GetTranslation("plugin_explorer_fail_to_set_text")); return false; } }, @@ -178,9 +176,8 @@ namespace Flow.Launcher.Plugin.Explorer } catch (Exception e) { - var message = $"Fail to set file/folder in clipboard"; - LogException(message, e); - Context.API.ShowMsg(message); + LogException($"Fail to set file/folder in clipboard", e); + Context.API.ShowMsgError(Context.API.GetTranslation("plugin_explorer_fail_to_set_files")); return false; } @@ -221,9 +218,8 @@ namespace Flow.Launcher.Plugin.Explorer } catch (Exception e) { - var message = $"Fail to delete {record.FullPath}"; - LogException(message, e); - Context.API.ShowMsgError(message); + LogException($"Fail to delete {record.FullPath}", e); + Context.API.ShowMsgError(string.Format(Context.API.GetTranslation("plugin_explorer_fail_to_delete"), record.FullPath)); return false; } @@ -265,9 +261,9 @@ namespace Flow.Launcher.Plugin.Explorer } catch (FileNotFoundException e) { - var name = "Plugin: Folder"; - var message = $"File not found: {e.Message}"; - Context.API.ShowMsgError(name, message); + Context.API.ShowMsgError( + Context.API.GetTranslation("plugin_explorer_plugin_name"), + string.Format(Context.API.GetTranslation("plugin_explorer_file_not_found"), e.Message)); return false; } @@ -334,9 +330,8 @@ namespace Flow.Launcher.Plugin.Explorer } catch (Exception e) { - var message = $"Fail to open file at {record.FullPath}"; - LogException(message, e); - Context.API.ShowMsgError(message); + LogException($"Fail to open file at {record.FullPath}", e); + Context.API.ShowMsgError(string.Format(Context.API.GetTranslation("plugin_explorer_fail_to_open"), record.FullPath)); return false; } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj index 93691814a..ea0803978 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows + net9.0-windows true true true @@ -48,7 +48,7 @@ - + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index 2e0f6a67d..a2e26a0f0 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -7,6 +7,8 @@ Please make a selection first Please select a folder path. Please choose a different name or folder path. + Are you sure you want to delete this quick access link? + Are you sure you want to delete this index search excluded path? Please select a folder link Are you sure you want to delete {0}? Are you sure you want to permanently delete this file? @@ -130,6 +132,11 @@ Show Windows Context Menu Open With Select a program to open with + Fail to delete {0} + File not found: {0} + Fail to open {0} + Fail to set text in clipboard + Fail to set files/folders in clipboard {0} free of {1} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index f1aea98b4..fbaefa9d6 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -1,4 +1,4 @@ -using Flow.Launcher.Plugin.Explorer.Helper; +using Flow.Launcher.Plugin.Explorer.Helper; using Flow.Launcher.Plugin.Explorer.Search; using Flow.Launcher.Plugin.Explorer.Search.Everything; using Flow.Launcher.Plugin.Explorer.ViewModels; @@ -10,10 +10,11 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; using Flow.Launcher.Plugin.Explorer.Exceptions; +using System.Linq; namespace Flow.Launcher.Plugin.Explorer { - public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n + public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n, IAsyncDialogJump { internal static PluginInitContext Context { get; set; } @@ -25,6 +26,8 @@ namespace Flow.Launcher.Plugin.Explorer private SearchManager searchManager; + private static readonly List _emptyDialogJumpResultList = new(); + public Control CreateSettingPanel() { return new ExplorerSettings(viewModel); @@ -108,5 +111,18 @@ namespace Flow.Launcher.Plugin.Explorer } } } + + public async Task> QueryDialogJumpAsync(Query query, CancellationToken token) + { + try + { + var results = await searchManager.SearchAsync(query, token); + return results.Select(r => DialogJumpResult.From(r, r.CopyText)).ToList(); + } + catch (Exception e) when (e is SearchException or EngineNotAvailableException) + { + return _emptyDialogJumpResultList; + } + } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index 7791a9881..cbf6f1f8b 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -283,15 +283,16 @@ namespace Flow.Launcher.Plugin.Explorer.Search internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) { - bool isMedia = IsMedia(Path.GetExtension(filePath)); - var title = Path.GetFileName(filePath); + var isMedia = IsMedia(Path.GetExtension(filePath)); + var title = Path.GetFileName(filePath) ?? string.Empty; + var directory = Path.GetDirectoryName(filePath) ?? string.Empty; /* Preview Detail */ var result = new Result { Title = title, - SubTitle = Path.GetDirectoryName(filePath), + SubTitle = directory, IcoPath = filePath, Preview = new Result.PreviewInfo { @@ -315,7 +316,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { if (c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift)) { - OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty, true); + OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty, true); } else if (c.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control) { @@ -323,7 +324,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search } else { - OpenFile(filePath, Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty); + OpenFile(filePath, Settings.UseLocationAsWorkingDir ? directory : string.Empty); } } catch (Exception ex) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index 5aa6a13be..90232ba08 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -431,10 +431,24 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels { case "QuickAccessLink": if (SelectedQuickAccessLink == null) return; + if (Context.API.ShowMsgBox( + Context.API.GetTranslation("plugin_explorer_delete_quick_access_link"), + Context.API.GetTranslation("plugin_explorer_delete"), + MessageBoxButton.OKCancel, + MessageBoxImage.Warning) + == MessageBoxResult.Cancel) + return; Settings.QuickAccessLinks.Remove(SelectedQuickAccessLink); break; case "IndexSearchExcludedPaths": if (SelectedIndexSearchExcludedPath == null) return; + if (Context.API.ShowMsgBox( + Context.API.GetTranslation("plugin_explorer_delete_index_search_excluded_path"), + Context.API.GetTranslation("plugin_explorer_delete"), + MessageBoxButton.OKCancel, + MessageBoxImage.Warning) + == MessageBoxResult.Cancel) + return; Settings.IndexSearchExcludedSubdirectoryPaths.Remove(SelectedIndexSearchExcludedPath); break; } diff --git a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj index 1e662de9e..d8db0abe1 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj +++ b/Plugins/Flow.Launcher.Plugin.PluginIndicator/Flow.Launcher.Plugin.PluginIndicator.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows + net9.0-windows {FDED22C8-B637-42E8-824A-63B5B6E05A3A} Properties Flow.Launcher.Plugin.PluginIndicator diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj index 8ff41a7ad..6abc1a580 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Flow.Launcher.Plugin.PluginsManager.csproj @@ -1,7 +1,7 @@  Library - net7.0-windows + net9.0-windows true true true diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj index 0c501b2d9..cd310fb35 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows + net9.0-windows Flow.Launcher.Plugin.ProcessKiller Flow.Launcher.Plugin.ProcessKiller Flow-Launcher diff --git a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj index 99c1a12e9..0c45a8590 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj +++ b/Plugins/Flow.Launcher.Plugin.Program/Flow.Launcher.Plugin.Program.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows10.0.19041.0 + net9.0-windows10.0.19041.0 {FDB3555B-58EF-4AE6-B5F1-904719637AB4} Properties Flow.Launcher.Plugin.Program diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index fd687bfae..73d893858 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -446,7 +446,7 @@ namespace Flow.Launcher.Plugin.Program var title = Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error"); var message = string.Format(Context.API.GetTranslation("flowlauncher_plugin_program_run_failed"), info.FileName); - Context.API.ShowMsg(title, string.Format(message, info.FileName), string.Empty); + Context.API.ShowMsgError(title, message); } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs index 28f774333..9a8326e9a 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs @@ -462,7 +462,7 @@ namespace Flow.Launcher.Plugin.Program.Programs var message = api.GetTranslation( "flowlauncher_plugin_program_run_as_administrator_not_supported_message"); - api.ShowMsg(title, message, string.Empty); + api.ShowMsgError(title, message); } return true; diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj b/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj index c7ea7cdd5..5c3475133 100644 --- a/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj +++ b/Plugins/Flow.Launcher.Plugin.Shell/Flow.Launcher.Plugin.Shell.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows + net9.0-windows {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} Properties Flow.Launcher.Plugin.Shell diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml index 645a0e14f..2fb9c6b67 100644 --- a/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml @@ -1,6 +1,7 @@ - + Replace Win+R Close Command Prompt after pressing any key @@ -16,4 +17,6 @@ Run As Administrator Copy the command Only show number of most used commands: + Command not found: {0} + Error running the command: {0} diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Main.cs b/Plugins/Flow.Launcher.Plugin.Shell/Main.cs index a51aadec7..888009976 100644 --- a/Plugins/Flow.Launcher.Plugin.Shell/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Shell/Main.cs @@ -335,15 +335,17 @@ namespace Flow.Launcher.Plugin.Shell } catch (FileNotFoundException e) { - var name = "Plugin: Shell"; - var message = $"Command not found: {e.Message}"; - Context.API.ShowMsg(name, message); + Context.API.ShowMsgError(GetTranslatedPluginTitle(), + string.Format(Context.API.GetTranslation("flowlauncher_plugin_cmd_command_not_found"), e.Message)); } catch (Win32Exception e) { - var name = "Plugin: Shell"; - var message = $"Error running the command: {e.Message}"; - Context.API.ShowMsg(name, message); + Context.API.ShowMsgError(GetTranslatedPluginTitle(), + string.Format(Context.API.GetTranslation("flowlauncher_plugin_cmd_error_running_command"), e.Message)); + } + catch (Exception e) + { + Context.API.LogException(ClassName, $"Error executing command: {info.FileName} {string.Join(" ", info.ArgumentList)}", e); } } diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj index dbc36ad42..1e2deb558 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj +++ b/Plugins/Flow.Launcher.Plugin.Sys/Flow.Launcher.Plugin.Sys.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows + net9.0-windows {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} Properties Flow.Launcher.Plugin.Sys @@ -37,7 +37,6 @@ - diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Sys/Languages/en.xaml index ad3f8553b..56899eef3 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Sys/Languages/en.xaml @@ -63,6 +63,8 @@ Are you sure you want to restart the computer? Are you sure you want to restart the computer with Advanced Boot Options? Are you sure you want to log off? + Error + Failed to empty the recycle bin. This might happen if:{0}- Some items are currently in use{0}- Some items can't be deleted due to permissions{0}Please close any applications that might be using these files and try again. Command Keyword Setting Custom Command Keyword diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs index 39bf49654..77278a054 100644 --- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs @@ -5,8 +5,6 @@ using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using System.Windows; -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.UserSettings; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Security; @@ -52,6 +50,8 @@ namespace Flow.Launcher.Plugin.Sys private const SHUTDOWN_REASON REASON = SHUTDOWN_REASON.SHTDN_REASON_MAJOR_OTHER | SHUTDOWN_REASON.SHTDN_REASON_FLAG_PLANNED; + private const string Documentation = "https://flowlauncher.com/docs/#/usage-tips"; + private PluginInitContext _context; private Settings _settings; private ThemeSelector _themeSelector; @@ -70,13 +70,19 @@ namespace Flow.Launcher.Plugin.Sys return _themeSelector.Query(query); } - var commands = Commands(); + var commands = Commands(query); var results = new List(); + var isEmptyQuery = string.IsNullOrWhiteSpace(query.Search); foreach (var c in commands) { var command = _settings.Commands.First(x => x.Key == c.Title); c.Title = command.Name; c.SubTitle = command.Description; + if (isEmptyQuery) + { + results.Add(c); + continue; + } // Match from localized title & localized subtitle & keyword var titleMatch = _context.API.FuzzySearch(query.Search, c.Title); @@ -188,7 +194,7 @@ namespace Flow.Launcher.Plugin.Sys } } - private List Commands() + private List Commands(Query query) { var results = new List(); var recycleBinFolder = "shell:RecycleBinFolder"; @@ -332,11 +338,9 @@ namespace Flow.Launcher.Plugin.Sys var result = PInvoke.SHEmptyRecycleBin(new(), string.Empty, 0); if (result != HRESULT.S_OK && result != HRESULT.E_UNEXPECTED) { - _context.API.ShowMsgBox("Failed to empty the recycle bin. This might happen if:\n" + - "- A file in the recycle bin is in use\n" + - "- You don't have permission to delete some items\n" + - "Please close any applications that might be using these files and try again.", - "Error", + _context.API.ShowMsgBox( + string.Format(_context.API.GetTranslation("flowlauncher_plugin_sys_dlgtext_empty_recycle_bin_failed"), Environment.NewLine), + _context.API.GetTranslation("flowlauncher_plugin_sys_dlgtitle_error"), MessageBoxButton.OK, MessageBoxImage.Error); } @@ -398,6 +402,8 @@ namespace Flow.Launcher.Plugin.Sys IcoPath = "Images\\app.png", Action = c => { + // Hide the window first then open setting dialog because main window can be topmost window which will still display on top of the setting dialog for a while + _context.API.HideMainWindow(); _context.API.OpenSettingDialog(); return true; } @@ -439,11 +445,11 @@ namespace Flow.Launcher.Plugin.Sys Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf12b"), Title = "Open Log Location", IcoPath = "Images\\app.png", - CopyText = DataLocation.VersionLogDirectory, - AutoCompleteText = DataLocation.VersionLogDirectory, + CopyText = _context.API.GetLogDirectory(), + AutoCompleteText = _context.API.GetLogDirectory(), Action = c => { - _context.API.OpenDirectory(DataLocation.VersionLogDirectory); + _context.API.OpenDirectory(_context.API.GetLogDirectory()); return true; } }, @@ -452,11 +458,11 @@ namespace Flow.Launcher.Plugin.Sys Title = "Flow Launcher Tips", Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe897"), IcoPath = "Images\\app.png", - CopyText = Constant.Documentation, - AutoCompleteText = Constant.Documentation, + CopyText = Documentation, + AutoCompleteText = Documentation, Action = c => { - _context.API.OpenUrl(Constant.Documentation); + _context.API.OpenUrl(Documentation); return true; } }, @@ -465,11 +471,11 @@ namespace Flow.Launcher.Plugin.Sys Title = "Flow Launcher UserData Folder", Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xf12b"), IcoPath = "Images\\app.png", - CopyText = DataLocation.DataDirectory(), - AutoCompleteText = DataLocation.DataDirectory(), + CopyText = _context.API.GetDataDirectory(), + AutoCompleteText = _context.API.GetDataDirectory(), Action = c => { - _context.API.OpenDirectory(DataLocation.DataDirectory()); + _context.API.OpenDirectory(_context.API.GetDataDirectory()); return true; } }, @@ -491,7 +497,15 @@ namespace Flow.Launcher.Plugin.Sys Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\ue790"), Action = c => { - _context.API.ChangeQuery($"{ThemeSelector.Keyword} "); + if (string.IsNullOrEmpty(query.ActionKeyword)) + { + _context.API.ChangeQuery($"{ThemeSelector.Keyword}{Plugin.Query.ActionKeywordSeparator}"); + } + else + { + _context.API.ChangeQuery($"{query.ActionKeyword}{Plugin.Query.ActionKeywordSeparator}{ThemeSelector.Keyword}{Plugin.Query.ActionKeywordSeparator}"); + + } return false; } } diff --git a/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj b/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj index 6d338733e..fdfe03224 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj +++ b/Plugins/Flow.Launcher.Plugin.Url/Flow.Launcher.Plugin.Url.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows + net9.0-windows {A3DCCBCA-ACC1-421D-B16E-210896234C26} true Properties diff --git a/Plugins/Flow.Launcher.Plugin.Url/Main.cs b/Plugins/Flow.Launcher.Plugin.Url/Main.cs index 03516636d..9fa52c8da 100644 --- a/Plugins/Flow.Launcher.Plugin.Url/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Url/Main.cs @@ -70,7 +70,7 @@ namespace Flow.Launcher.Plugin.Url } catch(Exception) { - context.API.ShowMsg(string.Format(context.API.GetTranslation("flowlauncher_plugin_url_cannot_open_url"), raw)); + context.API.ShowMsgError(string.Format(context.API.GetTranslation("flowlauncher_plugin_url_cannot_open_url"), raw)); return false; } } diff --git a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj index 73726ab37..42176376b 100644 --- a/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj +++ b/Plugins/Flow.Launcher.Plugin.WebSearch/Flow.Launcher.Plugin.WebSearch.csproj @@ -2,7 +2,7 @@ Library - net7.0-windows + net9.0-windows {403B57F2-1856-4FC7-8A24-36AB346B763E} Properties true diff --git a/Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj b/Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj index 73fcd9f83..879cea6f8 100644 --- a/Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj +++ b/Plugins/Flow.Launcher.Plugin.WindowsSettings/Flow.Launcher.Plugin.WindowsSettings.csproj @@ -1,7 +1,7 @@  Library - net7.0-windows + net9.0-windows true true false diff --git a/Scripts/flowlauncher.nuspec b/Scripts/flowlauncher.nuspec index 8d753bc8c..fa12150cc 100644 --- a/Scripts/flowlauncher.nuspec +++ b/Scripts/flowlauncher.nuspec @@ -11,6 +11,6 @@ Flow Launcher - Quick file search and app launcher for Windows with community-made plugins - + diff --git a/Scripts/post_build.ps1 b/Scripts/post_build.ps1 index e54852d32..8d2d14a80 100644 --- a/Scripts/post_build.ps1 +++ b/Scripts/post_build.ps1 @@ -99,7 +99,7 @@ function Pack-Squirrel-Installer ($path, $version, $output) { function Publish-Self-Contained ($p) { $csproj = Join-Path "$p" "Flow.Launcher/Flow.Launcher.csproj" -Resolve - $profile = Join-Path "$p" "Flow.Launcher/Properties/PublishProfiles/Net7.0-SelfContained.pubxml" -Resolve + $profile = Join-Path "$p" "Flow.Launcher/Properties/PublishProfiles/Net9.0-SelfContained.pubxml" -Resolve # we call dotnet publish on the main project. # The other projects should have been built in Release at this point. diff --git a/appveyor.yml b/appveyor.yml index 39e2a114c..a4a8e6f16 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -76,7 +76,7 @@ deploy: This build includes new changes from commit: $(APPVEYOR_REPO_COMMIT_MESSAGE) - See all changes in this early access by going to the [milstones](https://github.com/Flow-Launcher/Flow.Launcher/milestones?sort=title&direction=asc) section and choosing the upcoming milestone. + See all changes in this early access by going to the [milestones](https://github.com/Flow-Launcher/Flow.Launcher/milestones?sort=title&direction=asc) section and choosing the upcoming milestone. For latest production release visit [here](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest) Please report any bugs or issues over at the [main repository](https://github.com/Flow-Launcher/Flow.Launcher/issues)' diff --git a/global.json b/global.json index 1ee79d7a2..44db98b24 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.*", + "version": "9.0.*", "rollForward": "latestPatch" } }