diff --git a/src/Files.App/Assets/Data/settings_string_keys.json b/src/Files.App/Assets/Data/settings_string_keys.json
new file mode 100644
index 000000000000..1c3c89ae23db
--- /dev/null
+++ b/src/Files.App/Assets/Data/settings_string_keys.json
@@ -0,0 +1,139 @@
+{
+ "AdvancedPage": [
+ "ExportSettings",
+ "ImportSettings",
+ "EditSettingsFile",
+ "SettingsOpenInLogin",
+ "SettingsLeaveAppRunning",
+ "ShowSystemTrayIcon",
+ "SettingsSetAsDefaultFileManager",
+ "SettingsSetAsOpenDialog",
+ "ShowFlattenOptions",
+ "Advanced",
+ "ExperimentalFeatureFlags"
+ ],
+ "AboutPage": [
+ "SponsorUsOnGitHub",
+ "Documentation",
+ "QuestionsAndDiscussions",
+ "SubmitFeatureRequest",
+ "SubmitBugReport",
+ "OpenLogLocation",
+ "ImproveTranslation",
+ "OpenGitHubRepo",
+ "Privacy",
+ "Feedback",
+ "ThirdPartyLibraries",
+ "About",
+ "HelpAndSupport",
+ "OpenSource"
+ ],
+ "LayoutPage": [
+ "SyncFolderPreferencesAcrossDirectories",
+ "LayoutType",
+ "SortInDescendingOrder",
+ "SortPriority",
+ "GroupInDescendingOrder",
+ "GroupByDateUnit",
+ "TagColumn",
+ "SizeColumn",
+ "TypeColumn",
+ "DateColumn",
+ "DateCreatedColumn",
+ "SortBy",
+ "GroupBy",
+ "Columns",
+ "Layout",
+ "SortingAndGrouping",
+ "DetailsView"
+ ],
+ "GeneralPage": [
+ "Language",
+ "DateFormat",
+ "OpenTabInExistingInstance",
+ "AlwaysSwitchToNewlyOpenedTab",
+ "QuickAccess",
+ "Drives",
+ "NetworkLocations",
+ "FileTags",
+ "RecentFiles",
+ "SettingsMultitaskingAlwaysOpenDualPane",
+ "DualPaneSplitDirection",
+ "ShowOpenInNewTab",
+ "ShowOpenInNewWindow",
+ "ShowOpenInNewPane",
+ "ShowCopyPath",
+ "ShowCreateFolderWithSelection",
+ "ShowCreateAlternateDataStream",
+ "ShowCreateShortcut",
+ "ShowPinToSideBar",
+ "ShowCompressionOptions",
+ "ShowFlattenOptions",
+ "ShowSendToMenu",
+ "ShowOpenTerminal",
+ "ShowEditTagsMenu",
+ "ShowPinToStart",
+ "SettingsContextMenuOverflow",
+ "EnableSmoothScrolling",
+ "StartupSettings",
+ "Widgets",
+ "ContextMenuOptions",
+ "General",
+ "DualPane",
+ "Scrolling"
+ ],
+ "AppearancePage": [
+ "SettingsAppearanceTheme",
+ "BackdropMaterial",
+ "Opacity",
+ "ImageFit",
+ "VerticalAlignment",
+ "HorizontalAlignment",
+ "ShowTabActions",
+ "ShowShelfPaneButtonInAddressBar",
+ "ShowToolbar",
+ "ShowStatusCenterButton",
+ "ShowStatusBar",
+ "BackgroundColor",
+ "BackgroundImage",
+ "Toolbars",
+ "Appearance"
+ ],
+ "FoldersPage": [
+ "SettingsFilesAndFoldersShowHiddenItems",
+ "ShowDotFiles",
+ "ShowProtectedSystemFiles",
+ "ShowAlternateStreams",
+ "SettingsFilesAndFoldersShowFileExtensions",
+ "SettingsFilesAndFoldersShowThumbnails",
+ "ShowCheckboxesWhenSelectingItems",
+ "SettingsOpenItemsWithOneClick",
+ "OpenFolderWithOneClick",
+ "OpenFoldersInNewTab",
+ "ShowConfirmationWhenDeletingItems",
+ "ShowFileExtensionWarning",
+ "SelectFilesAndFoldersOnHover",
+ "DoubleClickBlankSpaceToGoUp",
+ "ScrollToPreviousFolderWhenNavigatingUp",
+ "SizeFormat",
+ "HiddenItems",
+ "OpeningItems",
+ "CalculateFolderSizes",
+ "FilesAndFolders",
+ "Display",
+ "Behaviors"
+ ],
+ "DevToolsPage": [
+ "ConnectToGitHub",
+ "ConnectedToGitHub",
+ "DisplayOpenIDE",
+ "DevTools"
+ ],
+ "ActionsPage": [
+ "Actions",
+ "Commands"
+ ],
+ "TagsPage": [
+ "FileTags"
+ ]
+}
diff --git a/src/Files.App/Constants.cs b/src/Files.App/Constants.cs
index c3a531e502dd..d6bf16fa1e14 100644
--- a/src/Files.App/Constants.cs
+++ b/src/Files.App/Constants.cs
@@ -189,6 +189,11 @@ public static class ResourceFilePaths
/// The path to the json file containing a list of file properties to be loaded in the preview pane.
///
public const string PreviewPaneDetailsPropertiesJsonPath = @"ms-appx:///Assets/Resources/PreviewPanePropertiesInformation.json";
+
+ ///
+ /// The path to the json file containing settings string keys used for localized settings search.
+ ///
+ public const string SettingsStringKeysJsonPath = @"ms-appx:///Assets/Data/settings_string_keys.json";
}
public static class Filesystem
diff --git a/src/Files.App/Dialogs/SettingsDialog.xaml b/src/Files.App/Dialogs/SettingsDialog.xaml
index 451a492f67d2..f63ad612722b 100644
--- a/src/Files.App/Dialogs/SettingsDialog.xaml
+++ b/src/Files.App/Dialogs/SettingsDialog.xaml
@@ -81,6 +81,26 @@
OpenPaneLength="260"
PaneDisplayMode="Left"
SelectionChanged="MainSettingsNavigationView_SelectionChanged">
+
+
+
+
+
+
+
+
+
diff --git a/src/Files.App/Dialogs/SettingsDialog.xaml.cs b/src/Files.App/Dialogs/SettingsDialog.xaml.cs
index 0a1857749ce8..786f08e85175 100644
--- a/src/Files.App/Dialogs/SettingsDialog.xaml.cs
+++ b/src/Files.App/Dialogs/SettingsDialog.xaml.cs
@@ -5,6 +5,14 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
+using CommunityToolkit.WinUI.Controls;
+using Microsoft.UI.Xaml.Media;
+using System.Threading.Tasks;
+using System.IO;
+using Windows.Storage;
+using Windows.Foundation;
+using Windows.UI.Xaml.Automation;
+using Microsoft.UI.Xaml.Automation;
namespace Files.App.Dialogs
{
@@ -15,12 +23,18 @@ public sealed partial class SettingsDialog : ContentDialog, IDialog (FrameworkElement)MainWindow.Instance.Content;
+
+
+
+
public SettingsDialog()
{
InitializeComponent();
MainWindow.Instance.SizeChanged += Current_SizeChanged;
+
UpdateDialogLayout();
+ LoadSettingsKeysAsync();
}
public new async Task ShowAsync()
@@ -79,6 +93,193 @@ private void MainSettingsNavigationView_SelectionChanged(NavigationView sender,
};
}
+ private Dictionary> settingsKeysByPage = new();
+ private Dictionary keyToPage = new();
+ private Dictionary keyToLocalized = new();
+
+ public async void LoadSettingsKeysAsync()
+ {
+ try
+ {
+ var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(Constants.ResourceFilePaths.SettingsStringKeysJsonPath));
+ using var stream = await file.OpenStreamForReadAsync();
+ var dict = await JsonSerializer.DeserializeAsync>>(stream);
+ if (dict != null)
+ {
+ settingsKeysByPage = dict;
+ keyToPage.Clear();
+ keyToLocalized.Clear();
+ var resourceLoader = Windows.ApplicationModel.Resources.ResourceLoader.GetForViewIndependentUse();
+ foreach (var kvp in dict)
+ {
+ foreach (var key in kvp.Value)
+ {
+ keyToPage[key] = kvp.Key;
+ string localized = resourceLoader.GetString(key);
+ if (string.IsNullOrEmpty(localized))
+ localized = key;
+ keyToLocalized[key] = localized;
+ }
+ }
+ }
+ }
+ catch { }
+ }
+
+ private void SettingsSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
+ {
+ if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
+ {
+ var query = sender.Text?.Trim().ToLowerInvariant() ?? string.Empty;
+
+ var resourceLoader = Windows.ApplicationModel.Resources.ResourceLoader.GetForViewIndependentUse();
+ string noResults = resourceLoader.GetString("NoResultsFound");
+ if (string.IsNullOrEmpty(query))
+ {
+ // Show a placeholder for empty search
+ sender.ItemsSource = new List {
+ new SettingSuggestion { Key = null, Localized = noResults }
+ };
+ return;
+ }
+
+ var suggestions = keyToLocalized
+ .Where(kvp => kvp.Value.ToLowerInvariant().Contains(query))
+ .Select(kvp => new SettingSuggestion { Key = kvp.Key, Localized = kvp.Value })
+ .DistinctBy(s => s.Localized)
+ .ToList();
+
+ if (suggestions.Count == 0)
+ {
+ // Show a placeholder for no results
+ sender.ItemsSource = new List {
+ new SettingSuggestion { Key = null, Localized = noResults }
+ };
+ }
+ else
+ {
+ sender.ItemsSource = suggestions;
+ }
+ }
+ }
+
+ private class SettingSuggestion
+ {
+ public string Key { get; set; }
+ public string Localized { get; set; }
+ public override string ToString() => Localized;
+ }
+
+ private void SettingsSearchBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
+ {
+ // No action needed
+ }
+
+ private async void SettingsSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
+ {
+ var query = args.QueryText?.Trim().ToLowerInvariant() ?? string.Empty;
+
+ if (args.ChosenSuggestion is SettingSuggestion suggestion && !string.IsNullOrEmpty(suggestion.Key))
+ {
+ if (keyToPage.TryGetValue(suggestion.Key, out var page))
+ {
+ await NavigateToPageByName(page, suggestion.Localized);
+ }
+ }
+ else
+ {
+ var suggestions = keyToLocalized
+ .Where(x => x.Value.ToLowerInvariant().Contains(query))
+ .Select(x => new SettingSuggestion { Key = x.Key, Localized = x.Value })
+ .DistinctBy(s => s.Localized)
+ .ToList();
+
+ if (suggestions.Count == 0)
+ {
+ var resourceLoader = Windows.ApplicationModel.Resources.ResourceLoader.GetForViewIndependentUse();
+ string noResults = resourceLoader.GetString("NoResultsFound");
+ sender.ItemsSource = new List {
+ new SettingSuggestion { Key = null, Localized = noResults }
+ };
+ }
+ else
+ {
+ sender.ItemsSource = suggestions;
+ }
+ }
+ }
+
+ private async Task NavigateToPageByName(string pageName, string? localizedName = null)
+ {
+ foreach (NavigationViewItem item in MainSettingsNavigationView.MenuItems)
+ {
+ if ((item.Tag as string) == pageName)
+ {
+ MainSettingsNavigationView.SelectedItem = item;
+ break;
+ }
+ }
+
+ if (localizedName is not null)
+ {
+ await Task.Delay(100); // Wait for UI to render
+ var page = SettingsContentFrame.Content as FrameworkElement;
+ if (page is not null)
+ {
+ var element = FindElementByText(page, localizedName);
+ if (element is not null)
+ {
+ // Check if inside an Expander
+ var expander = FindParent(element);
+ if (expander is not null && !expander.IsExpanded)
+ {
+ expander.IsExpanded = true;
+ await Task.Delay(100); // Wait for animation
+ }
+
+ // Scroll to the element
+ var transform = element.TransformToVisual(SettingsContentScrollViewer);
+ var position = transform.TransformPoint(new Point(0, 0));
+ SettingsContentScrollViewer.ChangeView(null, position.Y, null, false);
+ }
+ }
+ }
+ }
+
+ private FrameworkElement? FindElementByText(FrameworkElement root, string text)
+ {
+ if (root is TextBlock tb && tb.Text == text)
+ return root;
+ if (AutomationProperties.GetName(root) == text)
+ return root;
+ if (root is ContentControl cc && cc.Content is string s && s == text)
+ return root;
+
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
+ {
+ var child = VisualTreeHelper.GetChild(root, i) as FrameworkElement;
+ if (child is not null)
+ {
+ var found = FindElementByText(child, text);
+ if (found is not null)
+ return found;
+ }
+ }
+ return null;
+ }
+
+ private T? FindParent(DependencyObject child) where T : DependencyObject
+ {
+ var parent = VisualTreeHelper.GetParent(child);
+ while (parent is not null)
+ {
+ if (parent is T t)
+ return t;
+ parent = VisualTreeHelper.GetParent(parent);
+ }
+ return null;
+ }
+
private void ContentDialog_Closing(ContentDialog sender, ContentDialogClosingEventArgs args)
{
MainWindow.Instance.SizeChanged -= Current_SizeChanged;
diff --git a/src/Files.App/Files.App.csproj b/src/Files.App/Files.App.csproj
index 28240e497c9b..761f5b92310e 100644
--- a/src/Files.App/Files.App.csproj
+++ b/src/Files.App/Files.App.csproj
@@ -60,8 +60,15 @@
PreserveNewest
+
+ PreserveNewest
+
+
+
+
+
diff --git a/src/Files.App/Scripts/extract_settings_strings.ps1 b/src/Files.App/Scripts/extract_settings_strings.ps1
new file mode 100644
index 000000000000..808d47053d2c
--- /dev/null
+++ b/src/Files.App/Scripts/extract_settings_strings.ps1
@@ -0,0 +1,46 @@
+# PowerShell script to extract all resource string keys used in settings pages and output as JSON mapping page to keys
+# Place this script in src/Files.App/Scripts and ensure it is referenced in the build process
+
+param(
+ [string]$SettingsPagesPath = (Join-Path $PSScriptRoot "..\Views\Settings"),
+ [string]$OutputPath = (Join-Path $PSScriptRoot "..\Assets\Data\settings_string_keys.json")
+)
+
+$allKeys = @{}
+
+
+Get-ChildItem -Path $SettingsPagesPath -Filter *.xaml -Recurse | ForEach-Object {
+ $page = $_.BaseName
+ $content = Get-Content $_.FullName -Raw
+ $settingsCardPattern = '<(?:wctcontrols:)?SettingsCard[^>]*?Header="\{helpers:ResourceString\s+Name=([a-zA-Z0-9_]+)\}"'
+ $settingsExpanderPattern = '<(?:wctcontrols:)?SettingsExpander[^>]*?Header="\{helpers:ResourceString\s+Name=([a-zA-Z0-9_]+)\}"'
+ $textBlockPattern = '<(?:wctcontrols:)?TextBlock[^>]*?(FontSize\s*=\s*"(24|16)")[^>]*?\{helpers:ResourceString\s+Name=([a-zA-Z0-9_]+)\}'
+ $settingsCardMatches = [regex]::Matches($content, $settingsCardPattern)
+ $settingsExpanderMatches = [regex]::Matches($content, $settingsExpanderPattern)
+ $textBlockMatches = [regex]::Matches($content, $textBlockPattern)
+ $keys = @()
+ foreach ($match in $settingsCardMatches) {
+ $key = $match.Groups[1].Value
+ if ($key -and ($keys -notcontains $key)) {
+ $keys += $key
+ }
+ }
+ foreach ($match in $settingsExpanderMatches) {
+ $key = $match.Groups[1].Value
+ if ($key -and ($keys -notcontains $key)) {
+ $keys += $key
+ }
+ }
+ foreach ($match in $textBlockMatches) {
+ $key = $match.Groups[3].Value
+ if ($key -and ($keys -notcontains $key)) {
+ $keys += $key
+ }
+ }
+ if ($keys.Count -gt 0) {
+ $allKeys[$page] = $keys
+ }
+}
+
+# Output as JSON object
+$allKeys | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 $OutputPath
diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw
index 0f0330f5c51f..1c0d9813f543 100644
--- a/src/Files.App/Strings/en-US/Resources.resw
+++ b/src/Files.App/Strings/en-US/Resources.resw
@@ -189,6 +189,9 @@
Search
+
+ No results found
+
Files you've previously accessed will show up here