From 6e4a2898abee1bd912e0fc09663d56761dba1b0f Mon Sep 17 00:00:00 2001 From: Heiko <61519853+htcfreek@users.noreply.github.com> Date: Mon, 13 Feb 2023 17:30:18 +0100 Subject: [PATCH] [PT Run] Localized file paths (Part 1): Update helper class and Program plugin (#20024) * make helper non-static and add cache * uwp app: add localized path * Win32Program: Rename variable * spell fix * Win32Program: Localized paths * fix invalid var name * spell fix * fix build * test new shell localization helper * fixes * fix crash * replace old helper class * replace old helper class 2 * Helper improvements * last changes * add docs info * remove left-over * remove second left-over --- .../modules/launcher/plugins/program.md | 1 + .../Programs/Win32Tests.cs | 52 ++++++------ .../Storage/Win32ProgramRepositoryTest.cs | 2 +- .../Plugins/Microsoft.Plugin.Program/Main.cs | 3 + .../Microsoft.Plugin.Program/Programs/UWP.cs | 4 + .../Programs/UWPApplication.cs | 5 +- .../Programs/Win32Program.cs | 57 ++++++++----- .../Storage/Win32ProgramRepository.cs | 6 +- .../Common/Interfaces/IShellItem.cs | 46 +++++++++++ .../Wox.Plugin/Common/ShellLocalization.cs | 80 +++++++++---------- .../Wox.Plugin/Common/Win32/NativeMethods.cs | 17 ++++ 11 files changed, 178 insertions(+), 95 deletions(-) create mode 100644 src/modules/launcher/Wox.Plugin/Common/Interfaces/IShellItem.cs diff --git a/doc/devdocs/modules/launcher/plugins/program.md b/doc/devdocs/modules/launcher/plugins/program.md index cc370258ec..1e99e1d44d 100644 --- a/doc/devdocs/modules/launcher/plugins/program.md +++ b/doc/devdocs/modules/launcher/plugins/program.md @@ -41,3 +41,4 @@ There are broadly two different categories of applications: ### Additional Notes - Arguments can be provided to the program plugin by entering them after `--` (a double dash). +- The localization is done using the `Localization Helper`from `Wox.Plugin.Common` hosted at runtime in a variable of plugin's main class. diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs index 4456928c48..0a543b3e7d 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Programs/Win32Tests.cs @@ -22,7 +22,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "Imaging Devices", ExecutableName = "imagingdevices.exe", FullPath = "c:\\program files\\windows photo viewer\\imagingdevices.exe", - LnkResolvedPath = null, + LnkFilePath = null, AppType = Win32Program.ApplicationType.Win32Application, }; @@ -31,7 +31,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "Notepad", ExecutableName = "notepad.exe", FullPath = "c:\\windows\\system32\\notepad.exe", - LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk", + LnkFilePath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk", AppType = Win32Program.ApplicationType.Win32Application, }; @@ -40,7 +40,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "Notepad", ExecutableName = "notepad.exe", FullPath = "c:\\windows\\system32\\notepad.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk", + LnkFilePath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\notepad.lnk", AppType = Win32Program.ApplicationType.Win32Application, }; @@ -50,7 +50,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs ExecutableName = "cmd.exe", FullPath = "c:\\windows\\system32\\cmd.exe", Arguments = @"/E:ON /V:ON /K ""C:\Program Files\Microsoft SDKs\Azure\.NET SDK\v2.9\\bin\setenv.cmd""", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft azure\\microsoft azure sdk for .net\\v2.9\\microsoft azure command prompt - v2.9.lnk", + LnkFilePath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft azure\\microsoft azure sdk for .net\\v2.9\\microsoft azure command prompt - v2.9.lnk", AppType = Win32Program.ApplicationType.Win32Application, }; @@ -60,7 +60,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs ExecutableName = "cmd.exe", FullPath = "c:\\windows\\system32\\cmd.exe", Arguments = @"/k ""C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat""", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\visual studio 2019\\visual studio tools\\vc\\x64 native tools command prompt for vs 2019.lnk", + LnkFilePath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\visual studio 2019\\visual studio tools\\vc\\x64 native tools command prompt for vs 2019.lnk", AppType = Win32Program.ApplicationType.Win32Application, }; @@ -69,7 +69,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "Command Prompt", ExecutableName = "cmd.exe", FullPath = "c:\\windows\\system32\\cmd.exe", - LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\command prompt.lnk", + LnkFilePath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\command prompt.lnk", AppType = Win32Program.ApplicationType.Win32Application, }; @@ -78,7 +78,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "File Explorer", ExecutableName = "File Explorer.lnk", FullPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\system tools\\file explorer.lnk", - LnkResolvedPath = null, + LnkFilePath = null, AppType = Win32Program.ApplicationType.Win32Application, }; @@ -87,7 +87,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "File Explorer", ExecutableName = "explorer.exe", FullPath = "c:\\windows\\explorer.exe", - LnkResolvedPath = null, + LnkFilePath = null, AppType = Win32Program.ApplicationType.Win32Application, }; @@ -96,7 +96,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "Wordpad", ExecutableName = "wordpad.exe", FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\wordpad.lnk", + LnkFilePath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\accessories\\wordpad.lnk", AppType = Win32Program.ApplicationType.Win32Application, }; @@ -105,7 +105,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "WORDPAD", ExecutableName = "WORDPAD.EXE", FullPath = "c:\\program files\\windows nt\\accessories\\wordpad.exe", - LnkResolvedPath = null, + LnkFilePath = null, AppType = Win32Program.ApplicationType.Win32Application, }; @@ -113,7 +113,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs { Name = "Twitter", FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome_proxy.exe", - LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\chrome apps\\twitter.lnk", + LnkFilePath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\chrome apps\\twitter.lnk", Arguments = " --profile-directory=Default --app-id=jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi", AppType = 0, }; @@ -122,7 +122,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs { Name = "Web page", FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe", - LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\web page.lnk", + LnkFilePath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\web page.lnk", Arguments = "--profile-directory=Default --app-id=homljgmgpmcbpjbnjpfijnhipfkiclkd", AppType = 0, }; @@ -131,7 +131,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs { Name = "edge - Bing", FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge_proxy.exe", - LnkResolvedPath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\edge - bing.lnk", + LnkFilePath = "c:\\users\\powertoys\\appdata\\roaming\\microsoft\\windows\\start menu\\programs\\edge - bing.lnk", Arguments = " --profile-directory=Default --app-id=aocfnapldcnfbofgmbbllojgocaelgdd", AppType = 0, }; @@ -141,7 +141,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "Microsoft Edge", ExecutableName = "msedge.exe", FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\msedge.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft edge.lnk", + LnkFilePath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\microsoft edge.lnk", AppType = Win32Program.ApplicationType.Win32Application, }; @@ -150,7 +150,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "Google Chrome", ExecutableName = "chrome.exe", FullPath = "c:\\program files (x86)\\google\\chrome\\application\\chrome.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\google chrome.lnk", + LnkFilePath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\google chrome.lnk", AppType = Win32Program.ApplicationType.Win32Application, }; @@ -159,7 +159,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "Proxy App", ExecutableName = "test_proxy.exe", FullPath = "c:\\program files (x86)\\microsoft\\edge\\application\\test_proxy.exe", - LnkResolvedPath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\test proxy.lnk", + LnkFilePath = "c:\\programdata\\microsoft\\windows\\start menu\\programs\\test proxy.lnk", AppType = Win32Program.ApplicationType.Win32Application, }; @@ -168,7 +168,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Name = "cmd", ExecutableName = "cmd.exe", FullPath = "c:\\windows\\system32\\cmd.exe", - LnkResolvedPath = null, + LnkFilePath = null, AppType = Win32Program.ApplicationType.RunCommand, // Run command }; @@ -178,7 +178,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs Description = "Cmder: Lovely Console Emulator", ExecutableName = "Cmder.exe", FullPath = "c:\\tools\\cmder\\cmder.exe", - LnkResolvedPath = null, + LnkFilePath = null, AppType = Win32Program.ApplicationType.RunCommand, // Run command }; @@ -188,7 +188,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs ExecutableName = "Shop Titans.url", FullPath = "steam://rungameid/1258080", ParentDirectory = "C:\\Users\\temp\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Steam", - LnkResolvedPath = null, + LnkFilePath = null, AppType = Win32Program.ApplicationType.InternetShortcutApplication, }; @@ -198,7 +198,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs ExecutableName = "Shop Titans.url", FullPath = "steam://rungameid/1258080", ParentDirectory = "C:\\Users\\temp\\Desktop", - LnkResolvedPath = null, + LnkFilePath = null, AppType = Win32Program.ApplicationType.InternetShortcutApplication, }; @@ -208,7 +208,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs ExecutableName = "dummy.appref-ms", FullPath = "C:\\dummy.appref-ms", ParentDirectory = "C:\\", - LnkResolvedPath = null, + LnkFilePath = null, AppType = Win32Program.ApplicationType.ApprefApplication, }; @@ -218,7 +218,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs ExecutableName = "application.lnk", FullPath = "C:\\application.lnk", ParentDirectory = "C:\\", - LnkResolvedPath = "C:\\application.lnk", + LnkFilePath = "C:\\application.lnk", AppType = Win32Program.ApplicationType.ShortcutApplication, }; @@ -228,7 +228,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs ExecutableName = "application.lnk", FullPath = "C:\\dummy\\folder", ParentDirectory = "C:\\dummy\\", - LnkResolvedPath = "C:\\tools\\application.lnk", + LnkFilePath = "C:\\tools\\application.lnk", AppType = Win32Program.ApplicationType.Folder, }; @@ -238,7 +238,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs ExecutableName = "application.lnk", FullPath = "C:\\dummy\\file.pdf", ParentDirectory = "C:\\dummy\\", - LnkResolvedPath = "C:\\tools\\application.lnk", + LnkFilePath = "C:\\tools\\application.lnk", AppType = Win32Program.ApplicationType.GenericFile, }; @@ -303,7 +303,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs } [TestMethod] - public void DedupFunctionMustRemoveDuplicatesForExeExtensionsWithoutLnkResolvedPath() + public void DedupFunctionMustRemoveDuplicatesForExeExtensionsWithoutLnkFilePath() { // Arrange List prgms = new List @@ -317,7 +317,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Programs // Assert Assert.AreEqual(1, apps.Count); - Assert.IsTrue(!string.IsNullOrEmpty(apps[0].LnkResolvedPath)); + Assert.IsTrue(!string.IsNullOrEmpty(apps[0].LnkFilePath)); } [TestMethod] diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs index 5ae07c55ad..0d4aafb094 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program.UnitTests/Storage/Win32ProgramRepositoryTest.cs @@ -331,7 +331,7 @@ namespace Microsoft.Plugin.Program.UnitTests.Storage ExecutableName = "path.exe", ParentDirectory = "directory", FullPath = "directory\\path.exe", - LnkResolvedPath = "directory\\path.lnk", // This must be equal for lnk applications + LnkFilePath = "directory\\path.lnk", // This must be equal for lnk applications }; win32ProgramRepository.Add(item); diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs index 2aeb1c3f87..146ee7556c 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Main.cs @@ -13,6 +13,7 @@ using Microsoft.Plugin.Program.Programs; using Microsoft.Plugin.Program.Storage; using Wox.Infrastructure.Storage; using Wox.Plugin; +using Wox.Plugin.Common; using Stopwatch = Wox.Infrastructure.Stopwatch; namespace Microsoft.Plugin.Program @@ -30,6 +31,8 @@ namespace Microsoft.Plugin.Program internal static ProgramPluginSettings Settings { get; set; } + internal static readonly ShellLocalization ShellLocalizationHelper = new(); + public string Name => Properties.Resources.wox_plugin_program_plugin_name; public string Description => Properties.Resources.wox_plugin_program_plugin_description; diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs index ec57218705..22b8326c68 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWP.cs @@ -36,6 +36,9 @@ namespace Microsoft.Plugin.Program.Programs public string Location { get; set; } + // Localized path based on windows display language + public string LocationLocalized { get; set; } + public IList Apps { get; private set; } public PackageVersion Version { get; set; } @@ -57,6 +60,7 @@ namespace Microsoft.Plugin.Program.Programs public void InitializeAppInfo(string installedLocation) { Location = installedLocation; + LocationLocalized = Main.ShellLocalizationHelper.GetLocalizedPath(installedLocation); var path = Path.Combine(installedLocation, "AppxManifest.xml"); var namespaces = XmlNamespaces(path); diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWPApplication.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWPApplication.cs index 01e1694e23..04316a3a91 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWPApplication.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/UWPApplication.cs @@ -55,6 +55,9 @@ namespace Microsoft.Plugin.Program.Programs public string Location => Package.Location; + // Localized path based on windows display language + public string LocationLocalized => Package.LocationLocalized; + public bool Enabled { get; set; } public bool CanRunElevated { get; set; } @@ -119,7 +122,7 @@ namespace Microsoft.Plugin.Program.Programs // Using CurrentCulture since this is user facing var toolTipTitle = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", Properties.Resources.powertoys_run_plugin_program_file_name, result.Title); - var toolTipText = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", Properties.Resources.powertoys_run_plugin_program_file_path, Package.Location); + var toolTipText = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", Properties.Resources.powertoys_run_plugin_program_file_path, LocationLocalized); result.ToolTipData = new ToolTipData(toolTipTitle, toolTipText); return result; diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32Program.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32Program.cs index f5322fb52a..b6f76cf3ec 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32Program.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Programs/Win32Program.cs @@ -20,7 +20,6 @@ using Microsoft.Win32; using Wox.Infrastructure; using Wox.Infrastructure.FileSystemHelper; using Wox.Plugin; -using Wox.Plugin.Common; using Wox.Plugin.Logger; using DirectoryWrapper = Wox.Infrastructure.FileSystemHelper.DirectoryWrapper; @@ -38,24 +37,35 @@ namespace Microsoft.Plugin.Program.Programs public string Name { get; set; } + // Localized name based on windows display language + public string NameLocalized { get; set; } = string.Empty; + public string UniqueIdentifier { get; set; } public string IcoPath { get; set; } + public string Description { get; set; } = string.Empty; + + // Path of app executable or lnk target executable public string FullPath { get; set; } - public string LnkResolvedPath { get; set; } - - public string LnkResolvedExecutableName { get; set; } - - // Localized name based on windows display language - public string LocalizedName { get; set; } = string.Empty; + // Localized path based on windows display language + public string FullPathLocalized { get; set; } = string.Empty; public string ParentDirectory { get; set; } public string ExecutableName { get; set; } - public string Description { get; set; } = string.Empty; + // Localized executable name based on windows display language + public string ExecutableNameLocalized { get; set; } = string.Empty; + + // Path to the lnk file on LnkProgram + public string LnkFilePath { get; set; } + + public string LnkResolvedExecutableName { get; set; } + + // Localized path based on windows display language + public string LnkResolvedExecutableNameLocalized { get; set; } = string.Empty; public bool Valid { get; set; } @@ -102,11 +112,13 @@ namespace Microsoft.Plugin.Program.Programs private int Score(string query) { var nameMatch = StringMatcher.FuzzySearch(query, Name); - var locNameMatch = StringMatcher.FuzzySearch(query, LocalizedName); + var locNameMatch = StringMatcher.FuzzySearch(query, NameLocalized); var descriptionMatch = StringMatcher.FuzzySearch(query, Description); var executableNameMatch = StringMatcher.FuzzySearch(query, ExecutableName); + var locExecutableNameMatch = StringMatcher.FuzzySearch(query, ExecutableNameLocalized); var lnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableName); - var score = new[] { nameMatch.Score, locNameMatch.Score, descriptionMatch.Score / 2, executableNameMatch.Score, lnkResolvedExecutableNameMatch.Score }.Max(); + var locLnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableNameLocalized); + var score = new[] { nameMatch.Score, locNameMatch.Score, descriptionMatch.Score / 2, executableNameMatch.Score, locExecutableNameMatch.Score, lnkResolvedExecutableNameMatch.Score, locLnkResolvedExecutableNameMatch.Score }.Max(); return score; } @@ -227,7 +239,7 @@ namespace Microsoft.Plugin.Program.Programs var result = new Result { // To set the title for the result to always be the name of the application - Title = !string.IsNullOrEmpty(LocalizedName) ? LocalizedName : Name, + Title = !string.IsNullOrEmpty(NameLocalized) ? NameLocalized : Name, SubTitle = GetSubtitle(), IcoPath = IcoPath, Score = score, @@ -247,7 +259,8 @@ namespace Microsoft.Plugin.Program.Programs // Using CurrentCulture since this is user facing var toolTipTitle = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", Properties.Resources.powertoys_run_plugin_program_file_name, result.Title); - var toolTipText = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", Properties.Resources.powertoys_run_plugin_program_file_path, FullPath); + string filePath = !string.IsNullOrEmpty(FullPathLocalized) ? FullPathLocalized : FullPath; + var toolTipText = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", Properties.Resources.powertoys_run_plugin_program_file_path, filePath); result.ToolTipData = new ToolTipData(toolTipTitle, toolTipText); return result; @@ -346,7 +359,7 @@ namespace Microsoft.Plugin.Program.Programs { return new ProcessStartInfo { - FileName = LnkResolvedPath ?? FullPath, + FileName = LnkFilePath ?? FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true, Arguments = programArguments, @@ -376,17 +389,19 @@ namespace Microsoft.Plugin.Program.Programs ExecutableName = Path.GetFileName(path), IcoPath = path, - // Localized name based on windows display language - LocalizedName = ShellLocalization.GetLocalizedName(path), - // Using InvariantCulture since this is user facing - FullPath = path.ToLowerInvariant(), + FullPath = path, UniqueIdentifier = path, ParentDirectory = Directory.GetParent(path).FullName, Description = string.Empty, Valid = true, Enabled = true, AppType = ApplicationType.Win32Application, + + // Localized name, path and executable based on windows display language + NameLocalized = Main.ShellLocalizationHelper.GetLocalizedName(path), + FullPathLocalized = Main.ShellLocalizationHelper.GetLocalizedPath(path), + ExecutableNameLocalized = Path.GetFileName(Main.ShellLocalizationHelper.GetLocalizedPath(path)), }; } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) @@ -462,7 +477,7 @@ namespace Microsoft.Plugin.Program.Programs Name = Path.GetFileNameWithoutExtension(path), ExecutableName = Path.GetFileName(path), IcoPath = iconPath, - FullPath = urlPath.ToLowerInvariant(), + FullPath = urlPath, UniqueIdentifier = path, ParentDirectory = Directory.GetParent(path).FullName, Valid = true, @@ -500,11 +515,13 @@ namespace Microsoft.Plugin.Program.Programs return InvalidProgram; } - program.LnkResolvedPath = program.FullPath; + program.LnkFilePath = program.FullPath; program.LnkResolvedExecutableName = Path.GetFileName(target); + program.LnkResolvedExecutableNameLocalized = Path.GetFileName(Main.ShellLocalizationHelper.GetLocalizedPath(target)); // Using CurrentCulture since this is user facing - program.FullPath = Path.GetFullPath(target).ToLowerInvariant(); + program.FullPath = Path.GetFullPath(target); + program.FullPathLocalized = Main.ShellLocalizationHelper.GetLocalizedPath(target); program.Arguments = ShellLinkHelper.Arguments; diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs index d0360db3c3..d7eae7008c 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.Program/Storage/Win32ProgramRepository.cs @@ -150,7 +150,7 @@ namespace Microsoft.Plugin.Program.Storage // Using OrdinalIgnoreCase since this is used internally if (extension.Equals(LnkExtension, StringComparison.OrdinalIgnoreCase)) { - app = GetAppWithSameLnkResolvedPath(path); + app = GetAppWithSameLnkFilePath(path); if (app == null) { // Cancelled links won't have a resolved path. @@ -195,12 +195,12 @@ namespace Microsoft.Plugin.Program.Storage // To mitigate the issue faced (as stated above) when a shortcut application is renamed, the Exe FullPath and executable name must be obtained. // Unlike the rename event args, since we do not have a newPath, we iterate through all the programs and find the one with the same LnkResolved path. - private Programs.Win32Program GetAppWithSameLnkResolvedPath(string lnkResolvedPath) + private Programs.Win32Program GetAppWithSameLnkFilePath(string lnkFilePath) { foreach (Programs.Win32Program app in Items) { // Using Invariant / OrdinalIgnoreCase since we're comparing paths - if (lnkResolvedPath.ToUpperInvariant().Equals(app.LnkResolvedPath, StringComparison.OrdinalIgnoreCase)) + if (lnkFilePath.ToUpperInvariant().Equals(app.LnkFilePath, StringComparison.OrdinalIgnoreCase)) { return app; } diff --git a/src/modules/launcher/Wox.Plugin/Common/Interfaces/IShellItem.cs b/src/modules/launcher/Wox.Plugin/Common/Interfaces/IShellItem.cs new file mode 100644 index 0000000000..a110097f16 --- /dev/null +++ b/src/modules/launcher/Wox.Plugin/Common/Interfaces/IShellItem.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using Wox.Plugin.Common.Win32; + +namespace Wox.Plugin.Common.Interfaces +{ + /// + /// The following are ShellItem DisplayName types. + /// + [Flags] + public enum SIGDN : uint + { + NORMALDISPLAY = 0, + PARENTRELATIVEPARSING = 0x80018001, + PARENTRELATIVEFORADDRESSBAR = 0x8001c001, + DESKTOPABSOLUTEPARSING = 0x80028000, + PARENTRELATIVEEDITING = 0x80031001, + DESKTOPABSOLUTEEDITING = 0x8004c000, + FILESYSPATH = 0x80058000, + URL = 0x80068000, + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")] + public interface IShellItem + { + void BindToHandler( + IntPtr pbc, + [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid, + out IntPtr ppv); + + void GetParent(out IShellItem ppsi); + + void GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); + + void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); + + void Compare(IShellItem psi, uint hint, out int piOrder); + } +} diff --git a/src/modules/launcher/Wox.Plugin/Common/ShellLocalization.cs b/src/modules/launcher/Wox.Plugin/Common/ShellLocalization.cs index 4ce9e0cb83..97ff8bda38 100644 --- a/src/modules/launcher/Wox.Plugin/Common/ShellLocalization.cs +++ b/src/modules/launcher/Wox.Plugin/Common/ShellLocalization.cs @@ -2,67 +2,51 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; -using System.Text; +using Wox.Plugin.Common.Interfaces; +using Wox.Plugin.Common.Win32; namespace Wox.Plugin.Common { /// /// Class to get localized name of shell items like 'My computer'. The localization is based on the 'windows display language'. - /// Reused code from https://stackoverflow.com/questions/41423491/how-to-get-localized-name-of-known-folder for the method /// - public static class ShellLocalization + public class ShellLocalization { - internal const uint DONTRESOLVEDLLREFERENCES = 0x00000001; - internal const uint LOADLIBRARYASDATAFILE = 0x00000002; - - [DllImport("shell32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] - internal static extern int SHGetLocalizedName(string pszPath, StringBuilder pszResModule, ref int cch, out int pidsRes); - - [DllImport("user32.dll", EntryPoint = "LoadStringW", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] - internal static extern int LoadString(IntPtr hModule, int resourceID, StringBuilder resourceValue, int len); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, EntryPoint = "LoadLibraryExW")] - internal static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, uint dwFlags); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern int FreeLibrary(IntPtr hModule); - - [DllImport("kernel32.dll", EntryPoint = "ExpandEnvironmentStringsW", CharSet = CharSet.Unicode, ExactSpelling = true)] - internal static extern uint ExpandEnvironmentStrings(string lpSrc, StringBuilder lpDst, int nSize); + // Cache for already localized names. This makes localization of already localized string faster. + private Dictionary _localizationCache = new Dictionary(); /// /// Returns the localized name of a shell item. /// /// Path to the shell item (e. g. shortcut 'File Explorer.lnk'). /// The localized name as string or . - public static string GetLocalizedName(string path) + public string GetLocalizedName(string path) { - StringBuilder resourcePath = new StringBuilder(1024); - StringBuilder localizedName = new StringBuilder(1024); - int len, id; - len = resourcePath.Capacity; - - // If there is no resource to localize a file name the method returns a non zero value. - if (SHGetLocalizedName(path, resourcePath, ref len, out id) == 0) + // Checking cache if path is already localized + if (_localizationCache.ContainsKey(path.ToLowerInvariant())) { - _ = ExpandEnvironmentStrings(resourcePath.ToString(), resourcePath, resourcePath.Capacity); - IntPtr hMod = LoadLibraryEx(resourcePath.ToString(), IntPtr.Zero, DONTRESOLVEDLLREFERENCES | LOADLIBRARYASDATAFILE); - if (hMod != IntPtr.Zero) - { - if (LoadString(hMod, id, localizedName, localizedName.Capacity) != 0) - { - string lString = localizedName.ToString(); - _ = FreeLibrary(hMod); - return lString; - } - - _ = FreeLibrary(hMod); - } + return _localizationCache[path.ToLowerInvariant()]; } - return string.Empty; + Guid shellItemType = ShellItemTypeConstants.ShellItemGuid; + int retCode = NativeMethods.SHCreateItemFromParsingName(path, IntPtr.Zero, ref shellItemType, out IShellItem shellItem); + if (retCode != 0) + { + return string.Empty; + } + + shellItem.GetDisplayName(SIGDN.NORMALDISPLAY, out string filename); + + if (!_localizationCache.ContainsKey(path.ToLowerInvariant())) + { + // The if condition is required to not get timing problems when called from an parallel execution. + // Without the check we will get "key exists" exceptions. + _localizationCache.Add(path.ToLowerInvariant(), filename); + } + + return filename; } /// @@ -70,7 +54,7 @@ namespace Wox.Plugin.Common /// /// The path to localize /// The localized path or the original path if localized version is not available - public static string GetLocalizedPath(string path) + public string GetLocalizedPath(string path) { path = Environment.ExpandEnvironmentVariables(path); string ext = Path.GetExtension(path); @@ -79,6 +63,14 @@ namespace Wox.Plugin.Common for (int i = 0; i < pathParts.Length; i++) { + if (i == 0 && pathParts[i].EndsWith(':')) + { + // Skip the drive letter. + locPath[0] = pathParts[0]; + continue; + } + + // Localize path. int iElements = i + 1; string lName = GetLocalizedName(string.Join("\\", pathParts[..iElements])); locPath[i] = !string.IsNullOrEmpty(lName) ? lName : pathParts[i]; diff --git a/src/modules/launcher/Wox.Plugin/Common/Win32/NativeMethods.cs b/src/modules/launcher/Wox.Plugin/Common/Win32/NativeMethods.cs index d7f1228f7e..f54ec9b5af 100644 --- a/src/modules/launcher/Wox.Plugin/Common/Win32/NativeMethods.cs +++ b/src/modules/launcher/Wox.Plugin/Common/Win32/NativeMethods.cs @@ -5,6 +5,7 @@ using System; using System.Runtime.InteropServices; using System.Text; +using Wox.Plugin.Common.Interfaces; using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; #pragma warning disable SA1649, CA1051, CA1707, CA1028, CA1714, CA1069, SA1402 @@ -114,6 +115,9 @@ namespace Wox.Plugin.Common.Win32 [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] public static extern HRESULT SHCreateStreamOnFileEx(string fileName, STGM grfMode, uint attributes, bool create, System.Runtime.InteropServices.ComTypes.IStream reserved, out System.Runtime.InteropServices.ComTypes.IStream stream); + + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string path, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem); } [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "These are the names used by win32.")] @@ -141,6 +145,19 @@ namespace Wox.Plugin.Common.Win32 public const int SC_CLOSE = 0xF060; } + public static class ShellItemTypeConstants + { + /// + /// Guid for type IShellItem. + /// + public static readonly Guid ShellItemGuid = new Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"); + + /// + /// Guid for type IShellItem2. + /// + public static readonly Guid ShellItem2Guid = new Guid("7E9FB0D3-919F-4307-AB2E-9B1860310C93"); + } + public enum HRESULT : uint { ///