Files
ppsspp/UI/TabbedDialogScreen.cpp
T
Henrik RydgårdandClaude Opus 5.5 54e4b3a1c4 Settings: Show matching settings directly in the search tab
Instead of hiding the non-matching views in every tab and disabling the tabs
without matches, the search tab now builds each page and shows the matching
settings right there, where they can be changed, grouped under their tab and
section. Matching a section header shows the whole section, and matching a
hint shows its setting. Searches match both the translated and the English
text.

Each result has an arrow that switches to the setting's tab, scrolls it to the
middle and highlights it.

The other tabbed screens with search (Developer Tools) keep the old filter for
now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 14:46:46 +02:00

260 lines
8.9 KiB
C++

#include <algorithm>
#include "Common/Data/Text/I18n.h"
#include "Common/StringUtils.h"
#include "Common/System/NativeApp.h"
#include "Common/System/Request.h"
#include "Common/System/Display.h"
#include "Common/UI/TabHolder.h"
#include "Common/UI/ViewGroup.h"
#include "Common/UI/ScrollView.h"
#include "Common/UI/PopupScreens.h"
#include "Common/UI/ScreenManager.h"
#include "UI/MiscViews.h"
#include "Common/UI/Context.h"
#include "UI/TabbedDialogScreen.h"
UITabbedBaseDialogScreen::UITabbedBaseDialogScreen(const Path &gamePath, int *currentTabSetting, TabDialogFlags flags)
: UIBaseDialogScreen(gamePath), currentTabSetting_(currentTabSetting), flags_(flags) {}
void UITabbedBaseDialogScreen::AddTab(const char *tag, std::string_view title, ImageID imageId, std::function<void(UI::LinearLayout *)> createCallback, TabFlags flags) {
using namespace UI;
TabDialogFlags dialogFlags = flags_;
Path gamePath = gamePath_;
std::string cachedTitle(title);
tabHolder_->AddTabDeferred(title, imageId, [this, createCallback = std::move(createCallback), tag, flags, dialogFlags, gamePath, cachedTitle]() -> UI::ViewGroup * {
using namespace UI;
ViewGroup *scroll = nullptr;
if (!(flags & TabFlags::NonScrollable)) {
scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
scroll->SetTag(tag);
}
LinearLayout *contents = new LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(Margins(0, 0, 8, 0)));
contents->SetSpacing(0);
if (dialogFlags & TabDialogFlags::AddAutoTitles) {
auto di = GetI18NCategory(I18NCat::DIALOG);
contents->Add(new PaneTitleBar(gamePath, cachedTitle, ""));
}
// Tabs other than the first are created on demand, outside CreateViews. Their views still belong to the
// current tree, so their requests must get the views token, to be forgotten when the tree is deleted.
bool wasCreatingViews = IsCreatingViews();
SetCreatingViews(true);
createCallback(contents);
SetCreatingViews(wasCreatingViews);
if (scroll) {
scroll->Add(contents);
return scroll;
} else {
return contents;
}
});
}
void UITabbedBaseDialogScreen::CreateViews() {
PreCreateViews();
bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait || ForceHorizontalTabs();
// Information in the top left.
// Back button to the bottom left.
// Scrolling action menu to the right.
using namespace UI;
// Only created with the search tab, which depends on the orientation, so may not be there this time.
clearSearchChoice_ = nullptr;
noSearchResults_ = nullptr;
auto se = GetI18NCategory(I18NCat::SEARCH);
filterNotice_ = new TextView("(filter notice, you shouldn't see this text", new LinearLayoutParams(Margins(20, 5)));
filterNotice_->SetVisibility(V_GONE);
if (portrait) {
auto di = GetI18NCategory(I18NCat::DIALOG);
LinearLayout *verticalLayout = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(FILL_PARENT, FILL_PARENT));
TabHolderFlags tabHolderFlags = TabHolderFlags::BackButton;
if (flags_ & TabDialogFlags::HorizontalOnlyIcons) {
tabHolderFlags |= TabHolderFlags::HorizontalOnlyIcons;
}
std::function<void()> contextMenu;
if (flags_ & TabDialogFlags::ContextMenuInPortrait) {
contextMenu = [this]() {
this->screenManager()->push(new PopupCallbackScreen([this](UI::ViewGroup *parent) {
CreateExtraButtons(parent, 0);
}, nullptr));
};
}
tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 200, tabHolderFlags, filterNotice_, contextMenu, new LinearLayoutParams(1.0f));
verticalLayout->Add(tabHolder_);
if (!(flags_ & TabDialogFlags::ContextMenuInPortrait)) {
CreateExtraButtons(verticalLayout, 0);
}
root_ = verticalLayout;
} else {
TabHolderFlags tabHolderFlags = TabHolderFlags::Default;
if (flags_ & TabDialogFlags::VerticalShowIcons) {
tabHolderFlags |= TabHolderFlags::VerticalShowIcons;
}
tabHolder_ = new TabHolder(ORIENT_VERTICAL, 300, tabHolderFlags, filterNotice_, nullptr, new AnchorLayoutParams(10, 0, 0, 0));
CreateExtraButtons(tabHolder_->Container(), 10);
tabHolder_->AddBack(this);
root_ = tabHolder_;
}
tabHolder_->SetTag(tag()); // take the tag from the screen.
root_->SetDefaultFocusView(tabHolder_);
float leftSide = 40.0f;
if (!portrait) {
leftSide += 200.0f;
}
// Let the subclass create its tabs.
CreateTabs();
tabHolder_->OnChangeTab.Add([this](UI::EventParams &e) {
if (currentTabSetting_) {
*currentTabSetting_ = e.a;
}
});
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);
searchSettings->Add(new ItemHeader(se->T("Find settings")));
searchSettings->Add(new PopupTextInputChoice(GetRequesterToken(), &searchFilter_, se->T("Filter"), "", 64, screenManager()))->OnChange.Add([this](UI::EventParams &e) {
System_PostUIMessage(UIMessage::GAMESETTINGS_SEARCH, StripSpaces(searchFilter_));
});
clearSearchChoice_ = searchSettings->Add(new Choice(se->T("Clear filter")));
clearSearchChoice_->OnClick.Add([](UI::EventParams &e) {
System_PostUIMessage(UIMessage::GAMESETTINGS_SEARCH, "");
});
clearSearchChoice_->SetVisibility(searchFilter_.empty() ? UI::V_GONE : UI::V_VISIBLE);
noSearchResults_ = searchSettings->Add(new TextView("", new LinearLayoutParams(Margins(20, 5))));
});
}
// After the search tab, which the saved tab can be.
if (currentTabSetting_) {
tabHolder_->SetInitialTab(*currentTabSetting_);
} else {
tabHolder_->EnsureTab(tabHolder_->GetCurrentTab());
}
// Need to handle recreates, both important and accidental ones.
if (!searchFilter_.empty()) {
ApplySearchFilter();
}
}
void UITabbedBaseDialogScreen::sendMessage(UIMessage message, const char *value) {
UIBaseDialogScreen::sendMessage(message, value);
if (message == UIMessage::GAMESETTINGS_SEARCH) {
std::string filter = value ? value : "";
searchFilter_.resize(filter.size());
std::transform(filter.begin(), filter.end(), searchFilter_.begin(), tolower);
ApplySearchFilter();
}
}
void UITabbedBaseDialogScreen::EnsureTabs() {
_dbg_assert_(tabHolder_);
if (tabHolder_) {
tabHolder_->EnsureAllCreated();
}
}
int UITabbedBaseDialogScreen::GetCurrentTab() const {
return tabHolder_->GetCurrentTab();
}
void UITabbedBaseDialogScreen::SetCurrentTab(int tab) {
tabHolder_->SetCurrentTab(tab);
}
bool UITabbedBaseDialogScreen::CanShowSearch() {
if (!System_GetPropertyBool(SYSPROP_HAS_KEYBOARD) && !System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) {
return false;
}
// Hide search if screen is too small.
int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE);
return (g_display.dp_xres < g_display.dp_yres || g_display.dp_yres >= 500) && deviceType != DEVICE_TYPE_VR;
}
void UITabbedBaseDialogScreen::ApplySearchFilter() {
using namespace UI;
// GAMESETTINGS_SEARCH is broadcast to every screen, including ones whose views don't exist yet,
// and ones that search some other way.
if (!tabHolder_ || !ShowSearchControls()) {
return;
}
auto se = GetI18NCategory(I18NCat::SEARCH);
EnsureTabs();
// Show an indicator that a filter is applied.
filterNotice_->SetVisibility(searchFilter_.empty() ? UI::V_GONE : UI::V_VISIBLE);
filterNotice_->SetText(ApplySafeSubstitutions(se->T("Filtering settings by '%1'"), searchFilter_));
bool matches = searchFilter_.empty();
const std::vector<ViewGroup *> settingTabs = tabHolder_->GetTabContentViews();
for (int t = 0; t < (int)settingTabs.size(); ++t) {
const ViewGroup *tabContents = settingTabs[t];
std::string_view tag = tabContents->Tag();
// Dive down to the actual list of settings.
// TODO: Do this recursively instead.
while (tabContents->GetNumSubviews() == 1) {
View *v = tabContents->GetViewByIndex(0);
if (v->IsViewGroup()) {
tabContents = (ViewGroup *)v;
} else {
break;
}
}
if (tag == "GameSettingsSearch") {
continue;
}
bool tabMatches = searchFilter_.empty();
UI::View *lastHeading = nullptr;
for (int i = 1; i < tabContents->GetNumSubviews(); ++i) {
UI::View *v = tabContents->GetViewByIndex(i);
if (!v->CanBeFocused()) {
lastHeading = v;
}
std::string label = v->DescribeText();
std::transform(label.begin(), label.end(), label.begin(), tolower);
bool match = v->CanBeFocused() && label.find(searchFilter_) != label.npos;
tabMatches = tabMatches || match;
if (match && lastHeading)
lastHeading->SetVisibility(UI::V_VISIBLE);
v->SetVisibility((searchFilter_.empty() || match) ? UI::V_VISIBLE : UI::V_GONE);
}
tabHolder_->EnableTab(t, tabMatches);
matches = matches || tabMatches;
}
if (noSearchResults_) {
noSearchResults_->SetText(ApplySafeSubstitutions(se->T("No settings matched '%1'"), searchFilter_));
noSearchResults_->SetVisibility(matches ? UI::V_GONE : UI::V_VISIBLE);
}
if (clearSearchChoice_) {
clearSearchChoice_->SetVisibility(searchFilter_.empty() ? UI::V_GONE : UI::V_VISIBLE);
}
}