mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Localized search: <img width="1576" height="480" alt="image" src="https://github.com/user-attachments/assets/dd6e5e9f-419b-40b1-b796-f0799481ecfc" /> ## AI summary This pull request introduces infrastructure and code to support search functionality for PowerToys settings, including a new search index specification, a dedicated search library, and updates to the solution configuration. The main changes are the addition of a spec describing how settings should be indexed and navigated, the creation of a new `Common.Search` project with a fuzz search implementation, and updates to the solution file to include these new components. **Settings Search Feature Implementation** * Documentation: * Added a detailed specification (`settings-search.md`) describing the structure of PowerToys settings pages, how to index settings, navigation logic, runtime search, result grouping, build-time indexing strategy, and corner cases. * New Search Library: * Added the new `Common.Search` project to the solution, including its project file and implementation of a fuzz search service (`FuzzSearchService<T>`), match options, match results, and search precision scoring. [[1]](diffhunk://#diff-ddc06fa41e4e723e54181b0cb85cdd00f57f75725d51ceefa242d4d651a9a363R1-R8) [[2]](diffhunk://#diff-1a2ca29fc33bcccf338a7843a040ca2c31ba821e8cab7064fab0dbb1224d454cR1-R39) [[3]](diffhunk://#diff-242764d948b795f39653a84d9b6bfcdc52730100deab2e3a0995be95bb8e7868R1-R10) [[4]](diffhunk://#diff-61e525491ed916ebd65dabb66dd4f5dc720320d7e295ef1e0bd6d506ea0f7df6R1-R67) [[5]](diffhunk://#diff-a775f6de2e8d42982829b4161668f49dedbbd9dcbb05ce20003de7e62275c57aR1-R12) * Solution Configuration: * Updated `PowerToys.sln` to include `Common.Search` and `Settings.UI.XamlIndexBuilder` projects, and configured their build settings for various platforms and mapped project dependencies. [[1]](diffhunk://#diff-ca837ce490070b91656ffffe31cbad8865ba9174e0f020231f77baf35ff3f811R714-R716) [[2]](diffhunk://#diff-ca837ce490070b91656ffffe31cbad8865ba9174e0f020231f77baf35ff3f811R2704-R2727) [[3]](diffhunk://#diff-ca837ce490070b91656ffffe31cbad8865ba9174e0f020231f77baf35ff3f811R2889) [[4]](diffhunk://#diff-ca837ce490070b91656ffffe31cbad8865ba9174e0f020231f77baf35ff3f811R3157-R3158) **Spell-check Dictionary Updates** * Added new terms related to navigation and settings UI components (such as `Navigatable`, `NavigatablePage`, `settingscard`, `Tru`, `tweakable`) to the spell-check dictionary to support the new search and indexing features. [[1]](diffhunk://#diff-5dcab162c1b233a49973ae010f2b88c7ec4844382abd705e6154685e62bd5c4dR1020-R1021) [[2]](diffhunk://#diff-5dcab162c1b233a49973ae010f2b88c7ec4844382abd705e6154685e62bd5c4dR1498) [[3]](diffhunk://#diff-5dcab162c1b233a49973ae010f2b88c7ec4844382abd705e6154685e62bd5c4dR1755-R1761) --------- Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com> Co-authored-by: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
233 lines
7.6 KiB
Markdown
233 lines
7.6 KiB
Markdown
# 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 <controls:SettingsPageControl>. 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<SettingEntry> ModuleResult;
|
||
ObservableCollection<SettingsGroup> GroupedSettingsResults;
|
||
|
||
public class SettingsGroup : INotifyPropertyChanged
|
||
{
|
||
private string _groupName;
|
||
private ObservableCollection<SettingEntry> _settings;
|
||
public string GroupName
|
||
{
|
||
get => _groupName;
|
||
set
|
||
{
|
||
_groupName = value;
|
||
OnPropertyChanged();
|
||
}
|
||
}
|
||
public ObservableCollection<SettingEntry> 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 <small>(I suppose, please correct here if it's wrong)</small>, 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
|
||
<Target Name="GenerateSearchIndex" BeforeTargets="BeforeBuild">
|
||
<PropertyGroup>
|
||
<BuilderExe>$(MSBuildProjectDirectory)\..\Settings.UI.XamlIndexBuilder\bin\$(Configuration)\net8.0\XamlIndexBuilder.exe</BuilderExe>
|
||
<XamlDir>$(MSBuildProjectDirectory)\Views</XamlDir>
|
||
<GeneratedJson>$(MSBuildProjectDirectory)\Services\searchable_elements.json</GeneratedJson>
|
||
</PropertyGroup>
|
||
<Exec Command=""$(BuilderExe)" "$(XamlDir)" "$(GeneratedJson)"" />
|
||
</Target>
|
||
```
|
||
```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 |