From 52f25619373ccb9af0fa0fbc7da916297ecac897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Wed, 3 Dec 2025 17:16:25 +0100 Subject: [PATCH 1/3] CmdPal: Find app for WinGet package (#43943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request This PR introduces a bit of dark magic to resolve the correct installed app for a given WinGet package: - Packaged apps: matched using their package family name. - Everything else: matched using the product code (GUID) and heuristic registry lookup. - The registry rarely stores the executable path directly, so the logic compares install locations with known apps. - It attempts to pick the best candidate while avoiding uninstallers. - It’s not science — let’s call it `#666666` magic. - MSI API support was removed because it's too slow for this scenario. - If no reliable match is found, the command is skipped for now. The future plan is to redirect the user to the list of installed apps and search by display name, but that needs some supporting infrastructure first. - The command order for WinGet list entries was updated: **Install / Uninstall** is now the primary action, ensuring a stable UI since this command is always available. ## PR Checklist - [x] Closes: #43671 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .github/actions/spell-check/expect.txt | 2 + .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 5 +- .../AllAppsCommandProviderTests.cs | 6 +- .../AllAppsCommandProvider.cs | 68 +++++- .../ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs | 4 + .../Microsoft.CmdPal.Ext.Apps/AppListItem.cs | 8 + .../Helpers/UninstallRegistryAppLocator.cs | 205 ++++++++++++++++++ .../Programs/UWPApplication.cs | 1 + .../Programs/Win32Program.cs | 1 + .../Pages/InstallPackageCommand.cs | 2 +- .../Pages/InstallPackageListItem.cs | 97 +++++++-- .../WinGetExtensionCommandsProvider.cs | 6 +- .../WinGetStatics.cs | 4 +- 13 files changed, 376 insertions(+), 33 deletions(-) create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/UninstallRegistryAppLocator.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 0d68b5537c..77385974f4 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -773,6 +773,7 @@ INITGUID INITTOLOGFONTSTRUCT INLINEPREFIX inlines +Inno INPC inproc INPUTHARDWARE @@ -1848,6 +1849,7 @@ UNCPRIORITY UNDNAME UNICODETEXT unins +Uninstaller uninstalls Uniquifies unitconverter diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index f91b9e304a..53f47286b2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -135,8 +135,9 @@ public partial class App : Application try { var winget = new WinGetExtensionCommandsProvider(); - var callback = allApps.LookupApp; - winget.SetAllLookup(callback); + winget.SetAllLookup( + query => allApps.LookupAppByPackageFamilyName(query, requireSingleMatch: true), + query => allApps.LookupAppByProductCode(query, requireSingleMatch: true)); services.AddSingleton(winget); } catch (Exception ex) diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs index e7fbc6859d..cc24433931 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs @@ -58,7 +58,7 @@ public class AllAppsCommandProviderTests : AppsTestBase var provider = new AllAppsCommandProvider(page); // Act - var result = provider.LookupApp(string.Empty); + var result = provider.LookupAppByDisplayName(string.Empty); // Assert Assert.IsNotNull(result); @@ -77,7 +77,7 @@ public class AllAppsCommandProviderTests : AppsTestBase await WaitForPageInitializationAsync(); // Act - var result = provider.LookupApp("TestApp"); + var result = provider.LookupAppByDisplayName("TestApp"); // Assert Assert.IsNotNull(result); @@ -97,7 +97,7 @@ public class AllAppsCommandProviderTests : AppsTestBase await WaitForPageInitializationAsync(); // Act - var result = provider.LookupApp("NonExistentApp"); + var result = provider.LookupAppByDisplayName("NonExistentApp"); // Assert Assert.IsNull(result); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs index 7232e955d7..317087847e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; +using Microsoft.CmdPal.Ext.Apps.Helpers; +using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CommandPalette.Extensions; @@ -66,7 +68,71 @@ public partial class AllAppsCommandProvider : CommandProvider public override ICommandItem[] TopLevelCommands() => [_listItem, .. _page.GetPinnedApps()]; - public ICommandItem? LookupApp(string displayName) + public ICommandItem? LookupAppByPackageFamilyName(string packageFamilyName, bool requireSingleMatch) + { + if (string.IsNullOrEmpty(packageFamilyName)) + { + return null; + } + + var items = _page.GetItems(); + List matches = []; + + foreach (var item in items) + { + if (item is AppListItem appItem && string.Equals(packageFamilyName, appItem.App.PackageFamilyName, StringComparison.OrdinalIgnoreCase)) + { + matches.Add(item); + if (!requireSingleMatch) + { + // Return early if we don't require uniqueness. + return item; + } + } + } + + return requireSingleMatch && matches.Count == 1 ? matches[0] : null; + } + + public ICommandItem? LookupAppByProductCode(string productCode, bool requireSingleMatch) + { + if (string.IsNullOrEmpty(productCode)) + { + return null; + } + + if (!UninstallRegistryAppLocator.TryGetInstallInfo(productCode, out _, out var candidates) || candidates.Count <= 0) + { + return null; + } + + var items = _page.GetItems(); + List matches = []; + + foreach (var item in items) + { + if (item is not AppListItem appListItem || string.IsNullOrEmpty(appListItem.App.FullExecutablePath)) + { + continue; + } + + foreach (var candidate in candidates) + { + if (string.Equals(appListItem.App.FullExecutablePath, candidate, StringComparison.OrdinalIgnoreCase)) + { + matches.Add(item); + if (!requireSingleMatch) + { + return item; + } + } + } + } + + return requireSingleMatch && matches.Count == 1 ? matches[0] : null; + } + + public ICommandItem? LookupAppByDisplayName(string displayName) { var items = _page.GetItems(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs index b7d01593cb..14f9597418 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs @@ -29,6 +29,10 @@ public sealed class AppItem public string AppIdentifier { get; set; } = string.Empty; + public string? PackageFamilyName { get; set; } + + public string? FullExecutablePath { get; set; } + public AppItem() { } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs index e99ffae352..5689b70698 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs @@ -40,6 +40,8 @@ public sealed partial class AppListItem : ListItem public string AppIdentifier => _app.AppIdentifier; + public AppItem App => _app; + public AppListItem(AppItem app, bool useThumbnails, bool isPinned) { Command = _appCommand = new AppCommand(app); @@ -82,6 +84,12 @@ public sealed partial class AppListItem : ListItem metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } }); } +#if DEBUG + metadata.Add(new DetailsElement() { Key = "[DEBUG] AppIdentifier", Data = new DetailsLink() { Text = _app.AppIdentifier } }); + metadata.Add(new DetailsElement() { Key = "[DEBUG] ExePath", Data = new DetailsLink() { Text = _app.ExePath } }); + metadata.Add(new DetailsElement() { Key = "[DEBUG] IcoPath", Data = new DetailsLink() { Text = _app.IcoPath } }); +#endif + // Icon IconInfo? heroImage = null; if (_app.IsPackaged) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/UninstallRegistryAppLocator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/UninstallRegistryAppLocator.cs new file mode 100644 index 0000000000..8e59a26395 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/UninstallRegistryAppLocator.cs @@ -0,0 +1,205 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.Apps.Helpers; + +internal static class UninstallRegistryAppLocator +{ + private static readonly string[] UninstallBaseKeys = + [ + @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", + ]; + + /// + /// Tries to find install directory and a list of plausible main EXEs from an uninstall key + /// (e.g. Inno Setup keys like "{guid}_is1"). + /// may be empty if we couldn't pick any safe EXEs. + /// + /// + /// Returns true if the uninstall key is found and an install directory is resolved. + /// + public static bool TryGetInstallInfo( + string uninstallKeyName, + out string? installDir, + out IReadOnlyList exeCandidates, + string? expectedExeName = null) + { + installDir = null; + exeCandidates = []; + + if (string.IsNullOrWhiteSpace(uninstallKeyName)) + { + throw new ArgumentException("Key name must not be null or empty.", nameof(uninstallKeyName)); + } + + uninstallKeyName = uninstallKeyName.Trim(); + + foreach (var baseKeyPath in UninstallBaseKeys) + { + // HKLM + using (var key = Registry.LocalMachine.OpenSubKey($"{baseKeyPath}\\{uninstallKeyName}")) + { + if (TryFromUninstallKey(key, expectedExeName, out installDir, out exeCandidates)) + { + return true; + } + } + + // HKCU + using (var key = Registry.CurrentUser.OpenSubKey($"{baseKeyPath}\\{uninstallKeyName}")) + { + if (TryFromUninstallKey(key, expectedExeName, out installDir, out exeCandidates)) + { + return true; + } + } + } + + return false; + } + + private static bool TryFromUninstallKey( + RegistryKey? key, + string? expectedExeName, + out string? installDir, + out IReadOnlyList exeCandidates) + { + installDir = null; + exeCandidates = []; + + if (key is null) + { + return false; + } + + var location = (key.GetValue("InstallLocation") as string)?.Trim('"', ' ', '\t'); + if (string.IsNullOrEmpty(location)) + { + location = (key.GetValue("Inno Setup: App Path") as string)?.Trim('"', ' ', '\t'); + } + + if (string.IsNullOrEmpty(location)) + { + var uninstall = key.GetValue("UninstallString") as string; + var uninsExe = ExtractFirstPath(uninstall); + if (!string.IsNullOrEmpty(uninsExe)) + { + var dir = Path.GetDirectoryName(uninsExe); + if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) + { + location = dir; + } + } + } + + if (string.IsNullOrEmpty(location) || !Directory.Exists(location)) + { + return false; + } + + installDir = location; + + // Collect safe EXE candidates; may be empty if ambiguous or only uninstall exes exist. + exeCandidates = GetExeCandidates(location, expectedExeName); + return true; + } + + private static IReadOnlyList GetExeCandidates(string root, string? expectedExeName) + { + // Look at root and a "bin" subfolder (very common pattern) + var allExes = Directory.EnumerateFiles(root, "*.exe", SearchOption.TopDirectoryOnly) + .Concat(GetBinExes(root)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (allExes.Length == 0) + { + return []; + } + + var result = new List(); + + // 1) Exact match on expected exe name (if provided), ignoring case, and not uninstall/setup-like. + if (!string.IsNullOrWhiteSpace(expectedExeName)) + { + foreach (var exe in allExes) + { + if (string.Equals(Path.GetFileName(exe), expectedExeName, StringComparison.OrdinalIgnoreCase) && + !LooksLikeUninstallerOrSetup(exe)) + { + result.Add(exe); + } + } + } + + // 2) All other non-uninstall/setup exes + foreach (var exe in allExes) + { + if (LooksLikeUninstallerOrSetup(exe)) + { + continue; + } + + // Skip ones already added as expectedExeName matches + if (result.Contains(exe, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + result.Add(exe); + } + + // 3) We intentionally do NOT add uninstall/setup/update exes here. + // If you ever want them, you can add a separate API to expose them. + return result; + } + + private static IEnumerable GetBinExes(string root) + { + var bin = Path.Combine(root, "bin"); + return !Directory.Exists(bin) + ? [] + : Directory.EnumerateFiles(bin, "*.exe", SearchOption.TopDirectoryOnly); + } + + private static bool LooksLikeUninstallerOrSetup(string path) + { + var name = Path.GetFileName(path); + return name.StartsWith("unins", StringComparison.OrdinalIgnoreCase) // e.g. Inno: unins000.exe + || name.Contains("setup", StringComparison.OrdinalIgnoreCase) // setup.exe + || name.Contains("installer", StringComparison.OrdinalIgnoreCase) // installer.exe / MyAppInstaller.exe + || name.Contains("update", StringComparison.OrdinalIgnoreCase); // updater/updater.exe + } + + private static string? ExtractFirstPath(string? commandLine) + { + if (string.IsNullOrWhiteSpace(commandLine)) + { + return null; + } + + commandLine = commandLine.Trim(); + + if (commandLine.StartsWith('"')) + { + var endQuote = commandLine.IndexOf('"', 1); + if (endQuote > 1) + { + return commandLine[1..endQuote]; + } + } + + var firstSpace = commandLine.IndexOf(' '); + var candidate = firstSpace > 0 ? commandLine[..firstSpace] : commandLine; + candidate = candidate.Trim('"'); + return candidate.Length > 0 ? candidate : null; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs index 37698d972d..4ec9598483 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs @@ -558,6 +558,7 @@ public class UWPApplication : IUWPApplication IsPackaged = true, Commands = app.GetCommands(), AppIdentifier = app.GetAppIdentifier(), + PackageFamilyName = app.Package.FamilyName, }; return item; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs index 9b89afc425..fda8e2c697 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs @@ -1065,6 +1065,7 @@ public class Win32Program : IProgram DirPath = app.Location, Commands = app.GetCommands(), AppIdentifier = app.GetAppIdentifier(), + FullExecutablePath = app.FullPath, }; } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs index d2c1ea7283..7dbe740d95 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs @@ -62,7 +62,7 @@ public partial class InstallPackageCommand : InvokableCommand { PackageInstallCommandState.Install => Icons.DownloadIcon, PackageInstallCommandState.Update => Icons.UpdateIcon, - PackageInstallCommandState.Uninstall => Icons.CompletedIcon, + PackageInstallCommandState.Uninstall => Icons.DeleteIcon, _ => throw new NotImplementedException(), }; Name = InstallCommandState switch diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs index a8eda1bae9..1e1f337944 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs @@ -194,46 +194,95 @@ public partial class InstallPackageListItem : ListItem var isInstalled = _package.InstalledVersion is not null; var installedState = isInstalled ? - (_package.IsUpdateAvailable ? - PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) : + (_package.IsUpdateAvailable ? PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) : PackageInstallCommandState.Install; // might be an uninstall command InstallPackageCommand installCommand = new(_package, installedState); - if (isInstalled) + if (_package.InstalledVersion is not null) { - this.Icon = installCommand.Icon; - this.Command = new NoOpCommand(); +#if DEBUG + var installerType = _package.InstalledVersion.GetMetadata(PackageVersionMetadataField.InstallerType); + Subtitle = installerType + " | " + Subtitle; +#endif + List contextMenu = []; - CommandContextItem uninstallContextItem = new(installCommand) + Command = installCommand; + Icon = installedState switch { - IsCritical = true, - Icon = Icons.DeleteIcon, + PackageInstallCommandState.Install => Icons.DownloadIcon, + PackageInstallCommandState.Update => Icons.UpdateIcon, + PackageInstallCommandState.Uninstall => Icons.CompletedIcon, + _ => Icons.DownloadIcon, }; - if (WinGetStatics.AppSearchCallback is not null) + TryLocateAndAppendActionForApp(contextMenu); + + MoreCommands = contextMenu.ToArray(); + } + else + { + _installCommand = new InstallPackageCommand(_package, installedState); + _installCommand.InstallStateChanged += InstallStateChangedHandler; + Command = _installCommand; + Icon = _installCommand.Icon; + } + } + + private void TryLocateAndAppendActionForApp(List contextMenu) + { + try + { + // Let's try to connect it to an installed app if possible + // This is a bit of dark magic, since there's no direct link between + // WinGet packages and installed apps. + var lookupByPackageName = WinGetStatics.AppSearchByPackageFamilyNameCallback; + if (lookupByPackageName is not null) { - var callback = WinGetStatics.AppSearchCallback; - var installedApp = callback(_package.DefaultInstallVersion is null ? _package.Name : _package.DefaultInstallVersion.DisplayName); - if (installedApp is not null) + var names = _package.InstalledVersion.PackageFamilyNames; + for (var i = 0; i < names.Count; i++) { - this.Command = installedApp.Command; - contextMenu = [.. installedApp.MoreCommands]; + var installedAppByPfn = lookupByPackageName(names[i]); + if (installedAppByPfn is not null) + { + contextMenu.Add(new Separator()); + contextMenu.Add(new CommandContextItem(installedAppByPfn.Command)); + foreach (var item in installedAppByPfn.MoreCommands) + { + contextMenu.Add(item); + } + + return; + } } } - contextMenu.Add(uninstallContextItem); - this.MoreCommands = contextMenu.ToArray(); - return; + var lookupByProductCode = WinGetStatics.AppSearchByProductCodeCallback; + if (lookupByProductCode is not null) + { + var productCodes = _package.InstalledVersion.ProductCodes; + for (var i = 0; i < productCodes.Count; i++) + { + var installedAppByProductCode = lookupByProductCode(productCodes[i]); + if (installedAppByProductCode is not null) + { + contextMenu.Add(new Separator()); + contextMenu.Add(new CommandContextItem(installedAppByProductCode.Command)); + foreach (var item in installedAppByProductCode.MoreCommands) + { + contextMenu.Add(item); + } + + return; + } + } + } + } + catch (Exception ex) + { + Logger.LogError($"Failed to retrieve app context menu items for package '{_package?.Name ?? "Unknown"}'", ex); } - - // didn't find the app - _installCommand = new InstallPackageCommand(_package, installedState); - this.Command = _installCommand; - - Icon = _installCommand.Icon; - _installCommand.InstallStateChanged += InstallStateChangedHandler; } private void InstallStateChangedHandler(object? sender, InstallPackageCommand e) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs index 14c7ec0831..a2608ef8a8 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs @@ -41,5 +41,9 @@ public partial class WinGetExtensionCommandsProvider : CommandProvider public override void InitializeWithHost(IExtensionHost host) => WinGetExtensionHost.Instance.Initialize(host); - public void SetAllLookup(Func callback) => WinGetStatics.AppSearchCallback = callback; + public void SetAllLookup(Func lookupByPackageName, Func lookupByProductCode) + { + WinGetStatics.AppSearchByPackageFamilyNameCallback = lookupByPackageName; + WinGetStatics.AppSearchByProductCodeCallback = lookupByProductCode; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs index da591c566c..001ba5539d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs @@ -34,7 +34,9 @@ internal static class WinGetStatics private static readonly StatusMessage _errorMessage = new() { State = MessageState.Error }; - public static Func? AppSearchCallback { get; set; } + public static Func? AppSearchByPackageFamilyNameCallback { get; set; } + + public static Func? AppSearchByProductCodeCallback { get; set; } private static readonly CompositeFormat CreateCatalogErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.winget_create_catalog_error); From 503bcbdf2d44d2002293bc35c23846e7d572c58c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 07:33:37 +0100 Subject: [PATCH 2/3] Restore missing "Quick access" menu item in tray icon context menu (#42676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Restores the "Quick access" menu item that was accidentally removed from the PowerToys tray icon context menu. ## Issue Fixes #[issue_number] The "Quick access" menu item was missing from the tray icon's right-click context menu, preventing users from accessing this feature via the tray menu. **Expected menu:** ![Expected menu](https://github.com/user-attachments/assets/805b1436-5a08-42e7-a34d-b9848fd9a235) **Actual menu (before this fix):** ![Actual menu](https://github.com/user-attachments/assets/7584035d-e893-4f73-acc3-84d789e31e81) ## Changes - Added the missing `MENUITEM "Quick access\tLeft-click", ID_QUICK_ACCESS_MENU_COMMAND` entry as the first menu item in the `ID_TRAY_MENU` definition in `src/runner/runner.base.rc` ## Details The menu item was accidentally removed in commit f5797a065a5c2c448fd2b1780bd1353d712103c3. This PR restores it to its correct position as the first item in the tray menu. All supporting code was already in place: - The resource ID `ID_QUICK_ACCESS_MENU_COMMAND` (40006) is defined in `resource.base.h` - The resource string `QUICK_ACCESS_MENU_TEXT` is defined in `Resources.resx` - The menu command handler in `tray_icon.cpp` opens the Quick Access flyout window - The localization code updates the menu text at runtime ## Testing - ✅ Verified the menu item syntax is correct and matches existing patterns - ✅ Confirmed all supporting resource IDs and handler code exist - ✅ CI build verification pending After this fix, the tray menu will correctly display: 1. Quick access (Left-click) 2. Settings (Double-click) 3. Documentation 4. Report Bug 5. Close
Original prompt > > ---- > > *This section details on the original issue you should resolve* > > Quick Access missing in tray menu > ### Microsoft PowerToys version > > 0.95.0 > > ### Installation method > > PowerToys auto-update > > ### Area(s) with issue? > > System tray interaction > > ### Steps to reproduce > > 1. Right click the tray icon > 2. Look at the list of items which can be selected > 3. Notice that the "Quick access" is missing > > Expected menu ("Exit" is now "Close" I took this image from initial implementation): > > Image > > Actual menu right now: > > Image > > ### ✔️ Expected Behavior > > The "Quick access" menu item should be there > > ### ❌ Actual Behavior > > The menu item is missing > > ### Additional Information > > Windows 10 Pro 22H2 19045.6332 > > (Also it is the same on my other computers) > > ### Other Software > > _No response_ > > restore > > MENUITEM "Quick access\tLeft-click", ID_QUICK_ACCESS_MENU_COMMAND > > as first menu item of ID_TRAY_MENU MENU in src/runner/runner.base.rc > > ## Comments on the Issue (you are @copilot in this section) > > > @niels9001 > @davidegiacometti is this a regression from the PR where we updated the string names? > @davidegiacometti > Hi @niels9001 > I just realized that the menu was missing in https://github.com/microsoft/PowerToys/pull/40714 screenshots, but the regression was caused by https://github.com/microsoft/PowerToys/commit/f5797a065a5c2c448fd2b1780bd1353d712103c3. > Unfortunately, many of the `.rc` files in the PT codebase have UTF16-LE encoding and GitHub doesn't show diff. > >
Fixes microsoft/PowerToys#42618 --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: davidegiacometti <25966642+davidegiacometti@users.noreply.github.com> --- src/runner/runner.base.rc | Bin 2972 -> 3122 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/runner/runner.base.rc b/src/runner/runner.base.rc index 367735ade45b0cdbe086a95fb5a2ce45f3cec744..55b4e13fdd3476cd015adeb718e40f73826ed8b7 100644 GIT binary patch delta 104 zcmbOuzDZ(33uAB~Ln%WhLo!1)g91Y$kWOYuWhe&17={uCA0RsoNb3ScbD*k}7<7Ot rJsDgW;u!)NLV+x225%tS5lA~TxH1GY1W&%mtUfu8S!#0=qZAhad!`iu delta 12 TcmdlaF-Lqu3*%-fCJ8P89h?KC From 9dcddfd4b811207b04ae274e1e12fbd0eeb8d29c Mon Sep 17 00:00:00 2001 From: Valentin Arthur Thomas <64769541+warquys@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:57:01 +0100 Subject: [PATCH 3/3] Quotation mark (#30481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Add Quotation mark Add local quotation based on ~~VK_OEM_7(0xDE)~~ VK_OEM_COMMA(0xBC) key. Not all quotes have been added, only `‟ „ ” « » ‚ , ‘ ’ › ‹ '「 」 《 》 『 』〈 〉″ ‴ ⁗` Why not added : - ` ⹂ ⌜ ⌝ ❛ ❜ ❝ ❞ 🙶 🙷 🙸 ' 「 」 ` its redundant and would make too much and not readable. - ` ﹁ ﹂ ﹃ ﹄ ` I did not put them because there use for horizontal text ## PR Checklist - [x] Closes: https://github.com/microsoft/PowerToys/issues/29371 https://github.com/microsoft/PowerToys/issues/24832 - [ ] **Communication:** I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end user facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places (None) - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx This PR is currently a draft, I still need to know if adding language-related keyboard management is a good idea or specifying the use of a gobal key to make it easier to manage all the keyboards in one. Some languages ​​can use different keyboards, I think this would become problematic if the keyboard does not match the key used by default. However, using a universal key can also pose an issue to finding the key. that remains to be discussed ## Validation Steps Performed --- .../poweraccent/PowerAccent.Core/Languages.cs | 38 ++++++++++++++++--- .../PowerAccent.Core/PowerAccent.cs | 6 +++ .../PowerAccent.UI/Selector.xaml.cs | 7 ++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/modules/poweraccent/PowerAccent.Core/Languages.cs b/src/modules/poweraccent/PowerAccent.Core/Languages.cs index 06c3a2bea3..2917feff91 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Languages.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Languages.cs @@ -224,7 +224,7 @@ namespace PowerAccent.Core LetterKey.VK_X => new[] { "ẋ", "×" }, LetterKey.VK_Y => new[] { "ẏ", "ꝡ" }, LetterKey.VK_Z => new[] { "ʒ", "ǯ", "ℤ" }, - LetterKey.VK_COMMA => new[] { "∙", "₋", "⁻", "–", "√" }, // – is in VK_MINUS for other languages, but not VK_COMMA, so we add it here. + LetterKey.VK_COMMA => new[] { "∙", "₋", "⁻", "–", "√", "‟", "《", "》", "‛", "〈", "〉", "″", "‴", "⁗" }, // – is in VK_MINUS for other languages, but not VK_COMMA, so we add it here. LetterKey.VK_PERIOD => new[] { "…", "⁝", "\u0300", "\u0301", "\u0302", "\u0303", "\u0304", "\u0308", "\u030B", "\u030C" }, LetterKey.VK_MINUS => new[] { "~", "‐", "‑", "‒", "—", "―", "⁓", "−", "⸺", "⸻", "∓" }, LetterKey.VK_SLASH_ => new[] { "÷", "√" }, @@ -302,6 +302,7 @@ namespace PowerAccent.Core LetterKey.VK_E => new[] { "€" }, LetterKey.VK_S => new[] { "š" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "»", "«" }, _ => Array.Empty(), }; } @@ -317,6 +318,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "ü" }, LetterKey.VK_Z => new[] { "ž" }, LetterKey.VK_S => new[] { "š" }, + LetterKey.VK_COMMA => new[] { "„", "“", "«", "»" }, _ => Array.Empty(), }; } @@ -344,6 +346,7 @@ namespace PowerAccent.Core LetterKey.VK_A => new[] { "ä", "å" }, LetterKey.VK_E => new[] { "€" }, LetterKey.VK_O => new[] { "ö" }, + LetterKey.VK_COMMA => new[] { "”", "’", "»" }, _ => Array.Empty(), }; } @@ -360,6 +363,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ô", "ö", "ó", "ò", "õ", "œ" }, LetterKey.VK_U => new[] { "û", "ù", "ü", "ú" }, LetterKey.VK_Y => new[] { "ÿ", "ý" }, + LetterKey.VK_COMMA => new[] { "«", "»", "‹", "›", "“", "”", "‘", "’" }, _ => Array.Empty(), }; } @@ -376,6 +380,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "ú" }, LetterKey.VK_Y => new[] { "ý" }, LetterKey.VK_T => new[] { "þ" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘" }, _ => Array.Empty(), }; } @@ -393,7 +398,7 @@ namespace PowerAccent.Core LetterKey.VK_N => new[] { "ñ" }, LetterKey.VK_O => new[] { "ó" }, LetterKey.VK_U => new[] { "ú", "ü" }, - LetterKey.VK_COMMA => new[] { "¿", "?", "¡", "!" }, + LetterKey.VK_COMMA => new[] { "¿", "?", "¡", "!", "«", "»", "“", "”", "‘", "’" }, _ => Array.Empty(), }; } @@ -411,7 +416,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ò", "ó" }, LetterKey.VK_U => new[] { "ù", "ú", "ü" }, LetterKey.VK_L => new[] { "·" }, - LetterKey.VK_COMMA => new[] { "¿", "?", "¡", "!" }, + LetterKey.VK_COMMA => new[] { "¿", "?", "¡", "!", "«", "»", "“", "”", "‘", "’" }, _ => Array.Empty(), }; } @@ -427,6 +432,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ō" }, LetterKey.VK_S => new[] { "$" }, LetterKey.VK_U => new[] { "ū" }, + LetterKey.VK_COMMA => new[] { "“", "”", "‘", "’" }, _ => Array.Empty(), }; } @@ -443,6 +449,7 @@ namespace PowerAccent.Core LetterKey.VK_N => new[] { "ñ" }, LetterKey.VK_O => new[] { "ó", "ö", "ô" }, LetterKey.VK_U => new[] { "ú", "ü", "û" }, + LetterKey.VK_COMMA => new[] { "“", "„", "”", "‘", ",", "’" }, _ => Array.Empty(), }; } @@ -469,6 +476,7 @@ namespace PowerAccent.Core LetterKey.VK_V => new[] { "ü", "ǖ", "ǘ", "ǚ", "ǜ" }, LetterKey.VK_Y => new[] { "¥" }, LetterKey.VK_Z => new[] { "ẑ" }, + LetterKey.VK_COMMA => new[] { "“", "”", "‘", "’", "「", "」", "『", "』" }, _ => Array.Empty(), }; } @@ -505,6 +513,7 @@ namespace PowerAccent.Core LetterKey.VK_S => new[] { "ş" }, LetterKey.VK_T => new[] { "₺" }, LetterKey.VK_U => new[] { "ü", "û" }, + LetterKey.VK_COMMA => new[] { "“", "”", "‘", "’", "«", "»", "‹", "›" }, _ => Array.Empty(), }; } @@ -522,6 +531,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ó" }, LetterKey.VK_S => new[] { "ś" }, LetterKey.VK_Z => new[] { "ż", "ź" }, + LetterKey.VK_COMMA => new[] { "„", "”", "‘", "’", "»", "«" }, _ => Array.Empty(), }; } @@ -539,7 +549,7 @@ namespace PowerAccent.Core LetterKey.VK_P => new[] { "π" }, LetterKey.VK_S => new[] { "$" }, LetterKey.VK_U => new[] { "ú" }, - LetterKey.VK_COMMA => new[] { "≤", "≥", "≠", "≈", "≙", "±", "₊", "⁺" }, + LetterKey.VK_COMMA => new[] { "≤", "≥", "≠", "≈", "≙", "±", "₊", "⁺", "“", "”", "‘", "’", "«", "»" }, _ => Array.Empty(), }; } @@ -594,6 +604,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "ú" }, LetterKey.VK_Y => new[] { "ý" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘", "»", "«", "›", "‹" }, _ => Array.Empty(), }; } @@ -608,6 +619,7 @@ namespace PowerAccent.Core LetterKey.VK_I => new[] { "í" }, LetterKey.VK_O => new[] { "ó" }, LetterKey.VK_U => new[] { "ú" }, + LetterKey.VK_COMMA => new[] { "“", "”", "‘", "’" }, _ => Array.Empty(), }; } @@ -623,6 +635,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ò" }, LetterKey.VK_P => new[] { "£" }, LetterKey.VK_U => new[] { "ù" }, + LetterKey.VK_COMMA => new[] { "“", "”", "‘", "’" }, _ => Array.Empty(), }; } @@ -645,6 +658,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "ů", "ú" }, LetterKey.VK_Y => new[] { "ý" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘", "»", "«", "›", "‹" }, _ => Array.Empty(), }; } @@ -659,6 +673,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ö" }, LetterKey.VK_S => new[] { "ß" }, LetterKey.VK_U => new[] { "ü" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘", "»", "«", "›", "‹" }, _ => Array.Empty(), }; } @@ -689,6 +704,7 @@ namespace PowerAccent.Core LetterKey.VK_X => new string[] { "ξ" }, LetterKey.VK_Y => new string[] { "υ" }, LetterKey.VK_Z => new string[] { "ζ" }, + LetterKey.VK_COMMA => new[] { "“", "”", "«", "»", }, _ => Array.Empty(), }; } @@ -710,7 +726,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "וֹ", "וּ", "װ", "\u05b9" }, LetterKey.VK_X => new[] { "\u05b6", "\u05b1" }, LetterKey.VK_Y => new[] { "ױ" }, - LetterKey.VK_COMMA => new[] { "”", "’", "״", "׳" }, + LetterKey.VK_COMMA => new[] { "”", "’", "'", "״", "׳" }, LetterKey.VK_PERIOD => new[] { "\u05ab", "\u05bd", "\u05bf" }, LetterKey.VK_MINUS => new[] { "–", "־" }, _ => Array.Empty(), @@ -727,6 +743,7 @@ namespace PowerAccent.Core LetterKey.VK_I => new[] { "í" }, LetterKey.VK_O => new[] { "ó", "ő", "ö" }, LetterKey.VK_U => new[] { "ú", "ű", "ü" }, + LetterKey.VK_COMMA => new[] { "„", "”", "»", "«" }, _ => Array.Empty(), }; } @@ -740,6 +757,7 @@ namespace PowerAccent.Core LetterKey.VK_I => new[] { "î" }, LetterKey.VK_S => new[] { "ș" }, LetterKey.VK_T => new[] { "ț" }, + LetterKey.VK_COMMA => new[] { "„", "”", "«", "»" }, _ => Array.Empty(), }; } @@ -754,6 +772,7 @@ namespace PowerAccent.Core LetterKey.VK_I => new[] { "ì", "í" }, LetterKey.VK_O => new[] { "ò", "ó" }, LetterKey.VK_U => new[] { "ù", "ú" }, + LetterKey.VK_COMMA => new[] { "«", "»", "“", "”", "‘", "’" }, _ => Array.Empty(), }; } @@ -772,6 +791,7 @@ namespace PowerAccent.Core LetterKey.VK_R => new[] { "ř" }, LetterKey.VK_S => new[] { "ş" }, LetterKey.VK_U => new[] { "û", "ü" }, + LetterKey.VK_COMMA => new[] { "«", "»", "“", "”" }, _ => Array.Empty(), }; } @@ -789,6 +809,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "û", "ü", "ù", "ú" }, LetterKey.VK_Y => new[] { "ŷ", "ÿ", "ỳ", "ý" }, LetterKey.VK_W => new[] { "ŵ", "ẅ", "ẁ", "ẃ" }, + LetterKey.VK_COMMA => new[] { "‘", "’", "“", "“" }, _ => Array.Empty(), }; } @@ -801,6 +822,7 @@ namespace PowerAccent.Core LetterKey.VK_A => new[] { "å", "ä" }, LetterKey.VK_E => new[] { "é" }, LetterKey.VK_O => new[] { "ö" }, + LetterKey.VK_COMMA => new[] { "”", "’", "»", "«" }, _ => Array.Empty(), }; } @@ -814,6 +836,7 @@ namespace PowerAccent.Core LetterKey.VK_D => new[] { "đ" }, LetterKey.VK_S => new[] { "š" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "’", "»", "«", "›", "‹" }, _ => Array.Empty(), }; } @@ -838,6 +861,7 @@ namespace PowerAccent.Core { LetterKey.VK_E => new[] { "ѐ" }, LetterKey.VK_I => new[] { "ѝ" }, + LetterKey.VK_COMMA => new[] { "„", "“", "’", "‘" }, _ => Array.Empty(), }; } @@ -869,6 +893,7 @@ namespace PowerAccent.Core LetterKey.VK_E => new[] { "€", "é" }, LetterKey.VK_O => new[] { "ø" }, LetterKey.VK_S => new[] { "$" }, + LetterKey.VK_COMMA => new[] { "«", "»", ",", "‘", "’", "„", "“" }, _ => Array.Empty(), }; } @@ -881,6 +906,7 @@ namespace PowerAccent.Core LetterKey.VK_A => new[] { "å", "æ" }, LetterKey.VK_E => new[] { "€" }, LetterKey.VK_O => new[] { "ø" }, + LetterKey.VK_COMMA => new[] { "»", "«", "“", "”", "›", "‹", "‘", "’" }, _ => Array.Empty(), }; } @@ -897,6 +923,7 @@ namespace PowerAccent.Core LetterKey.VK_S => new[] { "š" }, LetterKey.VK_U => new[] { "ų", "ū" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘" }, _ => Array.Empty(), }; } @@ -910,6 +937,7 @@ namespace PowerAccent.Core LetterKey.VK_E => new[] { "€" }, LetterKey.VK_S => new[] { "š" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "»", "«" }, _ => Array.Empty(), }; } diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs index fa020ee4fe..a50eedfc1b 100644 --- a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs +++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs @@ -20,6 +20,7 @@ public partial class PowerAccent : IDisposable // Keys that show a description (like dashes) when ShowCharacterInfoSetting is 1 private readonly LetterKey[] _letterKeysShowingDescription = new LetterKey[] { LetterKey.VK_O }; + private const double ScreenMinPadding = 150; private bool _visible; private string[] _characters = Array.Empty(); @@ -332,6 +333,11 @@ public partial class PowerAccent : IDisposable return Calculation.GetRawCoordinatesFromPosition(position, screen, window); } + public double GetDisplayMaxWidth() + { + return WindowsFunctions.GetActiveDisplay().Size.Width - ScreenMinPadding; + } + public Position GetToolbarPosition() { return _settingService.Position; diff --git a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs index 311417851a..7eed6a9a1b 100644 --- a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs +++ b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs @@ -59,6 +59,7 @@ public partial class Selector : FluentWindow, IDisposable, INotifyPropertyChange _selectedIndex = index; characters.SelectedIndex = _selectedIndex; characterName.Text = _powerAccent.CharacterDescriptions[_selectedIndex]; + characters.ScrollIntoView(character); } private void PowerAccent_OnChangeDisplay(bool isActive, string[] chars) @@ -73,6 +74,7 @@ public partial class Selector : FluentWindow, IDisposable, INotifyPropertyChange characters.ItemsSource = chars; characters.SelectedIndex = _selectedIndex; this.UpdateLayout(); // Required for filling the actual width/height before positioning. + SetWindowsSize(); SetWindowPosition(); Show(); Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new PowerAccent.Core.Telemetry.PowerAccentShowAccentMenuEvent()); @@ -96,6 +98,11 @@ public partial class Selector : FluentWindow, IDisposable, INotifyPropertyChange this.Top = position.Y; } + private void SetWindowsSize() + { + this.characters.MaxWidth = _powerAccent.GetDisplayMaxWidth(); + } + protected override void OnClosed(EventArgs e) { _powerAccent.SaveUsageInfo();