diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index de9c5a062b..ef580c9681 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -1024,6 +1024,8 @@ MYICON
NAMECHANGE
namespaceanddescendants
nao
+Navigatable
+NavigatablePage
NCACTIVATE
ncc
NCCALCSIZE
@@ -1501,6 +1503,7 @@ SETRULES
SETSCREENSAVEACTIVE
SETSTICKYKEYS
SETTEXT
+settingscard
SETTINGCHANGE
SETTINGSCHANGED
settingsheader
@@ -1758,11 +1761,13 @@ transcodetomp
transicc
TRAYMOUSEMESSAGE
triaging
+Tru
trl
trx
tsa
tskill
tstoi
+tweakable
TWF
tymed
TYPEKEYBOARD
diff --git a/.gitignore b/.gitignore
index a1a09282a8..8859e53742 100644
--- a/.gitignore
+++ b/.gitignore
@@ -355,5 +355,8 @@ src/common/Telemetry/*.etl
# MSBuildCache
/MSBuildCacheLogs/
+# PowerToys Settings generated search index (legacy location) and obj outputs
+/src/settings-ui/Settings.UI/Assets/Settings/search.index.json
+
# PowerToysInstaller Build Temp Files
-installer/*/*.wxs.bk
+installer/*/*.wxs.bk
\ No newline at end of file
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index 6f8f817492..9cb1fcb7d5 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -28,6 +28,8 @@
"PowerToys.GPOWrapperProjection.dll",
"PowerToys.AllExperiments.dll",
+ "Common.Search.dll",
+
"PowerToys.AlwaysOnTop.exe",
"PowerToys.AlwaysOnTopModuleInterface.dll",
@@ -280,6 +282,7 @@
"Mono.Cecil.Pdb.dll",
"Mono.Cecil.Rocks.dll",
"Newtonsoft.Json.dll",
+ "CommunityToolkit.WinUI.Controls.TitleBar.dll",
"NLog.dll",
"HtmlAgilityPack.dll",
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 649c927a64..4c09a60664 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -23,6 +23,7 @@
+
diff --git a/NOTICE.md b/NOTICE.md
index bedc11379d..9861a58f4a 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1499,6 +1499,7 @@ SOFTWARE.
- CoenM.ImageSharp.ImageHash
- CommunityToolkit.Common
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
+- CommunityToolkit.Labs.WinUI.TitleBar
- CommunityToolkit.Mvvm
- CommunityToolkit.WinUI.Animations
- CommunityToolkit.WinUI.Collections
diff --git a/PowerToys.sln b/PowerToys.sln
index 2b6d42305f..cedc7f05b4 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -712,6 +712,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Preview.BgcodePreviewHandle
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Preview.BgcodeThumbnailProvider.UnitTests", "src\modules\previewpane\UnitTests-BgcodeThumbnailProvider\Preview.BgcodeThumbnailProvider.UnitTests.csproj", "{61CBF221-9452-4934-B685-146285E080D7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Search", "src\common\Common.Search\Common.Search.csproj", "{38F187B2-6638-5A40-072F-DBE5E54070A0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Settings.UI.XamlIndexBuilder", "src\settings-ui\Settings.UI.XamlIndexBuilder\Settings.UI.XamlIndexBuilder.csproj", "{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MouseUtils.UITests", "src\modules\MouseUtils\MouseUtils.UITests\MouseUtils.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Workspaces.Editor.UITests", "src\modules\Workspaces\WorkspacesEditorUITest\Workspaces.Editor.UITests.csproj", "{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}"
@@ -2707,6 +2711,30 @@ Global
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.Build.0 = Release|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.ActiveCfg = Release|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.Build.0 = Release|x64
+ {38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.Build.0 = Debug|ARM64
+ {38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.ActiveCfg = Debug|x64
+ {38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.Build.0 = Debug|x64
+ {38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.ActiveCfg = Release|ARM64
+ {38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.Build.0 = Release|ARM64
+ {38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.ActiveCfg = Release|x64
+ {38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.Build.0 = Release|x64
+ {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.Build.0 = Debug|ARM64
+ {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.ActiveCfg = Debug|x64
+ {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.Build.0 = Debug|x64
+ {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.ActiveCfg = Release|ARM64
+ {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.Build.0 = Release|ARM64
+ {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.ActiveCfg = Release|x64
+ {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.Build.0 = Release|x64
+ {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.Build.0 = Debug|ARM64
+ {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.ActiveCfg = Debug|x64
+ {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.Build.0 = Debug|x64
+ {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.ActiveCfg = Release|ARM64
+ {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.Build.0 = Release|ARM64
+ {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.ActiveCfg = Release|x64
+ {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.Build.0 = Release|x64
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.ActiveCfg = Debug|ARM64
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.Build.0 = Debug|ARM64
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|x64.ActiveCfg = Debug|x64
@@ -2900,6 +2928,7 @@ Global
{D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9} = {264B412F-DB8B-4CF8-A74B-96998B183045}
+ {1AFB6476-670D-4E80-A464-657E01DFF482} = {557C4636-D7E1-4838-A504-7D19B725EE95}
{1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
@@ -3167,6 +3196,8 @@ Global
{61CBF221-9452-4934-B685-146285E080D7} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704}
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {2C318EC3-BA86-4372-B1BC-DB0F33C208B2}
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9} = {68328142-5B31-4715-BCBB-7B6345EE0971}
+ {38F187B2-6638-5A40-072F-DBE5E54070A0} = {1AFB6476-670D-4E80-A464-657E01DFF482}
+ {DA0744BC-E822-680E-9CEB-D0FBA903A8EE} = {C3081D9A-1586-441A-B5F4-ED815B3719C1}
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{14AFD976-B4D2-49D0-9E6C-AA93CC061B8A} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{9D3F3793-EFE3-4525-8782-238015DABA62} = {66E1534A-1587-42B2-912F-45C994D32904}
diff --git a/doc/specs/search-result.png b/doc/specs/search-result.png
new file mode 100644
index 0000000000..4acde037e5
Binary files /dev/null and b/doc/specs/search-result.png differ
diff --git a/doc/specs/settings-search.md b/doc/specs/settings-search.md
new file mode 100644
index 0000000000..e99bb448b6
--- /dev/null
+++ b/doc/specs/settings-search.md
@@ -0,0 +1,233 @@
+# PowerToys Settings – Search Index (Hard-sealed)
+
+## 1. What to index
+
+This section describes the current structure of the settings pages in PowerToys. All user-facing settings are contained in the content of . The logical and visual structure of settings follows a nested layout as shown below:
+
+```css
+SettingsPageControl
+ └─ SettingsGroup
+ └─ [SettingsExpander]
+ └─ SettingsCard
+```
+* Each SettingsGroup defines a functional section within a settings page.
+
+* An optional SettingsExpander may be used to further organize related settings inside a group.
+
+* Each actual setting is represented by a SettingsCard, which contains one user-tweakable control or a group of closely related controls.
+
+>Note: Not all SettingsCard are necessarily wrapped in a SettingsExpander; they can exist directly under a SettingsGroup.
+
+> For indexing purposes, we are specifically targeting all SettingsCard elements. These are the smallest units of user interaction and correspond to individual configurable settings.
+
+> There could be setting item in expander, so we also need to index expander items as well.
+
+### Module
+Module is a primary type that needs to be indexed, for modules, we need to index the 'ModuleTitle' and the 'ModuleDescription'.
+So these two should be passed in by x:Uid and binding with a key.
+
+
+### SettingsCard/SettingsExpander
+
+Each SettingsCard/SettingsExpander should have an x:Uid for localization and indexing. The associated display strings are defined in the .resw files:
+
+{x:Uid}.Header – The visible label/title of the setting.
+{x:Uid}.Description – (optional) The tooltip or explanatory text.
+
+The index should be built around these SettingsCard elements and their x:Uid-bound resources, as they represent the actual settings users will search for.
+
+---
+
+## 2. How to Navigate
+
+### Entry
+```csharp
+enum EntryType
+{
+ SettingsPage,
+ SettingsCard,
+ SettingsExpander,
+}
+
+public class SearchableElementMetadata
+{
+ public string PageName { get; set; } // Used to navigate to a specific page
+ public EntryType Type { get; set; } // Used to know how should we navigate(As a page, a settingscard or an expander?)
+ public string ParentElementName { get; set; }
+ public string ElementName { get; set; }
+ public string ElementUid { get; set; }
+ public string Icon { get; set; }
+}
+```
+
+### Navigation
+The steps for navigate to an item:
+* Navigate among pages
+* [optional] Expand the expander if setting entry is inside an expander
+* [optional] Navigate within page
+
+> Use page name for navigation:
+```csharp
+Type GetPageTypeFromPageName(string PageName)
+{
+ var assembly = typeof(GeneralPage).Assembly;
+ return assembly.GetType($"Microsoft.PowerToys.Settings.UI.Views.{PageName}");
+}
+
+NavigationService.Navigate(PageType, ElementName,ParentElementName);
+```
+
+> Use ElementName and ParentElementName for in page navigation:
+```csharp
+Page.OnNavigateTo(ElementName, ParentElementName){
+ var element = this.FindName(name) as FrameworkElement;
+ var parentElement = this.FindName(ParentElementName) as FrameworkElement;
+
+ if(parentElement) {
+ expander = (Expander)parentElement;
+ if(expander){
+ expander.Expand();
+ }
+
+ // https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement.startbringintoview?view=winrt-26100
+ element.StartBringIntoView();
+ }
+}
+```
+
+## 3. Runtime Search
+When user start typing for an entry, e.g. shortcut or 快捷键(cn version of shortcut),
+we need to go through all the entries to see if an entry matches the search text.
+
+A naive approach will be try to match all the localized text one by one and see if they match.
+Total entry is within thousand(To fill in an exact number), performance is acceptable now.
+```csharp
+// Match
+query = UserInput();
+matched = {};
+
+indexes = BuildIndex();
+
+foreach(var entry in indexes) {
+ if(entry.Match(query)) {
+ matched.Add(entry);
+ }
+}
+```
+
+And we don't intend to introduce complexity on the match algorithm discussion, so let's use powertoys FuzzMatch impl for now.
+```csharp
+MatchResult Match(this Entry entry, string query) {
+ return FuzzMatch(entry.DisplayedText, query);
+}
+
+struct MatchResult{
+ int Score;
+ bool Result;
+}
+```
+
+## 4. Search Result Page
+
+After we got matched items, map these items to a search result page according to spec.
+```c#
+ObservableCollection ModuleResult;
+ObservableCollection GroupedSettingsResults;
+
+public class SettingsGroup : INotifyPropertyChanged
+{
+ private string _groupName;
+ private ObservableCollection _settings;
+ public string GroupName
+ {
+ get => _groupName;
+ set
+ {
+ _groupName = value;
+ OnPropertyChanged();
+ }
+ }
+ public ObservableCollection Settings
+ {
+ get => _settings;
+ set
+ {
+ _settings = value;
+ OnPropertyChanged();
+ }
+ }
+ public event PropertyChangedEventHandler PropertyChanged;
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
+```
+
+## 5. How to do Index
+### Runtime index or build time index?
+Now We need to build all the entries in our settings.
+
+Most of the entry properties are static, and in runtime, the `SettingsCard` is compiled into native winUI3 controls (I suppose, please correct here if it's wrong), it's hard to locate all the `SettingsCard`, and performance is terrible if we do search for all the pages' elements.
+
+### Build time indexing
+We can rely on xaml file parsing to get all the SettingsCard Entries.
+And we don't want xaml file to be brought into production bundle.
+Use a project for parsing and bring that index file into production bundle is a solution.
+```csproj
+
+
+ $(MSBuildProjectDirectory)\..\Settings.UI.XamlIndexBuilder\bin\$(Configuration)\net8.0\XamlIndexBuilder.exe
+ $(MSBuildProjectDirectory)\Views
+ $(MSBuildProjectDirectory)\Services\searchable_elements.json
+
+
+
+```
+```csharp
+for(xamlFile in xamlFiles){
+ var doc = Load(xamlFile);
+ var elements = doc.Descendants();
+
+ foreach(var element in elements){
+ if(element.Name == "SettingsCard") {
+ var entry = new Entry{
+ ElementName = element.Attribute["Name"],
+ PageName = FileName,
+ Type = "SettingsCard",
+ ElementUid = element.Attribute["Uid"],
+ DisplayedText = "",
+ }
+
+ var parent = element.GetParent();
+ if(parent.Name == "SettingsExpander"){
+ entry.ParentElementName = parent.Attribute["Name"];
+ }
+ }
+ }
+}
+```
+Runtime index loading:
+```
+var entries = LoadEntriesFromFile();
+foreach(var entry in entries){
+ entry.DisplayedText = ResourceLoader.GetString(entry.Uid);
+}
+```
+So now we have all the entries and entry properties.
+
+## Overall flow:
+
+
+
+
+## 6. Corner cases - that have not addressed yet
+
+1. CmdPal page is not in scope of this effort, that needs additional effort&design to launch and search within cmdpal settings page.
+
+2. Go back button
+
+3. Dynamic constructed settings page
+ - Shortcut guide, with visibility converter
+ - advanced paste dynamically configured setting items
+ - powertoys run's extensions
\ No newline at end of file
diff --git a/doc/specs/workflow.png b/doc/specs/workflow.png
new file mode 100644
index 0000000000..6920644fc5
Binary files /dev/null and b/doc/specs/workflow.png differ
diff --git a/src/common/Common.Search/Common.Search.csproj b/src/common/Common.Search/Common.Search.csproj
new file mode 100644
index 0000000000..b057bd658a
--- /dev/null
+++ b/src/common/Common.Search/Common.Search.csproj
@@ -0,0 +1,8 @@
+
+
+
+
+
+ enable
+
+
diff --git a/src/common/Common.Search/FuzzSearch/MatchOption.cs b/src/common/Common.Search/FuzzSearch/MatchOption.cs
new file mode 100644
index 0000000000..9f9f573f42
--- /dev/null
+++ b/src/common/Common.Search/FuzzSearch/MatchOption.cs
@@ -0,0 +1,10 @@
+// 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 Common.Search.FuzzSearch;
+
+public class MatchOption
+{
+ public bool IgnoreCase { get; set; } = true;
+}
diff --git a/src/common/Common.Search/FuzzSearch/MatchResult.cs b/src/common/Common.Search/FuzzSearch/MatchResult.cs
new file mode 100644
index 0000000000..f448f449a7
--- /dev/null
+++ b/src/common/Common.Search/FuzzSearch/MatchResult.cs
@@ -0,0 +1,67 @@
+// 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 Common.Search.FuzzSearch;
+
+public class MatchResult
+{
+ ///
+ /// The raw calculated search score without any search precision filtering applied.
+ ///
+ private int _rawScore;
+
+ public MatchResult(bool success, SearchPrecisionScore searchPrecision)
+ {
+ Success = success;
+ SearchPrecision = searchPrecision;
+ }
+
+ public MatchResult(bool success, SearchPrecisionScore searchPrecision, List matchData, int rawScore)
+ {
+ Success = success;
+ SearchPrecision = searchPrecision;
+ MatchData = matchData;
+ RawScore = rawScore;
+ }
+
+ public bool Success { get; set; }
+
+ ///
+ /// Gets the final score of the match result with search precision filters applied.
+ ///
+ public int Score { get; private set; }
+
+ public int RawScore
+ {
+ get => _rawScore;
+
+ set
+ {
+ _rawScore = value;
+ Score = ScoreAfterSearchPrecisionFilter(_rawScore);
+ }
+ }
+
+ ///
+ /// Gets matched data to highlight.
+ ///
+ public List MatchData { get; private set; } = new();
+
+ public SearchPrecisionScore SearchPrecision { get; set; }
+
+ public bool IsSearchPrecisionScoreMet()
+ {
+ return IsSearchPrecisionScoreMet(_rawScore);
+ }
+
+ private bool IsSearchPrecisionScoreMet(int rawScore)
+ {
+ return rawScore >= (int)SearchPrecision;
+ }
+
+ private int ScoreAfterSearchPrecisionFilter(int rawScore)
+ {
+ return IsSearchPrecisionScoreMet(rawScore) ? rawScore : 0;
+ }
+}
diff --git a/src/common/Common.Search/FuzzSearch/SearchPrecisionScore.cs b/src/common/Common.Search/FuzzSearch/SearchPrecisionScore.cs
new file mode 100644
index 0000000000..5f1917f14c
--- /dev/null
+++ b/src/common/Common.Search/FuzzSearch/SearchPrecisionScore.cs
@@ -0,0 +1,12 @@
+// 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 Common.Search.FuzzSearch;
+
+public enum SearchPrecisionScore
+{
+ Regular = 50,
+ Low = 20,
+ None = 0,
+}
diff --git a/src/common/Common.Search/FuzzSearch/StringMatcher.cs b/src/common/Common.Search/FuzzSearch/StringMatcher.cs
new file mode 100644
index 0000000000..be702bb8d9
--- /dev/null
+++ b/src/common/Common.Search/FuzzSearch/StringMatcher.cs
@@ -0,0 +1,272 @@
+// 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 Common.Search.FuzzSearch;
+
+public class StringMatcher
+{
+ public StringMatcher()
+ {
+ }
+
+ private static readonly char[] Separator = [' '];
+
+ ///
+ /// 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 static MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt = null)
+ {
+ opt = opt ?? new MatchOption();
+
+ if (string.IsNullOrEmpty(stringToCompare))
+ {
+ return new MatchResult(false, SearchPrecisionScore.Regular);
+ }
+
+ SearchPrecisionScore score = SearchPrecisionScore.Regular;
+
+ var bestResult = new MatchResult(false, score);
+
+ for (int 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 static MatchResult FuzzyMatch(string query, string stringToCompare, MatchOption opt, int startIndex)
+ {
+ if (string.IsNullOrEmpty(stringToCompare) || string.IsNullOrEmpty(query))
+ {
+ return new MatchResult(false, SearchPrecisionScore.Regular);
+ }
+
+ ArgumentNullException.ThrowIfNull(opt);
+
+ query = query.Trim();
+
+ // 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);
+ int currentQuerySubstringIndex = 0;
+ var currentQuerySubstring = querySubstrings[currentQuerySubstringIndex];
+ var currentQuerySubstringCharacterIndex = 0;
+
+ var firstMatchIndex = -1;
+ var firstMatchIndexInWord = -1;
+ var lastMatchIndex = 0;
+ bool allQuerySubstringsMatched = false;
+ bool matchFoundInPreviousLoop = false;
+ bool 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, SearchPrecisionScore.Regular, indexList, score);
+ }
+
+ return new MatchResult(false, SearchPrecisionScore.Regular);
+ }
+
+ // 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 (int 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 (int 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.
+ 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)
+ {
+ int count = query.Count(c => !char.IsWhiteSpace(c));
+ int 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;
+ }
+}
diff --git a/src/common/Common.Search/GlobalSuppressions.cs b/src/common/Common.Search/GlobalSuppressions.cs
new file mode 100644
index 0000000000..c1ccfc6882
--- /dev/null
+++ b/src/common/Common.Search/GlobalSuppressions.cs
@@ -0,0 +1,24 @@
+// 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.
+
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.MatchResult._rawScore")]
+[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._defaultMatchOption")]
+[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._instance")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.MatchResult.#ctor(System.Boolean,Common.Search.SearchPrecisionScore)")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.MatchResult.#ctor(System.Boolean,Common.Search.SearchPrecisionScore,System.Collections.Generic.List{System.Int32},System.Int32)")]
+[assembly: SuppressMessage("Compiler", "CS8618:Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.", Justification = "Coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._instance")]
+[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher._instance")]
+[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "coding style", Scope = "member", Target = "~F:Common.Search.StringMatcher.Separator")]
+[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.StringMatcher.#ctor")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.StringMatcher.FuzzyMatch(System.String,System.String)~Common.Search.MatchResult")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "coding style", Scope = "member", Target = "~M:Common.Search.StringMatcher.FuzzyMatch(System.String,System.String,Common.Search.MatchOption)~Common.Search.MatchResult")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1407:Arithmetic expressions should declare precedence", Justification = "migrate from stable code", Scope = "member", Target = "~M:Common.Search.StringMatcher.CalculateSearchScore(System.String,System.String,System.Int32,System.Int32,System.Boolean)~System.Int32")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "migrate from stable code", Scope = "member", Target = "~M:Common.Search.StringMatcher.FuzzyMatch(System.String,System.String,Common.Search.MatchOption,System.Int32)~Common.Search.MatchResult")]
+[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "migrate from stable code", Scope = "member", Target = "~M:Common.Search.StringMatcher.CalculateClosestSpaceIndex(System.Collections.Generic.List{System.Int32},System.Int32)~System.Int32")]
diff --git a/src/common/Common.Search/stylecop.json b/src/common/Common.Search/stylecop.json
new file mode 100644
index 0000000000..a338998bbc
--- /dev/null
+++ b/src/common/Common.Search/stylecop.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "documentationRules": {
+ "companyName": "Microsoft Corporation",
+ "copyrightText": "Copyright (c) {companyName}\r\nThe {companyName} licenses this file to you under the MIT license.\r\nSee the LICENSE file in the project root for more information.",
+ "xmlHeader": false,
+ "headerDecoration": "",
+ "fileNamingConvention": "metadata",
+ "documentInterfaces": false,
+ "documentExposedElements": false,
+ "documentInternalElements": false
+ },
+ "layoutRules": {
+ "newlineAtEndOfFile": "require"
+ },
+ "orderingRules": {
+ "usingDirectivesPlacement": "outsideNamespace",
+ "systemUsingDirectivesFirst": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI.Library/SettingEntry.cs b/src/settings-ui/Settings.UI.Library/SettingEntry.cs
new file mode 100644
index 0000000000..8f5f6ed0da
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/SettingEntry.cs
@@ -0,0 +1,44 @@
+// 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 Settings.UI.Library
+{
+ public enum EntryType
+ {
+ SettingsPage,
+ SettingsCard,
+ SettingsExpander,
+ }
+
+ public struct SettingEntry
+ {
+ public EntryType Type { get; set; }
+
+ public string Header { get; set; }
+
+ public string PageTypeName { get; set; }
+
+ public string ElementName { get; set; }
+
+ public string ElementUid { get; set; }
+
+ public string ParentElementName { get; set; }
+
+ public string Description { get; set; }
+
+ public string Icon { get; set; }
+
+ public SettingEntry(EntryType type, string header, string pageTypeName, string elementName, string elementUid, string parentElementName = null, string description = null, string icon = null)
+ {
+ Type = type;
+ Header = header;
+ PageTypeName = pageTypeName;
+ ElementName = elementName;
+ ElementUid = elementUid;
+ ParentElementName = parentElementName;
+ Description = description;
+ Icon = icon;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.XamlIndexBuilder/ModuleIconResolver.cs b/src/settings-ui/Settings.UI.XamlIndexBuilder/ModuleIconResolver.cs
new file mode 100644
index 0000000000..43c3ed2b05
--- /dev/null
+++ b/src/settings-ui/Settings.UI.XamlIndexBuilder/ModuleIconResolver.cs
@@ -0,0 +1,85 @@
+// 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.IO;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
+{
+ public static class ModuleIconResolver
+ {
+ // Hardcoded page-level overrides for module -> icon path
+ private static readonly System.Collections.Generic.Dictionary FileNameOverrides = new System.Collections.Generic.Dictionary(System.StringComparer.OrdinalIgnoreCase)
+ {
+ // Example overrides; expand as needed
+ { "FancyZonesPage.xaml", "/Assets/Settings/Icons/FancyZones.png" },
+ { "FileLocksmithPage.xaml", "/Assets/Settings/Icons/FileLocksmith.png" },
+ { "CmdNotFoundPage.xaml", "/Assets/Settings/Icons/CommandNotFound.png" },
+ { "PowerLauncherPage.xaml", "/Assets/Settings/Icons/PowerToysRun.png" },
+ };
+
+ // Contract:
+ // - Input: absolute path to the module XAML file (e.g., FancyZonesPage.xaml)
+ // - Output: app-relative icon path (e.g., "/Assets/Settings/Icons/FancyZones.png"), or null if not found
+ // - Strategy: take the first SettingsCard under the page and read its HeaderIcon value
+ public static string ResolveIconFromFirstSettingsCard(string xamlFilePath)
+ {
+ if (string.IsNullOrWhiteSpace(xamlFilePath))
+ {
+ return null;
+ }
+
+ try
+ {
+ var doc = XDocument.Load(xamlFilePath);
+
+ // Prefer looking inside SettingsPageControl.ModuleContent to avoid picking cards in Resources/DataTemplates
+ var pageControl = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "SettingsPageControl");
+
+ if (pageControl != null)
+ {
+ // Locate the property element
+ var moduleContent = pageControl
+ .Elements()
+ .FirstOrDefault(e => e.Name.LocalName.EndsWith(".ModuleContent", System.StringComparison.OrdinalIgnoreCase))
+ ?? pageControl
+ .Descendants()
+ .FirstOrDefault(e => e.Name.LocalName.EndsWith(".ModuleContent", System.StringComparison.OrdinalIgnoreCase));
+
+ if (moduleContent != null)
+ {
+ // Find the first SettingsCard under ModuleContent and try to read its HeaderIcon
+ var firstCardUnderModule = moduleContent
+ .Descendants()
+ .FirstOrDefault(e => e.Name.LocalName == "SettingsCard");
+
+ if (firstCardUnderModule != null)
+ {
+ var icon = Program.ExtractIconValue(firstCardUnderModule);
+ if (!string.IsNullOrWhiteSpace(icon))
+ {
+ return icon;
+ }
+ }
+ }
+ }
+
+ // Fallback to hardcoded overrides by file name
+ var fileName = Path.GetFileName(xamlFilePath);
+ if (!string.IsNullOrEmpty(fileName) && FileNameOverrides.TryGetValue(fileName, out var overrideIcon))
+ {
+ return overrideIcon;
+ }
+
+ return null;
+ }
+ catch
+ {
+ // Non-fatal: let caller decide fallback
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.XamlIndexBuilder/Program.cs b/src/settings-ui/Settings.UI.XamlIndexBuilder/Program.cs
new file mode 100644
index 0000000000..d3ebad0ff7
--- /dev/null
+++ b/src/settings-ui/Settings.UI.XamlIndexBuilder/Program.cs
@@ -0,0 +1,314 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Xml.Linq;
+
+namespace Microsoft.PowerToys.Tools.XamlIndexBuilder
+{
+ public class Program
+ {
+ private static readonly HashSet ExcludedXamlFiles = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "ShellPage.xaml",
+ };
+
+ private static JsonSerializerOptions serializeOption = new()
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ public static void Main(string[] args)
+ {
+ if (args.Length < 2)
+ {
+ Debug.WriteLine("Usage: XamlIndexBuilder ");
+ Environment.Exit(1);
+ }
+
+ string xamlDirectory = args[0];
+ string outputFile = args[1];
+
+ if (!Directory.Exists(xamlDirectory))
+ {
+ Debug.WriteLine($"Error: Directory '{xamlDirectory}' does not exist.");
+ Environment.Exit(1);
+ }
+
+ try
+ {
+ var searchableElements = new List();
+ var xamlFiles = Directory.GetFiles(xamlDirectory, "*.xaml", SearchOption.AllDirectories);
+
+ foreach (var xamlFile in xamlFiles)
+ {
+ var fileName = Path.GetFileName(xamlFile);
+ if (ExcludedXamlFiles.Contains(fileName))
+ {
+ // Skip ShellPage.xaml as it contains many elements not relevant for search
+ continue;
+ }
+
+ Debug.WriteLine($"Processing: {fileName}");
+ var elements = ExtractSearchableElements(xamlFile);
+ searchableElements.AddRange(elements);
+ }
+
+ searchableElements = searchableElements.OrderBy(e => e.PageTypeName).ThenBy(e => e.ElementName).ToList();
+
+ string json = JsonSerializer.Serialize(searchableElements, serializeOption);
+ File.WriteAllText(outputFile, json);
+
+ Debug.WriteLine($"Successfully generated index with {searchableElements.Count} elements.");
+ Debug.WriteLine($"Output written to: {outputFile}");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error: {ex.Message}");
+ Environment.Exit(1);
+ }
+ }
+
+ public static List ExtractSearchableElements(string xamlFile)
+ {
+ var elements = new List();
+ string pageName = Path.GetFileNameWithoutExtension(xamlFile);
+
+ try
+ {
+ // Load XAML as XML
+ var doc = XDocument.Load(xamlFile);
+
+ // Define namespaces
+ XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml";
+ XNamespace controls = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
+ XNamespace labs = "using:CommunityToolkit.Labs.WinUI";
+ XNamespace winui = "using:CommunityToolkit.WinUI.UI.Controls";
+
+ // Extract SettingsPageControl elements
+ var settingsPageElements = doc.Descendants()
+ .Where(e => e.Name.LocalName == "SettingsPageControl")
+ .Where(e => e.Attribute(x + "Uid") != null);
+
+ // Extract SettingsCard elements
+ var settingsElements = doc.Descendants()
+ .Where(e => e.Name.LocalName == "SettingsCard")
+ .Where(e => e.Attribute("Name") != null || e.Attribute(x + "Uid") != null);
+
+ // Extract SettingsExpander elements
+ var settingsExpanderElements = doc.Descendants()
+ .Where(e => e.Name.LocalName == "SettingsExpander")
+ .Where(e => e.Attribute("Name") != null || e.Attribute(x + "Uid") != null);
+
+ // Process SettingsPageControl elements
+ foreach (var element in settingsPageElements)
+ {
+ var elementUid = GetElementUid(element, x);
+
+ // Prefer the first SettingsCard.HeaderIcon as the module icon
+ var moduleImageSource = ModuleIconResolver.ResolveIconFromFirstSettingsCard(xamlFile);
+
+ if (!string.IsNullOrEmpty(elementUid))
+ {
+ elements.Add(new SettingEntry
+ {
+ PageTypeName = pageName,
+ Type = EntryType.SettingsPage,
+ ParentElementName = string.Empty,
+ ElementName = string.Empty,
+ ElementUid = elementUid,
+ Icon = moduleImageSource,
+ });
+ }
+ }
+
+ // Process SettingsCard elements
+ foreach (var element in settingsElements)
+ {
+ var elementName = GetElementName(element, x);
+ var elementUid = GetElementUid(element, x);
+ var headerIcon = ExtractIconValue(element);
+
+ if (!string.IsNullOrEmpty(elementName) || !string.IsNullOrEmpty(elementUid))
+ {
+ var parentElementName = GetParentElementName(element, x);
+
+ elements.Add(new SettingEntry
+ {
+ PageTypeName = pageName,
+ Type = EntryType.SettingsCard,
+ ParentElementName = parentElementName,
+ ElementName = elementName,
+ ElementUid = elementUid,
+ Icon = headerIcon,
+ });
+ }
+ }
+
+ // Process SettingsExpander elements
+ foreach (var element in settingsExpanderElements)
+ {
+ var elementName = GetElementName(element, x);
+ var elementUid = GetElementUid(element, x);
+ var headerIcon = ExtractIconValue(element);
+
+ if (!string.IsNullOrEmpty(elementName) || !string.IsNullOrEmpty(elementUid))
+ {
+ var parentElementName = GetParentElementName(element, x);
+
+ elements.Add(new SettingEntry
+ {
+ PageTypeName = pageName,
+ Type = EntryType.SettingsExpander,
+ ParentElementName = parentElementName,
+ ElementName = elementName,
+ ElementUid = elementUid,
+ Icon = headerIcon,
+ });
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error processing {xamlFile}: {ex.Message}");
+ }
+
+ return elements;
+ }
+
+ public static string GetElementName(XElement element, XNamespace x)
+ {
+ // Get Name attribute (we call it ElementName in our indexing system)
+ var name = element.Attribute("Name")?.Value;
+ return name;
+ }
+
+ public static string GetElementUid(XElement element, XNamespace x)
+ {
+ // Try x:Uid
+ var uid = element.Attribute(x + "Uid")?.Value;
+ return uid;
+ }
+
+ public static string GetParentElementName(XElement element, XNamespace x)
+ {
+ // Look for parent SettingsExpander
+ var current = element.Parent;
+ while (current != null)
+ {
+ // Check if we're inside a SettingsExpander.Items or just directly inside SettingsExpander
+ if (current.Name.LocalName == "Items")
+ {
+ // Check if the parent of Items is SettingsExpander
+ var expanderParent = current.Parent;
+ if (expanderParent?.Name.LocalName == "SettingsExpander")
+ {
+ var expanderName = expanderParent.Attribute("Name")?.Value;
+ if (!string.IsNullOrEmpty(expanderName))
+ {
+ return expanderName;
+ }
+ }
+ }
+ else if (current.Name.LocalName == "SettingsExpander")
+ {
+ // Direct child of SettingsExpander
+ var expanderName = current.Attribute("Name")?.Value;
+ if (!string.IsNullOrEmpty(expanderName))
+ {
+ return expanderName;
+ }
+ }
+
+ current = current.Parent;
+ }
+
+ return string.Empty;
+ }
+
+ public static string ExtractIconValue(XElement element)
+ {
+ var headerIconAttribute = element.Attribute("HeaderIcon")?.Value;
+
+ if (string.IsNullOrEmpty(headerIconAttribute))
+ {
+ // Try nested property element: ...
+ var headerIconProperty = element.Elements()
+ .FirstOrDefault(e => e.Name.LocalName.EndsWith(".HeaderIcon", StringComparison.OrdinalIgnoreCase));
+
+ if (headerIconProperty != null)
+ {
+ // Prefer explicit icon elements within the HeaderIcon property
+ var pathIcon = headerIconProperty.Descendants().FirstOrDefault(d => d.Name.LocalName == "PathIcon");
+ if (pathIcon != null)
+ {
+ var dataAttr = pathIcon.Attribute("Data")?.Value;
+ if (!string.IsNullOrWhiteSpace(dataAttr))
+ {
+ return dataAttr.Trim();
+ }
+ }
+
+ var fontIcon = headerIconProperty.Descendants().FirstOrDefault(d => d.Name.LocalName == "FontIcon");
+ if (fontIcon != null)
+ {
+ var glyphAttr = fontIcon.Attribute("Glyph")?.Value;
+ if (!string.IsNullOrWhiteSpace(glyphAttr))
+ {
+ return glyphAttr.Trim();
+ }
+ }
+
+ var bitmapIcon = headerIconProperty.Descendants().FirstOrDefault(d => d.Name.LocalName == "BitmapIcon");
+ if (bitmapIcon != null)
+ {
+ var sourceAttr = bitmapIcon.Attribute("Source")?.Value;
+ if (!string.IsNullOrWhiteSpace(sourceAttr))
+ {
+ return sourceAttr.Trim();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ // Parse different icon markup extensions
+ // Example: {ui:BitmapIcon Source=/Assets/Settings/Icons/AlwaysOnTop.png}
+ if (headerIconAttribute.Contains("BitmapIcon") && headerIconAttribute.Contains("Source="))
+ {
+ var sourceStart = headerIconAttribute.IndexOf("Source=", StringComparison.OrdinalIgnoreCase) + "Source=".Length;
+ var sourceEnd = headerIconAttribute.IndexOf('}', sourceStart);
+ if (sourceEnd == -1)
+ {
+ sourceEnd = headerIconAttribute.Length;
+ }
+
+ return headerIconAttribute.Substring(sourceStart, sourceEnd - sourceStart).Trim();
+ }
+
+ // Example: {ui:FontIcon Glyph=}
+ if (headerIconAttribute.Contains("FontIcon") && headerIconAttribute.Contains("Glyph="))
+ {
+ var glyphStart = headerIconAttribute.IndexOf("Glyph=", StringComparison.OrdinalIgnoreCase) + "Glyph=".Length;
+ var glyphEnd = headerIconAttribute.IndexOf('}', glyphStart);
+ if (glyphEnd == -1)
+ {
+ glyphEnd = headerIconAttribute.Length;
+ }
+
+ return headerIconAttribute.Substring(glyphStart, glyphEnd - glyphStart).Trim();
+ }
+
+ // If it doesn't match known patterns, return the original value
+ return headerIconAttribute;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.XamlIndexBuilder/SettingEntry.cs b/src/settings-ui/Settings.UI.XamlIndexBuilder/SettingEntry.cs
new file mode 100644
index 0000000000..0a95f8e816
--- /dev/null
+++ b/src/settings-ui/Settings.UI.XamlIndexBuilder/SettingEntry.cs
@@ -0,0 +1,44 @@
+// 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.PowerToys.Tools.XamlIndexBuilder
+{
+ public enum EntryType
+ {
+ SettingsPage,
+ SettingsCard,
+ SettingsExpander,
+ }
+
+ public struct SettingEntry
+ {
+ public EntryType Type { get; set; }
+
+ public string Header { get; set; }
+
+ public string PageTypeName { get; set; }
+
+ public string ElementName { get; set; }
+
+ public string ElementUid { get; set; }
+
+ public string ParentElementName { get; set; }
+
+ public string Description { get; set; }
+
+ public string Icon { get; set; }
+
+ public SettingEntry(EntryType type, string header, string pageTypeName, string elementName, string elementUid, string parentElementName = null, string description = null, string icon = null)
+ {
+ Type = type;
+ Header = header;
+ PageTypeName = pageTypeName;
+ ElementName = elementName;
+ ElementUid = elementUid;
+ ParentElementName = parentElementName;
+ Description = description;
+ Icon = icon;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj b/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj
new file mode 100644
index 0000000000..ef37b3eca9
--- /dev/null
+++ b/src/settings-ui/Settings.UI.XamlIndexBuilder/Settings.UI.XamlIndexBuilder.csproj
@@ -0,0 +1,44 @@
+
+
+
+
+
+ net9.0
+ Exe
+ Microsoft.PowerToys.Tools.XamlIndexBuilder
+ XamlIndexBuilder
+
+ false
+ false
+ AnyCPU
+
+
+ $(MSBuildProjectDirectory)\obj\XamlIndexBuilder\$(Configuration)\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ dotnet
+ $(MSBuildProjectDirectory)\..\Settings.UI\SettingsXAML\Views
+ $(MSBuildProjectDirectory)\..\Settings.UI\Assets\Settings\search.index.json
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/Converters/IconConverter.cs b/src/settings-ui/Settings.UI/Converters/IconConverter.cs
new file mode 100644
index 0000000000..45ba530e02
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Converters/IconConverter.cs
@@ -0,0 +1,133 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Markup;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Imaging;
+
+namespace Microsoft.PowerToys.Settings.UI.Converters
+{
+ public partial class IconConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ if (value is not string iconValue || string.IsNullOrEmpty(iconValue))
+ {
+ // Return a default icon based on the parameter
+ var defaultGlyph = parameter?.ToString() ?? "\uE8B7"; // Default gear icon
+ return new FontIcon { Glyph = defaultGlyph };
+ }
+
+ // Check if it's a single Unicode character (most common case after JSON deserialization)
+ if (iconValue.Length == 1)
+ {
+ return new FontIcon { Glyph = iconValue };
+ }
+
+ // Handle HTML numeric character references, e.g. "" or ""
+ if (iconValue.StartsWith("", StringComparison.Ordinal) && iconValue.EndsWith(';'))
+ {
+ var inner = iconValue.Substring(2, iconValue.Length - 3); // strip and ;
+ try
+ {
+ string glyph;
+ if (inner.StartsWith("x", StringComparison.OrdinalIgnoreCase))
+ {
+ var hex = inner.Substring(1);
+ if (int.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out int codePointHex))
+ {
+ glyph = char.ConvertFromUtf32(codePointHex);
+ return new FontIcon { Glyph = glyph };
+ }
+ }
+ else if (int.TryParse(inner, out int codePointDec))
+ {
+ glyph = char.ConvertFromUtf32(codePointDec);
+ return new FontIcon { Glyph = glyph };
+ }
+ }
+ catch
+ {
+ // fall through to other handlers
+ }
+ }
+
+ if (iconValue.StartsWith("\\u", StringComparison.OrdinalIgnoreCase) && iconValue.Length == 6)
+ {
+ var hexPart = iconValue.Substring(2); // Remove \u
+ if (int.TryParse(hexPart, System.Globalization.NumberStyles.HexNumber, null, out int codePoint))
+ {
+ var unicodeChar = char.ConvertFromUtf32(codePoint);
+ return new FontIcon { Glyph = unicodeChar };
+ }
+ }
+
+ // Check if it's an image path
+ if (iconValue.Contains('/') || iconValue.Contains('\\') || iconValue.Contains(".png", StringComparison.OrdinalIgnoreCase) || iconValue.Contains(".jpg", StringComparison.OrdinalIgnoreCase) || iconValue.Contains(".ico", StringComparison.OrdinalIgnoreCase) || iconValue.Contains(".svg", StringComparison.OrdinalIgnoreCase))
+ {
+ // Handle different path formats
+ var imagePath = iconValue;
+
+ // Convert ms-appx:/// paths to local paths
+ if (imagePath.StartsWith("ms-appx:///", StringComparison.OrdinalIgnoreCase))
+ {
+ imagePath = imagePath.Substring("ms-appx:///".Length);
+ }
+
+ // Ensure path starts with /
+ if (!imagePath.StartsWith('/'))
+ {
+ imagePath = "/" + imagePath;
+ }
+
+ var uri = new Uri($"ms-appx://{imagePath}");
+
+ if (imagePath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase))
+ {
+ // Render SVG using ImageIcon + SvgImageSource
+ return new ImageIcon
+ {
+ Source = new SvgImageSource(uri),
+ };
+ }
+ else
+ {
+ return new BitmapIcon
+ {
+ UriSource = uri,
+ ShowAsMonochrome = false,
+ };
+ }
+ }
+
+ // Try to interpret as raw SVG path data (PathIcon.Data)
+ // Many of our XAML PathIcon usages (e.g., AdvancedPastePage) provide a Data string like "M128 766q0-42 ...".
+ // If parsing succeeds, render it as a PathIcon.
+ try
+ {
+ var geometryObj = XamlBindingHelper.ConvertValue(typeof(Geometry), iconValue);
+ if (geometryObj is Geometry geometry)
+ {
+ return new PathIcon { Data = geometry };
+ }
+ }
+ catch
+ {
+ // Ignore parse errors and fall back below.
+ }
+
+ // If all else fails, return default icon
+ var fallbackGlyph = parameter?.ToString() ?? "\uE8B7";
+ return new FontIcon { Glyph = fallbackGlyph };
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Converters/SearchSuggestionTemplateSelector.cs b/src/settings-ui/Settings.UI/Converters/SearchSuggestionTemplateSelector.cs
new file mode 100644
index 0000000000..11320baaea
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Converters/SearchSuggestionTemplateSelector.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.ViewModels;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Microsoft.PowerToys.Settings.UI.Converters;
+
+public sealed partial class SearchSuggestionTemplateSelector : DataTemplateSelector
+{
+ public DataTemplate DefaultSuggestionTemplate { get; set; }
+
+ public DataTemplate NoResultsSuggestionTemplate { get; set; }
+
+ public DataTemplate ShowAllSuggestionTemplate { get; set; }
+
+ protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
+ {
+ if (item is SuggestionItem suggestionItem)
+ {
+ if (suggestionItem.IsNoResults)
+ {
+ return NoResultsSuggestionTemplate;
+ }
+
+ if (suggestionItem.IsShowAll)
+ {
+ return ShowAllSuggestionTemplate ?? NoResultsSuggestionTemplate ?? DefaultSuggestionTemplate;
+ }
+
+ return DefaultSuggestionTemplate;
+ }
+
+ return DefaultSuggestionTemplate;
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Converters/UriToImageConverter.cs b/src/settings-ui/Settings.UI/Converters/UriToImageConverter.cs
new file mode 100644
index 0000000000..dc079f5d01
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Converters/UriToImageConverter.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Media.Imaging;
+
+namespace Microsoft.PowerToys.Settings.UI.Converters
+{
+ public sealed partial class UriToImageSourceConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ return value is Uri uri ? new BitmapImage(uri) : null;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ => throw new NotImplementedException();
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Helpers/NavigatablePage.cs b/src/settings-ui/Settings.UI/Helpers/NavigatablePage.cs
new file mode 100644
index 0000000000..126b4f2105
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Helpers/NavigatablePage.cs
@@ -0,0 +1,144 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Numerics;
+using System.Threading.Tasks;
+using CommunityToolkit.WinUI.Controls;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Hosting;
+using Microsoft.UI.Xaml.Media;
+
+namespace Microsoft.PowerToys.Settings.UI.Helpers;
+
+public abstract partial class NavigatablePage : Page
+{
+ private const int ExpandWaitDuration = 500;
+ private const int AnimationDuration = 1000;
+
+ private NavigationParams _pendingNavigationParams;
+
+ public NavigatablePage()
+ {
+ Loaded += OnPageLoaded;
+ }
+
+ protected override void OnNavigatedTo(Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
+ {
+ base.OnNavigatedTo(e);
+
+ // Handle both old string parameter and new NavigationParams
+ if (e.Parameter is NavigationParams navParams)
+ {
+ _pendingNavigationParams = navParams;
+ }
+ else if (e.Parameter is string elementKey)
+ {
+ _pendingNavigationParams = new NavigationParams(elementKey);
+ }
+ }
+
+ private async void OnPageLoaded(object sender, RoutedEventArgs e)
+ {
+ if (_pendingNavigationParams != null && !string.IsNullOrEmpty(_pendingNavigationParams.ElementName))
+ {
+ // First, expand parent if specified
+ if (!string.IsNullOrEmpty(_pendingNavigationParams.ParentElementName))
+ {
+ var parentElement = FindElementByName(_pendingNavigationParams.ParentElementName);
+ if (parentElement is SettingsExpander expander)
+ {
+ expander.IsExpanded = true;
+
+ // Give time for the expander to animate
+ await Task.Delay(ExpandWaitDuration);
+ }
+ }
+
+ // Then find and navigate to the target element
+ var target = FindElementByName(_pendingNavigationParams.ElementName);
+
+ target?.StartBringIntoView(new BringIntoViewOptions
+ {
+ VerticalOffset = -20,
+ AnimationDesired = true,
+ });
+
+ await OnTargetElementNavigatedAsync(target, _pendingNavigationParams.ElementName);
+
+ _pendingNavigationParams = null;
+ }
+ }
+
+ protected virtual async Task OnTargetElementNavigatedAsync(FrameworkElement target, string elementKey)
+ {
+ if (target == null)
+ {
+ return;
+ }
+
+ // Get the visual and compositor
+ var visual = ElementCompositionPreview.GetElementVisual(target);
+ var compositor = visual.Compositor;
+
+ // Create a subtle glow effect using drop shadow
+ var dropShadow = compositor.CreateDropShadow();
+ dropShadow.Color = Microsoft.UI.Colors.Gray;
+ dropShadow.BlurRadius = 8f;
+ dropShadow.Opacity = 0f;
+ dropShadow.Offset = new Vector3(0, 0, 0);
+
+ var spriteVisual = compositor.CreateSpriteVisual();
+ spriteVisual.Size = new Vector2((float)target.ActualWidth + 16, (float)target.ActualHeight + 16);
+ spriteVisual.Shadow = dropShadow;
+ spriteVisual.Offset = new Vector3(-8, -8, 0);
+
+ // Insert the shadow visual behind the target element
+ ElementCompositionPreview.SetElementChildVisual(target, spriteVisual);
+
+ // Create a simple fade in/out animation
+ var fadeAnimation = compositor.CreateScalarKeyFrameAnimation();
+ fadeAnimation.InsertKeyFrame(0f, 0f);
+ fadeAnimation.InsertKeyFrame(0.5f, 0.3f);
+ fadeAnimation.InsertKeyFrame(1f, 0f);
+ fadeAnimation.Duration = TimeSpan.FromMilliseconds(AnimationDuration);
+
+ dropShadow.StartAnimation("Opacity", fadeAnimation);
+
+ if (target is Control ctrl)
+ {
+ // TODO: ability to adjust brush color and animation from settings.
+ var originalBackground = ctrl.Background;
+
+ var highlightBrush = new SolidColorBrush();
+ var grayColor = Microsoft.UI.Colors.Gray;
+ grayColor.A = 50; // Very subtle transparency
+ highlightBrush.Color = grayColor;
+
+ // Apply the highlight
+ ctrl.Background = highlightBrush;
+
+ // Wait for animation to complete
+ await Task.Delay(AnimationDuration);
+
+ // Restore original background
+ ctrl.Background = originalBackground;
+ }
+ else
+ {
+ // For non-control elements, just wait for the glow animation
+ await Task.Delay(AnimationDuration);
+ }
+
+ // Clean up the shadow visual
+ ElementCompositionPreview.SetElementChildVisual(target, null);
+ }
+
+ protected FrameworkElement FindElementByName(string name)
+ {
+ var element = this.FindName(name) as FrameworkElement;
+ return element;
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Helpers/NavigationParams.cs b/src/settings-ui/Settings.UI/Helpers/NavigationParams.cs
new file mode 100644
index 0000000000..30c27343ce
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Helpers/NavigationParams.cs
@@ -0,0 +1,18 @@
+// 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.PowerToys.Settings.UI.Helpers;
+
+public class NavigationParams
+{
+ public string ElementName { get; set; }
+
+ public string ParentElementName { get; set; }
+
+ public NavigationParams(string elementName, string parentElementName = null)
+ {
+ ElementName = elementName;
+ ParentElementName = parentElementName;
+ }
+}
diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
index 13aef6df63..aaa1c2bdc7 100644
--- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
+++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj
@@ -1,161 +1,193 @@
-
-
-
+
+
+
-
- WinExe
- Microsoft.PowerToys.Settings.UI
- app.manifest
- true
- true
- None
- false
- false
- Assets\Settings\icon.ico
- true
-
- ..\..\..\$(Platform)\$(Configuration)\WinUI3Apps
-
- PowerToys.Settings.pri
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ WinExe
+ Microsoft.PowerToys.Settings.UI
+ app.manifest
+ true
+ true
+ None
+ false
+ false
+ Assets\Settings\icon.ico
+ true
+
+ ..\..\..\$(Platform)\$(Configuration)\WinUI3Apps
+
+ PowerToys.Settings.pri
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- PowerToys.GPOWrapper;PowerToys.ZoomItSettingsInterop
- $(OutDir)
- false
-
+
+
+ PowerToys.GPOWrapper;PowerToys.ZoomItSettingsInterop
+ $(OutDir)
+ false
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101
-
+
+
+ $(MSBuildProjectDirectory)\Assets\Settings\search.index.json
+
-
-
- Always
-
-
+
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- MSBuild:Compile
-
-
- MSBuild:Compile
-
-
- MSBuild:Compile
-
-
+
+
+
+
+ VSTHRD002;VSTHRD110;VSTHRD100;VSTHRD200;VSTHRD101
+
-
-
- $(DefaultXamlRuntime)
-
-
- $(DefaultXamlRuntime)
-
-
- $(DefaultXamlRuntime)
-
-
+
-
+
+
+
+ Always
+
+
+
+ Microsoft.PowerToys.Settings.UI.Assets.search.index.json
+
+
+
+
+
+ Always
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+
+
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/Services/SearchIndexService.cs b/src/settings-ui/Settings.UI/Services/SearchIndexService.cs
new file mode 100644
index 0000000000..65915fed7b
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Services/SearchIndexService.cs
@@ -0,0 +1,338 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Common.Search.FuzzSearch;
+using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Views;
+using Microsoft.Windows.ApplicationModel.Resources;
+using Settings.UI.Library;
+
+namespace Microsoft.PowerToys.Settings.UI.Services
+{
+ public static class SearchIndexService
+ {
+ private static readonly object _lockObject = new();
+ private static readonly Dictionary _pageNameCache = [];
+ private static readonly Dictionary _normalizedTextCache = new();
+ private static readonly Dictionary _pageTypeCache = new();
+ private static ImmutableArray _index = [];
+ private static bool _isIndexBuilt;
+ private static bool _isIndexBuilding;
+ private const string PrebuiltIndexResourceName = "Microsoft.PowerToys.Settings.UI.Assets.search.index.json";
+ private static JsonSerializerOptions _serializerOptions = new() { PropertyNameCaseInsensitive = true };
+
+ public static ImmutableArray Index
+ {
+ get
+ {
+ lock (_lockObject)
+ {
+ return _index;
+ }
+ }
+ }
+
+ public static bool IsIndexReady
+ {
+ get
+ {
+ lock (_lockObject)
+ {
+ return _isIndexBuilt;
+ }
+ }
+ }
+
+ public static void BuildIndex()
+ {
+ lock (_lockObject)
+ {
+ if (_isIndexBuilt || _isIndexBuilding)
+ {
+ return;
+ }
+
+ _isIndexBuilding = true;
+
+ // Clear caches on rebuild
+ _normalizedTextCache.Clear();
+ _pageTypeCache.Clear();
+ }
+
+ try
+ {
+ var builder = ImmutableArray.CreateBuilder();
+ LoadIndexFromPrebuiltData(builder);
+
+ lock (_lockObject)
+ {
+ _index = builder.ToImmutable();
+ _isIndexBuilt = true;
+ _isIndexBuilding = false;
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[SearchIndexService] CRITICAL ERROR building search index: {ex.Message}\n{ex.StackTrace}");
+ lock (_lockObject)
+ {
+ _isIndexBuilding = false;
+ _isIndexBuilt = false;
+ }
+ }
+ }
+
+ private static void LoadIndexFromPrebuiltData(ImmutableArray.Builder builder)
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+ SettingEntry[] metadataList;
+
+ Debug.WriteLine($"[SearchIndexService] Attempting to load prebuilt index from: {PrebuiltIndexResourceName}");
+
+ try
+ {
+ using Stream stream = assembly.GetManifestResourceStream(PrebuiltIndexResourceName);
+ if (stream == null)
+ {
+ Debug.WriteLine($"[SearchIndexService] ERROR: Embedded resource '{PrebuiltIndexResourceName}' not found. Ensure it's correctly embedded and the name matches.");
+ return;
+ }
+
+ using StreamReader reader = new(stream);
+ string json = reader.ReadToEnd();
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ Debug.WriteLine("[SearchIndexService] ERROR: Embedded resource was empty.");
+ return;
+ }
+
+ metadataList = JsonSerializer.Deserialize(json, _serializerOptions);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[SearchIndexService] ERROR: Failed to load or deserialize prebuilt index: {ex.Message}");
+ return;
+ }
+
+ if (metadataList == null || metadataList.Length == 0)
+ {
+ Debug.WriteLine("[SearchIndexService] Prebuilt index is empty or deserialization failed.");
+ return;
+ }
+
+ foreach (ref var metadata in metadataList.AsSpan())
+ {
+ if (metadata.Type == EntryType.SettingsPage)
+ {
+ (metadata.Header, metadata.Description) = GetLocalizedModuleTitleAndDescription(resourceLoader, metadata.ElementUid);
+ }
+ else
+ {
+ (metadata.Header, metadata.Description) = GetLocalizedSettingHeaderAndDescription(resourceLoader, metadata.ElementUid);
+ }
+
+ if (string.IsNullOrEmpty(metadata.Header))
+ {
+ continue;
+ }
+
+ builder.Add(metadata);
+
+ // Cache the page name mapping for SettingsPage entries
+ if (metadata.Type == EntryType.SettingsPage && !string.IsNullOrEmpty(metadata.Header))
+ {
+ _pageNameCache[metadata.PageTypeName] = metadata.Header;
+ }
+ }
+
+ Debug.WriteLine($"[SearchIndexService] Finished loading index. Total entries: {builder.Count}");
+ }
+
+ private static (string Header, string Description) GetLocalizedSettingHeaderAndDescription(ResourceLoader resourceLoader, string elementUid)
+ {
+ string header = GetString(resourceLoader, $"{elementUid}/Header");
+ string description = GetString(resourceLoader, $"{elementUid}/Description");
+
+ if (string.IsNullOrEmpty(header))
+ {
+ Debug.WriteLine($"[SearchIndexService] WARNING: No header localization found for ElementUid: '{elementUid}'");
+ }
+
+ return (header, description);
+ }
+
+ private static (string Title, string Description) GetLocalizedModuleTitleAndDescription(ResourceLoader resourceLoader, string elementUid)
+ {
+ string title = GetString(resourceLoader, $"{elementUid}/ModuleTitle");
+ string description = GetString(resourceLoader, $"{elementUid}/ModuleDescription");
+
+ return (title, description);
+ }
+
+ private static string GetString(ResourceLoader rl, string key)
+ {
+ try
+ {
+ string value = rl.GetString(key);
+ return string.IsNullOrWhiteSpace(value) ? string.Empty : value;
+ }
+ catch (Exception)
+ {
+ return string.Empty;
+ }
+ }
+
+ public static List Search(string query)
+ {
+ return Search(query, CancellationToken.None);
+ }
+
+ public static List Search(string query, CancellationToken token)
+ {
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ return [];
+ }
+
+ var currentIndex = Index;
+ if (currentIndex.IsEmpty)
+ {
+ Debug.WriteLine("[SearchIndexService] Search called but index is empty.");
+ return [];
+ }
+
+ var normalizedQuery = NormalizeString(query);
+ var bag = new ConcurrentBag<(SettingEntry Hit, double Score)>();
+ var po = new ParallelOptions
+ {
+ CancellationToken = token,
+ MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount - 1),
+ };
+
+ try
+ {
+ Parallel.ForEach(currentIndex, po, entry =>
+ {
+ var (headerNorm, descNorm) = GetNormalizedTexts(entry);
+ var captionScoreResult = StringMatcher.FuzzyMatch(normalizedQuery, headerNorm);
+ double score = captionScoreResult.Score;
+
+ if (!string.IsNullOrEmpty(descNorm))
+ {
+ var descriptionScoreResult = StringMatcher.FuzzyMatch(normalizedQuery, descNorm);
+ if (descriptionScoreResult.Success)
+ {
+ score = Math.Max(score, descriptionScoreResult.Score * 0.8);
+ }
+ }
+
+ if (score > 0)
+ {
+ var pageType = GetPageTypeFromName(entry.PageTypeName);
+ if (pageType != null)
+ {
+ bag.Add((entry, score));
+ }
+ }
+ });
+ }
+ catch (OperationCanceledException)
+ {
+ return [];
+ }
+
+ return bag
+ .OrderByDescending(r => r.Score)
+ .Select(r => r.Hit)
+ .ToList();
+ }
+
+ private static Type GetPageTypeFromName(string pageTypeName)
+ {
+ if (string.IsNullOrEmpty(pageTypeName))
+ {
+ return null;
+ }
+
+ lock (_lockObject)
+ {
+ if (_pageTypeCache.TryGetValue(pageTypeName, out var cached))
+ {
+ return cached;
+ }
+
+ var assembly = typeof(GeneralPage).Assembly;
+ var type = assembly.GetType($"Microsoft.PowerToys.Settings.UI.Views.{pageTypeName}");
+ _pageTypeCache[pageTypeName] = type;
+ return type;
+ }
+ }
+
+ private static (string HeaderNorm, string DescNorm) GetNormalizedTexts(SettingEntry entry)
+ {
+ if (entry.ElementUid == null && entry.Header == null)
+ {
+ return (NormalizeString(entry.Header), NormalizeString(entry.Description));
+ }
+
+ var key = entry.ElementUid ?? $"{entry.PageTypeName}|{entry.ElementName}";
+ lock (_lockObject)
+ {
+ if (_normalizedTextCache.TryGetValue(key, out var cached))
+ {
+ return cached;
+ }
+ }
+
+ var headerNorm = NormalizeString(entry.Header);
+ var descNorm = NormalizeString(entry.Description);
+ lock (_lockObject)
+ {
+ _normalizedTextCache[key] = (headerNorm, descNorm);
+ }
+
+ return (headerNorm, descNorm);
+ }
+
+ private static string NormalizeString(string input)
+ {
+ if (string.IsNullOrEmpty(input))
+ {
+ return string.Empty;
+ }
+
+ var normalized = input.ToLowerInvariant().Normalize(NormalizationForm.FormKD);
+ var stringBuilder = new StringBuilder();
+ foreach (var c in normalized)
+ {
+ var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
+ if (unicodeCategory != UnicodeCategory.NonSpacingMark)
+ {
+ stringBuilder.Append(c);
+ }
+ }
+
+ return stringBuilder.ToString();
+ }
+
+ public static string GetLocalizedPageName(string pageTypeName)
+ {
+ return _pageNameCache.TryGetValue(pageTypeName, out string cachedName) ? cachedName : string.Empty;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml
index 2de03f636f..ea2a585b8b 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml
@@ -49,8 +49,12 @@
TrueValue="Collapsed" />
+
-
+
2
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml
index 118c9b7ca5..20af0c6629 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml
@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
+ 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"
@@ -11,6 +12,7 @@
1000
1020
+
@@ -38,10 +40,7 @@
-
+
@@ -52,14 +51,12 @@
-
-
-
-
-
+ CornerRadius="{StaticResource OverlayCornerRadius}"
+ Visibility="{x:Bind ModuleImageSource, Converter={StaticResource EmptyObjectToObjectConverter}}">
+
@@ -73,7 +70,8 @@
x:Name="PrimaryLinksControl"
Margin="0,8,0,0"
IsTabStop="False"
- ItemsSource="{x:Bind PrimaryLinks}">
+ ItemsSource="{x:Bind PrimaryLinks}"
+ Visibility="{x:Bind PrimaryLinks.Count, Converter={StaticResource DoubleToVisibilityConverter}}">
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml.cs
index dca1fbae8e..6e5fef091f 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml.cs
@@ -2,6 +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.ObjectModel;
using Microsoft.UI.Xaml;
@@ -30,9 +31,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set => SetValue(ModuleDescriptionProperty, value);
}
- public string ModuleImageSource
+ public Uri ModuleImageSource
{
- get => (string)GetValue(ModuleImageSourceProperty);
+ get => (Uri)GetValue(ModuleImageSourceProperty);
set => SetValue(ModuleImageSourceProperty, value);
}
@@ -60,13 +61,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set { SetValue(ModuleContentProperty, value); }
}
- public static readonly DependencyProperty ModuleTitleProperty = DependencyProperty.Register("ModuleTitle", typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
- public static readonly DependencyProperty ModuleDescriptionProperty = DependencyProperty.Register("ModuleDescription", typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
- public static readonly DependencyProperty ModuleImageSourceProperty = DependencyProperty.Register("ModuleImageSource", typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
- public static readonly DependencyProperty PrimaryLinksProperty = DependencyProperty.Register("PrimaryLinks", typeof(ObservableCollection), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection()));
- public static readonly DependencyProperty SecondaryLinksHeaderProperty = DependencyProperty.Register("SecondaryLinksHeader", typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
- public static readonly DependencyProperty SecondaryLinksProperty = DependencyProperty.Register("SecondaryLinks", typeof(ObservableCollection), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection()));
- public static readonly DependencyProperty ModuleContentProperty = DependencyProperty.Register("ModuleContent", typeof(object), typeof(SettingsPageControl), new PropertyMetadata(new Grid()));
+ public static readonly DependencyProperty ModuleTitleProperty = DependencyProperty.Register(nameof(ModuleTitle), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(defaultValue: null));
+ public static readonly DependencyProperty ModuleDescriptionProperty = DependencyProperty.Register(nameof(ModuleDescription), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(defaultValue: null));
+ public static readonly DependencyProperty ModuleImageSourceProperty = DependencyProperty.Register(nameof(ModuleImageSource), typeof(Uri), typeof(SettingsPageControl), new PropertyMetadata(null));
+ public static readonly DependencyProperty PrimaryLinksProperty = DependencyProperty.Register(nameof(PrimaryLinks), typeof(ObservableCollection), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection()));
+ public static readonly DependencyProperty SecondaryLinksHeaderProperty = DependencyProperty.Register(nameof(SecondaryLinksHeader), typeof(string), typeof(SettingsPageControl), new PropertyMetadata(default(string)));
+ public static readonly DependencyProperty SecondaryLinksProperty = DependencyProperty.Register(nameof(SecondaryLinks), typeof(ObservableCollection), typeof(SettingsPageControl), new PropertyMetadata(new ObservableCollection()));
+ public static readonly DependencyProperty ModuleContentProperty = DependencyProperty.Register(nameof(ModuleContent), typeof(object), typeof(SettingsPageControl), new PropertyMetadata(new Grid()));
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml
index a5e6f2de40..07b6a00c21 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml
@@ -21,7 +21,6 @@
-
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs
index 1b6a33ad4d..90b2577268 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs
@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
-
using ManagedCommon;
using Microsoft.PowerLauncher.Telemetry;
using Microsoft.PowerToys.Settings.UI.Helpers;
@@ -14,6 +13,7 @@ using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Windows.Data.Json;
+using WinRT.Interop;
using WinUIEx;
namespace Microsoft.PowerToys.Settings.UI
@@ -104,7 +104,7 @@ namespace Microsoft.PowerToys.Settings.UI
{
if (App.GetOobeWindow() == null)
{
- App.SetOobeWindow(new OobeWindow(Microsoft.PowerToys.Settings.UI.OOBE.Enums.PowerToysModules.Overview));
+ App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.Overview));
}
App.GetOobeWindow().Activate();
@@ -115,7 +115,7 @@ namespace Microsoft.PowerToys.Settings.UI
{
if (App.GetOobeWindow() == null)
{
- App.SetOobeWindow(new OobeWindow(Microsoft.PowerToys.Settings.UI.OOBE.Enums.PowerToysModules.WhatsNew));
+ App.SetOobeWindow(new OobeWindow(OOBE.Enums.PowerToysModules.WhatsNew));
}
else
{
@@ -160,6 +160,7 @@ namespace Microsoft.PowerToys.Settings.UI
});
this.InitializeComponent();
+ SetAppTitleBar();
// receive IPC Message
App.IPCMessageReceivedCallback = (string msg) =>
@@ -186,6 +187,13 @@ namespace Microsoft.PowerToys.Settings.UI
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
}
+ private void SetAppTitleBar()
+ {
+ // We need to assign the window here so it can configure the custom title bar area correctly.
+ shellPage.TitleBar.Window = this;
+ WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(this));
+ }
+
public void NavigateToSection(System.Type type)
{
ShellPage.Navigate(type);
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
similarity index 92%
rename from src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml
rename to src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
index 7aa8176621..58552b1c00 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml
@@ -1,9 +1,10 @@
-
-
+
@@ -37,7 +38,7 @@
-
+
@@ -46,6 +47,7 @@
Orientation="Vertical"
Spacing="2">
@@ -61,7 +63,6 @@
-
-
+
@@ -96,15 +100,16 @@
-
@@ -120,19 +125,22 @@
-
+
-
+
-
@@ -141,25 +149,27 @@
Click="AddCustomActionButton_Click"
Style="{ThemeResource AccentButtonStyle}" />
-
+
-
+
-
+
-
+
-
-
-
+
-
@@ -275,8 +290,8 @@
-
@@ -306,7 +323,6 @@
-
-
-
@@ -411,4 +425,4 @@
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml.cs
similarity index 98%
rename from src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
rename to src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml.cs
index 8442262688..095cb7167f 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml.cs
@@ -15,7 +15,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
- public sealed partial class AdvancedPastePage : Page, IRefreshablePage
+ public sealed partial class AdvancedPastePage : NavigatablePage, IRefreshablePage
{
private AdvancedPasteViewModel ViewModel { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml
index 1424b256a3..0bd34a3d57 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml
@@ -1,9 +1,10 @@
-
@@ -35,6 +37,7 @@
@@ -49,31 +52,42 @@
-
+
-
+
-
+
@@ -121,4 +136,4 @@
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs
index 2e22da3120..d82ccb1917 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs
@@ -9,7 +9,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
- public sealed partial class AlwaysOnTopPage : Page, IRefreshablePage
+ public sealed partial class AlwaysOnTopPage : NavigatablePage, IRefreshablePage
{
private AlwaysOnTopViewModel ViewModel { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml
index f4a190ab46..5ccd1e4413 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml
@@ -1,10 +1,11 @@
-
-
+
-
+
@@ -42,7 +44,10 @@
-
+
@@ -52,21 +57,23 @@
-
+
-
+
@@ -112,4 +119,4 @@
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml.cs
index 3399f425cc..cd085a2053 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AwakePage.xaml.cs
@@ -17,7 +17,7 @@ using PowerToys.GPOWrapper;
namespace Microsoft.PowerToys.Settings.UI.Views
{
- public sealed partial class AwakePage : Page, IRefreshablePage
+ public sealed partial class AwakePage : NavigatablePage, IRefreshablePage
{
private readonly string _appName = "Awake";
private readonly SettingsUtils _settingsUtils;
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml
index 34305e3529..aad05be69e 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml
@@ -1,9 +1,10 @@
-
@@ -84,7 +86,7 @@
-
+
@@ -117,7 +119,7 @@
-
+
@@ -168,4 +170,4 @@
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml.cs
index 8afa34700e..6f406d669c 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml.cs
@@ -2,12 +2,13 @@
// 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.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
- public sealed partial class CmdNotFoundPage : Page
+ public sealed partial class CmdNotFoundPage : NavigatablePage
{
private CmdNotFoundViewModel ViewModel { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml
index 9e7420d84d..23b41af139 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml
@@ -1,9 +1,10 @@
-
@@ -27,7 +29,10 @@
Severity="Informational" />
-
+
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs
index fb3a97e309..1cc7e411d3 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs
@@ -13,7 +13,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
- public sealed partial class CmdPalPage : Page, IRefreshablePage
+ public sealed partial class CmdPalPage : NavigatablePage, IRefreshablePage
{
private CmdPalViewModel ViewModel { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml
index 7ac03ead81..66b5c2d404 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml
@@ -1,9 +1,10 @@
-
@@ -38,11 +40,17 @@
-
+
-
+
@@ -50,25 +58,35 @@
-
+
-
+
-
+
@@ -80,7 +98,10 @@
-
+
-
+
-
+
-
+
-
+
-
+
@@ -230,6 +272,7 @@
@@ -256,4 +299,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs
index 61865c89fa..93296699f6 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs
@@ -9,7 +9,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
- public sealed partial class FancyZonesPage : Page, IRefreshablePage
+ public sealed partial class FancyZonesPage : NavigatablePage, IRefreshablePage
{
private FancyZonesViewModel ViewModel { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml
index 21de1910c7..7f3741df41 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml
@@ -1,9 +1,10 @@
-
@@ -32,7 +34,7 @@
-
+
@@ -51,4 +53,4 @@
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml.cs
index 4d4ffbf416..3e0a642718 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FileLocksmithPage.xaml.cs
@@ -9,7 +9,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
- public sealed partial class FileLocksmithPage : Page, IRefreshablePage
+ public sealed partial class FileLocksmithPage : NavigatablePage, IRefreshablePage
{
private FileLocksmithViewModel ViewModel { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
index 1a3c640ddf..7658d11237 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
@@ -1,19 +1,21 @@
-
-
+
+
-
+
@@ -209,6 +211,7 @@
-
+
-
+
@@ -262,10 +271,12 @@
-
+
-
-
+
-
+
@@ -327,6 +341,7 @@
@@ -390,7 +405,10 @@
IsTabStop="{x:Bind ViewModel.SettingsBackupRestoreMessageVisible, Mode=OneWay}"
Severity="{x:Bind ViewModel.BackupRestoreMessageSeverity, Converter={StaticResource StringToInfoBarSeverityConverter}}" />
-
+
@@ -499,4 +517,4 @@
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs
index 9a6a1f802c..1d6f83a7e0 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml.cs
@@ -19,7 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
///
/// General Settings Page.
///
- public sealed partial class GeneralPage : Page, IRefreshablePage
+ public sealed partial class GeneralPage : NavigatablePage, IRefreshablePage
{
private static DateTime OkToHideBackupAndRestoreMessageTime { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml
index 6ceffa96d4..9800fb60a3 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml
@@ -1,9 +1,10 @@
-
@@ -30,30 +32,41 @@
-
+
-
+
-
+
@@ -73,4 +86,4 @@
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs
index b74f03df44..9c3f883bbd 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs
@@ -9,7 +9,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
- public sealed partial class HostsPage : Page, IRefreshablePage
+ public sealed partial class HostsPage : NavigatablePage, IRefreshablePage
{
private HostsViewModel ViewModel { get; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml
index 2db21e91ad..fa4526bae0 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml
@@ -1,10 +1,11 @@
-
-
+
@@ -26,11 +27,12 @@
x:Key="BoolToComboBoxIndexConverter"
FalseValue="1"
TrueValue="0" />
-
+
@@ -49,7 +51,10 @@
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml.cs
index a059a26b2c..12763c0899 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml.cs
@@ -14,7 +14,7 @@ using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
- public sealed partial class ImageResizerPage : Page, IRefreshablePage
+ public sealed partial class ImageResizerPage : NavigatablePage, IRefreshablePage
{
public ImageResizerViewModel ViewModel { get; set; }
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml
index b816fccf09..188d75314d 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml
@@ -1,10 +1,11 @@
-
-
+