diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp index edb39ad582..ddd30ade1e 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp +++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp @@ -26,6 +26,7 @@ #include #include +#include namespace winrt { @@ -463,31 +464,31 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation }); } - void MainPage::_Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage) + void MainPage::_Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage, hstring elementToFocus) { _PreNavigateHelper(); if (clickedItemTag == launchTag) { - contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone))); + contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone), elementToFocus)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Launch/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == interactionTag) { - contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone.GlobalSettings()))); + contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone.GlobalSettings()), elementToFocus)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Interaction/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == renderingTag) { - contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone))); + contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone), elementToFocus)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Rendering/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == compatibilityTag) { - contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone))); + contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone), elementToFocus)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Compatibility/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } @@ -508,7 +509,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation else { // Navigate to the NewTabMenu page - contentFrame().Navigate(xaml_typename(), winrt::make(_newTabMenuPageVM)); + contentFrame().Navigate(xaml_typename(), winrt::make(_newTabMenuPageVM, elementToFocus)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } @@ -523,7 +524,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } else { - contentFrame().Navigate(xaml_typename(), winrt::make(_extensionsVM)); + contentFrame().Navigate(xaml_typename(), winrt::make(_extensionsVM, elementToFocus)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } @@ -536,7 +537,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _SetupProfileEventHandling(profileVM); - contentFrame().Navigate(xaml_typename(), winrt::make(profileVM, *this)); + contentFrame().Navigate(xaml_typename(), winrt::make(profileVM, *this, elementToFocus)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_ProfileDefaults/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); @@ -558,7 +559,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_ColorSchemes/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); - contentFrame().Navigate(xaml_typename(), winrt::make(_colorSchemesPageVM)); + contentFrame().Navigate(xaml_typename(), winrt::make(_colorSchemesPageVM, elementToFocus)); if (subPage == BreadcrumbSubPage::ColorSchemes_Edit) { @@ -567,13 +568,13 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } else if (clickedItemTag == globalAppearanceTag) { - contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone.GlobalSettings()))); + contentFrame().Navigate(xaml_typename(), winrt::make(winrt::make(_settingsClone.GlobalSettings()), elementToFocus)); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_Appearance/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); } else if (clickedItemTag == addProfileTag) { - auto addProfileState{ winrt::make(_settingsClone) }; + auto addProfileState{ winrt::make(_settingsClone, elementToFocus) }; addProfileState.AddNew({ get_weak(), &MainPage::_AddProfileHandler }); contentFrame().Navigate(xaml_typename(), addProfileState); const auto crumb = winrt::make(box_value(clickedItemTag), RS_(L"Nav_AddNewProfile/Content"), BreadcrumbSubPage::None); @@ -586,7 +587,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation // - NOTE: this does not update the selected item. // Arguments: // - profile - the profile object we are getting a view of - void MainPage::_Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage) + void MainPage::_Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage, hstring elementToFocus) { _PreNavigateHelper(); @@ -594,14 +595,14 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation if (profile.Orphaned()) { - contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); + contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this, elementToFocus)); const auto crumb = winrt::make(box_value(profile), profile.Name(), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); profile.CurrentPage(ProfileSubPage::Base); return; } - contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this)); + contentFrame().Navigate(xaml_typename(), winrt::make(profile, *this, elementToFocus)); const auto crumb = winrt::make(box_value(profile), profile.Name(), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); @@ -625,11 +626,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } } - void MainPage::_Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage) + void MainPage::_Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage, hstring elementToFocus) { _PreNavigateHelper(); - contentFrame().Navigate(xaml_typename(), winrt::make(_newTabMenuPageVM)); + contentFrame().Navigate(xaml_typename(), winrt::make(_newTabMenuPageVM, elementToFocus)); const auto crumb = winrt::make(box_value(newTabMenuTag), RS_(L"Nav_NewTabMenu/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); @@ -658,11 +659,11 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } } - void MainPage::_Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage) + void MainPage::_Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage, hstring elementToFocus) { _PreNavigateHelper(); - contentFrame().Navigate(xaml_typename(), winrt::make(_extensionsVM)); + contentFrame().Navigate(xaml_typename(), winrt::make(_extensionsVM, elementToFocus)); const auto crumb = winrt::make(box_value(extensionsTag), RS_(L"Nav_Extensions/Content"), BreadcrumbSubPage::None); _breadcrumbs.Append(crumb); @@ -906,7 +907,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation uint32_t vmIndex; if (_menuItemSource.IndexOf(profileVM, vmIndex)) { - _profileVMs.RemoveAt(vmIndex); + _profileVMs.RemoveAt(vmIndex); } // navigate to the profile next to this one @@ -1022,13 +1023,145 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation } } + safe_void_coroutine MainPage::SettingsSearchBox_TextChanged(const AutoSuggestBox& sender, const AutoSuggestBoxTextChangedEventArgs& args) + { + // TODO CARLOS: debug this function. May still need debounce? + if (args.Reason() != AutoSuggestionBoxTextChangeReason::UserInput) + { + // Only respond to user input, not programmatic text changes + co_return; + } + + // remove leading spaces + std::wstring queryW{ sender.Text() }; + const auto firstNonSpace{ queryW.find_first_not_of(L' ') }; + if (firstNonSpace == std::wstring::npos) + { + // only spaces + const auto& searchBox = SettingsSearchBox(); + searchBox.ItemsSource(nullptr); + searchBox.IsSuggestionListOpen(false); + co_return; + } + + const hstring sanitizedQuery{ queryW.substr(firstNonSpace) }; + if (sanitizedQuery.empty()) + { + // empty query + const auto& searchBox = SettingsSearchBox(); + searchBox.ItemsSource(nullptr); + searchBox.IsSuggestionListOpen(false); + co_return; + } + + // search the index on the background thread + co_await winrt::resume_background(); + const auto searchGeneration = _QuerySearchIndex(sanitizedQuery); + + // convert and display results on the foreground thread + co_await winrt::resume_foreground(Dispatcher()); + if (searchGeneration != _filteredSearchIndex.generation()) + { + // search index was updated while we were searching, discard results + co_return; + } + + // must be IInspectable to be used as ItemsSource + std::vector results; + if (_filteredSearchIndex->empty()) + { + // Explicitly show "no results" + + // TODO CARLOS: use RS_switchable_fmt from ActionArgs.cpp + const hstring noResultsText{ fmt::format(fmt::runtime(std::wstring{ RS_(L"Search_NoResults") }), sanitizedQuery) }; + + results.reserve(1); + results.push_back(winrt::make(noResultsText)); + } + else + { + for (const auto* indexEntry : *_filteredSearchIndex) + { + results.push_back(winrt::make(*indexEntry)); + } + } + + // Update the UI with the results + const auto& searchBox = SettingsSearchBox(); + searchBox.ItemsSource(winrt::single_threaded_observable_vector(std::move(results))); + searchBox.IsSuggestionListOpen(true); + } + + // Update _filteredSearchIndex with results matching queryText + til::generation_t MainPage::_QuerySearchIndex(const hstring& queryText) + { + auto filteredResults{ _filteredSearchIndex.write() }; + + filteredResults->clear(); + for (const auto& entry : _searchIndex) + { + // TODO CARLOS: replace with fuzzy search + const auto& displayText = entry.DisplayText; + if (til::contains_linguistic_insensitive(displayText, queryText)) + { + filteredResults->push_back(&entry); + } + } + return _filteredSearchIndex.generation(); + } + + void MainPage::SettingsSearchBox_QuerySubmitted(const AutoSuggestBox& /*sender*/, const AutoSuggestBoxQuerySubmittedEventArgs& args) + { + if (args.ChosenSuggestion()) + { + const auto& chosenResult{ args.ChosenSuggestion().as() }; + if (chosenResult->IsNoResultsPlaceholder()) + { + // don't navigate anywhere + return; + } + + // Navigate to the target page + const auto& indexEntry{ chosenResult->SearchIndexEntry() }; + const auto& navigationParam{ indexEntry.NavigationParam }; + if (navigationParam.try_as()) + { + _Navigate(navigationParam.as(), indexEntry.SubPage, indexEntry.ElementName); + } + else if (const auto profileVM = navigationParam.try_as()) + { + _Navigate(*profileVM, indexEntry.SubPage, indexEntry.ElementName); + } + else if (const auto ntmEntryVM = navigationParam.try_as()) + { + _Navigate(*ntmEntryVM, indexEntry.SubPage, indexEntry.ElementName); + } + else if (const auto extPkgVM = navigationParam.try_as()) + { + _Navigate(*extPkgVM, indexEntry.SubPage, indexEntry.ElementName); + } + } + } + + void MainPage::SettingsSearchBox_SuggestionChosen(const AutoSuggestBox&, const AutoSuggestBoxSuggestionChosenEventArgs&) + { + // Don't navigate on arrow keys + // Handle Enter/Click with QuerySubmitted() to instead + // AutoSuggestBox will pass the chosen item to QuerySubmitted() via args.ChosenSuggestion() + } + void MainPage::_UpdateSearchIndex() { _searchIndex = {}; + // TODO CARLOS: delay evaluating resources to here. + // - we also want to allow searching in English regardless or the selected language + // - we still want to only show the native language though + // - we should update the index to use USES_RESOURCE instead of RS_, then evaluate resources here + const auto buildIndex = LoadBuildTimeIndex(); _searchIndex.reserve(buildIndex.size()); - _searchIndex.assign(buildIndex.begin(), buildIndex.end()); + _searchIndex.insert(_searchIndex.end(), buildIndex.begin(), buildIndex.end()); // Load profiles for (const auto& profile : _profileVMs) diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.h b/src/cascadia/TerminalSettingsEditor/MainPage.h index 0386361bcb..1da7215c58 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.h +++ b/src/cascadia/TerminalSettingsEditor/MainPage.h @@ -5,8 +5,10 @@ #include "MainPage.g.h" #include "Breadcrumb.g.h" +#include "FilteredSearchResult.g.h" #include "Utils.h" #include "GeneratedSettingsIndex.g.h" +#include namespace winrt::Microsoft::Terminal::Settings::Editor::implementation { @@ -24,6 +26,44 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation WINRT_PROPERTY(BreadcrumbSubPage, SubPage); }; + struct FilteredSearchResult : FilteredSearchResultT + { + FilteredSearchResult(const winrt::hstring& label) : + _overrideLabel{ label } {} + + FilteredSearchResult(const IndexEntry& entry) : + _SearchIndexEntry{ &entry } {} + + hstring ToString() { return Label(); } + + winrt::hstring Label() const + { + if (_overrideLabel) + { + return _overrideLabel.value(); + } + else if (_SearchIndexEntry) + { + return _SearchIndexEntry->DisplayText; + } + return {}; + } + + bool IsNoResultsPlaceholder() const + { + return _overrideLabel.has_value(); + } + + const IndexEntry& SearchIndexEntry() const noexcept + { + return *_SearchIndexEntry; + } + + private: + std::optional _overrideLabel{ std::nullopt }; + const IndexEntry* _SearchIndexEntry{ nullptr }; + }; + struct MainPage : MainPageT { MainPage() = delete; @@ -31,6 +71,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void UpdateSettings(const Model::CascadiaSettings& settings); + safe_void_coroutine SettingsSearchBox_TextChanged(const Windows::UI::Xaml::Controls::AutoSuggestBox& sender, const Windows::UI::Xaml::Controls::AutoSuggestBoxTextChangedEventArgs& args); + void SettingsSearchBox_QuerySubmitted(const Windows::UI::Xaml::Controls::AutoSuggestBox& sender, const Windows::UI::Xaml::Controls::AutoSuggestBoxQuerySubmittedEventArgs& args); + void SettingsSearchBox_SuggestionChosen(const Windows::UI::Xaml::Controls::AutoSuggestBox& sender, const Windows::UI::Xaml::Controls::AutoSuggestBoxSuggestionChosenEventArgs& args); + void SettingsNav_Loaded(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); void SettingsNav_ItemInvoked(const Microsoft::UI::Xaml::Controls::NavigationView& sender, const Microsoft::UI::Xaml::Controls::NavigationViewItemInvokedEventArgs& args); void SaveButton_Click(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); @@ -68,15 +112,17 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation void _SetupProfileEventHandling(const winrt::Microsoft::Terminal::Settings::Editor::ProfileViewModel profile); void _PreNavigateHelper(); - void _Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage); - void _Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage); - void _Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage); - void _Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage); + void _Navigate(hstring clickedItemTag, BreadcrumbSubPage subPage, hstring elementToFocus = {}); + void _Navigate(const Editor::ProfileViewModel& profile, BreadcrumbSubPage subPage, hstring elementToFocus = {}); + void _Navigate(const Editor::NewTabMenuEntryViewModel& ntmEntryVM, BreadcrumbSubPage subPage, hstring elementToFocus = {}); + void _Navigate(const Editor::ExtensionPackageViewModel& extPkgVM, BreadcrumbSubPage subPage, hstring elementToFocus = {}); void _NavigateToProfileHandler(const IInspectable& sender, winrt::guid profileGuid); void _NavigateToColorSchemeHandler(const IInspectable& sender, const IInspectable& args); void _UpdateBackgroundForMica(); void _MoveXamlParsedNavItemsIntoItemSource(); + + til::generation_t _QuerySearchIndex(const hstring& queryText); void _UpdateSearchIndex(); Windows::Foundation::Collections::IVector _profileVMs{ nullptr }; @@ -85,6 +131,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation winrt::Microsoft::Terminal::Settings::Editor::ExtensionsViewModel _extensionsVM{ nullptr }; std::vector _searchIndex; + til::generational> _filteredSearchIndex; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _profileViewModelChangedRevoker; Windows::UI::Xaml::Data::INotifyPropertyChanged::PropertyChanged_revoker _colorSchemesPageViewModelChangedRevoker; diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.idl b/src/cascadia/TerminalSettingsEditor/MainPage.idl index 20f3bab869..9ea551bdfd 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.idl +++ b/src/cascadia/TerminalSettingsEditor/MainPage.idl @@ -26,6 +26,11 @@ namespace Microsoft.Terminal.Settings.Editor Extensions_Extension }; + runtimeclass FilteredSearchResult : Windows.Foundation.IStringable + { + String Label { get; }; + } + runtimeclass Breadcrumb : Windows.Foundation.IStringable { IInspectable Tag; diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.xaml b/src/cascadia/TerminalSettingsEditor/MainPage.xaml index 77c1bd8488..d6847dc5c4 100644 --- a/src/cascadia/TerminalSettingsEditor/MainPage.xaml +++ b/src/cascadia/TerminalSettingsEditor/MainPage.xaml @@ -99,6 +99,21 @@ + + + + + + + + + + Yes, clear the cache - + + Search for settings + Placeholder text for the main search box in the settings editor. + + + No results for {} + {Locked="{}"} Displayed when no results were found for a given query. "{}" will be replaced with the query. + + \ No newline at end of file