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;