From bab3a4e18152f472804e15d69aee6f405ff71583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 22 Oct 2026 14:54:39 +0200 Subject: [PATCH] TabbedDialog: Move the settings search into the base screen Any tabbed screen can now add tabs described by a SettingsPage with AddSettingsTab, and gets the search tab that shows matching settings with arrows to jump to them. The game settings screen is the first to use it. Screens without settings tabs keep the old filter for now. Co-Authored-By: Claude Opus 5.5 (1M context) --- UI/GameSettingsScreen.cpp | 169 ++------------------------------------ UI/GameSettingsScreen.h | 11 --- UI/TabbedDialogScreen.cpp | 164 +++++++++++++++++++++++++++++++++++- UI/TabbedDialogScreen.h | 23 ++++++ 4 files changed, 195 insertions(+), 172 deletions(-) diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index b548c83c99..cf2816a050 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -28,7 +28,6 @@ #include "Common/UI/Root.h" #include "Common/UI/View.h" #include "Common/UI/ViewGroup.h" -#include "Common/UI/ScrollView.h" #include "Common/UI/Context.h" #include "Common/UI/Notice.h" #include "Common/UI/ScreenManager.h" @@ -264,170 +263,20 @@ void GameSettingsScreen::CreateTabs() { int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE); bool showVR = deviceType == DEVICE_TYPE_VR || g_Config.bForceVR; - pages_.clear(); - pageViews_.clear(); - pendingFocusView_ = nullptr; for (const PageDef &def : PageDefs()) { if (def.build == &GameSettingsScreen::BuildVRSettings && !showVR) { continue; } - pages_.push_back(&def); - } - pageViews_.resize(pages_.size()); - - for (int i = 0; i < (int)pages_.size(); i++) { - const PageDef *def = pages_[i]; - AddTab(def->tag, ms->T(def->title), def->icon ? ImageID(def->icon) : ImageID::invalid(), [this, def, i](UI::LinearLayout *parent) { - if (def->helpTag) { - parent->Add(new PaneTitleBar(gamePath_, T(I18NCat::MAINSETTINGS, def->title), def->helpTag)); - } - SettingsPage page(def->cat); - (this->*def->build)(page); - pageViews_[i].clear(); - CreateSettingViews(page, parent, screenManager(), &pageViews_[i]); - }); - } - - if (CanShowSearch()) { - AddTab("GameSettingsSearch", ms->T("Search"), ImageID("I_SEARCH"), [this](UI::LinearLayout *parent) { - CreateSearchTab(parent); - }); - } -} - -static bool SettingTextMatches(const SettingText &text, I18NCat pageCat, std::string_view query) { - if (text.empty()) { - return false; - } - return containsNoCase(text.Translate(pageCat), query) || containsNoCase(text.English(), query); -} - -// Shows the matching settings themselves, so they can be changed right here. -void GameSettingsScreen::CreateSearchTab(UI::LinearLayout *parent) { - using namespace UI; - auto se = GetI18NCategory(I18NCat::SEARCH); - - parent->Add(new ItemHeader(se->T("Find settings"))); - parent->Add(new PopupTextInputChoice(GetRequesterToken(), &searchText_, se->T("Filter"), "", 64, screenManager()))->OnChange.Add([this](UI::EventParams &e) { - searchText_ = StripSpaces(searchText_); - RecreateViews(); - }); - if (searchText_.empty()) { - return; - } - parent->Add(new Choice(se->T("Clear filter")))->OnClick.Add([this](UI::EventParams &e) { - searchText_.clear(); - RecreateViews(); - }); - - bool found = false; - for (int p = 0; p < (int)pages_.size(); p++) { - const PageDef *def = pages_[p]; - SettingsPage page(def->cat); - (this->*def->build)(page); - const I18NCat cat = page.DefaultCat(); - const std::deque &items = page.Items(); - - // A matching header shows its whole section, and a matching hint the setting it belongs to. - std::vector show(items.size()); - bool sectionMatches = false; - int lastSetting = -1; - for (int i = 0; i < (int)items.size(); i++) { - const SettingItem &item = items[i]; - bool matches = SettingTextMatches(item.text, cat, searchText_); - switch (item.type) { - case SettingType::Header: - sectionMatches = matches; - break; - case SettingType::Hint: - if (matches && lastSetting >= 0) { - show[lastSetting] = true; - } - break; - default: - lastSetting = i; - show[i] = matches || sectionMatches; - break; - } + const PageDef *pageDef = &def; + std::function createHeader; + if (def.helpTag) { + createHeader = [this, pageDef](UI::LinearLayout *parent) { + parent->Add(new PaneTitleBar(gamePath_, T(I18NCat::MAINSETTINGS, pageDef->title), pageDef->helpTag)); + }; } - - const SettingItem *header = nullptr; - bool headerShown = false; - View *prevView = nullptr; - for (int i = 0; i < (int)items.size(); i++) { - const SettingItem &item = items[i]; - if (item.type == SettingType::Header) { - header = &item; - headerShown = false; - continue; - } - if (item.type == SettingType::Hint) { - if (prevView) { - CreateSettingView(item, cat, prevView, parent, screenManager()); - } - continue; - } - prevView = nullptr; - if (!show[i]) { - continue; - } - - LinearLayout *row = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); - row->SetSpacing(0.0f); - View *view = CreateSettingView(item, cat, nullptr, row, screenManager()); - if (!view) { - delete row; - continue; - } - // FILL_PARENT, since some views (like CheckBox) measure their width from their bounds. - view->ReplaceLayoutParams(new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f, Gravity::G_VCENTER)); - Choice *goTo = row->Add(new Choice(ImageID("I_ARROW_RIGHT"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT, 0.0f, Gravity::G_VCENTER))); - goTo->OnClick.Add([this, p, i](UI::EventParams &e) { - GoToSetting(p, i); - }); - - if (!headerShown) { - std::string title(T(I18NCat::MAINSETTINGS, def->title)); - if (header) { - title += " > "; - title += header->text.Translate(cat); - } - parent->Add(new ItemHeader(title)); - headerShown = true; - } - parent->Add(row); - prevView = view; - found = true; - } - } - - if (!found) { - parent->Add(new TextView(ApplySafeSubstitutions(se->T("No settings matched '%1'"), searchText_), new LinearLayoutParams(Margins(20, 5)))); - } -} - -// Switches to the setting's tab, and focuses it (in update) so it's highlighted and scrolled into view. -void GameSettingsScreen::GoToSetting(int pageIndex, int itemIndex) { - SetCurrentTab(pageIndex); - if (pageIndex >= (int)pageViews_.size() || itemIndex >= (int)pageViews_[pageIndex].size()) { - return; - } - pendingFocusView_ = pageViews_[pageIndex][itemIndex]; -} - -void GameSettingsScreen::update() { - UITabbedBaseDialogScreen::update(); - - // The tab may have just been created, or not been laid out while hidden. Scrolling needs the - // view's position, so wait for a layout. - if (pendingFocusView_ && pendingFocusView_->GetBounds().h > 0.0f) { - UI::ScrollView *scrollView = UI::GetEnclosingScrollView(pendingFocusView_); - if (scrollView) { - scrollView->ScrollToCenter(pendingFocusView_); - } - // Highlights it. Done directly, since SetFocus does nothing when focus movement is off (mouse or touch). - UI::SetFocusedView(pendingFocusView_, UI::FocusFlags::CAUSE_RESTORE); - pendingFocusView_ = nullptr; + AddSettingsTab(def.tag, ms->T(def.title), def.icon ? ImageID(def.icon) : ImageID::invalid(), def.cat, [this, pageDef](SettingsPage &page) { + (this->*pageDef->build)(page); + }, createHeader); } } diff --git a/UI/GameSettingsScreen.h b/UI/GameSettingsScreen.h index 872f01cadc..a7657ead86 100644 --- a/UI/GameSettingsScreen.h +++ b/UI/GameSettingsScreen.h @@ -40,7 +40,6 @@ public: ~GameSettingsScreen(); const char *tag() const override { return "GameSettings"; } - void update() override; protected: void CallbackRestoreDefaults(bool yes); @@ -48,8 +47,6 @@ protected: void dialogFinished(const Screen *dialog, DialogResult result) override; void CreateTabs() override; - // Has its own search tab, which shows the matching settings instead of filtering the tabs. - bool ShowSearchControls() const override { return false; } private: void PreCreateViews() override; @@ -74,14 +71,6 @@ private: void BuildVRSettings(SettingsPage &page); UI::View *AddNicknameChoice(UI::ViewGroup *parent); - void CreateSearchTab(UI::LinearLayout *parent); - void GoToSetting(int pageIndex, int itemIndex); - - // The tabs shown, and the views created for each page's items (filled in when a tab is created). - std::vector pages_; - std::vector> pageViews_; - std::string searchText_; - UI::View *pendingFocusView_ = nullptr; std::string gameID_; #if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) diff --git a/UI/TabbedDialogScreen.cpp b/UI/TabbedDialogScreen.cpp index e6e1617d1e..2755dfd042 100644 --- a/UI/TabbedDialogScreen.cpp +++ b/UI/TabbedDialogScreen.cpp @@ -10,8 +10,10 @@ #include "Common/UI/ScrollView.h" #include "Common/UI/PopupScreens.h" #include "Common/UI/ScreenManager.h" +#include "Common/UI/Root.h" #include "UI/MiscViews.h" #include "Common/UI/Context.h" +#include "UI/SettingsPage.h" #include "UI/TabbedDialogScreen.h" UITabbedBaseDialogScreen::UITabbedBaseDialogScreen(const Path &gamePath, int *currentTabSetting, TabDialogFlags flags) @@ -53,9 +55,27 @@ void UITabbedBaseDialogScreen::AddTab(const char *tag, std::string_view title, I }); } +void UITabbedBaseDialogScreen::AddSettingsTab(const char *tag, std::string_view title, ImageID imageId, I18NCat cat, std::function build, std::function createHeader) { + int index = (int)settingsTabs_.size(); + settingsTabs_.push_back(SettingsTab{ (int)tabHolder_->GetTabContentViews().size(), std::string(title), cat, std::move(build) }); + AddTab(tag, title, imageId, [this, index, createHeader = std::move(createHeader)](UI::LinearLayout *parent) { + if (createHeader) { + createHeader(parent); + } + SettingsTab &tab = settingsTabs_[index]; + SettingsPage page(tab.cat); + tab.build(page); + tab.views.clear(); + CreateSettingViews(page, parent, screenManager(), &tab.views); + }); +} + void UITabbedBaseDialogScreen::CreateViews() { PreCreateViews(); + settingsTabs_.clear(); + pendingFocusView_ = nullptr; + bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait || ForceHorizontalTabs(); // Information in the top left. @@ -121,7 +141,12 @@ void UITabbedBaseDialogScreen::CreateViews() { } }); - if (ShowSearchControls() && CanShowSearch()) { + if (ShowSearchControls() && CanShowSearch() && !settingsTabs_.empty()) { + auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); + AddTab("GameSettingsSearch", ms->T("Search"), ImageID("I_SEARCH"), [this](UI::LinearLayout *parent) { + CreateSearchTab(parent); + }); + } else if (ShowSearchControls() && CanShowSearch()) { auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); AddTab("GameSettingsSearch", ms->T("Search"), ImageID("I_SEARCH"), [this](UI::LinearLayout *searchSettings) { auto se = GetI18NCategory(I18NCat::SEARCH); @@ -257,3 +282,140 @@ void UITabbedBaseDialogScreen::ApplySearchFilter() { clearSearchChoice_->SetVisibility(searchFilter_.empty() ? UI::V_GONE : UI::V_VISIBLE); } } + +static bool SettingTextMatches(const SettingText &text, I18NCat pageCat, std::string_view query) { + if (text.empty()) { + return false; + } + return containsNoCase(text.Translate(pageCat), query) || containsNoCase(text.English(), query); +} + +// Shows the matching settings themselves, so they can be changed right here. +void UITabbedBaseDialogScreen::CreateSearchTab(UI::LinearLayout *parent) { + using namespace UI; + auto se = GetI18NCategory(I18NCat::SEARCH); + + parent->Add(new ItemHeader(se->T("Find settings"))); + parent->Add(new PopupTextInputChoice(GetRequesterToken(), &searchText_, se->T("Filter"), "", 64, screenManager()))->OnChange.Add([this](UI::EventParams &e) { + searchText_ = StripSpaces(searchText_); + RecreateViews(); + }); + if (searchText_.empty()) { + return; + } + parent->Add(new Choice(se->T("Clear filter")))->OnClick.Add([this](UI::EventParams &e) { + searchText_.clear(); + RecreateViews(); + }); + + bool found = false; + for (int t = 0; t < (int)settingsTabs_.size(); t++) { + const SettingsTab &tab = settingsTabs_[t]; + SettingsPage page(tab.cat); + tab.build(page); + const I18NCat cat = page.DefaultCat(); + const std::deque &items = page.Items(); + + // A matching header shows its whole section, and a matching hint the setting it belongs to. + std::vector show(items.size()); + bool sectionMatches = false; + int lastSetting = -1; + for (int i = 0; i < (int)items.size(); i++) { + const SettingItem &item = items[i]; + bool matches = SettingTextMatches(item.text, cat, searchText_); + switch (item.type) { + case SettingType::Header: + sectionMatches = matches; + break; + case SettingType::Hint: + if (matches && lastSetting >= 0) { + show[lastSetting] = true; + } + break; + default: + lastSetting = i; + show[i] = matches || sectionMatches; + break; + } + } + + const SettingItem *header = nullptr; + bool headerShown = false; + View *prevView = nullptr; + for (int i = 0; i < (int)items.size(); i++) { + const SettingItem &item = items[i]; + if (item.type == SettingType::Header) { + header = &item; + headerShown = false; + continue; + } + if (item.type == SettingType::Hint) { + if (prevView) { + CreateSettingView(item, cat, prevView, parent, screenManager()); + } + continue; + } + prevView = nullptr; + if (!show[i]) { + continue; + } + + LinearLayout *row = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); + row->SetSpacing(0.0f); + View *view = CreateSettingView(item, cat, nullptr, row, screenManager()); + if (!view) { + delete row; + continue; + } + // FILL_PARENT, since some views (like CheckBox) measure their width from their bounds. + view->ReplaceLayoutParams(new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f, Gravity::G_VCENTER)); + Choice *goTo = row->Add(new Choice(ImageID("I_ARROW_RIGHT"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT, 0.0f, Gravity::G_VCENTER))); + goTo->OnClick.Add([this, t, i](UI::EventParams &e) { + GoToSetting(t, i); + }); + + if (!headerShown) { + std::string title = tab.title; + if (header) { + title += " > "; + title += header->text.Translate(cat); + } + parent->Add(new ItemHeader(title)); + headerShown = true; + } + parent->Add(row); + prevView = view; + found = true; + } + } + + if (!found) { + parent->Add(new TextView(ApplySafeSubstitutions(se->T("No settings matched '%1'"), searchText_), new LinearLayoutParams(Margins(20, 5)))); + } +} + +// Switches to the setting's tab, and focuses it (in update) so it's highlighted and scrolled into view. +void UITabbedBaseDialogScreen::GoToSetting(int settingsTab, int itemIndex) { + const SettingsTab &tab = settingsTabs_[settingsTab]; + SetCurrentTab(tab.tabIndex); + // The views are filled in when the tab is created, which SetCurrentTab does if needed. + if (itemIndex < (int)tab.views.size()) { + pendingFocusView_ = tab.views[itemIndex]; + } +} + +void UITabbedBaseDialogScreen::update() { + UIBaseDialogScreen::update(); + + // The tab may have just been created, or not been laid out while hidden. Scrolling needs the + // view's position, so wait for a layout. + if (pendingFocusView_ && pendingFocusView_->GetBounds().h > 0.0f) { + UI::ScrollView *scrollView = UI::GetEnclosingScrollView(pendingFocusView_); + if (scrollView) { + scrollView->ScrollToCenter(pendingFocusView_); + } + // Highlights it. Done directly, since SetFocus does nothing when focus movement is off (mouse or touch). + UI::SetFocusedView(pendingFocusView_, UI::FocusFlags::CAUSE_RESTORE); + pendingFocusView_ = nullptr; + } +} diff --git a/UI/TabbedDialogScreen.h b/UI/TabbedDialogScreen.h index 198eb7b736..9afb115487 100644 --- a/UI/TabbedDialogScreen.h +++ b/UI/TabbedDialogScreen.h @@ -2,7 +2,10 @@ #include "ppsspp_config.h" #include +#include +#include +#include "Common/Data/Text/I18n.h" #include "Common/UI/UIScreen.h" #include "Common/Render/TextureAtlas.h" #include "Common/System/System.h" @@ -13,6 +16,8 @@ namespace UI { class TabHolder; } +class SettingsPage; + enum class TabDialogFlags { Default = 0, HorizontalOnlyIcons = 1, @@ -36,7 +41,11 @@ public: void AddTab(const char *tag, std::string_view title, std::function createCallback, TabFlags flags = TabFlags::Default) { AddTab(tag, title, ImageID::invalid(), createCallback, flags); } + // A tab described by a SettingsPage, whose settings show up in search. createHeader can add views above + // the page, like a title bar. build may be called at any time (also for search), so it shouldn't create views. + void AddSettingsTab(const char *tag, std::string_view title, ImageID imageId, I18NCat cat, std::function build, std::function createHeader = nullptr); void CreateViews() override; + void update() override; protected: // Load data and define your tabs here. @@ -57,6 +66,20 @@ protected: private: void ApplySearchFilter(); + void CreateSearchTab(UI::LinearLayout *parent); + void GoToSetting(int settingsTab, int itemIndex); + + struct SettingsTab { + int tabIndex; + std::string title; + I18NCat cat; + std::function build; + std::vector views; // One per page item, filled in when the tab is created. + }; + std::vector settingsTabs_; + std::string searchText_; + // Highlighted and scrolled to once laid out. + UI::View *pendingFocusView_ = nullptr; UI::TabHolder *tabHolder_ = nullptr; UI::TextView *filterNotice_ = nullptr;