From e88b4aa1a255718ee64f1152d291df0542919efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Sat, 20 Sep 2025 03:05:03 +0200 Subject: [PATCH 01/82] CmdPal: Cleanup .editorconfig for Command Palette (#41845) ## Summary of the Pull Request - Reformats .editorconfig file for CmdPal and adds comments to keep it organized - Explictly adds some defaults matching the current codebase - just to override local settings - This PR should not introduce new style or formatting to the codebase, only codify the existing one - Configuration changes - Adds `csharp_preserve_single_line_statements = false` (matches current default / StyleCop) - Adds `dotnet_separate_import_directive_groups = false` (matches current default / StyleCop) - Normalize new line chars to Unix style in file_header_template - Adds `insert_final_newline = true`(matches current default / StyleCop) - Removes duplicate `csharp_style_var_for_built_in_types` and keeps more severe variant true:warning Actual configuration diff: ```diff +csharp_preserve_single_line_statements = false +dotnet_separate_import_directive_groups = false -file_header_template = Copyright (c) Microsoft Corporation\r\nThe Microsoft Corporation licenses this file to you under the MIT license.\r\nSee the LICENSE file in the project root for more information. +file_header_template = Copyright (c) Microsoft Corporation\nThe Microsoft Corporation licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for more information. +insert_final_newline = true -csharp_style_var_for_built_in_types = true:suggestion -csharp_style_var_for_built_in_types = true:warning +csharp_style_var_for_built_in_types = true:warning ``` ## PR Checklist - [x] Closes: #41844 - [ ] **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 --- src/modules/cmdpal/.editorconfig | 131 ++++++++++++++++++++++--------- 1 file changed, 94 insertions(+), 37 deletions(-) diff --git a/src/modules/cmdpal/.editorconfig b/src/modules/cmdpal/.editorconfig index f93166a809..281fbeeee7 100644 --- a/src/modules/cmdpal/.editorconfig +++ b/src/modules/cmdpal/.editorconfig @@ -2,28 +2,44 @@ # You can modify the rules from these initially generated values to suit your own policies. # You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference. -[*.cs] +################################################## +# Global settings +################################################## -file_header_template = Copyright (c) Microsoft Corporation\r\nThe Microsoft Corporation licenses this file to you under the MIT license.\r\nSee the LICENSE file in the project root for more information. - -#Core editorconfig formatting - indentation - -#use soft tabs (spaces) for indentation +[*.{cs,vb}] +tab_width = 4 +indent_size = 4 +end_of_line = crlf indent_style = space +insert_final_newline = true +file_header_template = Copyright (c) Microsoft Corporation\nThe Microsoft Corporation licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for more information. -#Formatting - new line options +################################################## +# C# specific formatting +################################################## + +[*.cs] +# ---------------------------------------------- +# Core editorconfig formatting - indentation +# ---------------------------------------------- #place else statements on a new line csharp_new_line_before_else = true #require braces to be on a new line for lambdas, methods, control_blocks, types, properties, and accessors (also known as "Allman" style) csharp_new_line_before_open_brace = all -#Formatting - organize using options +# ---------------------------------------------- +# Formatting - organize using options +# ---------------------------------------------- -#sort System.* using directives alphabetically, and place them before other usings +# sort System.* using directives alphabetically, and place them before other usings dotnet_sort_system_directives_first = true +# Do not place System.* using directives before other using directives. +dotnet_separate_import_directive_groups = false -#Formatting - spacing options +# ---------------------------------------------- +# Formatting - spacing options +# ---------------------------------------------- #require NO space between a cast and the value csharp_space_after_cast = false @@ -44,17 +60,29 @@ csharp_space_between_method_declaration_empty_parameter_list_parentheses = false #place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. csharp_space_between_method_declaration_parameter_list_parentheses = false -#Formatting - wrapping options +# ---------------------------------------------- +# Formatting - wrapping options +# ---------------------------------------------- #leave code block on separate lines csharp_preserve_single_line_blocks = true +#put each statement on a separate line +csharp_preserve_single_line_statements = false -#Style - Code block preferences +################################################## +# C# style rules +################################################## + +# ---------------------------------------------- +# Style - Code block preferences +# ---------------------------------------------- #prefer curly braces even for one line of code csharp_prefer_braces = true:suggestion -#Style - expression bodied member options +# ---------------------------------------------- +# Style - expression bodied member options +# ---------------------------------------------- #prefer expression bodies for accessors csharp_style_expression_bodied_accessors = true:warning @@ -65,55 +93,73 @@ csharp_style_expression_bodied_methods = when_on_single_line:silent #prefer expression-bodied members for properties csharp_style_expression_bodied_properties = true:warning -#Style - expression level options +# ---------------------------------------------- +# Style - expression level options +# ---------------------------------------------- #prefer out variables to be declared before the method call csharp_style_inlined_variable_declaration = false:suggestion #prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them dotnet_style_predefined_type_for_member_access = true:suggestion -#Style - Expression-level preferences +# ---------------------------------------------- +# Style - Expression-level preferences +# ---------------------------------------------- #prefer default over default(T) csharp_prefer_simple_default_expression = true:suggestion #prefer objects to be initialized using object initializers when possible dotnet_style_object_initializer = true:suggestion -#Style - implicit and explicit types +# ---------------------------------------------- +# Style - implicit and explicit types +# ---------------------------------------------- #prefer var over explicit type in all cases, unless overridden by another code style rule csharp_style_var_elsewhere = true:suggestion #prefer var is used to declare variables with built-in system types such as int -csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_for_built_in_types = true:warning #prefer var when the type is already mentioned on the right-hand side of a declaration expression csharp_style_var_when_type_is_apparent = true:suggestion -#Style - language keyword and framework type options +# ---------------------------------------------- +# Style - language keyword and framework type options +# ---------------------------------------------- #prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -#Style - Language rules -csharp_style_implicit_object_creation_when_type_is_apparent = true:warning -csharp_style_var_for_built_in_types = true:warning +# ---------------------------------------------- +# Style - Language rules +# ---------------------------------------------- -#Style - modifier options +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning + +# ---------------------------------------------- +# Style - modifier options +# ---------------------------------------------- #prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion -#Style - Modifier preferences +# ---------------------------------------------- +# Style - Modifier preferences +# ---------------------------------------------- #when this rule is set to a list of modifiers, prefer the specified ordering. csharp_preferred_modifier_order = public,private,protected,internal,static,async,readonly,override,sealed,abstract,virtual:warning dotnet_style_readonly_field = true:warning -#Style - Pattern matching +# ---------------------------------------------- +# Style - Pattern matching +# ---------------------------------------------- #prefer pattern matching instead of is expression with type casts csharp_style_pattern_matching_over_as_with_null_check = true:warning -#Style - qualification options +# ---------------------------------------------- +# Style - qualification options +# ---------------------------------------------- #prefer events not to be prefaced with this. or Me. in Visual Basic dotnet_style_qualification_for_event = false:suggestion @@ -123,20 +169,26 @@ dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_method = false:suggestion #prefer properties not to be prefaced with this. or Me. in Visual Basic dotnet_style_qualification_for_property = false:suggestion -csharp_indent_labels = one_less_than_current -csharp_using_directive_placement = outside_namespace:silent -csharp_prefer_simple_using_statement = true:warning -csharp_style_namespace_declarations = file_scoped:warning + +# ---------------------------------------------- +# Style - expression bodies +# ---------------------------------------------- csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_indexers = true:silent csharp_style_expression_bodied_lambdas = true:silent csharp_style_expression_bodied_local_functions = false:silent +# ---------------------------------------------- +# Style - Miscellaneous preferences +# ---------------------------------------------- + +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:warning +csharp_style_namespace_declarations = file_scoped:warning + [*.{cs,vb}] dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -end_of_line = crlf dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion @@ -146,12 +198,13 @@ dotnet_style_collection_initializer = true:suggestion dotnet_style_prefer_simplified_boolean_expressions = true:suggestion dotnet_style_prefer_conditional_expression_over_assignment = true:silent dotnet_style_prefer_conditional_expression_over_return = true:silent -[*.{cs,vb}] - -#Style - Unnecessary code rules csharp_style_unused_value_assignment_preference = discard_variable:warning -#### Naming styles #### +################################################## +# Naming rules +################################################## + +[*.{cs,vb}] # Naming rules @@ -203,7 +256,11 @@ dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion dotnet_style_prefer_compound_assignment = true:warning dotnet_style_prefer_simplified_interpolation = true:suggestion -# Diagnostic configuration +################################################## +# Diagnostics +################################################## + +[*.{cs,vb}] # CS8305: Type is for evaluation purposes only and is subject to change or removal in future updates. dotnet_diagnostic.CS8305.severity = suggestion From 2ce76b861fcdcecbc3a1db9b7721a02aa68f1c82 Mon Sep 17 00:00:00 2001 From: leileizhang Date: Mon, 22 Sep 2025 19:10:35 +0800 Subject: [PATCH 02/82] Fix: central package version error (#41933) ## Summary of the Pull Request This PR updates the `BuildXamlIndexBeforeSettings` target to avoid running during DesignTimeBuild. Previously, the target was triggered before `CoreCompile`, which caused Visual Studio design-time builds to also invoke `Settings.UI.XamlIndexBuilder`. In design-time builds, the subproject may not fully inherit the central package version management configuration (e.g., `Directory.Packages.props` not included, or incomplete MSBuild property propagation). As a result, NuGet central package version management did not fully apply in design-time context, leading to false error such as: image ## PR Checklist - [ ] Closes: #xxx - [ ] **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 --- src/settings-ui/Settings.UI/PowerToys.Settings.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index 68d0348c7d..36f1a9b75e 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -184,7 +184,7 @@ - + From bb706fb5f1112e8db21cd5b3bd2773c40bdb51d0 Mon Sep 17 00:00:00 2001 From: Sam Rueby Date: Mon, 22 Sep 2025 10:20:39 -0400 Subject: [PATCH 03/82] Only include a margin if there is text to separate from the icon. (#41851) ## Summary of the Pull Request Implemented conditional margin for tags with icons that do not included text. ## PR Checklist - [ X ] Closes: #41828 - [ ] **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 Created a new IValueConverter for icon margin to conditionally remove margin when Text is empty. ## Validation Steps Performed Below is a screenshot of the previous behavior. image Below is a screenshot of the new behavior. image --- .../Controls/IconMarginConverter.cs | 20 +++++++++++++++++++ .../Microsoft.CmdPal.UI/Controls/Tag.xaml | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/IconMarginConverter.cs diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/IconMarginConverter.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/IconMarginConverter.cs new file mode 100644 index 0000000000..0fb1e736f5 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/IconMarginConverter.cs @@ -0,0 +1,20 @@ +// 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 Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; + +namespace Microsoft.CmdPal.UI.Controls; + +public sealed class IconMarginConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + // Only include a margin if there is text to separate from the icon. + var text = value as string; + return string.IsNullOrEmpty(text) ? new Thickness(0) : new Thickness(0, 0, 4, 0); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml index aec0bb380e..7cf917b21c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml @@ -27,6 +27,8 @@ 4,2,4,2 1 + + diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesMainPage.xaml b/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesMainPage.xaml index de06561b50..f2628cf375 100644 --- a/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesMainPage.xaml +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesMainPage.xaml @@ -21,157 +21,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 12 diff --git a/src/modules/Hosts/HostsUILib/HostsMainPage.xaml b/src/modules/Hosts/HostsUILib/HostsMainPage.xaml index 762b4264c9..77b71ef5f1 100644 --- a/src/modules/Hosts/HostsUILib/HostsMainPage.xaml +++ b/src/modules/Hosts/HostsUILib/HostsMainPage.xaml @@ -27,160 +27,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index 82b3d9fb2c..a661d0a2a7 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -74,7 +74,6 @@ - @@ -162,9 +161,6 @@ MSBuild:Compile - - $(DefaultXamlRuntime) - $(DefaultXamlRuntime) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml deleted file mode 100644 index 0cca33265e..0000000000 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml b/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml index 57e073d8a5..c33a9d9d7d 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml +++ b/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml @@ -93,6 +93,7 @@ Grid.Column="4" VerticalAlignment="Center" Command="{x:Bind PinCommand, Mode=OneWay}" + Style="{StaticResource SubtleButtonStyle}" ToolTipService.ToolTip="{x:Bind PinToolTip(Pinned), Mode=OneWay}"> - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Styles/Button.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Styles/Button.xaml deleted file mode 100644 index a64117889c..0000000000 --- a/src/settings-ui/Settings.UI/SettingsXAML/Styles/Button.xaml +++ /dev/null @@ -1,800 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From d07f40eec3a3c3e35dfc3c22282d6a6da110edae Mon Sep 17 00:00:00 2001 From: Michael Jolley Date: Thu, 25 Sep 2025 13:48:13 -0500 Subject: [PATCH 24/82] CmdPal go brrrr (performance improvements) (#41959) Still a WIP, but here's the deets so far: ## No more throwing canceled tokens Throwing exceptions is expensive and since we essentially cancel tokens anytime someone is typing beyond the debounce, we could be throwing exceptions a ton during search. Since we don't care about those past executions, now they just `return`. ## Reduced number of apps returned in search While users can specify how many apps (no limit, 1, 5), if they specify no limit, we hard limit it at 10. For a few reasons, fuzzy search gets _really_ fuzzy sometimes and gives answers that users would think is just plain wrong and they make the response list longer than it needs to be. ## Fuzzy search: still fuzzy, but faster Replaced `StringMatcher` class with `FuzzyStringMatcher`. `FuzzyStringMatcher` is a C# port by @zadjii-msft of the Rust port by @lhecker for [microsoft/edit](https://github.com/microsoft/edit), which I believe originally came from [VS Code](https://github.com/microsoft/vscode). It's a whole fuzzy rabbit hole. But it's faster than the `StringMatcher` class it replaced. ## Fallbacks, you need to fall back "In the beginning, fallbacks were created. This had made many people very angry and has been widely regarded as a bad move." Hitchhiker's Guide to the Galaxy jokes aside, fallbacks are one cause of slower search results. A few modifications have been made to get them out of the way without reverting their ability to do things dynamically. 1. Fallbacks are no longer scored and will always* appear at the bottom of the search results 2. In updating their search text, we now use a cancellation token to stop processing previous searches when a new keypress is recorded. ## * But Calculator & Run are special So, remember when I said that all fallbacks will not be ranked and always display at the bottom of the results? Surprise, some will be ranked and displayed based on that score. Specifically, Calculator and Run are fallbacks that are whitelisted from the restrictions mentioned above. They will continue to act as they do today. We do have the ability to add future fallbacks to that whitelist as well. --- ## Current preview Updated: 2025-09-24 https://github.com/user-attachments/assets/c74c9a8e-e438-4101-840b-1408d2acaefd --- Closes #39763 Closes #39239 Closes #39948 Closes #38594 Closes #40330 --- .../ContextMenuViewModel.cs | 6 +- .../ListItemViewModel.cs | 5 +- .../ListViewModel.cs | 60 ++-- .../Commands/MainListPage.cs | 219 ++++++++++-- .../Helpers/TypedEventHandlerExtensions.cs | 5 +- .../CommandPaletteUnitTestBase.cs | 8 +- .../AllAppsCommandProvider.cs | 8 +- .../AllAppsSettings.cs | 4 - .../CalculatorCommandProvider.cs | 2 +- .../Pages/ShellListPage.cs | 2 +- .../ShellCommandsProvider.cs | 2 +- .../FallbackSystemCommandItem.cs | 4 +- .../Helpers/AvailableResult.cs | 5 +- .../FuzzyStringMatcher.cs | 182 ++++++++++ .../ListHelpers.cs | 6 +- .../StringMatcher.cs | 311 ------------------ 16 files changed, 435 insertions(+), 394 deletions(-) create mode 100644 src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FuzzyStringMatcher.cs delete mode 100644 src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/StringMatcher.cs diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs index cd2143200a..569d0c541f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs @@ -107,11 +107,11 @@ public partial class ContextMenuViewModel : ObservableObject, return 0; } - var nameMatch = StringMatcher.FuzzySearch(query, item.Title); + var nameMatch = FuzzyStringMatcher.ScoreFuzzy(query, item.Title); - var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle); + var descriptionMatch = FuzzyStringMatcher.ScoreFuzzy(query, item.Subtitle); - return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max(); + return new[] { nameMatch, (descriptionMatch - 4) / 2, 0 }.Max(); } /// diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs index 43dc24f72f..6547339ca1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs @@ -5,7 +5,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CommandPalette.Extensions; -using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Core.ViewModels; @@ -109,8 +108,6 @@ public partial class ListItemViewModel(IListItem model, WeakReference StringMatcher.FuzzySearch(filter, Title).Success || StringMatcher.FuzzySearch(filter, Subtitle).Success; - public override string ToString() => $"{Name} ListItemViewModel"; public override bool Equals(object? obj) => obj is ListItemViewModel vm && vm.Model.Equals(this.Model); @@ -132,7 +129,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference removedItems = []; lock (_listLock) @@ -264,13 +283,7 @@ public partial class ListViewModel : PageViewModel, IDisposable _initializeItemsTask = new Task(() => { - try - { - InitializeItemsTask(_cancellationTokenSource.Token); - } - catch (OperationCanceledException) - { - } + InitializeItemsTask(_cancellationTokenSource.Token); }); _initializeItemsTask.Start(); @@ -304,7 +317,10 @@ public partial class ListViewModel : PageViewModel, IDisposable private void InitializeItemsTask(CancellationToken ct) { // Were we already canceled? - ct.ThrowIfCancellationRequested(); + if (ct.IsCancellationRequested) + { + return; + } ListItemViewModel[] iterable; lock (_listLock) @@ -314,7 +330,10 @@ public partial class ListViewModel : PageViewModel, IDisposable foreach (var item in iterable) { - ct.ThrowIfCancellationRequested(); + if (ct.IsCancellationRequested) + { + return; + } // TODO: GH #502 // We should probably remove the item from the list if it @@ -323,7 +342,10 @@ public partial class ListViewModel : PageViewModel, IDisposable // at once. item.SafeInitializeProperties(); - ct.ThrowIfCancellationRequested(); + if (ct.IsCancellationRequested) + { + return; + } } } @@ -345,9 +367,9 @@ public partial class ListViewModel : PageViewModel, IDisposable return 1; } - var nameMatch = StringMatcher.FuzzySearch(query, listItem.Title); - var descriptionMatch = StringMatcher.FuzzySearch(query, listItem.Subtitle); - return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max(); + var nameMatch = FuzzyStringMatcher.ScoreFuzzy(query, listItem.Title); + var descriptionMatch = FuzzyStringMatcher.ScoreFuzzy(query, listItem.Subtitle); + return new[] { nameMatch, (descriptionMatch - 4) / 2, 0 }.Max(); } private struct ScoredListItemViewModel diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs index 2dfb934f1c..6b0fa8c1f0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Collections.Specialized; +using System.Diagnostics; using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; using Microsoft.CmdPal.Common.Helpers; @@ -22,20 +23,27 @@ namespace Microsoft.CmdPal.UI.ViewModels.MainPage; /// public partial class MainListPage : DynamicListPage, IRecipient, - IRecipient + IRecipient, IDisposable { - private readonly IServiceProvider _serviceProvider; + private readonly string[] _specialFallbacks = [ + "com.microsoft.cmdpal.builtin.run", + "com.microsoft.cmdpal.builtin.calculator" + ]; + private readonly IServiceProvider _serviceProvider; private readonly TopLevelCommandManager _tlcManager; private IEnumerable>? _filteredItems; private IEnumerable>? _filteredApps; - private IEnumerable? _allApps; + private IEnumerable>? _fallbackItems; private bool _includeApps; private bool _filteredItemsIncludesApps; + private int _appResultLimit = 10; private InterlockedBoolean _refreshRunning; private InterlockedBoolean _refreshRequested; + private CancellationTokenSource? _cancellationTokenSource; + public MainListPage(IServiceProvider serviceProvider) { Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png"); @@ -50,12 +58,12 @@ public partial class MainListPage : DynamicListPage, // We just want to know when it is done. var allApps = AllAppsCommandProvider.Page; allApps.PropChanged += (s, p) => - { - if (p.PropertyName == nameof(allApps.IsLoading)) { - IsLoading = ActuallyLoading(); - } - }; + if (p.PropertyName == nameof(allApps.IsLoading)) + { + IsLoading = ActuallyLoading(); + } + }; WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); @@ -150,10 +158,23 @@ public partial class MainListPage : DynamicListPage, { lock (_tlcManager.TopLevelCommands) { + IEnumerable> limitedApps = Enumerable.Empty>(); + + // Fuzzy matching can produce a lot of results, so we want to limit the + // number of apps we show at once if it's a large set. + if (_filteredApps?.Any() == true) + { + limitedApps = _filteredApps.OrderByDescending(s => s.Score).Take(_appResultLimit); + } + var items = Enumerable.Empty>() .Concat(_filteredItems is not null ? _filteredItems : []) - .Concat(_filteredApps is not null ? _filteredApps : []) + .Concat(limitedApps) .OrderByDescending(o => o.Score) + + // Add fallback items post-sort so they are always at the end of the list + // and eventually ordered based on user preference + .Concat(_fallbackItems is not null ? _fallbackItems.Where(w => !string.IsNullOrEmpty(w.Item.Title)) : []) .Select(s => s.Item) .ToArray(); return items; @@ -163,10 +184,29 @@ public partial class MainListPage : DynamicListPage, public override void UpdateSearchText(string oldSearch, string newSearch) { + var timer = new Stopwatch(); + timer.Start(); + + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = new CancellationTokenSource(); + + var token = _cancellationTokenSource.Token; + if (token.IsCancellationRequested) + { + return; + } + // Handle changes to the filter text here if (!string.IsNullOrEmpty(SearchText)) { var aliases = _serviceProvider.GetService()!; + + if (token.IsCancellationRequested) + { + return; + } + if (aliases.CheckAlias(newSearch)) { if (_filteredItemsIncludesApps != _includeApps) @@ -176,7 +216,6 @@ public partial class MainListPage : DynamicListPage, _filteredItemsIncludesApps = _includeApps; _filteredItems = null; _filteredApps = null; - _allApps = null; } } @@ -184,10 +223,20 @@ public partial class MainListPage : DynamicListPage, } } + if (token.IsCancellationRequested) + { + return; + } + var commands = _tlcManager.TopLevelCommands; lock (commands) { - UpdateFallbacks(newSearch, commands.ToImmutableArray()); + UpdateFallbacks(SearchText, commands.ToImmutableArray(), token); + + if (token.IsCancellationRequested) + { + return; + } // Cleared out the filter text? easy. Reset _filteredItems, and bail out. if (string.IsNullOrEmpty(newSearch)) @@ -195,7 +244,7 @@ public partial class MainListPage : DynamicListPage, _filteredItemsIncludesApps = _includeApps; _filteredItems = null; _filteredApps = null; - _allApps = null; + _fallbackItems = null; RaiseItemsChanged(commands.Count); return; } @@ -206,7 +255,7 @@ public partial class MainListPage : DynamicListPage, { _filteredItems = null; _filteredApps = null; - _allApps = null; + _fallbackItems = null; } // If the internal state has changed, reset _filteredItems to reset the list. @@ -214,61 +263,149 @@ public partial class MainListPage : DynamicListPage, { _filteredItems = null; _filteredApps = null; - _allApps = null; + _fallbackItems = null; } - var newFilteredItems = _filteredItems?.Select(s => s.Item); + if (token.IsCancellationRequested) + { + return; + } + + IEnumerable newFilteredItems = Enumerable.Empty(); + IEnumerable newFallbacks = Enumerable.Empty(); + IEnumerable newApps = Enumerable.Empty(); + + if (_filteredItems is not null) + { + newFilteredItems = _filteredItems.Select(s => s.Item); + } + + if (token.IsCancellationRequested) + { + return; + } + + if (_filteredApps is not null) + { + newApps = _filteredApps.Select(s => s.Item); + } + + if (token.IsCancellationRequested) + { + return; + } + + if (_fallbackItems is not null) + { + newFallbacks = _fallbackItems.Select(s => s.Item); + } + + if (token.IsCancellationRequested) + { + return; + } // If we don't have any previous filter results to work with, start // with a list of all our commands & apps. - if (newFilteredItems is null && _filteredApps is null) + if (!newFilteredItems.Any() && !newApps.Any()) { - newFilteredItems = commands; + // We're going to start over with our fallbacks + newFallbacks = Enumerable.Empty(); + + newFilteredItems = commands.Where(s => !s.IsFallback || _specialFallbacks.Contains(s.CommandProviderId)); + + // Fallbacks are always included in the list, even if they + // don't match the search text. But we don't want to + // consider them when filtering the list. + newFallbacks = commands.Where(s => s.IsFallback && !_specialFallbacks.Contains(s.CommandProviderId)); + + if (token.IsCancellationRequested) + { + return; + } + _filteredItemsIncludesApps = _includeApps; if (_includeApps) { - _allApps = AllAppsCommandProvider.Page.GetItems(); + newApps = AllAppsCommandProvider.Page.GetItems().ToList(); } } + if (token.IsCancellationRequested) + { + return; + } + + if (token.IsCancellationRequested) + { + return; + } + // Produce a list of everything that matches the current filter. _filteredItems = ListHelpers.FilterListWithScores(newFilteredItems ?? [], SearchText, ScoreTopLevelItem); - // Produce a list of filtered apps with the appropriate limit - if (_allApps is not null) - { - _filteredApps = ListHelpers.FilterListWithScores(_allApps, SearchText, ScoreTopLevelItem); + // Defaulting scored to 1 but we'll eventually use user rankings + _fallbackItems = newFallbacks.Select(f => new Scored { Item = f, Score = 1 }); - var appResultLimit = AllAppsCommandProvider.TopLevelResultLimit; - if (appResultLimit >= 0) + if (token.IsCancellationRequested) + { + return; + } + + // Produce a list of filtered apps with the appropriate limit + if (newApps.Any()) + { + var scoredApps = ListHelpers.FilterListWithScores(newApps, SearchText, ScoreTopLevelItem); + + if (token.IsCancellationRequested) { - _filteredApps = _filteredApps.Take(appResultLimit); + return; } + + // We'll apply this limit in the GetItems method after merging with commands + // but we need to know the limit now to avoid re-scoring apps + var appLimit = AllAppsCommandProvider.TopLevelResultLimit; + + _filteredApps = scoredApps; } RaiseItemsChanged(); + + timer.Stop(); + Logger.LogDebug($"Filter with '{newSearch}' in {timer.ElapsedMilliseconds}ms"); } } - private void UpdateFallbacks(string newSearch, IReadOnlyList commands) + private void UpdateFallbacks(string newSearch, IReadOnlyList commands, CancellationToken token) { - // fire and forget - _ = Task.Run(() => + _ = Task.Run( + () => { var needsToUpdate = false; foreach (var command in commands) { + if (token.IsCancellationRequested) + { + return; + } + var changedVisibility = command.SafeUpdateFallbackTextSynchronous(newSearch); needsToUpdate = needsToUpdate || changedVisibility; } if (needsToUpdate) { + if (token.IsCancellationRequested) + { + return; + } + RaiseItemsChanged(); } - }); + }, + token); } private bool ActuallyLoading() @@ -322,19 +459,19 @@ public partial class MainListPage : DynamicListPage, // * otherwise full weight match var nameMatch = isWhiteSpace ? (title.Contains(query) ? 1 : 0) : - StringMatcher.FuzzySearch(query, title).Score; + FuzzyStringMatcher.ScoreFuzzy(query, title); // Subtitle: // * whitespace query: 1/2 point // * otherwise ~half weight match. Minus a bit, because subtitles tend to be longer var descriptionMatch = isWhiteSpace ? (topLevelOrAppItem.Subtitle.Contains(query) ? .5 : 0) : - (StringMatcher.FuzzySearch(query, topLevelOrAppItem.Subtitle).Score - 4) / 2.0; + (FuzzyStringMatcher.ScoreFuzzy(query, topLevelOrAppItem.Subtitle) - 4) / 2.0; // Extension title: despite not being visible, give the extension name itself some weight // * whitespace query: 0 points // * otherwise more weight than a subtitle, but not much - var extensionTitleMatch = isWhiteSpace ? 0 : StringMatcher.FuzzySearch(query, extensionDisplayName).Score / 1.5; + var extensionTitleMatch = isWhiteSpace ? 0 : FuzzyStringMatcher.ScoreFuzzy(query, extensionDisplayName) / 1.5; var scores = new[] { @@ -397,4 +534,22 @@ public partial class MainListPage : DynamicListPage, private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender); private void HotReloadSettings(SettingsModel settings) => ShowDetails = settings.ShowAppDetails; + + public void Dispose() + { + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + + _tlcManager.PropertyChanged -= TlcManager_PropertyChanged; + _tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged; + + var settings = _serviceProvider.GetService(); + if (settings is not null) + { + settings.SettingsChanged -= SettingsChangedHandler; + } + + WeakReferenceMessenger.Default.UnregisterAll(this); + GC.SuppressFinalize(this); + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TypedEventHandlerExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TypedEventHandlerExtensions.cs index 8671f90f81..70bfffe6b3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TypedEventHandlerExtensions.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TypedEventHandlerExtensions.cs @@ -55,7 +55,10 @@ public static class TypedEventHandlerExtensions .OfType>() .Select(invocationDelegate => { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } invocationDelegate(sender, eventArgs); diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.UnitTestsBase/CommandPaletteUnitTestBase.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.UnitTestsBase/CommandPaletteUnitTestBase.cs index a4da29e830..29a32784ad 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.UnitTestsBase/CommandPaletteUnitTestBase.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.UnitTestsBase/CommandPaletteUnitTestBase.cs @@ -2,11 +2,7 @@ // 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.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -14,7 +10,9 @@ namespace Microsoft.CmdPal.Ext.UnitTestBase; public class CommandPaletteUnitTestBase { - private bool MatchesFilter(string filter, IListItem item) => StringMatcher.FuzzySearch(filter, item.Title).Success || StringMatcher.FuzzySearch(filter, item.Subtitle).Success; + private bool MatchesFilter(string filter, IListItem item) => + FuzzyStringMatcher.ScoreFuzzy(filter, item.Title) > 0 || + FuzzyStringMatcher.ScoreFuzzy(filter, item.Subtitle) > 0; public IListItem[] Query(string query, IListItem[] candidates) { 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 84d915f540..3329456960 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs @@ -51,21 +51,21 @@ public partial class AllAppsCommandProvider : CommandProvider if (limitSetting is null) { - return -1; + return 10; } - var quantity = -1; + var quantity = 10; if (int.TryParse(limitSetting, out var result)) { - quantity = result; + quantity = result < 0 ? quantity : result; } return quantity; } } - public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()]; + public override ICommandItem[] TopLevelCommands() => [_listItem, .. _page.GetPinnedApps()]; public ICommandItem? LookupApp(string displayName) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs index 320501fcdc..bf326221f9 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs @@ -18,16 +18,12 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; - private static string Experimental(string propertyName) => $"{_namespace}.experimental.{propertyName}"; - private static readonly List _searchResultLimitChoices = [ - new ChoiceSetSetting.Choice(Resources.limit_none, "-1"), new ChoiceSetSetting.Choice(Resources.limit_0, "0"), new ChoiceSetSetting.Choice(Resources.limit_1, "1"), new ChoiceSetSetting.Choice(Resources.limit_5, "5"), new ChoiceSetSetting.Choice(Resources.limit_10, "10"), - new ChoiceSetSetting.Choice(Resources.limit_20, "20"), ]; #pragma warning disable SA1401 // Fields should be private diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs index cdf0ccfa47..1cb0c57f28 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs @@ -23,7 +23,7 @@ public partial class CalculatorCommandProvider : CommandProvider public CalculatorCommandProvider() { - Id = "Calculator"; + Id = "com.microsoft.cmdpal.builtin.calculator"; DisplayName = Resources.calculator_display_name; Icon = Icons.CalculatorIcon; Settings = ((SettingsManager)settings).Settings; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs index fde17ba14c..06fcf7025b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs @@ -263,7 +263,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable var filterHistory = (string query, KeyValuePair pair) => { // Fuzzy search on the key (command string) - var score = StringMatcher.FuzzySearch(query, pair.Key).Score; + var score = FuzzyStringMatcher.ScoreFuzzy(query, pair.Key); return score; }; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs index a4bbeec5ea..8893486464 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/ShellCommandsProvider.cs @@ -24,7 +24,7 @@ public partial class ShellCommandsProvider : CommandProvider { _historyService = runHistoryService; - Id = "Run"; + Id = "com.microsoft.cmdpal.builtin.run"; DisplayName = Resources.cmd_plugin_name; Icon = Icons.RunV2Icon; Settings = _settingsManager.Settings; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs index d97d352559..adaa9f7c26 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/FallbackSystemCommandItem.cs @@ -47,8 +47,8 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem { var title = command.Title; var subTitle = command.Subtitle; - var titleScore = StringMatcher.FuzzySearch(query, title).Score; - var subTitleScore = StringMatcher.FuzzySearch(query, subTitle).Score; + var titleScore = FuzzyStringMatcher.ScoreFuzzy(query, title); + var subTitleScore = FuzzyStringMatcher.ScoreFuzzy(query, subTitle); var maxScore = Math.Max(titleScore, subTitleScore); if (maxScore > resultScore) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs index 3f54ca8438..6938875f80 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs @@ -1,7 +1,6 @@ // 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.Runtime.CompilerServices; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; @@ -64,12 +63,12 @@ internal sealed class AvailableResult public int Score(string query, string label, string tags) { // Get match for label (or for tags if label score is <1) - var score = StringMatcher.FuzzySearch(query, label).Score; + var score = FuzzyStringMatcher.ScoreFuzzy(query, label); if (score < 1) { foreach (var t in tags.Split(";")) { - var tagScore = StringMatcher.FuzzySearch(query, t.Trim()).Score / 2; + var tagScore = FuzzyStringMatcher.ScoreFuzzy(query, t.Trim()) / 2; if (tagScore > score) { score = tagScore / 2; diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FuzzyStringMatcher.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FuzzyStringMatcher.cs new file mode 100644 index 0000000000..f4591bc443 --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FuzzyStringMatcher.cs @@ -0,0 +1,182 @@ +// 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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +// Inspired by the fuzzy.rs from edit.exe +public static class FuzzyStringMatcher +{ + private const int NOMATCH = 0; + + public static int ScoreFuzzy(string needle, string haystack, bool allowNonContiguousMatches = true) + { + var (s, _) = ScoreFuzzyWithPositions(needle, haystack, allowNonContiguousMatches); + return s; + } + + public static (int Score, List Positions) ScoreFuzzyWithPositions(string needle, string haystack, bool allowNonContiguousMatches) + { + if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(needle)) + { + return (NOMATCH, new List()); + } + + var target = haystack.ToCharArray(); + var query = needle.ToCharArray(); + + if (target.Length < query.Length) + { + return (NOMATCH, new List()); + } + + var targetUpper = FoldCase(haystack); + var queryUpper = FoldCase(needle); + var targetUpperChars = targetUpper.ToCharArray(); + var queryUpperChars = queryUpper.ToCharArray(); + + var area = query.Length * target.Length; + var scores = new int[area]; + var matches = new int[area]; + + for (var qi = 0; qi < query.Length; qi++) + { + var qiOffset = qi * target.Length; + var qiPrevOffset = qi > 0 ? (qi - 1) * target.Length : 0; + + for (var ti = 0; ti < target.Length; ti++) + { + var currentIndex = qiOffset + ti; + var diagIndex = (qi > 0 && ti > 0) ? qiPrevOffset + ti - 1 : 0; + var leftScore = ti > 0 ? scores[currentIndex - 1] : 0; + var diagScore = (qi > 0 && ti > 0) ? scores[diagIndex] : 0; + var matchSeqLen = (qi > 0 && ti > 0) ? matches[diagIndex] : 0; + + var score = (diagScore == 0 && qi != 0) ? 0 : + ComputeCharScore( + query[qi], + queryUpperChars[qi], + ti != 0 ? target[ti - 1] : null, + target[ti], + targetUpperChars[ti], + matchSeqLen); + + var isValidScore = score != 0 && diagScore + score >= leftScore && + (allowNonContiguousMatches || qi > 0 || + targetUpperChars.Skip(ti).Take(queryUpperChars.Length).SequenceEqual(queryUpperChars)); + + if (isValidScore) + { + matches[currentIndex] = matchSeqLen + 1; + scores[currentIndex] = diagScore + score; + } + else + { + matches[currentIndex] = NOMATCH; + scores[currentIndex] = leftScore; + } + } + } + + var positions = new List(); + if (query.Length > 0 && target.Length > 0) + { + var qi = query.Length - 1; + var ti = target.Length - 1; + + while (true) + { + var index = (qi * target.Length) + ti; + if (matches[index] == NOMATCH) + { + if (ti == 0) + { + break; + } + + ti--; + } + else + { + positions.Add(ti); + if (qi == 0 || ti == 0) + { + break; + } + + qi--; + ti--; + } + } + + positions.Reverse(); + } + + return (scores[area - 1], positions); + } + + private static string FoldCase(string input) + { + return input.ToUpperInvariant(); + } + + private static int ComputeCharScore( + char query, + char queryLower, + char? targetPrev, + char targetCurr, + char targetLower, + int matchSeqLen) + { + if (!ConsiderAsEqual(queryLower, targetLower)) + { + return 0; + } + + var score = 1; // Character match bonus + + if (matchSeqLen > 0) + { + score += matchSeqLen * 5; // Consecutive match bonus + } + + if (query == targetCurr) + { + score += 1; // Same case bonus + } + + if (targetPrev.HasValue) + { + var sepBonus = ScoreSeparator(targetPrev.Value); + if (sepBonus > 0) + { + score += sepBonus; + } + else if (char.IsUpper(targetCurr) && matchSeqLen == 0) + { + score += 2; // CamelCase bonus + } + } + else + { + score += 8; // Start of word bonus + } + + return score; + } + + private static bool ConsiderAsEqual(char a, char b) + { + return a == b || (a == '/' && b == '\\') || (a == '\\' && b == '/'); + } + + private static int ScoreSeparator(char ch) + { + return ch switch + { + '/' or '\\' => 5, + '_' or '-' or '.' or ' ' or '\'' or '"' or ':' => 4, + _ => 0, + }; + } +} diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListHelpers.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListHelpers.cs index 441de9c713..3847ab8e55 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListHelpers.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListHelpers.cs @@ -19,17 +19,17 @@ public partial class ListHelpers return 0; } - var nameMatch = StringMatcher.FuzzySearch(query, listItem.Title); + var nameMatchScore = FuzzyStringMatcher.ScoreFuzzy(query, listItem.Title); // var locNameMatch = StringMatcher.FuzzySearch(query, NameLocalized); - var descriptionMatch = StringMatcher.FuzzySearch(query, listItem.Subtitle); + var descriptionMatchScore = FuzzyStringMatcher.ScoreFuzzy(query, listItem.Subtitle); // var executableNameMatch = StringMatcher.FuzzySearch(query, ExePath); // var locExecutableNameMatch = StringMatcher.FuzzySearch(query, ExecutableNameLocalized); // var lnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableName); // var locLnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableNameLocalized); // var score = new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, executableNameMatch.Score }.Max(); - return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max(); + return new[] { nameMatchScore, (descriptionMatchScore - 4) / 2, 0 }.Max(); } public static IEnumerable FilterList(IEnumerable items, string query) diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/StringMatcher.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/StringMatcher.cs deleted file mode 100644 index 6d9009661a..0000000000 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/StringMatcher.cs +++ /dev/null @@ -1,311 +0,0 @@ -// 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.Globalization; - -namespace Microsoft.CommandPalette.Extensions.Toolkit; - -public partial class StringMatcher -{ - private readonly MatchOption _defaultMatchOption = new(); - - public SearchPrecisionScore UserSettingSearchPrecision { get; set; } - - // private readonly IAlphabet _alphabet; - public StringMatcher(/*IAlphabet alphabet = null*/) - { - // _alphabet = alphabet; - } - - private static StringMatcher? _instance; - - public static StringMatcher Instance - { - get - { - _instance ??= new StringMatcher(); - - return _instance; - } - set => _instance = value; - } - - private static readonly char[] Separator = new[] { ' ' }; - - public static MatchResult FuzzySearch(string query, string stringToCompare) - { - return Instance.FuzzyMatch(query, stringToCompare); - } - - public MatchResult FuzzyMatch(string query, string stringToCompare) - { - try - { - return FuzzyMatch(query, stringToCompare, _defaultMatchOption); - } - catch (IndexOutOfRangeException) - { - return new MatchResult(false, UserSettingSearchPrecision); - } - } - - /// - /// Current method: - /// Character matching + substring matching; - /// 1. Query search string is split into substrings, separator is whitespace. - /// 2. Check each query substring's characters against full compare string, - /// 3. if a character in the substring is matched, loop back to verify the previous character. - /// 4. If previous character also matches, and is the start of the substring, update list. - /// 5. Once the previous character is verified, move on to the next character in the query substring. - /// 6. Move onto the next substring's characters until all substrings are checked. - /// 7. Consider success and move onto scoring if every char or substring without whitespaces matched - /// - public MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt) - { - if (string.IsNullOrEmpty(stringToCompare)) - { - return new MatchResult(false, UserSettingSearchPrecision); - } - - var bestResult = new MatchResult(false, UserSettingSearchPrecision); - - for (var startIndex = 0; startIndex < stringToCompare.Length; startIndex++) - { - MatchResult result = FuzzyMatch(query, stringToCompare, opt, startIndex); - if (result.Success && (!bestResult.Success || result.Score > bestResult.Score)) - { - bestResult = result; - } - } - - return bestResult; - } - - private MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt, int startIndex) - { - if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query)) - { - return new MatchResult(false, UserSettingSearchPrecision); - } - - ArgumentNullException.ThrowIfNull(opt); - - query = query.Trim(); - - // if (_alphabet is not null) - // { - // query = _alphabet.Translate(query); - // stringToCompare = _alphabet.Translate(stringToCompare); - // } - - // Using InvariantCulture since this is internal - var fullStringToCompareWithoutCase = opt.IgnoreCase ? stringToCompare.ToUpper(CultureInfo.InvariantCulture) : stringToCompare; - var queryWithoutCase = opt.IgnoreCase ? query.ToUpper(CultureInfo.InvariantCulture) : query; - - var querySubstrings = queryWithoutCase.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - var currentQuerySubstringIndex = 0; - var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; - var currentQuerySubstringCharacterIndex = 0; - - var firstMatchIndex = -1; - var firstMatchIndexInWord = -1; - var lastMatchIndex = 0; - var allQuerySubstringsMatched = false; - var matchFoundInPreviousLoop = false; - var allSubstringsContainedInCompareString = true; - - var indexList = new List(); - List spaceIndices = new List(); - - for (var compareStringIndex = startIndex; compareStringIndex < fullStringToCompareWithoutCase.Length; compareStringIndex++) - { - // To maintain a list of indices which correspond to spaces in the string to compare - // To populate the list only for the first query substring - if (fullStringToCompareWithoutCase[compareStringIndex].Equals(' ') && currentQuerySubstringIndex == 0) - { - spaceIndices.Add(compareStringIndex); - } - - bool compareResult; - if (opt.IgnoreCase) - { - var fullStringToCompare = fullStringToCompareWithoutCase[compareStringIndex].ToString(); - var querySubstring = currentQuerySubstring[currentQuerySubstringCharacterIndex].ToString(); -#pragma warning disable CA1309 // Use ordinal string comparison (We are looking for a fuzzy match here) - compareResult = string.Compare(fullStringToCompare, querySubstring, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) != 0; -#pragma warning restore CA1309 // Use ordinal string comparison - } - else - { - compareResult = fullStringToCompareWithoutCase[compareStringIndex] != currentQuerySubstring[currentQuerySubstringCharacterIndex]; - } - - if (compareResult) - { - matchFoundInPreviousLoop = false; - continue; - } - - if (firstMatchIndex < 0) - { - // first matched char will become the start of the compared string - firstMatchIndex = compareStringIndex; - } - - if (currentQuerySubstringCharacterIndex == 0) - { - // first letter of current word - matchFoundInPreviousLoop = true; - firstMatchIndexInWord = compareStringIndex; - } - else if (!matchFoundInPreviousLoop) - { - // we want to verify that there is not a better match if this is not a full word - // in order to do so we need to verify all previous chars are part of the pattern - var startIndexToVerify = compareStringIndex - currentQuerySubstringCharacterIndex; - - if (AllPreviousCharsMatched(startIndexToVerify, currentQuerySubstringCharacterIndex, fullStringToCompareWithoutCase, currentQuerySubstring)) - { - matchFoundInPreviousLoop = true; - - // if it's the beginning character of the first query substring that is matched then we need to update start index - firstMatchIndex = currentQuerySubstringIndex == 0 ? startIndexToVerify : firstMatchIndex; - - indexList = GetUpdatedIndexList(startIndexToVerify, currentQuerySubstringCharacterIndex, firstMatchIndexInWord, indexList); - } - } - - lastMatchIndex = compareStringIndex + 1; - indexList.Add(compareStringIndex); - - currentQuerySubstringCharacterIndex++; - - // if finished looping through every character in the current substring - if (currentQuerySubstringCharacterIndex == currentQuerySubstring.Length) - { - // if any of the substrings was not matched then consider as all are not matched - allSubstringsContainedInCompareString = matchFoundInPreviousLoop && allSubstringsContainedInCompareString; - - currentQuerySubstringIndex++; - - allQuerySubstringsMatched = AllQuerySubstringsMatched(currentQuerySubstringIndex, querySubstrings.Length); - if (allQuerySubstringsMatched) - { - break; - } - - // otherwise move to the next query substring - currentQuerySubstring = querySubstrings[currentQuerySubstringIndex]; - currentQuerySubstringCharacterIndex = 0; - } - } - - // proceed to calculate score if every char or substring without whitespaces matched - if (allQuerySubstringsMatched) - { - var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex); - var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString); - - return new MatchResult(true, UserSettingSearchPrecision, indexList, score); - } - - return new MatchResult(false, UserSettingSearchPrecision); - } - - // To get the index of the closest space which precedes the first matching index - private static int CalculateClosestSpaceIndex(List spaceIndices, int firstMatchIndex) - { - if (spaceIndices.Count == 0) - { - return -1; - } - else - { - return spaceIndices.OrderBy(item => (firstMatchIndex - item)).Where(item => firstMatchIndex > item).FirstOrDefault(-1); - } - } - - private static bool AllPreviousCharsMatched(int startIndexToVerify, int currentQuerySubstringCharacterIndex, string fullStringToCompareWithoutCase, string currentQuerySubstring) - { - var allMatch = true; - for (var indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++) - { - if (fullStringToCompareWithoutCase[startIndexToVerify + indexToCheck] != - currentQuerySubstring[indexToCheck]) - { - allMatch = false; - } - } - - return allMatch; - } - - private static List GetUpdatedIndexList(int startIndexToVerify, int currentQuerySubstringCharacterIndex, int firstMatchIndexInWord, List indexList) - { - var updatedList = new List(); - - indexList.RemoveAll(x => x >= firstMatchIndexInWord); - - updatedList.AddRange(indexList); - - for (var indexToCheck = 0; indexToCheck < currentQuerySubstringCharacterIndex; indexToCheck++) - { - updatedList.Add(startIndexToVerify + indexToCheck); - } - - return updatedList; - } - - private static bool AllQuerySubstringsMatched(int currentQuerySubstringIndex, int querySubstringsLength) - { - return currentQuerySubstringIndex >= querySubstringsLength; - } - - private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen, bool allSubstringsContainedInCompareString) - { - // A match found near the beginning of a string is scored more than a match found near the end - // A match is scored more if the characters in the patterns are closer to each other, - // while the score is lower if they are more spread out - - // The length of the match is assigned a larger weight factor. - // I.e. the length is more important than the location where a match is found. - const int matchLenWeightFactor = 2; - - var score = 100 * (query.Length + 1) * matchLenWeightFactor / ((1 + firstIndex) + (matchLenWeightFactor * (matchLen + 1))); - - // A match with less characters assigning more weights - if (stringToCompare.Length - query.Length < 5) - { - score += 20; - } - else if (stringToCompare.Length - query.Length < 10) - { - score += 10; - } - - if (allSubstringsContainedInCompareString) - { - var count = query.Count(c => !char.IsWhiteSpace(c)); - var threshold = 4; - if (count <= threshold) - { - score += count * 10; - } - else - { - score += (threshold * 10) + ((count - threshold) * 5); - } - } - -#pragma warning disable CA1309 // Use ordinal string comparison (Using CurrentCultureIgnoreCase since this relates to queries input by user) - if (string.Equals(query, stringToCompare, StringComparison.CurrentCultureIgnoreCase)) - { - var bonusForExactMatch = 10; - score += bonusForExactMatch; - } -#pragma warning restore CA1309 // Use ordinal string comparison - - return score; - } -} From 08a3ae2dee939fc3132f6be8e1a32aabd758e43b Mon Sep 17 00:00:00 2001 From: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:28:55 -0700 Subject: [PATCH 25/82] Enable "Space" only to activate Peek (#41867) ## Summary of the Pull Request Closes: #26143 This pull request introduces a new "single Space key activation" mode for the Peek PowerToy, allowing users to open Peek with just the Space key when File Explorer or the Desktop is focused. The implementation includes settings UI changes, backend logic to enforce and manage this mode, eligibility checks for activation, and telemetry. It also enhances the user experience by disabling the activation shortcut control when space mode is enabled and providing appropriate tooltips and localization. **Key changes:** ### Feature: Single Space Key Activation Mode * Added a new setting (`EnableSpaceToActivate`) to allow users to enable Peek activation using only the Space key, restricted to File Explorer or Desktop focus. When enabled, the activation shortcut is forced to bare Space and the previous shortcut is stashed (not restored on toggle-off for simplicity). (`src/modules/peek/peek/dllmain.cpp`, `src/settings-ui/Settings.UI.Library/PeekProperties.cs`, `src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs`, `src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml`, `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`) [[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R132-R169) [[2]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R79-R80) [[3]](diffhunk://#diff-d482fce7c2d0abbe2b307351ef7588378ddf34d47b31ebf71411f264dcce07faR22) [[4]](diffhunk://#diff-d482fce7c2d0abbe2b307351ef7588378ddf34d47b31ebf71411f264dcce07faR33-R35) [[5]](diffhunk://#diff-3fb87fad8b86d17fa39d2319425f78d3029e3de89e88f4040d449d6a16d9d240R228-R257) [[6]](diffhunk://#diff-f474be48688a195b3cce5b395ea6c0cbc93d7a76d228dcb5dc4fc33f36f2ce83L17-R51) [[7]](diffhunk://#diff-dada9baae540a067141b033257982d33df5a6a504e1a1d492fa2961bd04b6a03R3155-R3165) image * UI will hide the activation shortcut control. Attempts to change the shortcut programmatically are ignored while in this mode. (`src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml`, `src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs`, `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`) [[1]](diffhunk://#diff-f474be48688a195b3cce5b395ea6c0cbc93d7a76d228dcb5dc4fc33f36f2ce83L17-R51) [[2]](diffhunk://#diff-3fb87fad8b86d17fa39d2319425f78d3029e3de89e88f4040d449d6a16d9d240R173-R178) [[3]](diffhunk://#diff-3fb87fad8b86d17fa39d2319425f78d3029e3de89e88f4040d449d6a16d9d240R228-R257) [[4]](diffhunk://#diff-dada9baae540a067141b033257982d33df5a6a504e1a1d492fa2961bd04b6a03R3155-R3165) image ### Activation Logic & Eligibility * Implemented a foreground window hook and debounce logic to determine if Peek can be activated by Space (only when File Explorer, Desktop, or Peek itself is focused). This minimizes CPU overhead when user repeatedly presses Space but not for Peek . (`src/modules/peek/peek/dllmain.cpp`) [[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R50-R60) [[2]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R188-R292) [[3]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039L457-R637) * Managed hook installation and cleanup based on Peek's enabled state and the space mode toggle. (`src/modules/peek/peek/dllmain.cpp`) [[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R188-R292) [[2]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R562) [[3]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R593) [[4]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R496) ### Settings & Telemetry * Added the new toggle to the settings serialization and XAML UI, with localization and descriptions. (`src/modules/peek/peek/dllmain.cpp`, `src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml`, `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`) [[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R530) [[2]](diffhunk://#diff-f474be48688a195b3cce5b395ea6c0cbc93d7a76d228dcb5dc4fc33f36f2ce83L17-R51) [[3]](diffhunk://#diff-dada9baae540a067141b033257982d33df5a6a504e1a1d492fa2961bd04b6a03R3155-R3165) * Added telemetry event for enabling/disabling space mode. (`src/modules/peek/peek/trace.cpp`, `src/modules/peek/peek/trace.h`) [[1]](diffhunk://#diff-db76a3e6fa1cc19889492b72d0c063835bdc8f67909cb9d91c9e7e47e248a87aR51-R60) [[2]](diffhunk://#diff-8f824b0a7dd76f7fcd4a15b7885233b5b3212403a56c4efd67b83c4c2d02e486R18-R20) ### Code Quality * Refactored includes and initialization logic for clarity and maintainability. (`src/modules/peek/peek/dllmain.cpp`) [[1]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039L2-R14) [[2]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R37-R39) [[3]](diffhunk://#diff-ac3987a14b7c287a047f57613d97a78265f0dcef56084fb5361021953328b039R483) These changes collectively provide a safer, more accessible, and user-friendly way to activate Peek with a single key, while ensuring users are clearly informed and accidental activations are minimized. --------- Co-authored-by: Niels Laute --- src/common/ManagedCommon/Logger.cs | 4 + .../Peek.UI/Helpers/FileExplorerHelper.cs | 52 ++++- src/modules/peek/peek/dllmain.cpp | 220 ++++++++++++++++-- src/modules/peek/peek/trace.cpp | 10 + src/modules/peek/peek/trace.h | 3 + .../Settings.UI.Library/PeekProperties.cs | 3 + .../Settings.UI.Library/PeekSettings.cs | 14 +- .../SettingsXAML/Views/PeekPage.xaml | 10 +- .../Settings.UI/Strings/en-us/Resources.resw | 13 ++ .../Settings.UI/ViewModels/PeekViewModel.cs | 33 +++ 10 files changed, 336 insertions(+), 26 deletions(-) diff --git a/src/common/ManagedCommon/Logger.cs b/src/common/ManagedCommon/Logger.cs index 150d6ea355..11115b1846 100644 --- a/src/common/ManagedCommon/Logger.cs +++ b/src/common/ManagedCommon/Logger.cs @@ -19,7 +19,9 @@ namespace ManagedCommon private static readonly string Error = "Error"; private static readonly string Warning = "Warning"; private static readonly string Info = "Info"; +#if DEBUG private static readonly string Debug = "Debug"; +#endif private static readonly string TraceFlag = "Trace"; private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "Unknown"; @@ -151,7 +153,9 @@ namespace ManagedCommon public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) { +#if DEBUG Log(message, Debug, memberName, sourceFilePath, sourceLineNumber); +#endif } public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) diff --git a/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs b/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs index fb57e80047..89e814b6f4 100644 --- a/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs +++ b/src/modules/peek/Peek.UI/Helpers/FileExplorerHelper.cs @@ -115,19 +115,55 @@ namespace Peek.UI.Helpers } /// - /// Returns whether the caret is visible in the specified window. + /// Heuristic to decide whether the user is actively typing so we should suppress Peek activation. + /// Current logic: + /// - If the focused control class name contains "Edit" or "Input" (e.g. Explorer search box or in-place rename), return true. + /// - Otherwise fall back to the legacy GUI_CARETBLINKING flag (covers other text contexts where class name differs but caret blinks). + /// - If we fail to retrieve GUI thread info, we default to false (do not suppress) to avoid blocking activation due to transient failures. + /// NOTE: This intentionally no longer walks ancestor chains; any Edit/Input focus inside the same top-level Explorer/Desktop window is treated as typing. /// - private static bool CaretVisible(HWND hwnd) + private static unsafe bool CaretVisible(HWND hwnd) { - GUITHREADINFO guiThreadInfo = new() { cbSize = (uint)Marshal.SizeOf() }; - - // Get information for the foreground thread - if (PInvoke_PeekUI.GetGUIThreadInfo(0, ref guiThreadInfo)) + GUITHREADINFO gi = new() { cbSize = (uint)Marshal.SizeOf() }; + if (!PInvoke_PeekUI.GetGUIThreadInfo(0, ref gi)) { - return guiThreadInfo.hwndActive == hwnd && (guiThreadInfo.flags & GUITHREADINFO_FLAGS.GUI_CARETBLINKING) != 0; + return false; // fail open (allow activation) } - return false; + // Quick sanity: restrict to same top-level window (match prior behavior) + if (gi.hwndActive != hwnd) + { + return false; + } + + HWND focus = gi.hwndFocus; + if (focus == HWND.Null) + { + return false; + } + + // Get focused window class (96 chars buffer; GetClassNameW bounds writes). Treat any class containing + // "Edit" or "Input" as a text field (search / titlebar) and suppress Peek. + Span buf = stackalloc char[96]; + fixed (char* p = buf) + { + int len = PInvoke_PeekUI.GetClassName(focus, p, buf.Length); + if (len > 0) + { + var focusClass = new string(p, 0, len); + if (focusClass.Contains("Edit", StringComparison.OrdinalIgnoreCase) || focusClass.Contains("Input", StringComparison.OrdinalIgnoreCase)) + { + return true; // treat any Edit/Input focus as typing. + } + else + { + ManagedCommon.Logger.LogDebug($"Peek suppression: focus class{focusClass}"); + } + } + } + + // Fallback: original caret blinking heuristic for other text-entry contexts + return (gi.flags & GUITHREADINFO_FLAGS.GUI_CARETBLINKING) != 0; } } } diff --git a/src/modules/peek/peek/dllmain.cpp b/src/modules/peek/peek/dllmain.cpp index 4c3da5d999..1127df38bd 100644 --- a/src/modules/peek/peek/dllmain.cpp +++ b/src/modules/peek/peek/dllmain.cpp @@ -1,15 +1,17 @@ #include "pch.h" -#include +#include "trace.h" +#include +#include +#include +#include #include #include -#include "trace.h" -#include -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include extern "C" IMAGE_DOS_HEADER __ImageBase; @@ -32,6 +34,9 @@ BOOL APIENTRY DllMain(HMODULE /*hModule*/, return TRUE; } +// Forward declare global Peek so anonymous namespace uses same type +class Peek; + namespace { const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; @@ -42,6 +47,17 @@ namespace const wchar_t JSON_KEY_CODE[] = L"code"; const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut"; const wchar_t JSON_KEY_ALWAYS_RUN_NOT_ELEVATED[] = L"AlwaysRunNotElevated"; + const wchar_t JSON_KEY_ENABLE_SPACE_TO_ACTIVATE[] = L"EnableSpaceToActivate"; + + // Space activation (single-space mode) state + std::atomic_bool g_foregroundHookActive{ false }; // Foreground hook installed + std::atomic_bool g_foregroundEligible{ false }; // Cached eligibility (Explorer/Desktop/Peek focused) + HWINEVENTHOOK g_foregroundHook = nullptr; // Foreground change hook handle + constexpr DWORD FOREGROUND_DEBOUNCE_MS = 40; // Delay before eligibility recompute (ms) + HANDLE g_foregroundDebounceTimer = nullptr; // One-shot scheduled timer + std::atomic g_foregroundLastScheduleTick{ 0 }; // Tick count when timer last scheduled + + Peek* g_instance = nullptr; // pointer to active instance (global Peek) } // The PowerToy name that will be shown in the settings. @@ -60,6 +76,7 @@ private: // If we should always try to run Peek non-elevated. bool m_alwaysRunNotElevated = true; + bool m_enableSpaceToActivate = false; // toggle from settings HANDLE m_hProcess = 0; DWORD m_processPid = 0; @@ -111,11 +128,55 @@ private: m_alwaysRunNotElevated = true; } + try + { + auto jsonEnableSpaceObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ENABLE_SPACE_TO_ACTIVATE); + m_enableSpaceToActivate = jsonEnableSpaceObject.GetNamedBoolean(L"value"); + } + catch (...) + { + m_enableSpaceToActivate = false; + } + + // Enforce design: if space toggle ON, force single-space hotkey and store previous combination once. + if (m_enableSpaceToActivate) + { + if (!(m_hotkey.win || m_hotkey.alt || m_hotkey.shift || m_hotkey.ctrl) && m_hotkey.key == ' ') + { + // already single space + } + else + { + m_hotkey.win = false; + m_hotkey.alt = false; + m_hotkey.shift = false; + m_hotkey.ctrl = false; + m_hotkey.key = ' '; + } + } + else + { + // If toggle off and current hotkey is bare space, revert to default (simplified policy) + if (!(m_hotkey.win || m_hotkey.alt || m_hotkey.shift || m_hotkey.ctrl) && m_hotkey.key == ' ') + { + set_default_key_settings(); + } + } + + manage_space_mode_hook(); + Trace::SpaceModeEnabled(m_enableSpaceToActivate); } else { - Logger::info("Peek settings are empty"); - set_default_key_settings(); + // First-run (no existing settings file or empty JSON): default to Space-only activation + Logger::info("Peek settings are empty - initializing first-run defaults (Space activation)"); + m_enableSpaceToActivate = true; + m_hotkey.win = false; + m_hotkey.alt = false; + m_hotkey.shift = false; + m_hotkey.ctrl = false; + m_hotkey.key = ' '; + Trace::SpaceModeEnabled(true); } } @@ -129,6 +190,111 @@ private: m_hotkey.key = ' '; } + // Eligibility recompute (debounced via timer) +public: // callable from anonymous namespace helper + void recompute_space_mode_eligibility() + { + if (!m_enableSpaceToActivate) + { + g_foregroundEligible.store(false, std::memory_order_relaxed); + return; + } + const bool eligible = is_peek_or_explorer_or_desktop_window_focused(); + g_foregroundEligible.store(eligible, std::memory_order_relaxed); + Logger::debug(L"Peek space-mode eligibility recomputed: {}", eligible); + } + +private: + static void CALLBACK ForegroundDebounceTimerProc(PVOID /*param*/, BOOLEAN /*fired*/) + { + if (!g_instance || !g_foregroundHookActive.load(std::memory_order_relaxed)) + { + return; + } + g_instance->recompute_space_mode_eligibility(); + } + + static void CALLBACK ForegroundWinEventProc(HWINEVENTHOOK /*hook*/, DWORD /*event*/, HWND /*hwnd*/, LONG /*idObject*/, LONG /*idChild*/, DWORD /*thread*/, DWORD /*time*/) + { + if (!g_foregroundHookActive.load(std::memory_order_relaxed) || !g_instance) + { + return; + } + const DWORD now = GetTickCount(); + const DWORD last = g_foregroundLastScheduleTick.load(std::memory_order_relaxed); + // If no timer or sufficient time since last schedule, create a new one. + if (!g_foregroundDebounceTimer || (now - last) >= FOREGROUND_DEBOUNCE_MS || now < last) + { + if (g_foregroundDebounceTimer) + { + // Best effort: cancel previous pending timer; ignore failure. + DeleteTimerQueueTimer(nullptr, g_foregroundDebounceTimer, INVALID_HANDLE_VALUE); + g_foregroundDebounceTimer = nullptr; + } + if (CreateTimerQueueTimer(&g_foregroundDebounceTimer, nullptr, ForegroundDebounceTimerProc, nullptr, FOREGROUND_DEBOUNCE_MS, 0, WT_EXECUTEDEFAULT)) + { + g_foregroundLastScheduleTick.store(now, std::memory_order_relaxed); + } + else + { + Logger::warn(L"Peek failed to create foreground debounce timer"); + // Fallback: compute immediately if timer creation failed. + g_instance->recompute_space_mode_eligibility(); + } + } + } + + void install_foreground_hook() + { + if (g_foregroundHook || !m_enableSpaceToActivate) + { + return; + } + + g_instance = this; + g_foregroundHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, nullptr, ForegroundWinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS); + if (g_foregroundHook) + { + g_foregroundHookActive.store(true, std::memory_order_relaxed); + recompute_space_mode_eligibility(); + } + else + { + g_foregroundHookActive.store(false, std::memory_order_relaxed); + Logger::warn(L"Peek failed to install foreground hook. Falling back to polling."); + } + } + + void uninstall_foreground_hook() + { + if (g_foregroundHook) + { + UnhookWinEvent(g_foregroundHook); + g_foregroundHook = nullptr; + } + if (g_foregroundDebounceTimer) + { + DeleteTimerQueueTimer(nullptr, g_foregroundDebounceTimer, INVALID_HANDLE_VALUE); + g_foregroundDebounceTimer = nullptr; + } + g_foregroundLastScheduleTick.store(0, std::memory_order_relaxed); + g_foregroundHookActive.store(false, std::memory_order_relaxed); + g_foregroundEligible.store(false, std::memory_order_relaxed); + g_instance = nullptr; + } + + void manage_space_mode_hook() + { + if (m_enableSpaceToActivate && m_enabled) + { + install_foreground_hook(); + } + else + { + uninstall_foreground_hook(); + } + } + void parse_hotkey(winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject) { try @@ -319,6 +485,7 @@ private: public: Peek() { + LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", "Peek"); init_settings(); m_hInvokeEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_PEEK_SHARED_EVENT); @@ -331,6 +498,7 @@ public: { } m_enabled = false; + uninstall_foreground_hook(); }; // Destroy the powertoy and free memory @@ -364,6 +532,7 @@ public: // Create a Settings object. PowerToysSettings::Settings settings(hinstance, get_name()); settings.set_description(MODULE_DESC); + settings.add_bool_toggle(JSON_KEY_ENABLE_SPACE_TO_ACTIVATE, L"Enable single Space key activation", m_enableSpaceToActivate); return settings.serialize_to_buffer(buffer, buffer_size); } @@ -395,6 +564,7 @@ public: launch_process(); m_enabled = true; Trace::EnablePeek(true); + manage_space_mode_hook(); } // Disable the powertoy @@ -425,6 +595,7 @@ public: m_enabled = false; Trace::EnablePeek(false); + uninstall_foreground_hook(); } // Returns if the powertoys is enabled @@ -454,11 +625,21 @@ public: { if (m_enabled) { - Logger::trace(L"Peek hotkey pressed"); - - // Only activate and consume the shortcut if a Peek, explorer or desktop window is the foreground application. - if (is_peek_or_explorer_or_desktop_window_focused()) + bool spaceMode = m_enableSpaceToActivate && !(m_hotkey.win || m_hotkey.alt || m_hotkey.shift || m_hotkey.ctrl) && m_hotkey.key == ' '; + bool eligible = false; + if (spaceMode && g_foregroundHookActive.load(std::memory_order_relaxed)) { + eligible = g_foregroundEligible.load(std::memory_order_relaxed); + } + else + { + eligible = is_peek_or_explorer_or_desktop_window_focused(); + } + + if (eligible) + { + Logger::trace(L"Peek hotkey pressed and eligible for launching"); + // TODO: fix VK_SPACE DestroyWindow in viewer app if (!is_viewer_running()) { @@ -468,7 +649,16 @@ public: SetEvent(m_hInvokeEvent); Trace::PeekInvoked(); - return true; + + + if (spaceMode) + { + return false; + } + else + { + return true; + } } } diff --git a/src/modules/peek/peek/trace.cpp b/src/modules/peek/peek/trace.cpp index 529abb94f3..a1dd6355a2 100644 --- a/src/modules/peek/peek/trace.cpp +++ b/src/modules/peek/peek/trace.cpp @@ -48,3 +48,13 @@ void Trace::SettingsTelemetry(PowertoyModuleIface::Hotkey& hotkey) noexcept TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), TraceLoggingWideString(hotKeyStr.c_str(), "HotKey")); } + +void Trace::SpaceModeEnabled(bool enabled) noexcept +{ + TraceLoggingWriteWrapper( + g_hProvider, + "Peek_SpaceModeEnabled", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} diff --git a/src/modules/peek/peek/trace.h b/src/modules/peek/peek/trace.h index c250fc6b45..b5c22e7645 100644 --- a/src/modules/peek/peek/trace.h +++ b/src/modules/peek/peek/trace.h @@ -15,4 +15,7 @@ public: // Event to send settings telemetry. static void Trace::SettingsTelemetry(PowertoyModuleIface::Hotkey& hotkey) noexcept; + // Space mode telemetry (single-key activation toggle) + static void SpaceModeEnabled(bool enabled) noexcept; + }; diff --git a/src/settings-ui/Settings.UI.Library/PeekProperties.cs b/src/settings-ui/Settings.UI.Library/PeekProperties.cs index f81a3bc9a6..e6eea746d6 100644 --- a/src/settings-ui/Settings.UI.Library/PeekProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PeekProperties.cs @@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library AlwaysRunNotElevated = new BoolProperty(true); CloseAfterLosingFocus = new BoolProperty(false); ConfirmFileDelete = new BoolProperty(true); + EnableSpaceToActivate = new BoolProperty(true); // Toggle is ON by default for new users. No impact on existing users. } public HotkeySettings ActivationShortcut { get; set; } @@ -29,6 +30,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library public BoolProperty ConfirmFileDelete { get; set; } + public BoolProperty EnableSpaceToActivate { get; set; } + public override string ToString() => JsonSerializer.Serialize(this); } } diff --git a/src/settings-ui/Settings.UI.Library/PeekSettings.cs b/src/settings-ui/Settings.UI.Library/PeekSettings.cs index 73993c72fa..8bc4f6ee76 100644 --- a/src/settings-ui/Settings.UI.Library/PeekSettings.cs +++ b/src/settings-ui/Settings.UI.Library/PeekSettings.cs @@ -15,7 +15,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library public class PeekSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig { public const string ModuleName = "Peek"; - public const string ModuleVersion = "0.0.1"; + public const string InitialModuleVersion = "0.0.1"; + public const string SpaceActivationIntroducedVersion = "0.0.2"; + public const string CurrentModuleVersion = SpaceActivationIntroducedVersion; private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions { @@ -28,7 +30,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public PeekSettings() { Name = ModuleName; - Version = ModuleVersion; + Version = CurrentModuleVersion; Properties = new PeekProperties(); } @@ -54,6 +56,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library public bool UpgradeSettingsConfiguration() { + if (string.IsNullOrEmpty(Version) || + Version.Equals(InitialModuleVersion, StringComparison.OrdinalIgnoreCase)) + { + Version = CurrentModuleVersion; + Properties.EnableSpaceToActivate.Value = false; + return true; + } + return false; } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml index 5ad3a998ad..49da343744 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml @@ -22,11 +22,19 @@ + + + + + + + + HeaderIcon="{ui:FontIcon Glyph=}" + Visibility="{x:Bind ViewModel.EnableSpaceToActivate, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"> diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index d02ecafc21..9a3009ca80 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -3152,6 +3152,19 @@ Right-click to remove the key combination, thereby deactivating the shortcut. You'll be asked to confirm before files are moved to the Recycle Bin + + Activation method + + + Use a shortcut or press the Spacebar when a file is selected + Spacebar is a physical keyboard key + + + Custom shortcut + + + Spacebar + Disable rounded corners when a window is snapped diff --git a/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs index 3688e2e14d..85ffbda2d9 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs @@ -170,6 +170,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { if (_peekSettings.Properties.ActivationShortcut != value) { + // If space mode toggle is on, ignore external attempts to change (UI will be disabled, but defensive). + if (EnableSpaceToActivate) + { + return; + } + _peekSettings.Properties.ActivationShortcut = value ?? _peekSettings.Properties.DefaultActivationShortcut; OnPropertyChanged(nameof(ActivationShortcut)); NotifySettingsChanged(); @@ -219,6 +225,33 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public bool EnableSpaceToActivate + { + get => _peekSettings.Properties.EnableSpaceToActivate.Value; + set + { + if (_peekSettings.Properties.EnableSpaceToActivate.Value != value) + { + _peekSettings.Properties.EnableSpaceToActivate.Value = value; + + if (value) + { + // Force single space (0x20) without modifiers. + _peekSettings.Properties.ActivationShortcut = new HotkeySettings(false, false, false, false, 0x20); + } + else + { + // Revert to default (design simplification, not restoring previous custom combo). + _peekSettings.Properties.ActivationShortcut = _peekSettings.Properties.DefaultActivationShortcut; + } + + OnPropertyChanged(nameof(EnableSpaceToActivate)); + OnPropertyChanged(nameof(ActivationShortcut)); + NotifySettingsChanged(); + } + } + } + public bool SourceCodeWrapText { get => _peekPreviewSettings.SourceCodeWrapText.Value; From aef46481d98626faf008d78e1cb9f65749b8e6fa Mon Sep 17 00:00:00 2001 From: Kai Tao <69313318+vanzue@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:03:00 +0800 Subject: [PATCH 26/82] [Find My Mouse] Adding transparency support for spotlight (#41701) ## Summary of the Pull Request ### Feature Separate the find my mouse's spotlight area with the backdrop, so that we could support the frequent ask - We should leave the circle transparent in find my mouse ### Engineering: 1. Modernize the framework - From UWP composition to WASDK composition api ## PR Checklist - [x] Closes: #15512 - [ ] **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 - [x] Data migration: Should nota break existing experience when upgrade - [x] Should be able to configure the background and spotlight opacity - [x] Should be able to work with different settings https://github.com/user-attachments/assets/6f311c03-fa79-41d3-94bb-589d853295f4 --------- Co-authored-by: Niels Laute --- .github/actions/spell-check/expect.txt | 5 + Directory.Build.props | 1 - Directory.Build.targets | 5 + Directory.Packages.props | 1 + nuget.config | 2 +- .../MouseUtils/FindMyMouse/FindMyMouse.cpp | 383 +++++++++++++----- .../MouseUtils/FindMyMouse/FindMyMouse.h | 3 +- .../FindMyMouse/FindMyMouse.vcxproj | 70 +++- .../MouseUtils/FindMyMouse/dllmain.cpp | 108 +++-- .../MouseUtils/FindMyMouse/packages.config | 12 +- src/modules/MouseUtils/FindMyMouse/pch.h | 11 +- src/runner/main.cpp | 2 + src/runner/packages.config | 11 + src/runner/runner.vcxproj | 40 +- .../FindMyMouseProperties.cs | 8 +- .../Controls/AlphaColorPickerButton.xaml | 41 -- .../Controls/AlphaColorPickerButton.xaml.cs | 59 --- .../Controls/ColorPickerButton.xaml | 6 +- .../Controls/ColorPickerButton.xaml.cs | 11 +- .../SettingsXAML/Views/MouseUtilsPage.xaml | 19 +- .../ViewModels/MouseUtilsViewModel.cs | 24 +- 21 files changed, 513 insertions(+), 309 deletions(-) delete mode 100644 src/settings-ui/Settings.UI/SettingsXAML/Controls/AlphaColorPickerButton.xaml delete mode 100644 src/settings-ui/Settings.UI/SettingsXAML/Controls/AlphaColorPickerButton.xaml.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index f202cf06c6..b273d11b1d 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -918,6 +918,7 @@ LWA lwin LZero MAGTRANSFORM +MAJORMINOR MAKEINTRESOURCE MAKEINTRESOURCEA MAKEINTRESOURCEW @@ -996,6 +997,9 @@ mousepointercrosshairs mouseutils MOVESIZEEND MOVESIZESTART +muxx +muxxc +muxxh MRM MRT mru @@ -1807,6 +1811,7 @@ ULONGLONG ums uncompilable UNCPRIORITY +undefining UNDNAME UNICODETEXT unins diff --git a/Directory.Build.props b/Directory.Build.props index 4184a8f2a3..e7b415cbca 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -30,7 +30,6 @@ <_PropertySheetDisplayName>PowerToys.Root.Props $(MsbuildThisFileDirectory)\Cpp.Build.props - all diff --git a/Directory.Build.targets b/Directory.Build.targets index cba7762d5f..6da66bc8a8 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -3,4 +3,9 @@ + + + + $(WindowsSdkDir)bin\x64\mt.exe + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 23dac30258..e355bf6d7c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -37,6 +37,7 @@ + diff --git a/nuget.config b/nuget.config index 51f9b3b3f7..6b8d13a023 100644 --- a/nuget.config +++ b/nuget.config @@ -9,4 +9,4 @@ - + \ No newline at end of file diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp index 3049d3740c..adf5075837 100644 --- a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp +++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp @@ -8,21 +8,28 @@ #include "common/utils/process_path.h" #include "common/utils/excluded_apps.h" #include "common/utils/MsWindowsSettings.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + #include -#ifdef COMPOSITION namespace winrt { using namespace winrt::Windows::System; - using namespace winrt::Windows::UI::Composition; } -namespace ABI -{ - using namespace ABI::Windows::System; - using namespace ABI::Windows::UI::Composition::Desktop; -} -#endif +namespace muxc = winrt::Microsoft::UI::Composition; +namespace muxx = winrt::Microsoft::UI::Xaml; +namespace muxxc = winrt::Microsoft::UI::Xaml::Controls; +namespace muxxh = winrt::Microsoft::UI::Xaml::Hosting; #pragma region Super_Sonar_Base_Code @@ -70,11 +77,11 @@ protected: int m_sonarRadius = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS; int m_sonarZoomFactor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM; DWORD m_fadeDuration = FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS; - int m_finalAlphaNumerator = FIND_MY_MOUSE_DEFAULT_OVERLAY_OPACITY; + int m_finalAlphaNumerator = 100; // legacy (root now always animates to 1.0; kept for GDI fallback compatibility) std::vector m_excludedApps; int m_shakeMinimumDistance = FIND_MY_MOUSE_DEFAULT_SHAKE_MINIMUM_DISTANCE; static constexpr int FinalAlphaDenominator = 100; - winrt::DispatcherQueueController m_dispatcherQueueController{ nullptr }; + winrt::Microsoft::UI::Dispatching::DispatcherQueueController m_dispatcherQueueController{ nullptr }; // Don't consider movements started past these milliseconds to detect shaking. int m_shakeIntervalMs = FIND_MY_MOUSE_DEFAULT_SHAKE_INTERVAL_MS; @@ -82,7 +89,6 @@ protected: int m_shakeFactor = FIND_MY_MOUSE_DEFAULT_SHAKE_FACTOR; private: - // Save the mouse movement that occurred in any direction. struct PointerRecentMovement { @@ -159,7 +165,6 @@ bool SuperSonar::Initialize(HINSTANCE hinst) SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); WNDCLASS wc{}; - if (!GetClassInfoW(hinst, className, &wc)) { wc.lpfnWndProc = s_WndProc; @@ -171,14 +176,28 @@ bool SuperSonar::Initialize(HINSTANCE hinst) if (!RegisterClassW(&wc)) { + Logger::error("RegisterClassW failed. GetLastError={}", GetLastError()); return false; } } + // else: class already registered m_hwndOwner = CreateWindow(L"static", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hinst, nullptr); + if (!m_hwndOwner) + { + Logger::error("Failed to create owner window. GetLastError={}", GetLastError()); + return false; + } - DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle(); - return CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this) != nullptr; + DWORD exStyle = WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle(); + HWND created = CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this); + if (!created) + { + Logger::error("CreateWindowExW failed. GetLastError={}", GetLastError()); + return false; + } + + return true; } template @@ -226,7 +245,8 @@ LRESULT SuperSonar::BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) n switch (message) { case WM_CREATE: - if(!OnSonarCreate()) return -1; + if (!OnSonarCreate()) + return -1; UpdateMouseSnooping(); return 0; @@ -314,8 +334,7 @@ void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input) return; } - if ((m_activationMethod != FindMyMouseActivationMethod::DoubleRightControlKey && m_activationMethod != FindMyMouseActivationMethod::DoubleLeftControlKey) - || input.data.keyboard.VKey != VK_CONTROL) + if ((m_activationMethod != FindMyMouseActivationMethod::DoubleRightControlKey && m_activationMethod != FindMyMouseActivationMethod::DoubleLeftControlKey) || input.data.keyboard.VKey != VK_CONTROL) { StopSonar(); return; @@ -326,8 +345,7 @@ void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input) bool leftCtrlPressed = (input.data.keyboard.Flags & RI_KEY_E0) == 0; bool rightCtrlPressed = (input.data.keyboard.Flags & RI_KEY_E0) != 0; - if ((m_activationMethod == FindMyMouseActivationMethod::DoubleRightControlKey && !rightCtrlPressed) - || (m_activationMethod == FindMyMouseActivationMethod::DoubleLeftControlKey && !leftCtrlPressed)) + if ((m_activationMethod == FindMyMouseActivationMethod::DoubleRightControlKey && !rightCtrlPressed) || (m_activationMethod == FindMyMouseActivationMethod::DoubleLeftControlKey && !leftCtrlPressed)) { StopSonar(); return; @@ -376,7 +394,6 @@ void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input) GetCursorPos(&m_lastKeyPos); UpdateMouseSnooping(); } - Logger::info("Detecting double left control click with {} ms interval.", doubleClickInterval); m_lastKeyTime = now; m_lastKeyPos = ptCursor; } @@ -402,14 +419,13 @@ template void SuperSonar::DetectShake() { ULONGLONG shakeStartTick = GetTickCount64() - m_shakeIntervalMs; - + // Prune the story of movements for those movements that started too long ago. std::erase_if(m_movementHistory, [shakeStartTick](const PointerRecentMovement& movement) { return movement.tick < shakeStartTick; }); - - + double distanceTravelled = 0; - LONGLONG currentX=0, minX=0, maxX=0; - LONGLONG currentY=0, minY=0, maxY=0; + LONGLONG currentX = 0, minX = 0, maxX = 0; + LONGLONG currentY = 0, minY = 0, maxY = 0; for (const PointerRecentMovement& movement : m_movementHistory) { @@ -421,23 +437,22 @@ void SuperSonar::DetectShake() minY = min(currentY, minY); maxY = max(currentY, maxY); } - + if (distanceTravelled < m_shakeMinimumDistance) { return; } // Size of the rectangle that the pointer moved in. - double rectangleWidth = static_cast(maxX) - minX; - double rectangleHeight = static_cast(maxY) - minY; + double rectangleWidth = static_cast(maxX) - minX; + double rectangleHeight = static_cast(maxY) - minY; double diagonal = sqrt(rectangleWidth * rectangleWidth + rectangleHeight * rectangleHeight); - if (diagonal > 0 && distanceTravelled / diagonal > (m_shakeFactor/100.f)) + if (diagonal > 0 && distanceTravelled / diagonal > (m_shakeFactor / 100.f)) { m_movementHistory.clear(); StartSonar(); } - } template @@ -453,7 +468,7 @@ void SuperSonar::OnSonarMouseInput(RAWINPUT const& input) { LONG relativeX = 0; LONG relativeY = 0; - if ((input.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) == MOUSE_MOVE_ABSOLUTE && (input.data.mouse.lLastX!=0 || input.data.mouse.lLastY!=0)) + if ((input.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) == MOUSE_MOVE_ABSOLUTE && (input.data.mouse.lLastX != 0 || input.data.mouse.lLastY != 0)) { // Getting absolute mouse coordinates. Likely inside a VM / RDP session. if (m_seenAnAbsoluteMousePosition) @@ -482,7 +497,7 @@ void SuperSonar::OnSonarMouseInput(RAWINPUT const& input) } else { - m_movementHistory.push_back({ .diff = { .x=relativeX, .y=relativeY }, .tick = GetTickCount64() }); + m_movementHistory.push_back({ .diff = { .x = relativeX, .y = relativeY }, .tick = GetTickCount64() }); // Mouse movement changed directions. Take the opportunity do detect shake. DetectShake(); } @@ -491,7 +506,6 @@ void SuperSonar::OnSonarMouseInput(RAWINPUT const& input) { m_movementHistory.push_back({ .diff = { .x = relativeX, .y = relativeY }, .tick = GetTickCount64() }); } - } if (input.data.mouse.usButtonFlags) @@ -518,7 +532,6 @@ void SuperSonar::StartSonar() return; } - Logger::info("Focusing the sonar on the mouse cursor."); Trace::MousePointerFocused(); // Cover the entire virtual screen. // HACK: Draw with 1 pixel off. Otherwise, Windows glitches the task bar transparency when a transparent window fill the whole screen. @@ -633,12 +646,26 @@ struct CompositionSpotlight : SuperSonar DWORD GetExtendedStyle() { - return WS_EX_NOREDIRECTIONBITMAP; + // Remove WS_EX_NOREDIRECTIONBITMAP for Composition/XAML to allow DWM redirection. + return 0; } void AfterMoveSonar() { - m_spotlight.Offset({ static_cast(m_sonarPos.x), static_cast(m_sonarPos.y), 0.0f }); + const float scale = static_cast(m_surface.XamlRoot().RasterizationScale()); + // Move gradient center + if (m_spotlightMaskGradient) + { + m_spotlightMaskGradient.EllipseCenter({ static_cast(m_sonarPos.x) / scale, + static_cast(m_sonarPos.y) / scale }); + } + // Move spotlight visual (color fill) below masked backdrop + if (m_spotlight) + { + m_spotlight.Offset({ static_cast(m_sonarPos.x) / scale, + static_cast(m_sonarPos.y) / scale, + 0.0f }); + } } LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept @@ -646,24 +673,29 @@ struct CompositionSpotlight : SuperSonar switch (message) { case WM_CREATE: - return OnCompositionCreate() && BaseWndProc(message, wParam, lParam); + if (!OnCompositionCreate()) + return -1; + return BaseWndProc(message, wParam, lParam); case WM_OPACITY_ANIMATION_COMPLETED: OnOpacityAnimationCompleted(); break; + case WM_SIZE: + UpdateIslandSize(); + break; } return BaseWndProc(message, wParam, lParam); } void SetSonarVisibility(bool visible) { - m_batch = m_compositor.GetCommitBatch(winrt::CompositionBatchTypes::Animation); + m_batch = m_compositor.GetCommitBatch(muxc::CompositionBatchTypes::Animation); BOOL isEnabledAnimations = GetAnimationsEnabled(); m_animation.Duration(std::chrono::milliseconds{ isEnabledAnimations ? m_fadeDuration : 1 }); m_batch.Completed([hwnd = m_hwnd](auto&&, auto&&) { PostMessage(hwnd, WM_OPACITY_ANIMATION_COMPLETED, 0, 0); }); - m_root.Opacity(visible ? static_cast(m_finalAlphaNumerator) / FinalAlphaDenominator : 0.0f); + m_root.Opacity(visible ? 1.0f : 0.0f); if (visible) { ShowWindow(m_hwnd, SW_SHOWNOACTIVATE); @@ -679,54 +711,138 @@ private: bool OnCompositionCreate() try { - // We need a dispatcher queue. - DispatcherQueueOptions options = { - sizeof(options), - DQTYPE_THREAD_CURRENT, - DQTAT_COM_ASTA, - }; - ABI::IDispatcherQueueController* controller; - winrt::check_hresult(CreateDispatcherQueueController(options, &controller)); - *winrt::put_abi(m_dispatcherQueueController) = controller; + // Creating composition resources + // Ensure a DispatcherQueue bound to this thread (required by WinAppSDK composition/XAML) + if (!m_dispatcherQueueController) + { + // Ensure COM is initialized + try + { + winrt::init_apartment(winrt::apartment_type::single_threaded); + // COM STA initialized + } + catch (const winrt::hresult_error& e) + { + Logger::error("Failed to initialize COM apartment: {}", winrt::to_string(e.message())); + return false; + } - // Create the compositor for our window. - m_compositor = winrt::Compositor(); - ABI::IDesktopWindowTarget* target; - winrt::check_hresult(m_compositor.as()->CreateDesktopWindowTarget(m_hwnd, false, &target)); - *winrt::put_abi(m_target) = target; + try + { + m_dispatcherQueueController = + winrt::Microsoft::UI::Dispatching::DispatcherQueueController::CreateOnCurrentThread(); + // DispatcherQueueController created + } + catch (const winrt::hresult_error& e) + { + Logger::error("Failed to create DispatcherQueueController: {}", winrt::to_string(e.message())); + return false; + } + } - // Our composition tree: + // 1) Create a XAML island and attach it to this HWND + try + { + m_island = winrt::Microsoft::UI::Xaml::Hosting::DesktopWindowXamlSource{}; + auto windowId = winrt::Microsoft::UI::GetWindowIdFromWindow(m_hwnd); + m_island.Initialize(windowId); + // Xaml source initialized + } + catch (const winrt::hresult_error& e) + { + Logger::error("Failed to create XAML island: {}", winrt::to_string(e.message())); + return false; + } + + UpdateIslandSize(); + // Island size set + + // 2) Create a XAML container to host the Composition child visual + m_surface = winrt::Microsoft::UI::Xaml::Controls::Grid{}; + + // A transparent background keeps hit-testing consistent vs. null brush + m_surface.Background(winrt::Microsoft::UI::Xaml::Media::SolidColorBrush{ + winrt::Microsoft::UI::Colors::Transparent() }); + m_surface.HorizontalAlignment(muxx::HorizontalAlignment::Stretch); + m_surface.VerticalAlignment(muxx::VerticalAlignment::Stretch); + + m_island.Content(m_surface); + + // 3) Get the compositor from the XAML visual tree (pure MUXC path) + try + { + auto elementVisual = + winrt::Microsoft::UI::Xaml::Hosting::ElementCompositionPreview::GetElementVisual(m_surface); + m_compositor = elementVisual.Compositor(); + // Compositor acquired + } + catch (const winrt::hresult_error& e) + { + Logger::error("Failed to get compositor: {}", winrt::to_string(e.message())); + return false; + } + + // 4) Build the composition tree // - // [root] ContainerVisual - // \ LayerVisual - // \[gray backdrop] - // [spotlight] + // [root] ContainerVisual (fills host) + // \ LayerVisual + // \ [backdrop dim * radial gradient mask (hole)] m_root = m_compositor.CreateContainerVisual(); - m_root.RelativeSizeAdjustment({ 1.0f, 1.0f }); // fill the parent + m_root.RelativeSizeAdjustment({ 1.0f, 1.0f }); m_root.Opacity(0.0f); - m_target.Root(m_root); + + // Insert our root as a hand-in Visual under the XAML element + winrt::Microsoft::UI::Xaml::Hosting::ElementCompositionPreview::SetElementChildVisual(m_surface, m_root); auto layer = m_compositor.CreateLayerVisual(); - layer.RelativeSizeAdjustment({ 1.0f, 1.0f }); // fill the parent + layer.RelativeSizeAdjustment({ 1.0f, 1.0f }); m_root.Children().InsertAtTop(layer); - m_backdrop = m_compositor.CreateSpriteVisual(); - m_backdrop.RelativeSizeAdjustment({ 1.0f, 1.0f }); // fill the parent - m_backdrop.Brush(m_compositor.CreateColorBrush(m_backgroundColor)); - layer.Children().InsertAtTop(m_backdrop); + const float scale = static_cast(m_surface.XamlRoot().RasterizationScale()); + const float rDip = m_sonarRadiusFloat / scale; + const float zoom = static_cast(m_sonarZoomFactor); - m_circleGeometry = m_compositor.CreateEllipseGeometry(); // radius set via expression animation + // Spotlight shape (below backdrop, visible through hole) + m_circleGeometry = m_compositor.CreateEllipseGeometry(); m_circleShape = m_compositor.CreateSpriteShape(m_circleGeometry); m_circleShape.FillBrush(m_compositor.CreateColorBrush(m_spotlightColor)); - m_circleShape.Offset({ m_sonarRadiusFloat * m_sonarZoomFactor, m_sonarRadiusFloat * m_sonarZoomFactor }); + m_circleShape.Offset({ rDip * zoom, rDip * zoom }); m_spotlight = m_compositor.CreateShapeVisual(); - m_spotlight.Size({ m_sonarRadiusFloat * 2 * m_sonarZoomFactor, m_sonarRadiusFloat * 2 * m_sonarZoomFactor }); + m_spotlight.Size({ rDip * 2 * zoom, rDip * 2 * zoom }); m_spotlight.AnchorPoint({ 0.5f, 0.5f }); m_spotlight.Shapes().Append(m_circleShape); - layer.Children().InsertAtTop(m_spotlight); - // Implicitly animate the alpha. + // Dim color (source) + m_dimColorBrush = m_compositor.CreateColorBrush(m_backgroundColor); + // Radial gradient mask (center transparent, outer opaque) + m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush(); + m_spotlightMaskGradient.MappingMode(muxc::CompositionMappingMode::Absolute); + m_maskStopCenter = m_compositor.CreateColorGradientStop(); + m_maskStopCenter.Offset(0.0f); + m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); + m_maskStopInner = m_compositor.CreateColorGradientStop(); + m_maskStopInner.Offset(0.995f); + m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); + m_maskStopOuter = m_compositor.CreateColorGradientStop(); + m_maskStopOuter.Offset(1.0f); + m_maskStopOuter.Color(winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255)); + m_spotlightMaskGradient.ColorStops().Append(m_maskStopCenter); + m_spotlightMaskGradient.ColorStops().Append(m_maskStopInner); + m_spotlightMaskGradient.ColorStops().Append(m_maskStopOuter); + m_spotlightMaskGradient.EllipseCenter({ rDip * zoom, rDip * zoom }); + m_spotlightMaskGradient.EllipseRadius({ rDip * zoom, rDip * zoom }); + + m_maskBrush = m_compositor.CreateMaskBrush(); + m_maskBrush.Source(m_dimColorBrush); + m_maskBrush.Mask(m_spotlightMaskGradient); + + m_backdrop = m_compositor.CreateSpriteVisual(); + m_backdrop.RelativeSizeAdjustment({ 1.0f, 1.0f }); + m_backdrop.Brush(m_maskBrush); + layer.Children().InsertAtTop(m_backdrop); + + // 5) Implicit opacity animation on the root m_animation = m_compositor.CreateScalarKeyFrameAnimation(); m_animation.Target(L"Opacity"); m_animation.InsertExpressionKeyFrame(1.0f, L"this.FinalValue"); @@ -735,20 +851,31 @@ private: collection.Insert(L"Opacity", m_animation); m_root.ImplicitAnimations(collection); - // Radius of spotlight shrinks as opacity increases. - // At opacity zero, it is m_sonarRadius * SonarZoomFactor. - // At maximum opacity, it is m_sonarRadius. + // 6) Spotlight radius shrinks as opacity increases (expression animation) auto radiusExpression = m_compositor.CreateExpressionAnimation(); radiusExpression.SetReferenceParameter(L"Root", m_root); - wchar_t expressionText[256]; - winrt::check_hresult(StringCchPrintfW(expressionText, ARRAYSIZE(expressionText), L"Lerp(Vector2(%d, %d), Vector2(%d, %d), Root.Opacity * %d / %d)", m_sonarRadius * m_sonarZoomFactor, m_sonarRadius * m_sonarZoomFactor, m_sonarRadius, m_sonarRadius, FinalAlphaDenominator, m_finalAlphaNumerator)); - radiusExpression.Expression(expressionText); - m_circleGeometry.StartAnimation(L"Radius", radiusExpression); + wchar_t expressionText[256]; + winrt::check_hresult(StringCchPrintfW( + expressionText, ARRAYSIZE(expressionText), L"Lerp(Vector2(%d, %d), Vector2(%d, %d), Root.Opacity)", m_sonarRadius * m_sonarZoomFactor, m_sonarRadius * m_sonarZoomFactor, m_sonarRadius, m_sonarRadius)); + + radiusExpression.Expression(expressionText); + m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", radiusExpression); + // Also animate spotlight geometry radius for visual consistency + if (m_circleGeometry) + { + auto radiusExpression2 = m_compositor.CreateExpressionAnimation(); + radiusExpression2.SetReferenceParameter(L"Root", m_root); + radiusExpression2.Expression(expressionText); + m_circleGeometry.StartAnimation(L"Radius", radiusExpression2); + } + + // Composition created successfully return true; } - catch (...) + catch (const winrt::hresult_error& e) { + Logger::error("Failed to create FindMyMouse visual: {}", winrt::to_string(e.message())); return false; } @@ -760,11 +887,27 @@ private: } } + void UpdateIslandSize() + { + if (!m_island) + return; + + RECT rc{}; + if (!GetClientRect(m_hwnd, &rc)) + return; + + const int width = rc.right - rc.left; + const int height = rc.bottom - rc.top; + + auto bridge = m_island.SiteBridge(); + bridge.MoveAndResize(winrt::Windows::Graphics::RectInt32{ 0, 0, width, height }); + } + public: - void ApplySettings(const FindMyMouseSettings& settings, bool applyToRuntimeObjects) { + void ApplySettings(const FindMyMouseSettings& settings, bool applyToRuntimeObjects) + { if (!applyToRuntimeObjects) { - // Runtime objects not created yet. Just update fields. m_sonarRadius = settings.spotlightRadius; m_sonarRadiusFloat = static_cast(m_sonarRadius); m_backgroundColor = settings.backgroundColor; @@ -773,7 +916,6 @@ public: m_includeWinKey = settings.includeWinKey; m_doNotActivateOnGameMode = settings.doNotActivateOnGameMode; m_fadeDuration = settings.animationDurationMs > 0 ? settings.animationDurationMs : 1; - m_finalAlphaNumerator = settings.overlayOpacity; m_sonarZoomFactor = settings.spotlightInitialZoom; m_excludedApps = settings.excludedApps; m_shakeMinimumDistance = settings.shakeMinimumDistance; @@ -782,11 +924,9 @@ public: } else { - // Runtime objects already created. Should update in the owner thread. if (m_dispatcherQueueController == nullptr) { Logger::warn("Tried accessing the dispatch queue controller before it was initialized."); - // No dispatcher Queue Controller? Means initialization still hasn't run, so settings will be applied then. return; } auto dispatcherQueue = m_dispatcherQueueController.DispatcherQueue(); @@ -794,7 +934,6 @@ public: bool enqueueSucceeded = dispatcherQueue.TryEnqueue([=]() { if (!m_destroyed) { - // Runtime objects not created yet. Just update fields. m_sonarRadius = localSettings.spotlightRadius; m_sonarRadiusFloat = static_cast(m_sonarRadius); m_backgroundColor = localSettings.backgroundColor; @@ -803,7 +942,6 @@ public: m_includeWinKey = localSettings.includeWinKey; m_doNotActivateOnGameMode = localSettings.doNotActivateOnGameMode; m_fadeDuration = localSettings.animationDurationMs > 0 ? localSettings.animationDurationMs : 1; - m_finalAlphaNumerator = localSettings.overlayOpacity; m_sonarZoomFactor = localSettings.spotlightInitialZoom; m_excludedApps = localSettings.excludedApps; m_shakeMinimumDistance = localSettings.shakeMinimumDistance; @@ -812,20 +950,41 @@ public: UpdateMouseSnooping(); // For the shake mouse activation method // Apply new settings to runtime composition objects. - m_backdrop.Brush().as().Color(m_backgroundColor); - m_circleShape.FillBrush().as().Color(m_spotlightColor); - m_circleShape.Offset({ m_sonarRadiusFloat * m_sonarZoomFactor, m_sonarRadiusFloat * m_sonarZoomFactor }); - m_spotlight.Size({ m_sonarRadiusFloat * 2 * m_sonarZoomFactor, m_sonarRadiusFloat * 2 * m_sonarZoomFactor }); - m_animation.Duration(std::chrono::milliseconds{ m_fadeDuration }); - m_circleGeometry.StopAnimation(L"Radius"); - - // Update animation + if (m_dimColorBrush) + { + m_dimColorBrush.Color(m_backgroundColor); + } + if (m_circleShape) + { + if (auto brush = m_circleShape.FillBrush().try_as()) + { + brush.Color(m_spotlightColor); + } + } + const float scale = static_cast(m_surface.XamlRoot().RasterizationScale()); + const float rDip = m_sonarRadiusFloat / scale; + const float zoom = static_cast(m_sonarZoomFactor); + m_spotlightMaskGradient.StopAnimation(L"EllipseRadius"); + m_spotlightMaskGradient.EllipseCenter({ rDip * zoom, rDip * zoom }); + if (m_spotlight) + { + m_spotlight.Size({ rDip * 2 * zoom, rDip * 2 * zoom }); + m_circleShape.Offset({ rDip * zoom, rDip * zoom }); + } auto radiusExpression = m_compositor.CreateExpressionAnimation(); radiusExpression.SetReferenceParameter(L"Root", m_root); wchar_t expressionText[256]; - winrt::check_hresult(StringCchPrintfW(expressionText, ARRAYSIZE(expressionText), L"Lerp(Vector2(%d, %d), Vector2(%d, %d), Root.Opacity * %d / %d)", m_sonarRadius * m_sonarZoomFactor, m_sonarRadius * m_sonarZoomFactor, m_sonarRadius, m_sonarRadius, FinalAlphaDenominator, m_finalAlphaNumerator)); + winrt::check_hresult(StringCchPrintfW(expressionText, ARRAYSIZE(expressionText), L"Lerp(Vector2(%d, %d), Vector2(%d, %d), Root.Opacity)", m_sonarRadius * m_sonarZoomFactor, m_sonarRadius * m_sonarZoomFactor, m_sonarRadius, m_sonarRadius)); radiusExpression.Expression(expressionText); - m_circleGeometry.StartAnimation(L"Radius", radiusExpression); + m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", radiusExpression); + if (m_circleGeometry) + { + m_circleGeometry.StopAnimation(L"Radius"); + auto radiusExpression2 = m_compositor.CreateExpressionAnimation(); + radiusExpression2.SetReferenceParameter(L"Root", m_root); + radiusExpression2.Expression(expressionText); + m_circleGeometry.StartAnimation(L"Radius", radiusExpression2); + } } }); if (!enqueueSucceeded) @@ -836,17 +995,27 @@ public: } private: - winrt::Compositor m_compositor{ nullptr }; - winrt::Desktop::DesktopWindowTarget m_target{ nullptr }; - winrt::ContainerVisual m_root{ nullptr }; - winrt::CompositionEllipseGeometry m_circleGeometry{ nullptr }; - winrt::ShapeVisual m_spotlight{ nullptr }; - winrt::CompositionCommitBatch m_batch{ nullptr }; - winrt::SpriteVisual m_backdrop{ nullptr }; - winrt::CompositionSpriteShape m_circleShape{ nullptr }; + muxc::Compositor m_compositor{ nullptr }; + muxxh::DesktopWindowXamlSource m_island{ nullptr }; + muxxc::Grid m_surface{ nullptr }; + + muxc::ContainerVisual m_root{ nullptr }; + muxc::CompositionCommitBatch m_batch{ nullptr }; + muxc::SpriteVisual m_backdrop{ nullptr }; + // Spotlight shape visuals + muxc::CompositionEllipseGeometry m_circleGeometry{ nullptr }; + muxc::ShapeVisual m_spotlight{ nullptr }; + muxc::CompositionSpriteShape m_circleShape{ nullptr }; + // Radial gradient mask components + muxc::CompositionMaskBrush m_maskBrush{ nullptr }; + muxc::CompositionColorBrush m_dimColorBrush{ nullptr }; + muxc::CompositionRadialGradientBrush m_spotlightMaskGradient{ nullptr }; + muxc::CompositionColorGradientStop m_maskStopCenter{ nullptr }; + muxc::CompositionColorGradientStop m_maskStopInner{ nullptr }; + muxc::CompositionColorGradientStop m_maskStopOuter{ nullptr }; winrt::Windows::UI::Color m_backgroundColor = FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR; winrt::Windows::UI::Color m_spotlightColor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR; - winrt::ScalarKeyFrameAnimation m_animation{ nullptr }; + muxc::ScalarKeyFrameAnimation m_animation{ nullptr }; }; template @@ -1047,7 +1216,6 @@ struct GdiCrosshairs : GdiSonar #pragma endregion Super_Sonar_Base_Code - #pragma region Super_Sonar_API CompositionSpotlight* m_sonar = nullptr; @@ -1055,7 +1223,6 @@ void FindMyMouseApplySettings(const FindMyMouseSettings& settings) { if (m_sonar != nullptr) { - Logger::info("Applying settings."); m_sonar->ApplySettings(settings, true); } } @@ -1064,7 +1231,6 @@ void FindMyMouseDisable() { if (m_sonar != nullptr) { - Logger::info("Terminating a sonar instance."); m_sonar->Terminate(); } } @@ -1077,7 +1243,6 @@ bool FindMyMouseIsEnabled() // Based on SuperSonar's original wWinMain. int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings) { - Logger::info("Starting a sonar instance."); if (m_sonar != nullptr) { Logger::error("A sonar instance was still working when trying to start a new one."); @@ -1092,7 +1257,6 @@ int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings) return 0; } m_sonar = &sonar; - Logger::info("Initialized the sonar instance."); InitializeWinhookEventIds(); @@ -1105,7 +1269,6 @@ int FindMyMouseMain(HINSTANCE hinst, const FindMyMouseSettings& settings) DispatchMessage(&msg); } - Logger::info("Sonar message loop ended."); m_sonar = nullptr; return (int)msg.wParam; diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h index fb52bf11e5..857ec8b84f 100644 --- a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h +++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.h @@ -11,9 +11,9 @@ enum struct FindMyMouseActivationMethod : int }; constexpr bool FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE = true; +// Default colors now include full alpha. Opacity is encoded directly in color alpha (legacy overlay_opacity migrated into A channel) const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 0, 0, 0); const winrt::Windows::UI::Color FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR = winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255); -constexpr int FIND_MY_MOUSE_DEFAULT_OVERLAY_OPACITY = 50; constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS = 100; constexpr int FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS = 500; constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM = 9; @@ -30,7 +30,6 @@ struct FindMyMouseSettings bool doNotActivateOnGameMode = FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE; winrt::Windows::UI::Color backgroundColor = FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR; winrt::Windows::UI::Color spotlightColor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR; - int overlayOpacity = FIND_MY_MOUSE_DEFAULT_OVERLAY_OPACITY; int spotlightRadius = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS; int animationDurationMs = FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS; int spotlightInitialZoom = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM; diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj index 9d4dbd2b28..d127de245e 100644 --- a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj +++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj @@ -1,5 +1,12 @@ + + + + + + + 15.0 @@ -7,6 +14,14 @@ Win32Proj FindMyMouse FindMyMouse + true + false + false + false + true + false + + packages.config @@ -30,6 +45,7 @@ + ..\..\..\..\$(Platform)\$(Configuration)\ @@ -79,7 +95,8 @@ - $(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories) + + $(GeneratedFilesDir);$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;$(MSBuildThisFileDirectory)..\..\..\..\src\;$(MSBuildThisFileDirectory)..\..\..\..\src\modules;$(MSBuildThisFileDirectory)..\..\..\..\src\common\Telemetry;%(AdditionalIncludeDirectories) @@ -98,6 +115,7 @@ + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} @@ -112,16 +130,56 @@ + + + + + + + NotUsing + + + + + <_ToDelete Include="$(OutDir)Microsoft.Web.WebView2.Core.dll" /> + + + + - - - + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/FindMyMouse/dllmain.cpp b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp index 0518f468c2..b7ffb6177a 100644 --- a/src/modules/MouseUtils/FindMyMouse/dllmain.cpp +++ b/src/modules/MouseUtils/FindMyMouse/dllmain.cpp @@ -18,7 +18,7 @@ namespace const wchar_t JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE[] = L"do_not_activate_on_game_mode"; const wchar_t JSON_KEY_BACKGROUND_COLOR[] = L"background_color"; const wchar_t JSON_KEY_SPOTLIGHT_COLOR[] = L"spotlight_color"; - const wchar_t JSON_KEY_OVERLAY_OPACITY[] = L"overlay_opacity"; + const wchar_t JSON_KEY_OVERLAY_OPACITY[] = L"overlay_opacity"; // legacy only (migrated into color alpha) const wchar_t JSON_KEY_SPOTLIGHT_RADIUS[] = L"spotlight_radius"; const wchar_t JSON_KEY_ANIMATION_DURATION_MS[] = L"animation_duration_ms"; const wchar_t JSON_KEY_SPOTLIGHT_INITIAL_ZOOM[] = L"spotlight_initial_zoom"; @@ -204,6 +204,22 @@ void FindMyMouse::init_settings() } } +inline static uint8_t LegacyOpacityToAlpha(int overlayOpacityPercent) +{ + if (overlayOpacityPercent < 0) + { + return 255; // fallback: fully opaque + } + + if (overlayOpacityPercent > 100) + { + overlayOpacityPercent = 100; + } + + // Round to nearest integer (0255) + return static_cast((overlayOpacityPercent * 255 + 50) / 100); +} + void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings) { auto settingsObject = settings.get_raw_json(); @@ -224,14 +240,13 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings) } else { - findMyMouseSettings.activationMethod = static_cast(value); - } + findMyMouseSettings.activationMethod = static_cast(value); + } } else { throw std::runtime_error("Invalid Activation Method value"); } - } catch (...) { @@ -255,19 +270,49 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings) { Logger::warn("Failed to get 'do not activate on game mode' setting"); } + // Colors + legacy overlay opacity migration + // Desired behavior: + // - Old schema: colors stored as RGB (no alpha) + separate overlay_opacity (0-100). We should migrate by applying that opacity as alpha. + // - New schema: colors stored as ARGB (alpha embedded). Ignore overlay_opacity even if still present. + int legacyOverlayOpacity = -1; + bool backgroundColorHadExplicitAlpha = false; + bool spotlightColorHadExplicitAlpha = false; try { - // Parse background color - auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_BACKGROUND_COLOR); - auto backgroundColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE); - uint8_t r, g, b; - if (!checkValidRGB(backgroundColor, &r, &g, &b)) + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_OVERLAY_OPACITY); + int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); + if (value >= 0 && value <= 100) { - Logger::error("Background color RGB value is invalid. Will use default value"); + legacyOverlayOpacity = value; + } + } + catch (...) + { + // overlay_opacity may not exist anymore + } + try + { + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_BACKGROUND_COLOR); + auto backgroundColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE); + uint8_t a = 255, r, g, b; + bool parsed = false; + if (checkValidARGB(backgroundColorStr, &a, &r, &g, &b)) + { + parsed = true; + backgroundColorHadExplicitAlpha = true; // New schema with alpha present + } + else if (checkValidRGB(backgroundColorStr, &r, &g, &b)) + { + a = LegacyOpacityToAlpha(legacyOverlayOpacity); + parsed = true; // Old schema (no alpha component) + } + if (parsed) + { + findMyMouseSettings.backgroundColor = winrt::Windows::UI::ColorHelper::FromArgb(a, r, g, b); } else { - findMyMouseSettings.backgroundColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b); + Logger::error("Background color value is invalid. Will use default"); } } catch (...) @@ -276,17 +321,27 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings) } try { - // Parse spotlight color auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SPOTLIGHT_COLOR); - auto spotlightColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE); - uint8_t r, g, b; - if (!checkValidRGB(spotlightColor, &r, &g, &b)) + auto spotlightColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE); + uint8_t a = 255, r, g, b; + bool parsed = false; + if (checkValidARGB(spotlightColorStr, &a, &r, &g, &b)) { - Logger::error("Spotlight color RGB value is invalid. Will use default value"); + parsed = true; + spotlightColorHadExplicitAlpha = true; + } + else if (checkValidRGB(spotlightColorStr, &r, &g, &b)) + { + a = LegacyOpacityToAlpha(legacyOverlayOpacity); + parsed = true; + } + if (parsed) + { + findMyMouseSettings.spotlightColor = winrt::Windows::UI::ColorHelper::FromArgb(a, r, g, b); } else { - findMyMouseSettings.spotlightColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b); + Logger::error("Spotlight color value is invalid. Will use default"); } } catch (...) @@ -294,24 +349,6 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings) Logger::warn("Failed to initialize spotlight color from settings. Will use default value"); } try - { - // Parse Overlay Opacity - auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_OVERLAY_OPACITY); - int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); - if (value >= 0) - { - findMyMouseSettings.overlayOpacity = value; - } - else - { - throw std::runtime_error("Invalid Overlay Opacity value"); - } - } - catch (...) - { - Logger::warn("Failed to initialize Overlay Opacity from settings. Will use default value"); - } - try { // Parse Spotlight Radius auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SPOTLIGHT_RADIUS); @@ -492,7 +529,6 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings) m_findMyMouseSettings = findMyMouseSettings; } - extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() { return new FindMyMouse(); diff --git a/src/modules/MouseUtils/FindMyMouse/packages.config b/src/modules/MouseUtils/FindMyMouse/packages.config index 09bfc449e2..cff3aa8705 100644 --- a/src/modules/MouseUtils/FindMyMouse/packages.config +++ b/src/modules/MouseUtils/FindMyMouse/packages.config @@ -1,4 +1,12 @@ - + - \ No newline at end of file + + + + + + + + + diff --git a/src/modules/MouseUtils/FindMyMouse/pch.h b/src/modules/MouseUtils/FindMyMouse/pch.h index 26da2455f2..a0a8f1819c 100644 --- a/src/modules/MouseUtils/FindMyMouse/pch.h +++ b/src/modules/MouseUtils/FindMyMouse/pch.h @@ -5,15 +5,22 @@ #include #include #include +// Required for IUnknown and DECLARE_INTERFACE_* used by interop headers +#include #ifdef COMPOSITION -#include #include #include #include -#include +#include +#include +#include #endif #include #include #include + +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif \ No newline at end of file diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 31afbd0378..cc8e6ca2b0 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -335,6 +335,7 @@ int WINAPI WinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPSTR l GdiplusStartup(&gpToken, &gpStartupInput, NULL); winrt::init_apartment(); + const wchar_t* securityDescriptor = L"O:BA" // Owner: Builtin (local) administrator L"G:BA" // Group: Builtin (local) administrator @@ -526,5 +527,6 @@ int WINAPI WinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPSTR l } } stop_tray_icon(); + return result; } diff --git a/src/runner/packages.config b/src/runner/packages.config index ff4b059648..74d5ef5747 100644 --- a/src/runner/packages.config +++ b/src/runner/packages.config @@ -2,4 +2,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index 90dafb5e45..afff599d8e 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -1,8 +1,7 @@  - - + 81010002 @@ -15,10 +14,20 @@ + + + + + + + + Application v143 + None + true @@ -131,15 +140,38 @@ - - + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs b/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs index a028eb9e43..f1fe32e1ca 100644 --- a/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs +++ b/src/settings-ui/Settings.UI.Library/FindMyMouseProperties.cs @@ -31,9 +31,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("spotlight_color")] public StringProperty SpotlightColor { get; set; } - [JsonPropertyName("overlay_opacity")] - public IntProperty OverlayOpacity { get; set; } - [JsonPropertyName("spotlight_radius")] public IntProperty SpotlightRadius { get; set; } @@ -61,9 +58,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library IncludeWinKey = new BoolProperty(false); ActivationShortcut = DefaultActivationShortcut; DoNotActivateOnGameMode = new BoolProperty(true); - BackgroundColor = new StringProperty("#000000"); - SpotlightColor = new StringProperty("#FFFFFF"); - OverlayOpacity = new IntProperty(50); + BackgroundColor = new StringProperty("#FF000000"); // ARGB (#AARRGGBB) + SpotlightColor = new StringProperty("#FFFFFFFF"); SpotlightRadius = new IntProperty(100); AnimationDurationMs = new IntProperty(500); SpotlightInitialZoom = new IntProperty(9); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/AlphaColorPickerButton.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/AlphaColorPickerButton.xaml deleted file mode 100644 index c077042d96..0000000000 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/AlphaColorPickerButton.xaml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/AlphaColorPickerButton.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/AlphaColorPickerButton.xaml.cs deleted file mode 100644 index 24a0d0e448..0000000000 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/AlphaColorPickerButton.xaml.cs +++ /dev/null @@ -1,59 +0,0 @@ -// 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 Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Windows.UI; - -namespace Microsoft.PowerToys.Settings.UI.Controls -{ - public sealed partial class AlphaColorPickerButton : UserControl - { - private Color _selectedColor; - - public Color SelectedColor - { - get - { - return _selectedColor; - } - - set - { - if (_selectedColor != value) - { - _selectedColor = value; - SetValue(SelectedColorProperty, value); - } - } - } - - public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register("SelectedColor", typeof(Color), typeof(AlphaColorPickerButton), new PropertyMetadata(null)); - - public AlphaColorPickerButton() - { - this.InitializeComponent(); - IsEnabledChanged -= AlphaColorPickerButton_IsEnabledChanged; - SetEnabledState(); - IsEnabledChanged += AlphaColorPickerButton_IsEnabledChanged; - } - - private void AlphaColorPickerButton_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) - { - SetEnabledState(); - } - - private void SetEnabledState() - { - if (this.IsEnabled) - { - ColorPreviewBorder.Opacity = 1; - } - else - { - ColorPreviewBorder.Opacity = 0.2; - } - } - } -} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorPickerButton.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorPickerButton.xaml index 10a1e01236..4a46b7dc29 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorPickerButton.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorPickerButton.xaml @@ -27,9 +27,9 @@ (bool)GetValue(IsAlphaEnabledProperty); + set => SetValue(IsAlphaEnabledProperty, value); + } public ColorPickerButton() { diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml index 537d5a1071..514e959629 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml @@ -117,19 +117,12 @@ IsEnabled="{x:Bind ViewModel.IsFindMyMouseEnabled, Mode=OneWay}" IsExpanded="False"> - - - + - + - + - + - + - + Date: Fri, 26 Sep 2025 13:29:30 +0200 Subject: [PATCH 27/82] Removing WCT 7.x references (#41733) ## Summary of the Pull Request This PR introduces the following changes: - Replace the MarkdownTextBlock with the latest version in CommunityToolkit Labs, and removing the 7.x version. - Replacing WrapPanel from 7.x with the 8.0 version. - Replacing converters from 7.x with the 8.0 version. - Remove unused namespaces related to the Toolkit No visual or behavior changes, except for the release notes that now look better :): Before: image After: image ## PR Checklist - [ ] Closes: #xxx - [ ] **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 --------- Co-authored-by: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com> Co-authored-by: Gordon Lam (SH) --- Directory.Packages.props | 5 ++- NOTICE.md | 1 - .../ShortcutDialogContentControl.xaml | 4 +- .../ShortcutWithTextLabelControl.xaml | 4 +- .../ExtViews/ContentPage.xaml | 16 +++----- .../Microsoft.CmdPal.UI/Pages/ShellPage.xaml | 9 ++--- .../RegistryPreviewXAML/MainWindow.xaml | 3 -- .../Settings.UI.XamlIndexBuilder/Program.cs | 2 - .../Settings.UI/PowerToys.Settings.csproj | 2 +- .../Controls/ColorFormatEditor.xaml | 4 +- .../Controls/PowerAccentShortcutControl.xaml | 4 +- .../SettingsPageControl.xaml | 6 +-- .../ShortcutDialogContentControl.xaml | 5 +-- .../ShortcutWithTextLabelControl.xaml | 5 +-- .../OOBE/Views/OobeAlwaysOnTop.xaml | 4 +- .../SettingsXAML/OOBE/Views/OobeAwake.xaml | 6 +-- .../OOBE/Views/OobeCmdNotFound.xaml | 4 +- .../SettingsXAML/OOBE/Views/OobeCmdPal.xaml | 4 +- .../OOBE/Views/OobeColorPicker.xaml | 4 +- .../OOBE/Views/OobeFancyZones.xaml | 6 +-- .../OOBE/Views/OobeFileExplorer.xaml | 4 +- .../OOBE/Views/OobeFileLocksmith.xaml | 6 +-- .../OOBE/Views/OobeImageResizer.xaml | 6 +-- .../SettingsXAML/OOBE/Views/OobeKBM.xaml | 6 +-- .../OOBE/Views/OobeMeasureTool.xaml | 4 +- .../OOBE/Views/OobeMouseUtils.xaml | 10 ++--- .../OOBE/Views/OobeMouseWithoutBorders.xaml | 6 +-- .../SettingsXAML/OOBE/Views/OobeNewPlus.xaml | 6 +-- .../OOBE/Views/OobeOverviewPlaceholder.xaml | 1 - .../OOBE/Views/OobePowerAccent.xaml | 4 +- .../SettingsXAML/OOBE/Views/OobePowerOCR.xaml | 4 +- .../OOBE/Views/OobePowerRename.xaml | 6 +-- .../OOBE/Views/OobeRegistryPreview.xaml | 6 +-- .../SettingsXAML/OOBE/Views/OobeRun.xaml | 4 +- .../SettingsXAML/OOBE/Views/OobeWhatsNew.xaml | 39 +++++++++++-------- .../OOBE/Views/OobeWhatsNew.xaml.cs | 18 ++------- .../OOBE/Views/OobeWorkspaces.xaml | 6 +-- .../SettingsXAML/OOBE/Views/OobeZoomIt.xaml | 4 +- .../SettingsXAML/Views/AwakePage.xaml.cs | 2 - .../SettingsXAML/Views/ImageResizerPage.xaml | 4 +- .../Views/MouseWithoutBordersPage.xaml | 6 +-- 41 files changed, 114 insertions(+), 136 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e355bf6d7c..08bf6febee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,7 @@  true + true @@ -21,11 +22,11 @@ - - + + diff --git a/NOTICE.md b/NOTICE.md index bedc11379d..fc9f9b9696 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1509,7 +1509,6 @@ SOFTWARE. - CommunityToolkit.WinUI.Converters - CommunityToolkit.WinUI.Extensions - CommunityToolkit.WinUI.UI.Controls.DataGrid -- CommunityToolkit.WinUI.UI.Controls.Markdown - ControlzEx - HelixToolkit - HelixToolkit.Core.Wpf diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml index 8ab0fb7586..0000685ac3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml @@ -4,8 +4,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:Microsoft.CmdPal.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" x:Name="ShortcutContentControl" mc:Ignorable="d"> @@ -66,7 +66,7 @@ IsTabStop="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}" Severity="Warning" /> - @@ -36,7 +36,7 @@ - - - + - - + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml index a610e24726..5aecf7425a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml @@ -10,9 +10,8 @@ xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:help="using:Microsoft.CmdPal.UI.Helpers" - xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock" - xmlns:markdownTextBlockRns="using:CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" xmlns:toolkit="using:CommunityToolkit.WinUI.Controls" xmlns:ui="using:CommunityToolkit.WinUI" xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels" @@ -147,11 +146,11 @@ - - + @@ -422,7 +421,7 @@ TextWrapping="WrapWholeWords" Visibility="{x:Bind ViewModel.Details.Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}" /> - - + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml index ea6d1c9f22..743ced3204 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml @@ -9,7 +9,7 @@ xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls" + xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" mc:Ignorable="d"> @@ -61,7 +61,7 @@ - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/PowerAccentShortcutControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/PowerAccentShortcutControl.xaml index 09b2d7d26a..14de03d176 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/PowerAccentShortcutControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/PowerAccentShortcutControl.xaml @@ -5,7 +5,7 @@ xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls" + xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" d:DesignHeight="300" d:DesignWidth="400" mc:Ignorable="d"> @@ -35,7 +35,7 @@ - @@ -81,7 +81,7 @@ - + @@ -123,7 +123,7 @@ - + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml index b7fec717c7..3f345b8650 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml @@ -5,7 +5,7 @@ xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls" + xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" x:Name="ShortcutContentControl" mc:Ignorable="d"> @@ -71,9 +71,8 @@ Message="{Binding ElementName=ShortcutContentControl, Path=ConflictMessage, Mode=OneWay}" Severity="Warning" /> - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml index ea3be0bff8..bc46c9d17e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml @@ -5,7 +5,7 @@ xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls" + xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" d:DesignHeight="300" d:DesignWidth="400" mc:Ignorable="d"> @@ -38,11 +38,10 @@ - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAlwaysOnTop.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAlwaysOnTop.xaml index e029aa41f6..191fae9d1d 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAlwaysOnTop.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeAlwaysOnTop.xaml @@ -5,7 +5,7 @@ xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls" + xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" mc:Ignorable="d"> @@ -17,7 +17,7 @@ - +