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