From b91bd6af8631f95d2235f54b909409eb1097e430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 25 Oct 2026 13:46:47 +0100 Subject: [PATCH] Settings: Build the settings screen from pages described as data Each tab is now a SettingsPage: a list of items (headers, settings, buttons, hints, links to other pages) that a function builds and views are created from. Settings are added with page->Setting(&g_Config.x), which takes the label, the kind of view, the range or choices, units, labels for special values and the default from the config. The pages keep what depends on the UI (which settings to show and when, enable rules, and behavior that needs the screen), and reach the screen through the page's SettingsPageHost. A page can link to other pages, which open on top of it in the same tab, under a bar showing where you are, like Tools > Developer tools > Graphics, where the earlier pages can be clicked to go back to them. The bar has back, search and help buttons, and the back key also goes back a page (or out of a search) before leaving the screen. Each page remembers its scroll position. In portrait there's no tab strip: a page links to the tabs, and the bar shows just the current page's title. Rotating keeps showing the same page. Moving between pages only focuses an item when navigating with a keyboard or controller. The developer tools are now pages linked from the Tools tab, instead of a screen of their own. The in-game developer menu and --start-screen=developertools open the settings on that page. The toggles of live state (the remote debugger and the MIPS tracer) show and change it through functions, and pretending to be in-game for the notification tests is a flag on the OSD. The memory map and memstick tests, only reachable from commented-out code, are removed. Search lists the matching settings from all tabs and the pages they link to, grouped by where they are. Each result goes to the setting on its page, scrolled to and highlighted (outlined, when picked with a mouse or touch), or for a page, opens it. This replaces the search tab and the old filter that hid the views that didn't match, and with them ShowSearchControls. Changing tabs leaves the search. The pages, the bar and search are in SettingsPagesDialogScreen, between the tabbed dialog screen and the settings screen, so the other tabbed screens keep plain tabs. Other visible changes: - Reset buttons that went to the wrong value now go to the real default: the touch control auto-hide delay (was 20 seconds instead of 8), the mouse wheel delay (1 ms, below its own range, instead of 80) and the UI size (0 instead of -1 on desktop). The alternative speeds get a reset button. - Units are shown on more sliders (frames, MHz, seconds, Hz), and popups no longer spell out what 0 means in the units, since the zero label does. Units of up to three characters (%, x, ms, MHz) aren't translated. - The Android HW scaler hides the scales the display is too small for, instead of cutting the list short. - Choices are translated in the setting's own section. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/System/OSD.cpp | 3 + Common/System/OSD.h | 4 + Common/System/System.h | 1 - Common/UI/PopupScreens.h | 1 + Core/Config.cpp | 1 - Core/Config.h | 1 - UI/CMakeLists.txt | 4 + UI/DevScreens.cpp | 6 +- UI/DevScreens.h | 1 - UI/DeveloperToolsScreen.cpp | 721 +++++++-------- UI/DeveloperToolsScreen.h | 59 +- UI/DriverManagerScreen.h | 1 - UI/GameSettingsScreen.cpp | 1420 +++++++++++++---------------- UI/GameSettingsScreen.h | 39 +- UI/JitCompareScreen.h | 1 - UI/MiscViews.cpp | 4 + UI/MiscViews.h | 3 + UI/NativeApp.cpp | 5 +- UI/RemoteISOScreen.h | 1 - UI/RetroAchievementScreens.h | 3 - UI/SavedataScreen.h | 1 - UI/SettingsPage.cpp | 374 ++++++++ UI/SettingsPage.h | 214 +++++ UI/SettingsPagesDialogScreen.cpp | 594 ++++++++++++ UI/SettingsPagesDialogScreen.h | 124 +++ UI/SystemInfoScreen.h | 1 - UI/TabbedDialogScreen.cpp | 145 +-- UI/TabbedDialogScreen.h | 19 +- UI/UI.vcxproj | 4 + UI/UI.vcxproj.filters | 12 + UWP/UI_UWP/UI_UWP.vcxproj | 4 + UWP/UI_UWP/UI_UWP.vcxproj.filters | 12 + android/jni/Android.mk | 2 + 33 files changed, 2342 insertions(+), 1443 deletions(-) create mode 100644 UI/SettingsPage.cpp create mode 100644 UI/SettingsPage.h create mode 100644 UI/SettingsPagesDialogScreen.cpp create mode 100644 UI/SettingsPagesDialogScreen.h diff --git a/Common/System/OSD.cpp b/Common/System/OSD.cpp index 68d800ba57..075bd0b67f 100644 --- a/Common/System/OSD.cpp +++ b/Common/System/OSD.cpp @@ -35,6 +35,9 @@ void OnScreenDisplay::NudgeIngameNotifications() { } float OnScreenDisplay::IngameAlpha() const { + if (pretendIngame_) { + return 1.0f; + } double timeSinceNudge = time_now_d() - sideBarShowTime_; // Fade out in 1/4 second, 0.1s after the last nudge. diff --git a/Common/System/OSD.h b/Common/System/OSD.h index 5f1831fb2b..a54cf50dbf 100644 --- a/Common/System/OSD.h +++ b/Common/System/OSD.h @@ -84,6 +84,9 @@ public: // Call every frame to keep the sidebar visible. Otherwise it'll fade out. void NudgeIngameNotifications(); float IngameAlpha() const; + // Shows the in-game notifications outside a game too, for testing how they look. + void SetPretendIngame(bool pretend) { pretendIngame_ = pretend; } + bool PretendIngame() const { return pretendIngame_; } // Fades out everything related to achievements. Should be used on game shutdown. void ClearAchievementStuff(); @@ -128,6 +131,7 @@ private: std::mutex mutex_; double sideBarShowTime_ = 0.0; + bool pretendIngame_ = false; }; extern OnScreenDisplay g_OSD; diff --git a/Common/System/System.h b/Common/System/System.h index 99a5911682..ac22218c69 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -315,7 +315,6 @@ enum class UIMessage { POSTSHADER_UPDATED, ACHIEVEMENT_LOGIN_STATE_CHANGE, SAVESTATE_DISPLAY_SLOT, - GAMESETTINGS_SEARCH, SAVEDATA_SEARCH, RESTART_GRAPHICS, RECENT_FILES_CHANGED, diff --git a/Common/UI/PopupScreens.h b/Common/UI/PopupScreens.h index 35bafbb316..e0cbdcf579 100644 --- a/Common/UI/PopupScreens.h +++ b/Common/UI/PopupScreens.h @@ -316,6 +316,7 @@ public: void HideChoice(int c) { hidden_.insert(c); } + int NumChoices() const { return numChoices_; } bool IsChoiceHidden(int c) const { return hidden_.find(c) != hidden_.end(); } diff --git a/Core/Config.cpp b/Core/Config.cpp index 6078887f28..6e269003c2 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -482,7 +482,6 @@ static constexpr ConfigSetting generalSettings[] = { // Current settings tabs // Decided for now that these should not be saved, so commented out from here. Still saved within the session, of course. // ConfigSetting("SettingsCurrentTab", SETTING(g_Config, iSettingsCurrentTab), 0, CfgFlag::DEFAULT), - // ConfigSetting("DeveloperSettingsCurrentTab", SETTING(g_Config, iDeveloperSettingsCurrentTab), 0, CfgFlag::DEFAULT), #if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) ConfigSetting("TopMost", SETTING(g_Config, bTopMost), false, CfgFlag::DEFAULT), diff --git a/Core/Config.h b/Core/Config.h index a781358d32..ce8ff9a610 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -471,7 +471,6 @@ public: int iBackgroundAnimation; // enum BackgroundAnimation bool bTransparentBackground; int iSettingsCurrentTab; - int iDeveloperSettingsCurrentTab; std::string sThemeName; diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 9354bf4e16..a1041fd2d1 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -61,6 +61,10 @@ list(APPEND UISource SimpleDialogScreen.cpp TabbedDialogScreen.h TabbedDialogScreen.cpp + SettingsPage.h + SettingsPage.cpp + SettingsPagesDialogScreen.h + SettingsPagesDialogScreen.cpp GameScreen.h GameScreen.cpp GamePacker.h diff --git a/UI/DevScreens.cpp b/UI/DevScreens.cpp index 67f4b7b67e..b687d9d093 100644 --- a/UI/DevScreens.cpp +++ b/UI/DevScreens.cpp @@ -78,7 +78,7 @@ #include "UI/EmuScreen.h" #include "UI/OnScreenDisplay.h" #include "UI/ControlMappingScreen.h" -#include "UI/DeveloperToolsScreen.h" +#include "UI/GameSettingsScreen.h" #include "UI/JitCompareScreen.h" #include "android/jni/app-android.h" @@ -173,7 +173,9 @@ void DevMenuScreen::CreatePopupContents(UI::ViewGroup *parent) { items->Add(new Choice(sy->T("Developer Tools")))->OnClick.Add([this](UI::EventParams &e) { UpdateUIState(UISTATE_PAUSEMENU); - screenManager()->push(new DeveloperToolsScreen(gamePath_)); + GameSettingsScreen *settings = new GameSettingsScreen(gamePath_); + settings->OpenDeveloperTools(); + screenManager()->push(settings); }); // Debug overlay diff --git a/UI/DevScreens.h b/UI/DevScreens.h index 5613dc5e93..0eb76a091f 100644 --- a/UI/DevScreens.h +++ b/UI/DevScreens.h @@ -148,7 +148,6 @@ public: const char *tag() const override { return "FrameDumpTest"; } private: - bool ShowSearchControls() const override { return false; } std::vector files_; std::shared_ptr listing_; diff --git a/UI/DeveloperToolsScreen.cpp b/UI/DeveloperToolsScreen.cpp index ab2288dc5c..d6bfc3e426 100644 --- a/UI/DeveloperToolsScreen.cpp +++ b/UI/DeveloperToolsScreen.cpp @@ -49,6 +49,7 @@ #include "UI/OnScreenDisplay.h" #include "UI/IconCache.h" #include "UI/MiscViews.h" +#include "UI/SettingsPage.h" #if PPSSPP_PLATFORM(ANDROID) @@ -84,36 +85,57 @@ static std::string PostShaderTranslateName(std::string_view value) { } } -DeveloperToolsScreen::DeveloperToolsScreen(const Path &gamePath) - : UITabbedBaseDialogScreen(gamePath, &g_Config.iDeveloperSettingsCurrentTab, TabDialogFlags::AddAutoTitles) {} +enum class HasIni { + NO, + YES, + MAYBE, +}; -void DeveloperToolsScreen::CreateTextureReplacementTab(UI::LinearLayout *list) { +// The pages' handlers, defined after them. +static void OnLoggingChanged(UI::EventParams &e); +static void OpenTexturesIniFile(HasIni *hasIni); +static void OnJitDebugTools(SettingsPageHost *host); +static void OnGPUDriverTest(SettingsPageHost *host); +static void OnJitAffectingSetting(UI::EventParams &e); +#if PPSSPP_PLATFORM(IOS) +static void OnCopyStatesToRoot(UI::EventParams &e); +#endif +static void SetRemoteDebuggerAllowed(bool allow); +#if !PPSSPP_PLATFORM(ANDROID) && !PPSSPP_PLATFORM(IOS) && !PPSSPP_PLATFORM(SWITCH) +static void SetMIPSTracerEnabled(bool enabled); +static void OnMIPSTracerPathChanged(SettingsPageHost *host); +static void OnMIPSTracerFlushTrace(UI::EventParams &e); +static void OnMIPSTracerClearJitCache(UI::EventParams &e); +static void OnMIPSTracerClearTracer(UI::EventParams &e); +#endif + +static void BuildTextureReplacementPage(SettingsPage *page) { using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - auto di = GetI18NCategory(I18NCat::DIALOG); - ChoiceGroup *textureReplacement = list->Add(new ChoiceGroup(dev->T("Texture Replacement"))); - textureReplacement->Add(new CheckBox(&g_Config.bSaveNewTextures, dev->T("Save new textures"))); - textureReplacement->Add(new CheckBox(&g_Config.bReplaceTextures, dev->T("Replace textures"))); + page->Header("Texture Replacement"); + page->Setting(&g_Config.bSaveNewTextures); + page->Setting(&g_Config.bReplaceTextures); - Choice *createTextureIni = textureReplacement->Add(new Choice(dev->T("Create/Open textures.ini file for current game"))); - createTextureIni->OnClick.Handle(this, &DeveloperToolsScreen::OnOpenTexturesIniFile); - createTextureIni->SetEnabledFunc([&] { + // Checked once per build of the page, when first needed. + std::shared_ptr hasTexturesIni = std::make_shared(HasIni::MAYBE); + page->Choice("Create/Open textures.ini file for current game").OnClick([hasTexturesIni](UI::EventParams &e) { + OpenTexturesIniFile(hasTexturesIni.get()); + }).EnabledFunc([hasTexturesIni] { if (!PSP_IsInited()) return false; // Disable the choice to Open/Create if the textures.ini file already exists, and we can't open it due to platform support limitations. if (!System_GetPropertyBool(SYSPROP_SUPPORTS_OPEN_FILE_IN_EDITOR)) { - if (hasTexturesIni_ == HasIni::MAYBE) - hasTexturesIni_ = TextureReplacer::IniExists(g_paramSFO.GetDiscID()) ? HasIni::YES : HasIni::NO; - return hasTexturesIni_ != HasIni::YES; + if (*hasTexturesIni == HasIni::MAYBE) + *hasTexturesIni = TextureReplacer::IniExists(g_paramSFO.GetDiscID()) ? HasIni::YES : HasIni::NO; + return *hasTexturesIni != HasIni::YES; } return true; }); if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) { // Best string we have - textureReplacement->Add(new Choice(di->T("Show in folder")))->OnClick.Add([=](UI::EventParams &) { + page->Choice({ I18NCat::DIALOG, "Show in folder" }).OnClick([](UI::EventParams &) { Path path; if (PSP_IsInited()) { std::string gameID = g_paramSFO.GetDiscID(); @@ -126,57 +148,59 @@ void DeveloperToolsScreen::CreateTextureReplacementTab(UI::LinearLayout *list) { }); } - static const char *texLoadSpeeds[] = { "Slow (smooth)", "Medium", "Fast", "Instant (may stutter)" }; - PopupMultiChoice *texLoadSpeed = textureReplacement->Add(new PopupMultiChoice(&g_Config.iReplacementTextureLoadSpeed, dev->T("Replacement texture load speed"), texLoadSpeeds, 0, ARRAY_SIZE(texLoadSpeeds), I18NCat::DEVELOPER, screenManager())); - texLoadSpeed->SetChoiceIcon(3, ImageID("I_WARNING")); + page->Setting(&g_Config.iReplacementTextureLoadSpeed).Setup([](View *view) { + static_cast(view)->SetChoiceIcon(3, ImageID("I_WARNING")); + }); } -void DeveloperToolsScreen::CreateGeneralTab(UI::LinearLayout *list) { +static void BuildGeneralPage(SettingsPage *page) { using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - auto sy = GetI18NCategory(I18NCat::SYSTEM); - auto gr = GetI18NCategory(I18NCat::GRAPHICS); - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); + SettingsPageHost *host = page->Host(); - ChoiceGroup *cpuSettings = list->Add(new ChoiceGroup(sy->T("CPU Core"))); + page->Header({ I18NCat::SYSTEM, "CPU Core" }); bool canUseJit = System_GetPropertyBool(SYSPROP_CAN_JIT); // iOS can now use JIT on all modes, apparently. // The bool may come in handy for future non-jit platforms though (UWP XB1?) // In iOS App Store builds, we disable the JIT. - static const char *cpuCores[] = { "Interpreter", "Dynarec/JIT (recommended)", "IR Interpreter", "JIT using IR" }; - PopupMultiChoice *core = cpuSettings->Add(new PopupMultiChoice(&g_Config.iCpuCore, sy->T("CPU Core"), cpuCores, 0, ARRAY_SIZE(cpuCores), I18NCat::SYSTEM, screenManager())); - core->OnChoice.Add([=](UI::EventParams &e) { + page->Setting(&g_Config.iCpuCore).OnChange([](UI::EventParams &e) { OnJitAffectingSetting(e); g_Config.NotifyUpdatedCpuCore(); + }).Setup([canUseJit](View *view) { + PopupMultiChoice *core = static_cast(view); + if (!canUseJit) { + core->HideChoice(1); + } + // Every native JIT is IR-based, so "JIT using IR" is the same as "JIT". + core->HideChoice(3); }); - if (!canUseJit) { - core->HideChoice(1); - } - // Every native JIT is IR-based, so "JIT using IR" is the same as "JIT". - core->HideChoice(3); // Only the interpreter raises these, but don't hide it behind the CPU core choice - it'd just // look like the setting had vanished. - cpuSettings->Add(new CheckBox(&g_Config.bEnableFPUExceptionTraps, dev->T("Enable FPU exceptions"))); + page->Setting(&g_Config.bEnableFPUExceptionTraps); // Experimental, so not translated yet. - cpuSettings->Add(new CheckBox(&g_Config.bEmulateMediaEngine, dev->T("Enable ME emulation for homebrew"))); + page->Setting(&g_Config.bEmulateMediaEngine); - cpuSettings->Add(new Choice(dev->T("JIT debug tools")))->OnClick.Handle(this, &DeveloperToolsScreen::OnJitDebugTools); - cpuSettings->Add(new CheckBox(&g_Config.bShowDeveloperMenu, dev->T("Show in-game developer menu"))); - - AddOverlayList(cpuSettings, screenManager()); - - ChoiceGroup *generalSettings = list->Add(new ChoiceGroup(sy->T("General"))); - - generalSettings->Add(new CheckBox(&g_Config.bEnableLogging, dev->T("Enable Logging")))->OnClick.Handle(this, &DeveloperToolsScreen::OnLoggingChanged); - generalSettings->Add(new Choice(dev->T("Logging Channels")))->OnClick.Add([this](UI::EventParams &e) { - screenManager()->push(new LogConfigScreen()); + page->Choice("JIT debug tools").OnClick([host](UI::EventParams &e) { + OnJitDebugTools(host); }); - generalSettings->Add(new CheckBox(&g_Config.bEnableFileLogging, dev->T("Log to file")))->SetEnabledPtr(&g_Config.bEnableLogging); + page->Setting(&g_Config.bShowDeveloperMenu); + + page->Custom("Debug overlay", [host](UI::ViewGroup *parent) -> UI::View * { + AddOverlayList(parent, host->PageScreenManager()); + return parent->GetViewByIndex(parent->GetNumSubviews() - 1); + }); + + page->Header({ I18NCat::SYSTEM, "General" }); + + page->Bool(&g_Config.bEnableLogging, "Enable Logging").OnChange(&OnLoggingChanged); + page->Choice("Logging Channels").OnClick([host](UI::EventParams &e) { + host->PageScreenManager()->push(new LogConfigScreen()); + }); + page->Setting(&g_Config.bEnableFileLogging).Enabled(&g_Config.bEnableLogging); if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_DESKTOP) { - generalSettings->Add(new Choice(dev->T("Show log file in folder")))->OnClick.Add([](UI::EventParams &e) { + page->Choice("Show log file in folder").OnClick([](UI::EventParams &e) { Path logFilePath = g_logManager.GetLogFilePath(); if (logFilePath.empty()) { ERROR_LOG(Log::System, "No log file path configured."); @@ -190,117 +214,123 @@ void DeveloperToolsScreen::CreateGeneralTab(UI::LinearLayout *list) { }); } - generalSettings->Add(new CheckBox(&g_Config.bLogFrameDrops, dev->T("Log Dropped Frame Statistics"))); + page->Setting(&g_Config.bLogFrameDrops); if (GetGPUBackend() == GPUBackend::VULKAN) { - generalSettings->Add(new CheckBox(&g_Config.bGpuLogProfiler, dev->T("GPU log profiler"))); + page->Setting(&g_Config.bGpuLogProfiler); } - allowDebugger_ = !WebServerStopped(WebServerFlags::DEBUGGER); - canAllowDebugger_ = !WebServerStopping(WebServerFlags::DEBUGGER); - CheckBox *allowDebugger = new CheckBox(&allowDebugger_, dev->T("Allow remote debugger")); - generalSettings->Add(allowDebugger)->OnClick.Handle(this, &DeveloperToolsScreen::OnRemoteDebugger); - allowDebugger->SetEnabledPtr(&canAllowDebugger_); + auto debuggerAllowed = [] { + return !WebServerStopped(WebServerFlags::DEBUGGER); + }; + page->Bool("Allow remote debugger", debuggerAllowed, &SetRemoteDebuggerAllowed).EnabledFunc([] { + return !WebServerStopping(WebServerFlags::DEBUGGER); + }); + page->Setting(&g_Config.bRemoteDebuggerLocal).EnabledFunc(debuggerAllowed); - CheckBox *localDebugger = generalSettings->Add(new CheckBox(&g_Config.bRemoteDebuggerLocal, dev->T("Use locally hosted remote debugger"))); - localDebugger->SetEnabledPtr(&allowDebugger_); + page->Setting(&g_Config.bAutoSaveLoadSymbols); - generalSettings->Add(new CheckBox(&g_Config.bAutoSaveLoadSymbols, dev->T("Auto save/load symbols"))); - - generalSettings->Add(new Choice(dev->T("GPI/GPO switches/LEDs")))->OnClick.Add([=](UI::EventParams &e) { - screenManager()->push(new GPIGPOScreen(dev->T("GPI/GPO switches/LEDs"))); + page->Choice("GPI/GPO switches/LEDs").OnClick([host](UI::EventParams &e) { + host->PageScreenManager()->push(new GPIGPOScreen(T(I18NCat::DEVELOPER, "GPI/GPO switches/LEDs"))); }); - generalSettings->Add(new CheckBox(&g_Config.bShowSaveLoadIndicator, dev->T("Show indicator when saving/loading"))); + page->Setting(&g_Config.bShowSaveLoadIndicator); #if PPSSPP_PLATFORM(ANDROID) - static const char *framerateModes[] = { "Default", "Request 60 Hz", "Force 60Hz" }; - PopupMultiChoice *framerateMode = generalSettings->Add(new PopupMultiChoice(&g_Config.iDisplayFramerateMode, gr->T("Framerate mode"), framerateModes, 0, ARRAY_SIZE(framerateModes), I18NCat::GRAPHICS, screenManager())); - framerateMode->SetEnabledFunc([]() { return System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 30; }); - framerateMode->OnChoice.Add([](UI::EventParams &e) { + page->Setting(&g_Config.iDisplayFramerateMode).EnabledFunc([]() { + return System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 30; + }).OnChange([](UI::EventParams &e) { System_Notify(SystemNotification::FORCE_RECREATE_ACTIVITY); }); #endif #if PPSSPP_PLATFORM(IOS) - generalSettings->Add(new NoticeView(NoticeLevel::WARN, ms->T("Moving the memstick directory is NOT recommended on iOS"), ""))->SetWrapText(true); - generalSettings->Add(new Choice(sy->T("Set Memory Stick folder")))->OnClick.Add( - [=](UI::EventParams &) { - SetMemStickDirDarwin(GetRequesterToken()); + page->Custom({ I18NCat::MAINSETTINGS, "Moving the memstick directory is NOT recommended on iOS" }, [](UI::ViewGroup *parent) -> UI::View * { + NoticeView *notice = parent->Add(new NoticeView(NoticeLevel::WARN, T(I18NCat::MAINSETTINGS, "Moving the memstick directory is NOT recommended on iOS"), "")); + notice->SetWrapText(true); + return notice; + }); + page->Choice({ I18NCat::SYSTEM, "Set Memory Stick folder" }).OnClick([host](UI::EventParams &) { + SetMemStickDirDarwin(host->PageRequesterToken()); }); #endif // Makes it easy to get savestates out of an iOS device. The file listing shown in MacOS doesn't allow // you to descend into directories. #if PPSSPP_PLATFORM(IOS) - generalSettings->Add(new Choice(dev->T("Copy savestates to memstick root")))->OnClick.Handle(this, &DeveloperToolsScreen::OnCopyStatesToRoot); + page->Choice("Copy savestates to memstick root").OnClick(&OnCopyStatesToRoot); #endif - auto di = GetI18NCategory(I18NCat::DIALOG); // Reuse strings to the max, heh. - generalSettings->Add(new Choice(ApplySafeSubstitutions("%1: ppsspp.ini", di->T("Copy to clipboard"))))->OnClick.Add([=](UI::EventParams &) { - auto di = GetI18NCategory(I18NCat::DIALOG); - std::string configStr = g_Config.GetConfigAsString(); - if (!configStr.empty()) { - System_CopyStringToClipboard(configStr); - g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions(di->T("Copied to clipboard: %1"), "ppsspp.ini"), 0.0f, "copyToClip"); - } + page->Custom({ I18NCat::DIALOG, "Copy to clipboard" }, [](UI::ViewGroup *parent) -> UI::View * { + Choice *copy = parent->Add(new Choice(ApplySafeSubstitutions("%1: ppsspp.ini", T(I18NCat::DIALOG, "Copy to clipboard")))); + copy->OnClick.Add([](UI::EventParams &) { + auto di = GetI18NCategory(I18NCat::DIALOG); + std::string configStr = g_Config.GetConfigAsString(); + if (!configStr.empty()) { + System_CopyStringToClipboard(configStr); + g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions(di->T("Copied to clipboard: %1"), "ppsspp.ini"), 0.0f, "copyToClip"); + } + }); + return copy; }); #if PLATFORM_SUPPORTS_FILE_HANDLER_PLUGINS - generalSettings->Add(new CheckBox(&g_Config.bEnableFileHandlerPlugins, dev->T("Enable file handler plugins (insecure)"))); + page->Setting(&g_Config.bEnableFileHandlerPlugins); #endif } -void DeveloperToolsScreen::CreateTestsTab(UI::LinearLayout *list) { +static void BuildTestsPage(SettingsPage *page) { using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); + SettingsPageHost *host = page->Host(); - ChoiceGroup *tests = list->Add(new ChoiceGroup()); - tests->Add(new Choice(dev->T("Touchscreen Test")))->OnClick.Add([this](UI::EventParams &e) { - screenManager()->push(new TouchTestScreen(gamePath_)); - // Handle touchscreen test event + page->Choice("Touchscreen Test").OnClick([host](UI::EventParams &e) { + host->PageScreenManager()->push(new TouchTestScreen(host->PageGamePath())); }); - // tests->Add(new Choice(dev->T("Memstick Test")))->OnClick.Handle(this, &DeveloperToolsScreen::OnMemstickTest); - Choice *frameDumpTests = tests->Add(new Choice(dev->T("Framedump tests"))); - frameDumpTests->OnClick.Add([this](UI::EventParams &e) { - screenManager()->push(new FrameDumpTestScreen()); + page->Choice("Framedump tests").OnClick([host](UI::EventParams &e) { + host->PageScreenManager()->push(new FrameDumpTestScreen()); + }).EnabledFunc([] { + return !PSP_IsInited(); }); - frameDumpTests->SetEnabled(!PSP_IsInited()); // For now, we only implement GPU driver tests for Vulkan and OpenGL. This is simply // because the D3D drivers are generally solid enough to not need this type of investigation. if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN || g_Config.iGPUBackend == (int)GPUBackend::OPENGL) { - tests->Add(new Choice(dev->T("GPU Driver Test")))->OnClick.Handle(this, &DeveloperToolsScreen::OnGPUDriverTest); + page->Choice("GPU Driver Test").OnClick([host](UI::EventParams &e) { + OnGPUDriverTest(host); + }); } - // Not useful enough to be made visible. - /* - auto memmapTest = tests->Add(new Choice(dev->T("Memory map test"))); - memmapTest->OnClick.Add([this](UI::EventParams &e) { - MemoryMapTest(); - }); - memmapTest->SetEnabled(PSP_IsInited()); - */ } -void DeveloperToolsScreen::CreateDumpFileTab(UI::LinearLayout *list) { +static void BuildDumpFilePage(SettingsPage *page) { using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - ChoiceGroup *dumpSettings = list->Add(new ChoiceGroup(dev->T("Dump files"))); - dumpSettings->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::EBOOT, dev->T("Dump Decrypted Eboot", "Dump Decrypted EBOOT.BIN (If Encrypted) When Booting Game"))); - dumpSettings->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::PRX, dev->T("PRX"))); - dumpSettings->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::Atrac3, dev->T("Atrac3/3+"))); - dumpSettings->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)DumpFileType::PBP_ISO, dev->T("ISO from PBP"))); + auto addDumpType = [page](DumpFileType type, SettingText text) { + page->Custom(text, [type, text](UI::ViewGroup *parent) -> UI::View * { + return parent->Add(new BitCheckBox(&g_Config.iDumpFileTypes, (int)type, text.Translate(I18NCat::DEVELOPER))); + }); + }; + + page->Header("Dump files"); + addDumpType(DumpFileType::EBOOT, { I18NCat::DEVELOPER, "Dump Decrypted Eboot", "Dump Decrypted EBOOT.BIN (If Encrypted) When Booting Game" }); + addDumpType(DumpFileType::PRX, "PRX"); + addDumpType(DumpFileType::Atrac3, "Atrac3/3+"); + addDumpType(DumpFileType::PBP_ISO, "ISO from PBP"); } -void DeveloperToolsScreen::CreateHLETab(UI::LinearLayout *list) { +static void BuildHLEPage(SettingsPage *page) { using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - ChoiceGroup *atracSettings = list->Add(new ChoiceGroup()); - atracSettings->Add(new CheckBox(&g_Config.bUseOldAtrac, dev->T("Use the old sceAtrac implementation"))); + page->Setting(&g_Config.bUseOldAtrac); - ChoiceGroup *disableHLE = list->Add(new ChoiceGroup(dev->T("Disable HLE"))); + auto addModule = [page](int *flags, DisableHLEFlags flag, const char *modname) { + page->Custom(SettingText::Raw(modname), [flags, flag, modname](UI::ViewGroup *parent) -> UI::View * { + return parent->Add(new BitCheckBox(flags, (int)flag, modname)); + }).EnabledFunc([] { + return !PSP_IsInited(); + }); + }; + + page->Header("Disable HLE"); for (int i = 0; i < (int)DisableHLEFlags::Count; i++) { DisableHLEFlags flag = (DisableHLEFlags)(1 << i); @@ -309,13 +339,12 @@ void DeveloperToolsScreen::CreateHLETab(UI::LinearLayout *list) { if (!(flag & AlwaysDisableHLEFlags())) { const HLEModuleMeta *meta = GetHLEModuleMetaByFlag(flag); if (meta) { - BitCheckBox *checkBox = disableHLE->Add(new BitCheckBox(&g_Config.iDisableHLE, (int)flag, meta->modname)); - checkBox->SetEnabled(!PSP_IsInited()); + addModule(&g_Config.iDisableHLE, flag, meta->modname); } } } - ChoiceGroup *forceEnableHLE = list->Add(new ChoiceGroup(dev->T("Force-enable HLE"))); + page->Header("Force-enable HLE"); for (int i = 0; i < (int)DisableHLEFlags::Count; i++) { DisableHLEFlags flag = (DisableHLEFlags)(1 << i); @@ -324,255 +353,215 @@ void DeveloperToolsScreen::CreateHLETab(UI::LinearLayout *list) { if (flag & AlwaysDisableHLEFlags()) { const HLEModuleMeta *meta = GetHLEModuleMetaByFlag(flag); if (meta) { - BitCheckBox *checkBox = forceEnableHLE->Add(new BitCheckBox(&g_Config.iForceEnableHLE, (int)flag, meta->modname)); - checkBox->SetEnabled(!PSP_IsInited()); + addModule(&g_Config.iForceEnableHLE, flag, meta->modname); } } } } -void DeveloperToolsScreen::CreateMIPSTracerTab(UI::LinearLayout *list) { +#if !PPSSPP_PLATFORM(ANDROID) && !PPSSPP_PLATFORM(IOS) && !PPSSPP_PLATFORM(SWITCH) +static void BuildMIPSTracerPage(SettingsPage *page) { using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - ChoiceGroup *tracerSettings = list->Add(new ChoiceGroup(dev->T("MIPSTracer"))); + SettingsPageHost *host = page->Host(); - MIPSTracerEnabled_ = mipsTracer.tracing_enabled; - CheckBox *MIPSTracerEnabled = new CheckBox(&MIPSTracerEnabled_, dev->T("MIPSTracer enabled")); - tracerSettings->Add(MIPSTracerEnabled)->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerEnabled); - MIPSTracerEnabled->SetEnabledFunc([]() { + page->Header("MIPSTracer"); + + page->Bool("MIPSTracer enabled", [] { return mipsTracer.tracing_enabled; }, &SetMIPSTracerEnabled).EnabledFunc([]() { bool temp = g_Config.iCpuCore == static_cast(CPUCore::IR_INTERPRETER) && PSP_IsInited(); return temp && Core_IsStepping() && coreState != CORE_POWERDOWN; }); - Choice *TraceDumpPath = tracerSettings->Add(new Choice(dev->T("Select the file path for the trace"))); - TraceDumpPath->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerPathChanged); - TraceDumpPath->SetEnabledFunc([]() { + page->Choice("Select the file path for the trace").OnClick([host](UI::EventParams &e) { + OnMIPSTracerPathChanged(host); + }).EnabledFunc([]() { if (!PSP_IsInited()) return false; return true; }); - tracerSettings->Add(new InfoItem(dev->T("Current log file"), mipsTracer.get_logging_path())); + page->Info("Current log file", mipsTracer.get_logging_path()); - PopupSliderChoice* storage_capacity = tracerSettings->Add( - new PopupSliderChoice( - &mipsTracer.in_storage_capacity, 0x4'0000, 0x40'0000, 0x10'0000, dev->T("Storage capacity"), 0x10000, screenManager() - ) - ); - storage_capacity->SetFormat("0x%x asm opcodes"); - storage_capacity->OnChange.Add([&](UI::EventParams &) { + page->IntSlider(&mipsTracer.in_storage_capacity, 0x4'0000, 0x40'0000, "Storage capacity").Default(0x10'0000).Step(0x10000).Format("0x%x asm opcodes").OnChange([](UI::EventParams &) { INFO_LOG(Log::JIT, "User changed the tracer's storage capacity to 0x%x", mipsTracer.in_storage_capacity); }); - - PopupSliderChoice* trace_max_size = tracerSettings->Add( - new PopupSliderChoice( - &mipsTracer.in_max_trace_size, 0x1'0000, 0x40'0000, 0x10'0000, dev->T("Max allowed trace size"), 0x10000, screenManager() - ) - ); - trace_max_size->SetFormat("%d basic blocks"); - trace_max_size->OnChange.Add([&](UI::EventParams &) { + page->IntSlider(&mipsTracer.in_max_trace_size, 0x1'0000, 0x40'0000, "Max allowed trace size").Default(0x10'0000).Step(0x10000).Format("%d basic blocks").OnChange([](UI::EventParams &) { INFO_LOG(Log::JIT, "User changed the tracer's max trace size to %d", mipsTracer.in_max_trace_size); }); - ChoiceGroup *tracerActions = list->Add(new ChoiceGroup(dev->T("MIPSTracer actions"))); - Choice *FlushTrace = tracerActions->Add(new Choice(dev->T("Flush the trace"))); - FlushTrace->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerFlushTrace); + page->Header("MIPSTracer actions"); + page->Choice("Flush the trace").OnClick(&OnMIPSTracerFlushTrace); + page->Choice("Clear the JIT cache").OnClick(&OnMIPSTracerClearJitCache); + page->Choice("Clear the MIPSTracer").OnClick(&OnMIPSTracerClearTracer); +} +#endif - Choice *InvalidateJitCache = tracerActions->Add(new Choice(dev->T("Clear the JIT cache"))); - InvalidateJitCache->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerClearJitCache); - - Choice *ClearMIPSTracer = tracerActions->Add(new Choice(dev->T("Clear the MIPSTracer"))); - ClearMIPSTracer->OnClick.Handle(this, &DeveloperToolsScreen::OnMIPSTracerClearTracer); +static void BuildAudioPage(SettingsPage *page) { + page->Bool(&g_Config.bForceFfmpegForAudioDec, "Use FFMPEG for all compressed audio"); } -void DeveloperToolsScreen::CreateAudioTab(UI::LinearLayout *list) { +static void BuildUIPage(SettingsPage *page) { using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - ChoiceGroup *audioSettings = list->Add(new ChoiceGroup()); - audioSettings->Add(new CheckBox(&g_Config.bForceFfmpegForAudioDec, dev->T("Use FFMPEG for all compressed audio"))); -} + SettingsPageHost *host = page->Host(); -void DeveloperToolsScreen::CreateUITab(UI::LinearLayout *list) { - using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - UIContext *uiContext = screenManager()->getUIContext(); - ChoiceGroup *atlasGroup = list->Add(new ChoiceGroup()); - atlasGroup->Add(new Choice(dev->T("Reload UI atlas")))->OnClick.Add([uiContext](UI::EventParams &) { + UIContext *uiContext = host->PageScreenManager()->getUIContext(); + page->Choice("Reload UI atlas").OnClick([uiContext](UI::EventParams &) { uiContext->InvalidateAtlas(); }); - auto di = GetI18NCategory(I18NCat::DIALOG); - auto si = GetI18NCategory(I18NCat::SYSINFO); - auto sy = GetI18NCategory(I18NCat::SYSTEM); - auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS); - // TODO: Move most of these strings out of the SysInfo category. + const I18NCat si = I18NCat::SYSINFO; - ChoiceGroup *iconCache = list->Add(new ChoiceGroup(si->T("Icon cache"))); + page->Header({ si, "Icon cache" }); IconCacheStats iconStats = g_iconCache.GetStats(); - iconCache->Add(new InfoItem(si->T("Image data count"), StringFromFormat("%d", iconStats.cachedCount))); - iconCache->Add(new InfoItem(si->T("Texture count"), StringFromFormat("%d", iconStats.textureCount))); - iconCache->Add(new InfoItem(si->T("Data size"), NiceSizeFormat(iconStats.dataSize))); - iconCache->Add(new Choice(di->T("Clear")))->OnClick.Add([&](UI::EventParams &) { + page->Info({ si, "Image data count" }, StringFromFormat("%d", iconStats.cachedCount)); + page->Info({ si, "Texture count" }, StringFromFormat("%d", iconStats.textureCount)); + page->Info({ si, "Data size" }, NiceSizeFormat(iconStats.dataSize)); + page->Choice({ I18NCat::DIALOG, "Clear" }).OnClick([host](UI::EventParams &) { g_iconCache.ClearData(); - RecreateViews(); + host->RecreatePageViews(); }); - ChoiceGroup *fontCache = list->Add(new ChoiceGroup(si->T("Font cache"))); - const TextDrawer *text = screenManager()->getUIContext()->Text(); + page->Header({ si, "Font cache" }); + const TextDrawer *text = host->PageScreenManager()->getUIContext()->Text(); if (text) { - fontCache->Add(new InfoItem(si->T("Texture count"), StringFromFormat("%d", text->GetStringCacheSize()))); - fontCache->Add(new InfoItem(si->T("Data size"), NiceSizeFormat(text->GetCacheDataSize()))); + page->Info({ si, "Texture count" }, StringFromFormat("%d", text->GetStringCacheSize())); + page->Info({ si, "Data size" }, NiceSizeFormat(text->GetCacheDataSize())); } - ChoiceGroup *sliderTest = list->Add(new ChoiceGroup(si->T("Slider test"))); - sliderTest->Add(new Slider(&testSliderValue_, 0, 100, 1, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + page->Header({ si, "Slider test" }); + page->Custom({ si, "Slider test" }, [](UI::ViewGroup *parent) -> UI::View * { + static int testSliderValue = 0; + return parent->Add(new Slider(&testSliderValue, 0, 100, 1, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + }); - // Same encoding as in GameSettingsScreen: None is -1. - static const char *positions[] = { "None", "Bottom Left", "Bottom Center", "Bottom Right", "Top Left", "Top Center", "Top Right", "Center Left", "Center Right" }; - - ChoiceGroup *notificationTests = list->Add(new ChoiceGroup(si->T("Notification tests"))); - notificationTests->Add(new Choice(si->T("Error")))->OnClick.Add([&](UI::EventParams &) { + page->Header({ si, "Notification tests" }); + page->Choice({ si, "Error" }).OnClick([](UI::EventParams &) { std::string str = "Error " + CodepointToUTF8(0x1F41B) + CodepointToUTF8(0x1F41C) + CodepointToUTF8(0x1F914); g_OSD.Show(OSDType::MESSAGE_ERROR, str); }); - notificationTests->Add(new Choice(si->T("Warning")))->OnClick.Add([&](UI::EventParams &) { + page->Choice({ si, "Warning" }).OnClick([](UI::EventParams &) { g_OSD.Show(OSDType::MESSAGE_WARNING, "Warning, a pretty long warning heading", "Some\nAdditional\nDetail, some of which is very, very long and wide and will need line wrapping on most screens."); }); - notificationTests->Add(new Choice(si->T("Info")))->OnClick.Add([&](UI::EventParams &) { + page->Choice({ si, "Info" }).OnClick([](UI::EventParams &) { g_OSD.Show(OSDType::MESSAGE_INFO, "Info, info info info info info info info info info info"); }); // This one is clickable - notificationTests->Add(new Choice(si->T("Success")))->OnClick.Add([&](UI::EventParams &) { + page->Choice({ si, "Success" }).OnClick([](UI::EventParams &) { g_OSD.Show(OSDType::MESSAGE_SUCCESS, "Success", 0.0f, "clickable"); g_OSD.SetClickCallback("clickable", []() { System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.google.com/"); }); }); - notificationTests->Add(new Choice(sy->T("RetroAchievements")))->OnClick.Add([&](UI::EventParams &) { + page->Choice({ I18NCat::SYSTEM, "RetroAchievements" }).OnClick([](UI::EventParams &) { g_OSD.Show(OSDType::MESSAGE_WARNING, "RetroAchievements warning", "", "I_RETROACHIEVEMENTS_LOGO"); }); - ChoiceGroup *progressTests = list->Add(new ChoiceGroup(si->T("Progress tests"))); - progressTests->Add(new Choice(si->T("30%")))->OnClick.Add([&](UI::EventParams &) { + page->Header({ si, "Progress tests" }); + page->Choice({ si, "30%" }).OnClick([](UI::EventParams &) { g_OSD.SetProgressBar("testprogress", "Test Progress", 1, 100, 30, 0.0f); }); - progressTests->Add(new Choice(si->T("100%")))->OnClick.Add([&](UI::EventParams &) { + page->Choice({ si, "100%" }).OnClick([](UI::EventParams &) { g_OSD.SetProgressBar("testprogress", "Test Progress", 1, 100, 100, 1.0f); }); - progressTests->Add(new Choice(si->T("N/A%")))->OnClick.Add([&](UI::EventParams &) { + page->Choice({ si, "N/A%" }).OnClick([](UI::EventParams &) { g_OSD.SetProgressBar("testprogress", "Test Progress", 0, 0, 0, 0.0f); }); - progressTests->Add(new Choice(si->T("Success")))->OnClick.Add([&](UI::EventParams &) { + page->Choice({ si, "Success" }).OnClick([](UI::EventParams &) { g_OSD.RemoveProgressBar("testprogress", true, 0.5f); }); - progressTests->Add(new Choice(si->T("Failure")))->OnClick.Add([&](UI::EventParams &) { + page->Choice({ si, "Failure" }).OnClick([](UI::EventParams &) { g_OSD.RemoveProgressBar("testprogress", false, 0.5f); }); - ChoiceGroup *achievementTests = list->Add(new ChoiceGroup(si->T("Achievement tests"))); - achievementTests->Add(new Choice(si->T("Leaderboard tracker: Show")))->OnClick.Add([=](UI::EventParams &) { + page->Header({ si, "Achievement tests" }); + page->Choice({ si, "Leaderboard tracker: Show" }).OnClick([](UI::EventParams &) { g_OSD.ShowLeaderboardTracker(1, "My leaderboard tracker", true); }); - achievementTests->Add(new Choice(si->T("Leaderboard tracker: Update")))->OnClick.Add([=](UI::EventParams &) { + page->Choice({ si, "Leaderboard tracker: Update" }).OnClick([](UI::EventParams &) { g_OSD.ShowLeaderboardTracker(1, "Updated tracker", true); }); - achievementTests->Add(new Choice(si->T("Leaderboard tracker: Hide")))->OnClick.Add([=](UI::EventParams &) { + page->Choice({ si, "Leaderboard tracker: Hide" }).OnClick([](UI::EventParams &) { g_OSD.ShowLeaderboardTracker(1, "", false); }); - ChoiceGroup *notificationSettings = list->Add(new ChoiceGroup(ac->T("Notifications"))); - notificationSettings->Add(new PopupMultiChoice(&g_Config.iNotificationPos, sy->T("Notification screen position"), positions, -1, ARRAY_SIZE(positions), I18NCat::DIALOG, screenManager())); - notificationSettings->Add(new PopupMultiChoice(&g_Config.iAchievementsLeaderboardTrackerPos, ac->T("Leaderboard tracker"), positions, -1, ARRAY_SIZE(positions), I18NCat::DIALOG, screenManager())); + page->Header({ I18NCat::ACHIEVEMENTS, "Notifications" }); + page->Setting(&g_Config.iNotificationPos); + page->Setting(&g_Config.iAchievementsLeaderboardTrackerPos); #ifdef _DEBUG - notificationSettings->Add(new CheckBox(&pretendIngame_, ac->T("Pretend to be in-game (for testing)"))); -#endif - -#if defined(_DEBUG) || PPSSPP_PLATFORM(ANDROID) - ChoiceGroup *crashTests = list->Add(new ChoiceGroup(ac->T("Assert"))); -#endif -#ifdef _DEBUG + page->Bool({ I18NCat::ACHIEVEMENTS, "Pretend to be in-game (for testing)" }, [] { + return g_OSD.PretendIngame(); + }, [](bool pretend) { + g_OSD.SetPretendIngame(pretend); + }); // Untranslated string because this is debug mode only, only for PPSSPP developers. - crashTests->Add(new Choice("Assert"))->OnClick.Add([=](UI::EventParams &) { + page->Header({ I18NCat::ACHIEVEMENTS, "Assert" }); + page->Choice(SettingText::Raw("Assert")).OnClick([](UI::EventParams &) { _dbg_assert_msg_(false, "Test assert message"); }); #endif #if PPSSPP_PLATFORM(ANDROID) - crashTests->Add(new Choice(si->T("Exception")))->OnClick.Add([&](UI::EventParams &) { + page->Choice({ si, "Exception" }).OnClick([](UI::EventParams &) { System_Notify(SystemNotification::TEST_JAVA_EXCEPTION); }); #endif } -void DeveloperToolsScreen::CreateNetworkTab(UI::LinearLayout *list) { - using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - auto ri = GetI18NCategory(I18NCat::REMOTEISO); - auto nw = GetI18NCategory(I18NCat::NETWORKING); - ChoiceGroup *networkSettings = list->Add(new ChoiceGroup(ms->T("Networking"))); - networkSettings->Add(new CheckBox(&g_Config.bDontDownloadInfraJson, dev->T("Don't download infra-dns.json"))); +static void BuildNetworkPage(SettingsPage *page) { + page->Header({ I18NCat::MAINSETTINGS, "Networking" }); + page->Setting(&g_Config.bDontDownloadInfraJson); // This is shared between RemoteISO and the remote debugger. - networkSettings->Add(new PopupSliderChoice(&g_Config.iRemoteISOPort, 0, 65535, 0, ri->T("Local Server Port", "Local Server Port"), 100, screenManager())); - ChoiceGroup *adhocSettings = list->Add(new ChoiceGroup(nw->T("AdHoc server"))); - adhocSettings->Add(new CheckBox(&g_Config.bAdhocServerShowPlayerPorts, nw->T("Show player port numbers"))); + page->Setting(&g_Config.iRemoteISOPort); + page->Header({ I18NCat::NETWORKING, "AdHoc server" }); + page->Setting(&g_Config.bAdhocServerShowPlayerPorts); } // TODO: Make this generic extern int DefaultDepthRaster(); -void DeveloperToolsScreen::CreateGraphicsTab(UI::LinearLayout *list) { +static void BuildGraphicsPage(SettingsPage *page) { using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - auto gr = GetI18NCategory(I18NCat::GRAPHICS); - auto ps = GetI18NCategory(I18NCat::POSTSHADERS); - auto sy = GetI18NCategory(I18NCat::SYSTEM); - auto si = GetI18NCategory(I18NCat::SYSINFO); + SettingsPageHost *host = page->Host(); - Draw::DrawContext *draw = screenManager()->getDrawContext(); + Draw::DrawContext *draw = host->PageScreenManager()->getDrawContext(); - ChoiceGroup *generalSettings = list->Add(new ChoiceGroup(sy->T("General"))); - generalSettings->Add(new CheckBox(&g_Config.bVendorBugChecksEnabled, dev->T("Enable driver bug workarounds"))); - generalSettings->Add(new CheckBox(&g_Config.bShaderCache, dev->T("Enable shader cache"))); - generalSettings->Add(new CheckBox(&g_Config.bHardwareTransform, gr->T("Hardware Transform"))); + page->Header({ I18NCat::SYSTEM, "General" }); + page->Setting(&g_Config.bVendorBugChecksEnabled); + page->Setting(&g_Config.bShaderCache); + page->Setting(&g_Config.bHardwareTransform); - auto displayRefreshRate = generalSettings->Add(new PopupSliderChoice(&g_Config.iDisplayRefreshRate, 60, 1000, 60, dev->T("Display refresh rate"), 1, screenManager())); - displayRefreshRate->SetFormat(si->T("%d Hz")); + page->Setting(&g_Config.iDisplayRefreshRate); - ChoiceGroup *vulkanSettings = list->Add(new ChoiceGroup(dev->T("Vulkan"))); - vulkanSettings->Add(new CheckBox(&g_Config.bVulkanDisableImplicitLayers, dev->T("Prevent loading overlays"))); + page->Header("Vulkan"); + page->Setting(&g_Config.bVulkanDisableImplicitLayers); if (g_Config.iGPUBackend == (int)GPUBackend::VULKAN) { - vulkanSettings->Add(new CheckBox(&g_Config.bRenderMultiThreading, dev->T("Multi-threaded rendering"), ""))->OnClick.Add([](UI::EventParams &e) { + page->Setting(&g_Config.bRenderMultiThreading).OnChange([](UI::EventParams &e) { // TODO: Not translating yet. Will combine with other translations of settings that need restart. g_OSD.Show(OSDType::MESSAGE_WARNING, "Restart required"); }); } if (GetGPUBackend() == GPUBackend::VULKAN && SupportsCustomDriver()) { - auto driverChoice = vulkanSettings->Add(new Choice(gr->T("AdrenoTools driver manager"))); - driverChoice->OnClick.Add([=](UI::EventParams &e) { - screenManager()->push(new DriverManagerScreen(gamePath_)); + page->Choice({ I18NCat::GRAPHICS, "AdrenoTools driver manager" }).OnClick([host](UI::EventParams &e) { + host->PageScreenManager()->push(new DriverManagerScreen(host->PageGamePath())); }); } - static const char *depthRasterModes[] = { "Auto", "Low", "Off", "Always on" }; + page->Setting(&g_Config.iDepthRasterMode).Disabled(&g_Config.bSoftwareRendering).Setup([](View *view) { + PopupMultiChoice *depthRasterMode = static_cast(view); + depthRasterMode->SetDefault(DefaultDepthRaster()); + depthRasterMode->SetChoiceIcon(3, ImageID("I_WARNING")); // It's a performance trap. + }); - PopupMultiChoice *depthRasterMode = vulkanSettings->Add(new PopupMultiChoice(&g_Config.iDepthRasterMode, gr->T("Lens flare occlusion"), depthRasterModes, 0, ARRAY_SIZE(depthRasterModes), I18NCat::GRAPHICS, screenManager())); - depthRasterMode->SetDisabledPtr(&g_Config.bSoftwareRendering); - depthRasterMode->SetDefault(DefaultDepthRaster()); - depthRasterMode->SetChoiceIcon(3, ImageID("I_WARNING")); // It's a performance trap. - - ChoiceGroup *ubershaderSettings = list->Add(new ChoiceGroup(dev->T("Ubershaders"))); + page->Header("Ubershaders"); if (draw->GetShaderLanguageDesc().bitwiseOps && !draw->GetBugs().Has(Draw::Bugs::UNIFORM_INDEXING_BROKEN)) { // If the above if fails, the checkbox is redundant since it'll be force disabled anyway. - ubershaderSettings->Add(new CheckBox(&g_Config.bUberShaderVertex, dev->T("Vertex"))); + page->Setting(&g_Config.bUberShaderVertex); } // Experimental, allow some VR features without OpenXR if (GetGPUBackend() == GPUBackend::OPENGL) { - auto vr = GetI18NCategory(I18NCat::VR); - ChoiceGroup *vrSettings = list->Add(new ChoiceGroup(vr->T("Virtual reality"))); - vrSettings->Add(new CheckBox(&g_Config.bForceVR, vr->T("VR camera"))); + page->Header({ I18NCat::VR, "Virtual reality" }); + page->Setting(&g_Config.bForceVR); } // Experimental, will move to main graphics settings later. @@ -583,49 +572,52 @@ void DeveloperToolsScreen::CreateGraphicsTab(UI::LinearLayout *list) { }; if (multiViewSupported) { - ChoiceGroup *stereoSettings = list->Add(new ChoiceGroup(gr->T("Stereo rendering"))); - stereoSettings->Add(new CheckBox(&g_Config.bStereoRendering, gr->T("Stereo rendering"))); + page->Header({ I18NCat::GRAPHICS, "Stereo rendering" }); + page->Setting(&g_Config.bStereoRendering); - ChoiceWithValueDisplay *stereoShaderChoice = stereoSettings->Add(new ChoiceWithValueDisplay(&g_Config.sStereoToMonoShader, gr->T("Stereo display shader"), &PostShaderTranslateName)); - stereoShaderChoice->SetEnabledFunc(enableStereo); - stereoShaderChoice->OnClick.Add([=](EventParams &e) { - auto gr = GetI18NCategory(I18NCat::GRAPHICS); - auto procScreen = new PostProcScreen(gr->T("Stereo display shader"), 0, true); - procScreen->OnChoice.Add([this](EventParams &) { - RecreateViews(); + page->Custom({ I18NCat::GRAPHICS, "Stereo display shader" }, [host](UI::ViewGroup *parent) -> UI::View * { + ChoiceWithValueDisplay *stereoShaderChoice = parent->Add(new ChoiceWithValueDisplay(&g_Config.sStereoToMonoShader, T(I18NCat::GRAPHICS, "Stereo display shader"), &PostShaderTranslateName)); + stereoShaderChoice->OnClick.Add([host](EventParams &e) { + auto gr = GetI18NCategory(I18NCat::GRAPHICS); + auto procScreen = new PostProcScreen(gr->T("Stereo display shader"), 0, true); + procScreen->OnChoice.Add([host](EventParams &) { + host->RecreatePageViews(); + }); + if (e.v) + procScreen->SetPopupOrigin(e.v); + host->PageScreenManager()->push(procScreen); }); - if (e.v) - procScreen->SetPopupOrigin(e.v); - screenManager()->push(procScreen); - }); + return stereoShaderChoice; + }).EnabledFunc(enableStereo); const ShaderInfo *shaderInfo = GetPostShaderInfo(g_Config.sStereoToMonoShader); if (shaderInfo) { for (size_t i = 0; i < ARRAY_SIZE(shaderInfo->settings); ++i) { auto &setting = shaderInfo->settings[i]; - if (!setting.name.empty()) { + if (setting.name.empty()) { + continue; + } + // The value is looked up (and added if missing) when the slider is created, not when the page is built. + page->Custom({ I18NCat::POSTSHADERS, setting.name.c_str() }, [host, shaderInfo, i](UI::ViewGroup *parent) -> UI::View * { + auto &setting = shaderInfo->settings[i]; std::string key = StringFromFormat("%sSettingCurrentValue%d", shaderInfo->section.c_str(), i + 1); bool keyExisted = g_Config.mPostShaderSetting.find(key) != g_Config.mPostShaderSetting.end(); auto &value = g_Config.mPostShaderSetting[key]; if (!keyExisted) value = setting.value; - - PopupSliderChoiceFloat *settingValue = stereoSettings->Add(new PopupSliderChoiceFloat(&value, setting.minValue, setting.maxValue, setting.value, ps->T(setting.name), setting.step, screenManager())); - settingValue->SetEnabledFunc([=] { - return !g_Config.bSkipBufferEffects && enableStereo(); - }); - } + return parent->Add(new PopupSliderChoiceFloat(&value, setting.minValue, setting.maxValue, setting.value, T(I18NCat::POSTSHADERS, setting.name), setting.step, host->PageScreenManager())); + }).EnabledFunc([=] { + return !g_Config.bSkipBufferEffects && enableStereo(); + }); } } } } -void DeveloperToolsScreen::CreateCrashHistoryTab(UI::LinearLayout *list) { +static void CreateCrashHistoryList(UI::ViewGroup *crashHistory) { using namespace UI; - auto dev = GetI18NCategory(I18NCat::DEVELOPER); auto di = GetI18NCategory(I18NCat::DIALOG); std::vector reports = Android_GetNativeCrashHistory(20); - ChoiceGroup *crashHistory = list->Add(new ChoiceGroup(dev->T("Crash history"))); if (reports.empty()) { crashHistory->Add(new TextView(di->T("None"))); return; @@ -643,66 +635,41 @@ void DeveloperToolsScreen::CreateCrashHistoryTab(UI::LinearLayout *list) { } } -void DeveloperToolsScreen::CreateTabs() { - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - auto sy = GetI18NCategory(I18NCat::SYSTEM); - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); +static void BuildCrashHistoryPage(SettingsPage *page) { + // A list of reports rather than settings. + page->Custom({ I18NCat::DEVELOPER, "Crash history" }, [](UI::ViewGroup *parent) -> UI::View * { + CreateCrashHistoryList(parent); + return parent->GetViewByIndex(parent->GetNumSubviews() - 1); + }); +} - AddTab("General", sy->T("General"), [this](UI::LinearLayout *parent) { - CreateGeneralTab(parent->Add(new UI::ChoiceGroupList())); - }); - AddTab("TextureReplacement", dev->T("Texture Replacement"), [this](UI::LinearLayout *parent) { - CreateTextureReplacementTab(parent->Add(new UI::ChoiceGroupList())); - }); - AddTab("Graphics", ms->T("Graphics"), [this](UI::LinearLayout *parent) { - CreateGraphicsTab(parent->Add(new UI::ChoiceGroupList())); - }); - AddTab("Networking", ms->T("Networking"), [this](UI::LinearLayout *parent) { - CreateNetworkTab(parent->Add(new UI::ChoiceGroupList())); - }); - AddTab("Audio", ms->T("Audio"), [this](UI::LinearLayout *parent) { - CreateAudioTab(parent->Add(new UI::ChoiceGroupList())); - }); - AddTab("Tests", dev->T("Tests"), [this](UI::LinearLayout *parent) { - CreateTestsTab(parent->Add(new UI::ChoiceGroupList())); - }); - AddTab("UI", dev->T("UI"), [this](UI::LinearLayout *parent) { - CreateUITab(parent->Add(new UI::ChoiceGroupList())); - }); - AddTab("DumpFiles", dev->T("Dump files"), [this](UI::LinearLayout *parent) { - CreateDumpFileTab(parent->Add(new UI::ChoiceGroupList())); - }); - AddTab("HLE", dev->T("HLE"), [this](UI::LinearLayout *parent) { - CreateHLETab(parent->Add(new UI::ChoiceGroupList())); - }); +void BuildDeveloperToolsPage(SettingsPage *page) { + auto link = [page](SettingText title, void (*build)(SettingsPage *)) { + page->Link(title, I18NCat::DEVELOPER, build); + }; + + link({ I18NCat::SYSTEM, "General" }, &BuildGeneralPage); + link({ I18NCat::DEVELOPER, "Texture Replacement" }, &BuildTextureReplacementPage); + link({ I18NCat::MAINSETTINGS, "Graphics" }, &BuildGraphicsPage); + link({ I18NCat::MAINSETTINGS, "Networking" }, &BuildNetworkPage); + link({ I18NCat::MAINSETTINGS, "Audio" }, &BuildAudioPage); + link({ I18NCat::DEVELOPER, "Tests" }, &BuildTestsPage); + link({ I18NCat::DEVELOPER, "UI" }, &BuildUIPage); + link({ I18NCat::DEVELOPER, "Dump files" }, &BuildDumpFilePage); + link({ I18NCat::DEVELOPER, "HLE" }, &BuildHLEPage); #if !PPSSPP_PLATFORM(ANDROID) && !PPSSPP_PLATFORM(IOS) && !PPSSPP_PLATFORM(SWITCH) - AddTab("MIPSTracer", dev->T("MIPSTracer"), [this](UI::LinearLayout *parent) { - CreateMIPSTracerTab(parent->Add(new UI::ChoiceGroupList())); - }); + link({ I18NCat::DEVELOPER, "MIPSTracer" }, &BuildMIPSTracerPage); #endif -//#if PPSSPP_PLATFORM(ANDROID) if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 30) { - AddTab("Crash history", dev->T("Crash history"), [this](UI::LinearLayout *parent) { - CreateCrashHistoryTab(parent->Add(new UI::ChoiceGroupList())); - }); + link({ I18NCat::DEVELOPER, "Crash history" }, &BuildCrashHistoryPage); } -//#endif - - // Reconsider whenever recreating views. - hasTexturesIni_ = HasIni::MAYBE; } -void DeveloperToolsScreen::onFinish(DialogResult result) { - UIScreen::onFinish(result); - g_Config.Save("DeveloperToolsScreen::onFinish"); - System_PostUIMessage(UIMessage::GPU_CONFIG_CHANGED); -} - -void DeveloperToolsScreen::OnLoggingChanged(UI::EventParams &e) { +static void OnLoggingChanged(UI::EventParams &e) { System_Notify(SystemNotification::TOGGLE_DEBUG_CONSOLE); } -void DeveloperToolsScreen::OnOpenTexturesIniFile(UI::EventParams &e) { +static void OpenTexturesIniFile(HasIni *hasIni) { std::string gameID = g_paramSFO.GetDiscID(); Path generatedFilename; @@ -715,23 +682,24 @@ void DeveloperToolsScreen::OnOpenTexturesIniFile(UI::EventParams &e) { System_Toast((GetFriendlyPath(generatedFilename) + ": " + dev->T_cstr("Texture ini file created")).c_str()); } - hasTexturesIni_ = HasIni::YES; + *hasIni = HasIni::YES; } } -void DeveloperToolsScreen::OnJitDebugTools(UI::EventParams &e) { - screenManager()->push(new JitDebugScreen()); +static void OnJitDebugTools(SettingsPageHost *host) { + host->PageScreenManager()->push(new JitDebugScreen()); } -void DeveloperToolsScreen::OnGPUDriverTest(UI::EventParams &e) { - screenManager()->push(new GPUDriverTestScreen()); +static void OnGPUDriverTest(SettingsPageHost *host) { + host->PageScreenManager()->push(new GPUDriverTestScreen()); } -void DeveloperToolsScreen::OnJitAffectingSetting(UI::EventParams &e) { +static void OnJitAffectingSetting(UI::EventParams &e) { System_PostUIMessage(UIMessage::REQUEST_CLEAR_JIT); } -void DeveloperToolsScreen::OnCopyStatesToRoot(UI::EventParams &e) { +#if PPSSPP_PLATFORM(IOS) +static void OnCopyStatesToRoot(UI::EventParams &e) { Path savestate_dir = GetSysDirectory(DIRECTORY_SAVESTATE); Path root_dir = GetSysDirectory(DIRECTORY_MEMSTICK_ROOT); @@ -745,9 +713,10 @@ void DeveloperToolsScreen::OnCopyStatesToRoot(UI::EventParams &e) { File::Copy(src, dst); } } +#endif -void DeveloperToolsScreen::OnRemoteDebugger(UI::EventParams &e) { - if (allowDebugger_) { +static void SetRemoteDebuggerAllowed(bool allow) { + if (allow) { // Only needed for connecting from another device, but that's the usual case. System_RequestLocalNetworkPermission(); StartWebServer(WebServerFlags::DEBUGGER); @@ -755,11 +724,12 @@ void DeveloperToolsScreen::OnRemoteDebugger(UI::EventParams &e) { StopWebServer(WebServerFlags::DEBUGGER); } // Persist the setting. Maybe should separate? - g_Config.bRemoteDebuggerOnStartup = allowDebugger_; + g_Config.bRemoteDebuggerOnStartup = allow; } -void DeveloperToolsScreen::OnMIPSTracerEnabled(UI::EventParams &e) { - if (MIPSTracerEnabled_) { +#if !PPSSPP_PLATFORM(ANDROID) && !PPSSPP_PLATFORM(IOS) && !PPSSPP_PLATFORM(SWITCH) +static void SetMIPSTracerEnabled(bool enabled) { + if (enabled) { u32 capacity = mipsTracer.in_storage_capacity; u32 trace_size = mipsTracer.in_max_trace_size; @@ -770,100 +740,33 @@ void DeveloperToolsScreen::OnMIPSTracerEnabled(UI::EventParams &e) { } } -void DeveloperToolsScreen::OnMIPSTracerPathChanged(UI::EventParams &e) { +static void OnMIPSTracerPathChanged(SettingsPageHost *host) { auto dev = GetI18NCategory(I18NCat::DEVELOPER); System_BrowseForFileSave( - GetRequesterToken(), + host->PageRequesterToken(), dev->T("Select the log file"), "trace.txt", BrowseFileType::ANY, - [this](std::string_view value, int) { + [host](std::string_view value, int) { mipsTracer.set_logging_path(std::string(value)); // The views may have been recreated while the dialog was open, so don't keep a pointer to the path view. - RecreateViews(); + host->RecreatePageViews(); } ); } -void DeveloperToolsScreen::OnMIPSTracerFlushTrace(UI::EventParams &e) { +static void OnMIPSTracerFlushTrace(UI::EventParams &e) { mipsTracer.flush_to_file(); // The error logs are emitted inside the tracer } -void DeveloperToolsScreen::OnMIPSTracerClearJitCache(UI::EventParams &e) { +static void OnMIPSTracerClearJitCache(UI::EventParams &e) { INFO_LOG(Log::JIT, "Clearing the jit cache..."); System_PostUIMessage(UIMessage::REQUEST_CLEAR_JIT); } -void DeveloperToolsScreen::OnMIPSTracerClearTracer(UI::EventParams &e) { +static void OnMIPSTracerClearTracer(UI::EventParams &e) { INFO_LOG(Log::JIT, "Clearing the MIPSTracer..."); mipsTracer.clear(); } - -void DeveloperToolsScreen::update() { - UIBaseDialogScreen::update(); - allowDebugger_ = !WebServerStopped(WebServerFlags::DEBUGGER); - canAllowDebugger_ = !WebServerStopping(WebServerFlags::DEBUGGER); - - // For the UI tab's notification tests. - if (pretendIngame_) { - g_OSD.NudgeIngameNotifications(); - } -} - -void DeveloperToolsScreen::MemoryMapTest() { - int sum = 0; - for (uint64_t addr = 0; addr < 0x100000000ULL; addr += 0x1000) { - const u32 addr32 = (u32)addr; - if (Memory::IsValidAddress(addr32)) { - sum += Memory::ReadUnchecked_U32(addr32); - } - } - // Just to force the compiler to do things properly. - INFO_LOG(Log::JIT, "Total sum: %08x", sum); -} - -static bool RunMemstickTest(std::string *error) { - Path testRoot = GetSysDirectory(PSPDirectories::DIRECTORY_CACHE) / "test"; - - *error = "N/A"; - - File::CreateDir(testRoot); - if (!File::Exists(testRoot)) { - return false; - } - - Path testFilePath = testRoot / "temp.txt"; - File::CreateEmptyFile(testFilePath); - - // Attempt to delete the test root. This should fail since it still contains files. - File::DeleteDir(testRoot); - if (!File::Exists(testRoot)) { - *error = "testroot was deleted with a file in it!"; - return false; - } - - File::Delete(testFilePath); - if (File::Exists(testFilePath)) { - *error = "testfile wasn't deleted"; - return false; - } - - File::DeleteDir(testRoot); - if (File::Exists(testRoot)) { - *error = "testroot wasn't deleted, even when empty"; - return false; - } - - *error = "passed"; - return true; -} - -void DeveloperToolsScreen::OnMemstickTest(UI::EventParams &e) { - std::string error; - if (RunMemstickTest(&error)) { - g_OSD.Show(OSDType::MESSAGE_SUCCESS, "Memstick test passed", error, 6.0f); - } else { - g_OSD.Show(OSDType::MESSAGE_ERROR, "Memstick test failed", error, 6.0f); - } -} +#endif diff --git a/UI/DeveloperToolsScreen.h b/UI/DeveloperToolsScreen.h index be5720f9f4..6885157515 100644 --- a/UI/DeveloperToolsScreen.h +++ b/UI/DeveloperToolsScreen.h @@ -1,59 +1,6 @@ #pragma once -#include "ppsspp_config.h" +class SettingsPage; -#include "UI/TabbedDialogScreen.h" - -class DeveloperToolsScreen : public UITabbedBaseDialogScreen { -public: - DeveloperToolsScreen(const Path &gamePath); - - void CreateTabs() override; - void update() override; - void onFinish(DialogResult result) override; - - const char *tag() const override { return "DeveloperTools"; } - -private: - void CreateTextureReplacementTab(UI::LinearLayout *parent); - void CreateGeneralTab(UI::LinearLayout *parent); - void CreateDumpFileTab(UI::LinearLayout *parent); - void CreateHLETab(UI::LinearLayout *parent); - void CreateMIPSTracerTab(UI::LinearLayout *list); - void CreateTestsTab(UI::LinearLayout *list); - void CreateAudioTab(UI::LinearLayout *list); - void CreateGraphicsTab(UI::LinearLayout *list); - void CreateNetworkTab(UI::LinearLayout *list); - void CreateUITab(UI::LinearLayout *list); - void CreateCrashHistoryTab(UI::LinearLayout *list); - - void OnLoggingChanged(UI::EventParams &e); - void OnOpenTexturesIniFile(UI::EventParams &e); - void OnJitAffectingSetting(UI::EventParams &e); - void OnJitDebugTools(UI::EventParams &e); - void OnRemoteDebugger(UI::EventParams &e); - void OnMIPSTracerEnabled(UI::EventParams &e); - void OnMIPSTracerPathChanged(UI::EventParams &e); - void OnMIPSTracerFlushTrace(UI::EventParams &e); - void OnMIPSTracerClearJitCache(UI::EventParams &e); - void OnMIPSTracerClearTracer(UI::EventParams &e); - void OnGPUDriverTest(UI::EventParams &e); - void OnMemstickTest(UI::EventParams &e); - void OnCopyStatesToRoot(UI::EventParams &e); - - void MemoryMapTest(); - - bool allowDebugger_ = false; - bool canAllowDebugger_ = true; - enum class HasIni { - NO, - YES, - MAYBE, - }; - HasIni hasTexturesIni_ = HasIni::MAYBE; - - bool MIPSTracerEnabled_ = false; - - int testSliderValue_ = 0; - bool pretendIngame_ = false; -}; +// Links to the developer tools pages. +void BuildDeveloperToolsPage(SettingsPage *page); diff --git a/UI/DriverManagerScreen.h b/UI/DriverManagerScreen.h index 42e666be16..5efb5c6583 100644 --- a/UI/DriverManagerScreen.h +++ b/UI/DriverManagerScreen.h @@ -16,7 +16,6 @@ public: protected: void CreateTabs() override; - bool ShowSearchControls() const override { return false; } private: void OnCustomDriverInstall(UI::EventParams &e); diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index b6bc67d806..a032f247c7 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -44,6 +44,7 @@ #include "Common/UI/PopupScreens.h" #include "UI/EmuScreen.h" #include "UI/GameSettingsScreen.h" +#include "UI/SettingsPage.h" #include "UI/GameInfoCache.h" #include "UI/GamepadEmu.h" #include "UI/ControlMappingScreen.h" @@ -115,10 +116,7 @@ void SetMemStickDirDarwin(int requesterToken) { #endif GameSettingsScreen::GameSettingsScreen(const Path &gamePath, std::string gameID, bool editThenRestore) - : UITabbedBaseDialogScreen(gamePath, &g_Config.iSettingsCurrentTab, TabDialogFlags::HorizontalOnlyIcons | TabDialogFlags::VerticalShowIcons), gameID_(gameID), editGameSpecificThenRestore_(editThenRestore) { - prevInflightFrames_ = g_Config.iInflightFrames; - analogSpeedMapped_ = KeyMap::InputMappingsFromPspButton(VIRTKEY_SPEED_ANALOG, nullptr, true); - + : SettingsPagesDialogScreen(gamePath, &g_Config.iSettingsCurrentTab, TabDialogFlags::HorizontalOnlyIcons | TabDialogFlags::VerticalShowIcons), gameID_(gameID), editGameSpecificThenRestore_(editThenRestore) { if (editGameSpecificThenRestore_) { std::shared_ptr info = g_gameInfoCache->GetInfo(nullptr, gamePath_, GameInfoFlags::PARAM_SFO); g_Config.LoadGameConfig(gameID_); @@ -160,7 +158,14 @@ GameSettingsScreen::~GameSettingsScreen() { System_PostUIMessage(UIMessage::GPU_CONFIG_CHANGED); } +void GameSettingsScreen::OpenDeveloperTools() { + OpenPage("GameSettingsTools", "Developer Tools"); +} + void GameSettingsScreen::PreCreateViews() { + if (RecreatingForPageChange()) { + return; + } ReloadAllPostShaderInfo(screenManager()->getDrawContext()); ReloadAllThemeInfo(); #if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) @@ -239,102 +244,117 @@ void GameSettingsScreen::CheckMemstickInstallState() { } #endif +// The tabs other than System, which needs more of the screen. +static void BuildGraphicsSettings(SettingsPage *page); +static void BuildControlsSettings(SettingsPage *page); +static void BuildAudioSettings(SettingsPage *page); +static void BuildNetworkingSettings(SettingsPage *page); +static void BuildToolsSettings(SettingsPage *page); +static void BuildVRSettings(SettingsPage *page); + +const std::vector &GameSettingsScreen::PageDefs() { + static const std::vector pages = { + { "GameSettingsGraphics", "Graphics", "I_DISPLAY", "graphics", I18NCat::GRAPHICS, &BuildGraphicsSettings }, + { "GameSettingsControls", "Controls", "I_CONTROLLER", "controls", I18NCat::CONTROLS, &BuildControlsSettings }, + { "GameSettingsAudio", "Audio", "I_SPEAKER_MAX", "audio", I18NCat::AUDIO, &BuildAudioSettings }, + { "GameSettingsNetworking", "Networking", "I_WIFI", "network", I18NCat::NETWORKING, &BuildNetworkingSettings }, + { "GameSettingsTools", "Tools", "I_TOOLS", "tools", I18NCat::MAINSETTINGS, &BuildToolsSettings }, + { "GameSettingsSystem", "System", "I_PSP", "system", I18NCat::SYSTEM, nullptr }, + { "GameSettingsVR", "VR", nullptr, nullptr, I18NCat::VR, &BuildVRSettings }, + }; + return pages; +} + void GameSettingsScreen::CreateTabs() { using namespace UI; auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - AddTab("GameSettingsGraphics", ms->T("Graphics"), ImageID("I_DISPLAY"), [this](UI::LinearLayout *parent) { - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - parent->Add(new PaneTitleBar(gamePath_, ms->T("Graphics"), "graphics", new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); - CreateGraphicsSettings(parent->Add(new ChoiceGroupList())); - }); - - AddTab("GameSettingsControls", ms->T("Controls"), ImageID("I_CONTROLLER"), [this](UI::LinearLayout *parent) { - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - parent->Add(new PaneTitleBar(gamePath_, ms->T("Controls"), "controls")); - CreateControlsSettings(parent->Add(new ChoiceGroupList())); - }); - - AddTab("GameSettingsAudio", ms->T("Audio"), ImageID("I_SPEAKER_MAX"), [this](UI::LinearLayout *parent) { - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - parent->Add(new PaneTitleBar(gamePath_, ms->T("Audio"), "audio")); - CreateAudioSettings(parent->Add(new ChoiceGroupList())); - }); - - AddTab("GameSettingsNetworking", ms->T("Networking"), ImageID("I_WIFI"), [this](UI::LinearLayout *parent) { - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - parent->Add(new PaneTitleBar(gamePath_, ms->T("Networking"), "network")); - CreateNetworkingSettings(parent->Add(new ChoiceGroupList())); - }); - - AddTab("GameSettingsTools", ms->T("Tools"), ImageID("I_TOOLS"), [this](UI::LinearLayout *parent) { - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - parent->Add(new PaneTitleBar(gamePath_, ms->T("Tools"), "tools")); - CreateToolsSettings(parent->Add(new ChoiceGroupList())); - }); - - AddTab("GameSettingsSystem", ms->T("System"), ImageID("I_PSP"), [this](UI::LinearLayout *parent) { - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - parent->Add(new PaneTitleBar(gamePath_, ms->T("System"), "system")); - CreateSystemSettings(parent->Add(new ChoiceGroupList())); - }); - int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE); - if ((deviceType == DEVICE_TYPE_VR) || g_Config.bForceVR) { - AddTab("GameSettingsVR", ms->T("VR"), ImageID::invalid(), [this](UI::LinearLayout *parent) { - CreateVRSettings(parent->Add(new ChoiceGroupList())); - }); + bool showVR = deviceType == DEVICE_TYPE_VR || g_Config.bForceVR; + + for (const PageDef &def : PageDefs()) { + if (def.build == &BuildVRSettings && !showVR) { + continue; + } + std::function build = def.build; + if (!build) { + build = [this](SettingsPage *page) { + BuildSystemSettings(page); + }; + } + AddSettingsTab(def.tag, ms->T(def.title), def.icon ? ImageID(def.icon) : ImageID::invalid(), def.cat, build, def.helpTag); } } // TODO: Make this generic extern int DefaultDepthRaster(); -// Graphics -void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings) { - auto gr = GetI18NCategory(I18NCat::GRAPHICS); - auto sy = GetI18NCategory(I18NCat::SYSTEM); - auto vr = GetI18NCategory(I18NCat::VR); - auto dev = GetI18NCategory(I18NCat::DEVELOPER); +static void OnRenderingBackend(SettingsPageHost *host) { + // It only makes sense to show the restart prompt if the backend was actually changed. + if (g_Config.iGPUBackend != (int)GetGPUBackend()) { + host->RestartOrDo([]() { + g_Config.iGPUBackend = (int)GetGPUBackend(); + }); + } +} +static void OnRenderingDevice(SettingsPageHost *host) { + // It only makes sense to show the restart prompt if the device was actually changed. + std::string *deviceNameSetting = GPUDeviceNameSetting(); + if (deviceNameSetting && *deviceNameSetting != GetGPUBackendDevice()) { + host->RestartOrDo([host]() { + std::string *deviceNameSetting = GPUDeviceNameSetting(); + if (deviceNameSetting) + *deviceNameSetting = GetGPUBackendDevice(); + // Needed to redraw the setting. + host->RecreatePageViews(); + }); + } +} + +static void BuildGraphicsSettings(SettingsPage *page) { using namespace UI; + SettingsPageHost *host = page->Host(); - ChoiceGroup *renderingSettings = graphicsSettings->Add(new ChoiceGroup(gr->T("Rendering Mode"))); + Draw::DrawContext *draw = host->PageScreenManager()->getDrawContext(); + int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE); - Draw::DrawContext *draw = screenManager()->getDrawContext(); + page->Header("Rendering Mode"); #if !PPSSPP_PLATFORM(UWP) - static const char *renderingBackend[] = { "OpenGL", "Direct3D 9", "Direct3D 11", "Vulkan", "Metal" }; - PopupMultiChoice *renderingBackendChoice = renderingSettings->Add(new PopupMultiChoice(&g_Config.iGPUBackend, gr->T("Backend"), renderingBackend, (int)GPUBackend::OPENGL, ARRAY_SIZE(renderingBackend), I18NCat::GRAPHICS, screenManager())); - renderingBackendChoice->SetPreOpenCallback([this](UI::PopupMultiChoice *choice) { - // Don't filter until the last possible moment, since it involves trying to initialize Vulkan, if we were - // started in OpenGL mode on Android. - choice->HideChoice(1); - choice->OnChoice.Handle(this, &GameSettingsScreen::OnRenderingBackend); + page->Setting(&g_Config.iGPUBackend).Setup([host](View *view) { + PopupMultiChoice *renderingBackendChoice = static_cast(view); + renderingBackendChoice->SetPreOpenCallback([host](UI::PopupMultiChoice *choice) { + // Don't filter until the last possible moment, since it involves trying to initialize Vulkan, if we were + // started in OpenGL mode on Android. + choice->HideChoice(1); + choice->OnChoice.Add([host](UI::EventParams &) { + OnRenderingBackend(host); + }); - if (!g_Config.IsBackendEnabled(GPUBackend::OPENGL)) - choice->HideChoice((int)GPUBackend::OPENGL); - if (!g_Config.IsBackendEnabled(GPUBackend::DIRECT3D11)) - choice->HideChoice((int)GPUBackend::DIRECT3D11); - if (!g_Config.IsBackendEnabled(GPUBackend::VULKAN)) - choice->HideChoice((int)GPUBackend::VULKAN); - if (!g_Config.IsBackendEnabled(GPUBackend::METAL)) - choice->HideChoice((int)GPUBackend::METAL); + if (!g_Config.IsBackendEnabled(GPUBackend::OPENGL)) + choice->HideChoice((int)GPUBackend::OPENGL); + if (!g_Config.IsBackendEnabled(GPUBackend::DIRECT3D11)) + choice->HideChoice((int)GPUBackend::DIRECT3D11); + if (!g_Config.IsBackendEnabled(GPUBackend::VULKAN)) + choice->HideChoice((int)GPUBackend::VULKAN); + if (!g_Config.IsBackendEnabled(GPUBackend::METAL)) + choice->HideChoice((int)GPUBackend::METAL); + }); + + if (!IsFirstInstance()) { + // If we're not the first instance, can't save the setting, and it requires a restart, so... + renderingBackendChoice->SetEnabled(false); + } }); - - if (!IsFirstInstance()) { - // If we're not the first instance, can't save the setting, and it requires a restart, so... - renderingBackendChoice->SetEnabled(false); - } #endif #if (PPSSPP_PLATFORM(IOS) && !PPSSPP_PLATFORM(TVOS)) || PPSSPP_PLATFORM(ANDROID) // Showing the game there needs a second swapchain. Otherwise the system mirrors the screen. - static const char *externalDisplayModes[] = { "Show the game", "Mirror the screen" }; - PopupMultiChoice *externalDisplayChoice = renderingSettings->Add(new PopupMultiChoice(&g_Config.iExternalDisplay, gr->T("External display"), externalDisplayModes, 0, ARRAY_SIZE(externalDisplayModes), I18NCat::GRAPHICS, screenManager())); - externalDisplayChoice->SetEnabled(draw->GetDeviceCaps().multipleSwapchains); - // Picking the current value again would rebuild the TV's output for nothing (iOS starts over on a change). - externalDisplayChoice->OnChoice.Add([previous = g_Config.iExternalDisplay](UI::EventParams &e) mutable { + page->Setting(&g_Config.iExternalDisplay).EnabledFunc([draw] { + return draw->GetDeviceCaps().multipleSwapchains; + }).OnChange([previous = g_Config.iExternalDisplay](UI::EventParams &e) mutable { + // Picking the current value again would rebuild the TV's output for nothing (iOS starts over on a change). if (g_Config.iExternalDisplay == previous) { return; } @@ -347,111 +367,84 @@ void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings) if (draw->GetDeviceList().size() > 1) { std::string *deviceNameSetting = GPUDeviceNameSetting(); if (deviceNameSetting) { - PopupMultiChoiceDynamic *deviceChoice = renderingSettings->Add(new PopupMultiChoiceDynamic(deviceNameSetting, gr->T("Device"), draw->GetDeviceList(), I18NCat::NONE, screenManager())); - deviceChoice->OnChoice.Handle(this, &GameSettingsScreen::OnRenderingDevice); + page->Setting(deviceNameSetting, draw->GetDeviceList()).OnChange([host](UI::EventParams &e) { + OnRenderingDevice(host); + }); } } - static const char *internalResolutions[] = { "Auto (1:1)", "1x PSP", "2x PSP", "3x PSP", "4x PSP", "5x PSP", "6x PSP", "7x PSP", "8x PSP", "9x PSP", "10x PSP" }; - PopupMultiChoice *resolutionChoice = renderingSettings->Add(new PopupMultiChoice(&g_Config.iInternalResolution, gr->T("Rendering Resolution"), internalResolutions, 0, ARRAY_SIZE(internalResolutions), I18NCat::GRAPHICS, screenManager())); - resolutionChoice->OnChoice.Add([](UI::EventParams &e) { - if (g_Config.iAndroidHwScale == 1) { - System_RecreateActivity(); - } - Reporting::UpdateConfig(); - System_PostUIMessage(UIMessage::GPU_RENDER_RESIZED); - }); - resolutionChoice->SetEnabledFunc([] { + page->Setting(&g_Config.iInternalResolution).EnabledFunc([] { return !g_Config.bSoftwareRendering && !g_Config.bSkipBufferEffects; }); - int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE); - if (deviceType != DEVICE_TYPE_VR) { - CheckBox *softwareGPU = renderingSettings->Add(new CheckBox(&g_Config.bSoftwareRendering, gr->T("Software Rendering", "Software Rendering (slow)"))); - softwareGPU->SetEnabled(!PSP_IsInited()); + page->Setting(&g_Config.bSoftwareRendering).EnabledFunc([] { + return !PSP_IsInited(); + }); } if (draw->GetDeviceCaps().multiSampleLevelsMask != 1) { - static const char *msaaModes[] = { "Off", "2x", "4x", "8x", "16x" }; - auto msaaChoice = renderingSettings->Add(new PopupMultiChoice(&g_Config.iMultiSampleLevel, gr->T("Antialiasing (MSAA)"), msaaModes, 0, ARRAY_SIZE(msaaModes), I18NCat::GRAPHICS, screenManager())); - msaaChoice->OnChoice.Add([&](UI::EventParams &) -> void { - System_PostUIMessage(UIMessage::GPU_RENDER_RESIZED); - }); - msaaChoice->SetEnabledFunc([] { + page->Setting(&g_Config.iMultiSampleLevel).EnabledFunc([] { return !g_Config.bSoftwareRendering && !g_Config.bSkipBufferEffects; - }); - if (g_Config.iMultiSampleLevel > 1 && draw->GetDeviceCaps().isTilingGPU) { - msaaChoice->SetIconRight(ImageID("I_WARNING"), 0.7f); - } - msaaChoice->SetEnabledFunc([] { - return !g_Config.bSoftwareRendering && !g_Config.bSkipBufferEffects; - }); - - // Hide unsupported levels. - for (int i = 1; i < 5; i++) { - if ((draw->GetDeviceCaps().multiSampleLevelsMask & (1 << i)) == 0) { - msaaChoice->HideChoice(i); - } else if (i > 0 && draw->GetDeviceCaps().isTilingGPU) { - msaaChoice->SetChoiceIcon(i, ImageID("I_WARNING")); + }).Setup([draw](View *view) { + PopupMultiChoice *msaaChoice = static_cast(view); + if (g_Config.iMultiSampleLevel > 1 && draw->GetDeviceCaps().isTilingGPU) { + msaaChoice->SetIconRight(ImageID("I_WARNING"), 0.7f); } - } + // Hide unsupported levels. + for (int i = 1; i < msaaChoice->NumChoices(); i++) { + if ((draw->GetDeviceCaps().multiSampleLevelsMask & (1 << i)) == 0) { + msaaChoice->HideChoice(i); + } else if (i > 0 && draw->GetDeviceCaps().isTilingGPU) { + msaaChoice->SetChoiceIcon(i, ImageID("I_WARNING")); + } + } + }); } #if PPSSPP_PLATFORM(ANDROID) if ((deviceType != DEVICE_TYPE_TV) && (deviceType != DEVICE_TYPE_VR)) { - static const char *deviceResolutions[] = { "Native device resolution", "Same as Rendering resolution", "1x PSP", "2x PSP", "3x PSP", "4x PSP", "5x PSP" }; - int max_res_temp = std::max(System_GetPropertyInt(SYSPROP_DISPLAY_XRES), System_GetPropertyInt(SYSPROP_DISPLAY_YRES)) / 480 + 2; - if (max_res_temp == 3) - max_res_temp = 4; // At least allow 2x - int max_res = std::min(max_res_temp, (int)ARRAY_SIZE(deviceResolutions)); - UI::PopupMultiChoice *hwscale = renderingSettings->Add(new PopupMultiChoice(&g_Config.iAndroidHwScale, gr->T("Display Resolution (HW scaler)"), deviceResolutions, 0, max_res, I18NCat::GRAPHICS, screenManager())); - hwscale->OnChoice.Add([](UI::EventParams &) { - System_PostUIMessage(UIMessage::GPU_RENDER_RESIZED); - System_PostUIMessage(UIMessage::GPU_DISPLAY_RESIZED); - System_RecreateActivity(); + int max_res = std::max(System_GetPropertyInt(SYSPROP_DISPLAY_XRES), System_GetPropertyInt(SYSPROP_DISPLAY_YRES)) / 480 + 2; + if (max_res == 3) + max_res = 4; // At least allow 2x + page->Setting(&g_Config.iAndroidHwScale).Setup([max_res](View *view) { + // Hide the scales the display is too small for. + PopupMultiChoice *choice = static_cast(view); + for (int i = max_res; i < choice->NumChoices(); i++) { + choice->HideChoice(i); + } }); } #endif - DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation()); + DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(g_display.GetDeviceOrientation()); - renderingSettings->Add(new CheckBox(&g_Config.bReplaceTextures, dev->T("Replace textures"))); + page->Setting(&g_Config.bReplaceTextures); - ChoiceGroup *displaySettings = graphicsSettings->Add(new ChoiceGroup(gr->T("Display"))); + page->Header("Display"); if (deviceType != DEVICE_TYPE_VR) { #if !defined(MOBILE_DEVICE) - CheckBox *fullscreenCheckbox = displaySettings->Add(new CheckBox(&g_Config.bFullScreen, gr->T("Full screen"))); - fullscreenCheckbox->OnClick.Add([](UI::EventParams &e) { - System_ApplyFullscreenState(); - }); + page->Setting(&g_Config.bFullScreen); if (draw->GetDeviceCaps().fullScreenExclusiveSupported) { - CheckBox* fullscreenExclusive = displaySettings->Add(new CheckBox(&g_Config.bAllowFullScreenExclusive, gr->T("Allow exclusive fullscreen"))); - fullscreenExclusive->SetEnabledFunc([] { + page->Setting(&g_Config.bAllowFullScreenExclusive).EnabledFunc([] { return g_Config.bFullScreen && !g_Config.bFullScreenMulti; - }); - fullscreenExclusive->OnClick.Add([this](UI::EventParams &e) { - TriggerRestartOrDo([this]() { + }).OnChange([host](UI::EventParams &e) { + host->RestartOrDo([host]() { g_Config.bAllowFullScreenExclusive = !g_Config.bAllowFullScreenExclusive; - RecreateViews(); + host->RecreatePageViews(); }); }); } if (System_GetPropertyInt(SYSPROP_DISPLAY_COUNT) > 1) { - CheckBox *fullscreenMulti = displaySettings->Add(new CheckBox(&g_Config.bFullScreenMulti, gr->T("Use all displays"))); - fullscreenMulti->SetEnabledFunc([] { + page->Setting(&g_Config.bFullScreenMulti).EnabledFunc([] { return g_Config.bFullScreen && !g_Config.bAllowFullScreenExclusive; }); - fullscreenMulti->OnClick.Add([](UI::EventParams &e) { - System_ApplyFullscreenState(); - }); } #endif // Display Layout Editor: To avoid overlapping touch controls on large tablets, meet geeky demands for integer zoom/unstretched image etc. - Choice *displayEditor = displaySettings->Add(new Choice(gr->T("Display layout & effects"))); - displayEditor->OnClick.Add([&](UI::EventParams &) -> void { - screenManager()->push(new DisplayLayoutScreen(gamePath_)); + page->Choice("Display layout & effects").OnClick([host](UI::EventParams &) { + host->PageScreenManager()->push(new DisplayLayoutScreen(host->PageGamePath())); }); } @@ -459,161 +452,121 @@ void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings) if (CountSetBits((u32)draw->GetDeviceCaps().presentModesSupported) > 1) { // Immediate means non-synchronized, tearing. if (draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::IMMEDIATE) { - CheckBox *vSync = displaySettings->Add(new CheckBox(&g_Config.bVSync, gr->T("VSync"))); - vSync->OnClick.Add([=](EventParams &e) { + page->Setting(&g_Config.bVSync).OnChange([](UI::EventParams &e) { NativeResized(); // TODO: Remove }); } if (draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::MAILBOX) { - CheckBox *lowLatency = displaySettings->Add(new CheckBox(&g_Config.bLowLatencyPresent, gr->T("Low latency display"))); - lowLatency->OnClick.Add([=](EventParams &e) { + SettingItem &lowLatency = page->Setting(&g_Config.bLowLatencyPresent).OnChange([](UI::EventParams &e) { NativeResized(); // TODO: Remove }); // If the immediate mode is supported, we can tie low latency present to VSync. if (draw->GetDeviceCaps().presentModesSupported & Draw::PresentMode::IMMEDIATE) { - lowLatency->SetEnabledPtr(&g_Config.bVSync); + lowLatency.Enabled(&g_Config.bVSync); } } } - ChoiceGroup *frameRateSettings = graphicsSettings->Add(new ChoiceGroup(gr->T("Frame Rate Control"))); + page->Header("Frame Rate Control"); - PopupSliderChoice *altSpeed1 = frameRateSettings->Add(new PopupSliderChoice(&g_Config.iAltSpeedPercent1, 0, 1000, UI::NO_DEFAULT_INT, gr->T("Alternative Speed", "Alternative speed"), 5, screenManager(), gr->T("%, 0:unlimited"))); - altSpeed1->SetFormat("%i%%"); - altSpeed1->SetZeroLabel(gr->T("Unlimited")); - altSpeed1->SetNegativeDisable(gr->T("Disabled")); - - PopupSliderChoice *altSpeed2 = frameRateSettings->Add(new PopupSliderChoice(&g_Config.iAltSpeedPercent2, 0, 1000, UI::NO_DEFAULT_INT, gr->T("Alternative Speed 2", "Alternative speed 2 (in %, 0 = unlimited)"), 5, screenManager(), gr->T("%, 0:unlimited"))); - altSpeed2->SetFormat("%i%%"); - altSpeed2->SetZeroLabel(gr->T("Unlimited")); - altSpeed2->SetNegativeDisable(gr->T("Disabled")); - - if (analogSpeedMapped_) { - PopupSliderChoice *analogSpeed = frameRateSettings->Add(new PopupSliderChoice(&g_Config.iAnalogAltSpeedPercent, 1, 1000, UI::NO_DEFAULT_INT, gr->T("Analog alternative speed", "Analog alternative speed (in %)"), 5, screenManager(), "%")); - analogSpeed->SetFormat("%i%%"); + page->Setting(&g_Config.iAltSpeedPercent1); + page->Setting(&g_Config.iAltSpeedPercent2); + if (KeyMap::InputMappingsFromPspButton(VIRTKEY_SPEED_ANALOG, nullptr, true)) { + page->Setting(&g_Config.iAnalogAltSpeedPercent); } - ChoiceGroup *speedHackSettings = graphicsSettings->Add(new ChoiceGroup(gr->T("Speed Hacks", "Speed Hacks (can cause rendering errors!)"))); + page->Header({ I18NCat::GRAPHICS, "Speed Hacks", "Speed Hacks (can cause rendering errors!)" }); - static const char *frameSkip[] = {"Off", "1", "2", "3", "4"}; - PopupMultiChoice *frameSkipping = speedHackSettings->Add(new PopupMultiChoice(&g_Config.iFrameSkip, gr->T("Frame Skipping"), frameSkip, 0, ARRAY_SIZE(frameSkip), I18NCat::GRAPHICS, screenManager())); - frameSkipping->SetChoicesUntranslated(1, (int)ARRAY_SIZE(frameSkip) - 1); - frameSkipping->SetEnabledFunc([] { + page->Setting(&g_Config.iFrameSkip).EnabledFunc([] { return !g_Config.bAutoFrameSkip; + }).Setup([](View *view) { + PopupMultiChoice *choice = static_cast(view); + choice->SetChoicesUntranslated(1, choice->NumChoices() - 1); }); - CheckBox *frameSkipAuto = speedHackSettings->Add(new CheckBox(&g_Config.bAutoFrameSkip, gr->T("Auto FrameSkip"))); - frameSkipAuto->OnClick.Add([](UI::EventParams &e) { - g_Config.UpdateAfterSettingAutoFrameSkip(); + page->Setting(&g_Config.bAutoFrameSkip); + + page->Setting(&g_Config.bSkipBufferEffects).Disabled(&g_Config.bSoftwareRendering); + page->Hint({ I18NCat::GRAPHICS, "RenderingMode NonBuffered Tip", "Faster, but graphics may be missing in some games" }); + + page->Setting(&g_Config.iSkipGPUReadbackMode).Disabled(&g_Config.bSoftwareRendering).Setup([](View *view) { + static_cast(view)->SetDefault(0); }); - CheckBox *skipBufferEffects = speedHackSettings->Add(new CheckBox(&g_Config.bSkipBufferEffects, gr->T("Skip Buffer Effects"))); - speedHackSettings->Add(new SettingHint(gr->T("RenderingMode NonBuffered Tip", "Faster, but graphics may be missing in some games"), skipBufferEffects)); - skipBufferEffects->OnClick.Add([=](EventParams &e) { - if (g_Config.bSkipBufferEffects) { - g_Config.bAutoFrameSkip = false; - } - - System_PostUIMessage(UIMessage::GPU_RENDER_RESIZED); - System_PostUIMessage(UIMessage::GPU_CONFIG_CHANGED); + page->Setting(&g_Config.iDepthRasterMode).Disabled(&g_Config.bSoftwareRendering).Setup([](View *view) { + PopupMultiChoice *depthRasterMode = static_cast(view); + depthRasterMode->SetChoiceIcon(3, ImageID("I_WARNING")); // It's a performance trap. + depthRasterMode->SetDefault(DefaultDepthRaster()); + if (g_Config.iDepthRasterMode != 3) + depthRasterMode->HideChoice(3); }); - skipBufferEffects->SetDisabledPtr(&g_Config.bSoftwareRendering); - static const char *skipGpuReadbackModes[] = { "No", "Skip", "Copy to texture" }; - - PopupMultiChoice *skipGPUReadbacks = speedHackSettings->Add(new PopupMultiChoice(&g_Config.iSkipGPUReadbackMode, gr->T("Skip GPU Readbacks"), skipGpuReadbackModes, 0, ARRAY_SIZE(skipGpuReadbackModes), I18NCat::GRAPHICS, screenManager())); - skipGPUReadbacks->SetDisabledPtr(&g_Config.bSoftwareRendering); - skipGPUReadbacks->SetDefault(0); - - static const char *depthRasterModes[] = { "Auto", "Low", "Off", "Always on" }; - - PopupMultiChoice *depthRasterMode = speedHackSettings->Add(new PopupMultiChoice(&g_Config.iDepthRasterMode, gr->T("Lens flare occlusion"), depthRasterModes, 0, ARRAY_SIZE(depthRasterModes), I18NCat::GRAPHICS, screenManager())); - depthRasterMode->SetDisabledPtr(&g_Config.bSoftwareRendering); - depthRasterMode->SetChoiceIcon(3, ImageID("I_WARNING")); // It's a performance trap. - depthRasterMode->SetDefault(DefaultDepthRaster()); - if (g_Config.iDepthRasterMode != 3) - depthRasterMode->HideChoice(3); - - static const char *bloomHackOptions[] = {"Off", "Safe", "Balanced", "Aggressive"}; - PopupMultiChoice *bloomHack = speedHackSettings->Add(new PopupMultiChoice(&g_Config.iBloomHack, gr->T("Lower resolution for effects"), bloomHackOptions, 0, ARRAY_SIZE(bloomHackOptions), I18NCat::GRAPHICS, screenManager())); - bloomHack->SetEnabledFunc([] { + page->Setting(&g_Config.iBloomHack).EnabledFunc([] { return !g_Config.bSoftwareRendering && g_Config.iInternalResolution != 1; }); - speedHackSettings->Add(new SettingHint(gr->T("Reduces artifacts"), bloomHack)); + page->Hint("Reduces artifacts"); - ChoiceGroup *performanceSettings = graphicsSettings->Add(new ChoiceGroup(gr->T("Performance"))); - CheckBox *frameDuplication = performanceSettings->Add(new CheckBox(&g_Config.bRenderDuplicateFrames, gr->T("Render duplicate frames to 60hz"))); - frameDuplication->SetEnabledFunc([] { + page->Header("Performance"); + page->Setting(&g_Config.bRenderDuplicateFrames).EnabledFunc([] { return !g_Config.bSkipBufferEffects && g_Config.iFrameSkip == 0; }); - performanceSettings->Add(new SettingHint(gr->T("RenderDuplicateFrames Tip", "Can make framerate smoother in games that run at lower framerates"), frameDuplication)); + page->Hint({ I18NCat::GRAPHICS, "RenderDuplicateFrames Tip", "Can make framerate smoother in games that run at lower framerates" }); if (draw->GetDeviceCaps().setMaxFrameLatencySupported) { - static const char *bufferOptions[] = { "No buffer", "Up to 1" }; - PopupMultiChoice *inflightChoice = performanceSettings->Add(new PopupMultiChoice(&g_Config.iInflightFrames, gr->T("Buffer graphics commands"), bufferOptions, 1, ARRAY_SIZE(bufferOptions), I18NCat::GRAPHICS, screenManager())); - inflightChoice->OnChoice.Handle(this, &GameSettingsScreen::OnInflightFramesChoice); - performanceSettings->Add(new SettingHint(gr->T("Faster, input lag"), inflightChoice)); // TODO: This hint could use improvement. + // Changing it needs a restart, so the value in use is the one the page was built with. + page->Setting(&g_Config.iInflightFrames).OnChange([host, previous = g_Config.iInflightFrames](UI::EventParams &e) { + if (g_Config.iInflightFrames != previous) { + host->RestartOrDo([previous]() { + g_Config.iInflightFrames = previous; + }); + } + }); + page->Hint("Faster, input lag"); // TODO: This hint could use improvement. } - ChoiceGroup *upscalingSettings = graphicsSettings->Add(new ChoiceGroup(gr->T("Texture upscaling"))); + page->Header("Texture upscaling"); if (GetGPUBackend() == GPUBackend::VULKAN || GetGPUBackend() == GPUBackend::METAL) { - ChoiceWithValueDisplay *textureShaderChoice = upscalingSettings->Add(new ChoiceWithValueDisplay(&g_Config.sTextureShaderName, gr->T("GPU texture upscaler (fast)"), &TextureTranslateName)); - textureShaderChoice->OnClick.Add([this](UI::EventParams &e) { + page->Custom("GPU texture upscaler (fast)", [host](UI::ViewGroup *parent) -> UI::View * { auto gr = GetI18NCategory(I18NCat::GRAPHICS); - auto shaderScreen = new TextureShaderScreen(gr->T("GPU texture upscaler (fast)")); - shaderScreen->OnChoice.Add([this](UI::EventParams &e) { - System_PostUIMessage(UIMessage::GPU_CONFIG_CHANGED); - RecreateViews(); // Update setting name - g_Config.bTexHardwareScaling = g_Config.sTextureShaderName != "Off"; + ChoiceWithValueDisplay *textureShaderChoice = parent->Add(new ChoiceWithValueDisplay(&g_Config.sTextureShaderName, gr->T("GPU texture upscaler (fast)"), &TextureTranslateName)); + textureShaderChoice->OnClick.Add([host](UI::EventParams &e) { + auto gr = GetI18NCategory(I18NCat::GRAPHICS); + auto shaderScreen = new TextureShaderScreen(gr->T("GPU texture upscaler (fast)")); + shaderScreen->OnChoice.Add([host](UI::EventParams &e) { + System_PostUIMessage(UIMessage::GPU_CONFIG_CHANGED); + host->RecreatePageViews(); // Update setting name + g_Config.bTexHardwareScaling = g_Config.sTextureShaderName != "Off"; + }); + if (e.v) + shaderScreen->SetPopupOrigin(e.v); + host->PageScreenManager()->push(shaderScreen); }); - if (e.v) - shaderScreen->SetPopupOrigin(e.v); - screenManager()->push(shaderScreen); - }); - textureShaderChoice->SetDisabledPtr(&g_Config.bSoftwareRendering); + return textureShaderChoice; + }).Disabled(&g_Config.bSoftwareRendering); } -#ifndef MOBILE_DEVICE - static const char *texScaleLevels[] = {"Off", "2x", "3x", "4x", "5x"}; -#else - static const char *texScaleLevels[] = {"Off", "2x", "3x"}; -#endif - - static const char *texScaleAlgos[] = { "xBRZ", "Hybrid", "Bicubic", "Hybrid + Bicubic", }; - PopupMultiChoice *texScalingType = upscalingSettings->Add(new PopupMultiChoice(&g_Config.iTexScalingType, gr->T("CPU texture upscaler (slow)"), texScaleAlgos, 0, ARRAY_SIZE(texScaleAlgos), I18NCat::GRAPHICS, screenManager())); - texScalingType->SetEnabledFunc([]() { + page->Setting(&g_Config.iTexScalingType).EnabledFunc([]() { return !g_Config.bSoftwareRendering && !UsingHardwareTextureScaling(); }); - PopupMultiChoice *texScalingChoice = upscalingSettings->Add(new PopupMultiChoice(&g_Config.iTexScalingLevel, gr->T("Upscale Level"), texScaleLevels, 1, ARRAY_SIZE(texScaleLevels), I18NCat::GRAPHICS, screenManager())); - upscalingSettings->Add(new SettingHint(gr->T("UpscaleLevel Tip", "CPU heavy - some scaling may be delayed to avoid stutter"), texScalingChoice)); - - texScalingChoice->SetEnabledFunc([]() { + page->Setting(&g_Config.iTexScalingLevel).EnabledFunc([]() { return !g_Config.bSoftwareRendering && !UsingHardwareTextureScaling(); }); + page->Hint({ I18NCat::GRAPHICS, "UpscaleLevel Tip", "CPU heavy - some scaling may be delayed to avoid stutter" }); - CheckBox *deposterize = upscalingSettings->Add(new CheckBox(&g_Config.bTexDeposterize, gr->T("Deposterize"))); - deposterize->SetEnabledFunc([]() { + page->Setting(&g_Config.bTexDeposterize).EnabledFunc([]() { return !g_Config.bSoftwareRendering && !UsingHardwareTextureScaling(); }); - upscalingSettings->Add(new SettingHint(gr->T("Deposterize Tip", "Fixes visual banding glitches in upscaled textures"), deposterize)); + page->Hint({ I18NCat::GRAPHICS, "Deposterize Tip", "Fixes visual banding glitches in upscaled textures" }); - ChoiceGroup *filteringSettings = graphicsSettings->Add(new ChoiceGroup(gr->T("Texture Filtering"))); + page->Header("Texture Filtering"); - static const char *anisoLevels[] = { "Off", "2x", "4x", "8x", "16x" }; - PopupMultiChoice *anisoFiltering = filteringSettings->Add(new PopupMultiChoice(&g_Config.iAnisotropyLevel, gr->T("Anisotropic Filtering"), anisoLevels, 0, ARRAY_SIZE(anisoLevels), I18NCat::GRAPHICS, screenManager())); - anisoFiltering->SetDisabledPtr(&g_Config.bSoftwareRendering); - anisoFiltering->OnChoice.Add([](UI::EventParams &e) { - System_PostUIMessage(UIMessage::GPU_CONFIG_CHANGED); - }); + page->Setting(&g_Config.iAnisotropyLevel).Disabled(&g_Config.bSoftwareRendering); - static const char *texFilters[] = { "Auto", "Nearest", "Linear", "Auto Max Quality"}; - PopupMultiChoice *filters = filteringSettings->Add(new PopupMultiChoice(&g_Config.iTexFiltering, gr->T("Texture Filter"), texFilters, 1, ARRAY_SIZE(texFilters), I18NCat::GRAPHICS, screenManager())); - filters->SetDisabledPtr(&g_Config.bSoftwareRendering); + page->Setting(&g_Config.iTexFiltering).Disabled(&g_Config.bSoftwareRendering); - CheckBox *smartFiltering = filteringSettings->Add(new CheckBox(&g_Config.bSmart2DTexFiltering, gr->T("Smart 2D texture filtering"))); - smartFiltering->SetDisabledPtr(&g_Config.bSoftwareRendering); + page->Setting(&g_Config.bSmart2DTexFiltering).Disabled(&g_Config.bSoftwareRendering); #if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS) bool showCardboardSettings = deviceType != DEVICE_TYPE_VR; @@ -622,97 +575,64 @@ void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings) bool showCardboardSettings = config.bEnableCardboardVR; #endif if (showCardboardSettings) { - ChoiceGroup *cardboardSettings = graphicsSettings->Add(new ChoiceGroup(gr->T("Cardboard VR Settings", "Cardboard VR Settings"))); - cardboardSettings->Add(new CheckBox(&config.bEnableCardboardVR, gr->T("Enable Cardboard VR", "Enable Cardboard VR"))); - PopupSliderChoice *cardboardScreenSize = cardboardSettings->Add(new PopupSliderChoice(&config.iCardboardScreenSize, 30, 150, 50, gr->T("Cardboard Screen Size", "Screen Size (in % of the viewport)"), 1, screenManager(), gr->T("% of viewport"))); - cardboardScreenSize->SetEnabledPtr(&config.bEnableCardboardVR); - PopupSliderChoice *cardboardXShift = cardboardSettings->Add(new PopupSliderChoice(&config.iCardboardXShift, -150, 150, 0, gr->T("Cardboard Screen X Shift", "X Shift (in % of the void)"), 1, screenManager(), gr->T("% of the void"))); - cardboardXShift->SetEnabledPtr(&config.bEnableCardboardVR); - PopupSliderChoice *cardboardYShift = cardboardSettings->Add(new PopupSliderChoice(&config.iCardboardYShift, -100, 100, 0, gr->T("Cardboard Screen Y Shift", "Y Shift (in % of the void)"), 1, screenManager(), gr->T("% of the void"))); - cardboardYShift->SetEnabledPtr(&config.bEnableCardboardVR); + page->Header("Cardboard VR Settings"); + page->Setting(&config.bEnableCardboardVR); + page->Setting(&config.iCardboardScreenSize).Enabled(&config.bEnableCardboardVR); + page->Setting(&config.iCardboardXShift).Enabled(&config.bEnableCardboardVR); + page->Setting(&config.iCardboardYShift).Enabled(&config.bEnableCardboardVR); } - ChoiceGroup *overlaySettings = graphicsSettings->Add(new ChoiceGroup(gr->T("Overlay Information"))); - overlaySettings->Add(new BitCheckBox(&g_Config.iShowStatusFlags, (int)ShowStatusFlags::FPS_COUNTER, gr->T("Show FPS Counter"))); - overlaySettings->Add(new BitCheckBox(&g_Config.iShowStatusFlags, (int)ShowStatusFlags::SPEED_COUNTER, gr->T("Show Speed"))); + page->Header("Overlay Information"); + auto addStatusFlag = [page](ShowStatusFlags flag, const char *text) { + page->Custom(text, [flag, text](UI::ViewGroup *parent) -> UI::View * { + return parent->Add(new BitCheckBox(&g_Config.iShowStatusFlags, (int)flag, T(I18NCat::GRAPHICS, text))); + }); + }; + addStatusFlag(ShowStatusFlags::FPS_COUNTER, "Show FPS Counter"); + addStatusFlag(ShowStatusFlags::SPEED_COUNTER, "Show Speed"); if (System_GetPropertyBool(SYSPROP_CAN_READ_BATTERY_PERCENTAGE)) { - overlaySettings->Add(new BitCheckBox(&g_Config.iShowStatusFlags, (int)ShowStatusFlags::BATTERY_PERCENT, gr->T("Show Battery %"))); + addStatusFlag(ShowStatusFlags::BATTERY_PERCENT, "Show Battery %"); } - AddOverlayList(overlaySettings, screenManager()); + page->Custom({ I18NCat::DEVELOPER, "Debug overlay" }, [host](UI::ViewGroup *parent) -> UI::View * { + AddOverlayList(parent, host->PageScreenManager()); + return parent->GetViewByIndex(parent->GetNumSubviews() - 1); + }); } -void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { +static void BuildAudioSettings(SettingsPage *page) { using namespace UI; - auto a = GetI18NCategory(I18NCat::AUDIO); - auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS); - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - auto di = GetI18NCategory(I18NCat::DIALOG); - -#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS) - ChoiceGroup *systemIntegrationSettings = audioSettings->Add(new ChoiceGroup()); // Don't need a title. #if PPSSPP_PLATFORM(IOS) - CheckBox *respectSilentMode = systemIntegrationSettings->Add(new CheckBox(&g_Config.bAudioRespectSilentMode, a->T("Respect silent mode"))); - respectSilentMode->OnClick.Add([=](EventParams &e) { - System_Notify(SystemNotification::AUDIO_MODE_CHANGED); - }); - respectSilentMode->SetEnabledPtr(&g_Config.bEnableSound); + page->Setting(&g_Config.bAudioRespectSilentMode).Enabled(&g_Config.bEnableSound); #endif - - CheckBox *mixWithOthers = systemIntegrationSettings->Add(new CheckBox(&g_Config.bAudioMixWithOthers, a->T("Mix audio with other apps"))); - mixWithOthers->OnClick.Add([=](EventParams &e) { - System_Notify(SystemNotification::AUDIO_MODE_CHANGED); - }); - mixWithOthers->SetEnabledPtr(&g_Config.bEnableSound); +#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS) + page->Setting(&g_Config.bAudioMixWithOthers).Enabled(&g_Config.bEnableSound); #endif + page->Header("Audio playback"); - ChoiceGroup *playbackSettings = audioSettings->Add(new ChoiceGroup(a->T("Audio playback"))); - - static const char *syncModes[] = { "Smooth (reduces artifacts)", "Classic (lowest latency)" }; - - playbackSettings->Add(new PopupMultiChoice(&g_Config.iAudioPlaybackMode, a->T("Playback mode"), syncModes, 0, ARRAY_SIZE(syncModes), I18NCat::AUDIO, screenManager())); - playbackSettings->Add(new CheckBox(&g_Config.bFillAudioGaps, a->T("Fill audio gaps")))->SetEnabledFunc([]() { + page->Setting(&g_Config.iAudioPlaybackMode); + page->Setting(&g_Config.bFillAudioGaps).EnabledFunc([]() { return g_Config.iAudioPlaybackMode == (int)AudioSyncMode::GRANULAR; }); - ChoiceGroup *volumeSettings = audioSettings->Add(new ChoiceGroup(a->T("Volume settings"))); + page->Header("Game volume"); // This is here because it now only applies to in-game. Muting the menu sounds is separate. - CheckBox *enableSound = volumeSettings->Add(new CheckBox(&g_Config.bEnableSound, a->T("Enable Sound"))); + page->Setting(&g_Config.bEnableSound); - PopupSliderChoice *volume = volumeSettings->Add(new PopupSliderChoice(&g_Config.iGameVolume, VOLUME_OFF, VOLUMEHI_FULL, Config::GetDefaultValueInt(&g_Config.iGameVolume), a->T("Game volume"), screenManager())); - volume->SetFormat("%d%%"); - volume->SetEnabledPtr(&g_Config.bEnableSound); - volume->SetZeroLabel(a->T("Mute")); - - PopupSliderChoice *reverbVolume = volumeSettings->Add(new PopupSliderChoice(&g_Config.iReverbVolume, VOLUME_OFF, 2 * VOLUMEHI_FULL, Config::GetDefaultValueInt(&g_Config.iReverbVolume), a->T("Reverb volume"), screenManager())); - reverbVolume->SetFormat("%d%%"); - reverbVolume->SetEnabledPtr(&g_Config.bEnableSound); - reverbVolume->SetZeroLabel(a->T("Disabled")); - - PopupSliderChoice *altVolume = volumeSettings->Add(new PopupSliderChoice(&g_Config.iAltSpeedVolume, VOLUME_OFF, VOLUMEHI_FULL, Config::GetDefaultValueInt(&g_Config.iAltSpeedVolume), a->T("Alternate speed volume"), screenManager())); - altVolume->SetFormat("%d%%"); - altVolume->SetEnabledPtr(&g_Config.bEnableSound); - altVolume->SetZeroLabel(a->T("Mute")); - - PopupSliderChoice *achievementVolume = volumeSettings->Add(new PopupSliderChoice(&g_Config.iAchievementVolume, VOLUME_OFF, VOLUMEHI_FULL, Config::GetDefaultValueInt(&g_Config.iAchievementVolume), T(I18NCat::AUDIO, "Achievement sound volume"), screenManager())); - achievementVolume->SetFormat("%d%%"); - achievementVolume->SetEnabledPtr(&g_Config.bEnableSound); - achievementVolume->SetZeroLabel(a->T("Mute")); - achievementVolume->OnChange.Add([](UI::EventParams &e) { + page->Setting(&g_Config.iGameVolume).Enabled(&g_Config.bEnableSound); + page->Setting(&g_Config.iReverbVolume).Enabled(&g_Config.bEnableSound); + page->Setting(&g_Config.iAltSpeedVolume).Enabled(&g_Config.bEnableSound); + page->Setting(&g_Config.iAchievementVolume).Enabled(&g_Config.bEnableSound).OnChange([](EventParams &e) { // Audio preview float achievementVolume = Volume100ToMultiplier(g_Config.iAchievementVolume); g_BackgroundAudio.SFX().Play(UI::UISound::ACHIEVEMENT_UNLOCKED, achievementVolume); }); - ChoiceGroup *uiSoundSettings = audioSettings->Add(new ChoiceGroup()); + page->Header(); - uiSoundSettings->Add(new CheckBox(&g_Config.bUISound, a->T("UI sound"))); - PopupSliderChoice *uiVolume = uiSoundSettings->Add(new PopupSliderChoice(&g_Config.iUIVolume, 0, VOLUMEHI_FULL, Config::GetDefaultValueInt(&g_Config.iUIVolume), a->T("UI volume"), screenManager())); - uiVolume->SetFormat("%d%%"); - uiVolume->SetZeroLabel(a->T("Mute")); - uiVolume->SetLiveUpdate(true); - uiVolume->OnChange.Add([](UI::EventParams &e) { + page->Setting(&g_Config.bUISound); + page->Setting(&g_Config.iUIVolume).LiveUpdate().Enabled(&g_Config.bUISound).OnChange([](EventParams &e) { static double lastTimePlayed = 0.0; double now = time_now_d(); if (now - lastTimePlayed < 0.1) { @@ -722,28 +642,27 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { // Audio preview PlayUISound(UI::UISound::CONFIRM); }); - uiVolume->SetEnabledPtr(&g_Config.bUISound); - PopupSliderChoice *gamePreviewVolume = uiSoundSettings->Add(new PopupSliderChoice(&g_Config.iGamePreviewVolume, VOLUME_OFF, VOLUMEHI_FULL, Config::GetDefaultValueInt(&g_Config.iGamePreviewVolume), a->T("Game preview volume"), screenManager())); - gamePreviewVolume->SetFormat("%d%%"); - gamePreviewVolume->SetZeroLabel(a->T("Mute")); + page->Setting(&g_Config.iGamePreviewVolume); bool sdlAudio = false; - ChoiceGroup *audioBackendSettings = audioSettings->Add(new ChoiceGroup(a->T("Audio backend"))); - #if defined(SDL) + page->Header("Audio backend"); std::vector audioDeviceList; SplitString(System_GetProperty(SYSPROP_AUDIO_DEVICE_LIST), '\0', audioDeviceList); - audioDeviceList.insert(audioDeviceList.begin(), a->T_cstr("Auto")); - PopupMultiChoiceDynamic *audioDevice = audioBackendSettings->Add(new PopupMultiChoiceDynamic(&g_Config.sAudioDevice, a->T("Device"), audioDeviceList, I18NCat::NONE, screenManager())); - audioDevice->OnChoice.Handle(this, &GameSettingsScreen::OnAudioDevice); + audioDeviceList.insert(audioDeviceList.begin(), std::string(T(I18NCat::AUDIO, "Auto"))); + page->Setting(&g_Config.sAudioDevice, std::move(audioDeviceList)).OnChange([](EventParams &e) { + if (g_Config.sAudioDevice == T(I18NCat::AUDIO, "Auto")) { + g_Config.sAudioDevice.clear(); + } + System_Notify(SystemNotification::AUDIO_RESET_DEVICE); + }); sdlAudio = true; static const int bufferSizes[] = {128, 256, 512, 1024, 2048}; - PopupSliderChoice *bufferSize = audioBackendSettings->Add(new PopupSliderChoice(&g_Config.iSDLAudioBufferSize, 0, 2048, 256, a->T("Buffer size"), screenManager())); - bufferSize->RestrictChoices(bufferSizes, ARRAY_SIZE(bufferSizes)); - audioBackendSettings->Add(new SettingHint(di->T("This change will not take effect until PPSSPP is restarted."), bufferSize)); + page->IntSlider(&g_Config.iSDLAudioBufferSize, 0, 2048, "Buffer size").Default(256).RestrictTo(bufferSizes, ARRAY_SIZE(bufferSizes)); + page->Hint({ I18NCat::DIALOG, "This change will not take effect until PPSSPP is restarted." }); #endif #if PPSSPP_PLATFORM(WINDOWS) @@ -755,16 +674,16 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { std::vector deviceDescs; g_audioBackend->EnumerateDevices(&deviceDescs); if (!deviceDescs.empty()) { + page->Header("Audio backend"); for (auto &desc : deviceDescs) { audioDeviceNames.push_back(desc.name); audioDeviceIds.push_back(desc.uniqueId); } - audioDeviceNames.insert(audioDeviceNames.begin(), std::string(a->T("Auto"))); + audioDeviceNames.insert(audioDeviceNames.begin(), std::string(T(I18NCat::AUDIO, "Auto"))); audioDeviceIds.insert(audioDeviceIds.begin(), ""); - PopupMultiChoiceDynamic *audioDevice = audioBackendSettings->Add(new PopupMultiChoiceDynamic(&g_Config.sAudioDevice, a->T("Device"), audioDeviceNames, I18NCat::NONE, screenManager(), &audioDeviceIds)); - audioDevice->OnChoice.Add([](UI::EventParams &) { + page->Setting(&g_Config.sAudioDevice, std::move(audioDeviceNames), std::move(audioDeviceIds)).OnChange([](EventParams &) { bool reverted; if (g_audioBackend->InitOutputDevice(g_Config.sAudioDevice, LatencyMode::Aggressive, &reverted)) { if (reverted) { @@ -774,178 +693,152 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { WARN_LOG(Log::Audio, "InitOutputDevice failed"); } }); - CheckBox *autoAudio = audioBackendSettings->Add(new CheckBox(&g_Config.bAutoSwitchAudioDevice, a->T("Use new audio devices automatically"))); - autoAudio->SetEnabledFunc([]()->bool { + page->Setting(&g_Config.bAutoSwitchAudioDevice).EnabledFunc([]()->bool { return g_Config.sAudioDevice.empty(); }); } - const bool isWindows = true; #else - const bool isWindows = false; if (sdlAudio) { - audioBackendSettings->Add(new CheckBox(&g_Config.bAutoSwitchAudioDevice, a->T("Use new audio devices automatically"))); + page->Setting(&g_Config.bAutoSwitchAudioDevice); + } else { + page->Header("Audio backend"); } #if PPSSPP_PLATFORM(ANDROID) - CheckBox *extraAudio = audioBackendSettings->Add(new CheckBox(&g_Config.bExtraAudioBuffering, a->T("AudioBufferingForBluetooth", "Bluetooth-friendly buffer (slower)"))); + page->Setting(&g_Config.bExtraAudioBuffering); auto restartAudio = [](UI::EventParams &) { AndroidAudio_Restart(g_audioState); }; // AAudio isn't stable before Android 8.1, so there it's always OpenSL ES. if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 27) { - static const char *audioApis[] = { "AAudio", "OpenSL ES" }; - PopupMultiChoice *audioApi = audioBackendSettings->Add(new PopupMultiChoice(&g_Config.iAndroidAudioApi, a->T("Audio API"), audioApis, 0, ARRAY_SIZE(audioApis), I18NCat::AUDIO, screenManager())); - audioApi->OnChoice.Add(restartAudio); - audioBackendSettings->Add(new SettingHint(a->T("AudioApiHint", "Try the other one if you have no sound or other audio problems."), audioApi)); + page->Setting(&g_Config.iAndroidAudioApi).OnChange(restartAudio); + page->Hint({ I18NCat::AUDIO, "AudioApiHint", "Try the other one if you have no sound or other audio problems." }); - CheckBox *mmap = audioBackendSettings->Add(new CheckBox(&g_Config.bAndroidAudioMMap, a->T("Low-latency audio (MMAP)"))); - mmap->OnClick.Add(restartAudio); - mmap->SetEnabledFunc([]() { + page->Setting(&g_Config.bAndroidAudioMMap).OnChange(restartAudio).EnabledFunc([]() { return g_Config.iAndroidAudioApi == (int)AndroidAudioApi::AAUDIO; }); - audioBackendSettings->Add(new SettingHint(a->T("AudioMMapHint", "Lowest audio delay. On some devices, it causes silence over Bluetooth or other connected audio devices. Turn it off if you get no sound."), mmap)); + page->Hint({ I18NCat::AUDIO, "AudioMMapHint", "Lowest audio delay. On some devices, it causes silence over Bluetooth or other connected audio devices. Turn it off if you get no sound." }); } // Show Oboe debug info const std::string audioErrorStr = AndroidAudio_GetErrorString(g_audioState); if (!audioErrorStr.empty()) { - audioBackendSettings->Add(new InfoItem(a->T("Audio Error"), audioErrorStr)); + page->Info("Audio Error", audioErrorStr); } #endif #endif std::vector micList = Microphone::getDeviceList(); if (!micList.empty()) { - ChoiceGroup *microphoneSettings = audioSettings->Add(new ChoiceGroup(a->T("Microphone"))); + page->Header("Microphone"); std::vector micValues = micList; - micList.emplace(micList.begin(), a->T("Auto")); + micList.emplace(micList.begin(), T(I18NCat::AUDIO, "Auto")); micValues.insert(micValues.begin(), ""); - PopupMultiChoiceDynamic *MicChoice = microphoneSettings->Add(new PopupMultiChoiceDynamic(&g_Config.sMicDevice, a->T("Microphone Device"), micList, I18NCat::NONE, screenManager(), &micValues)); - MicChoice->OnChoice.Add([](UI::EventParams &e) { - Microphone::onMicDeviceChange(); - }); + page->Setting(&g_Config.sMicDevice, std::move(micList), std::move(micValues)); } } -void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings) { +static void BuildControlsSettings(SettingsPage *page) { using namespace UI; - - auto co = GetI18NCategory(I18NCat::CONTROLS); - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - auto di = GetI18NCategory(I18NCat::DIALOG); + SettingsPageHost *host = page->Host(); int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE); - ChoiceGroup *generalSettings = controlsSettings->Add(new ChoiceGroup(ms->T("Controls"))); - generalSettings->Add(new Choice(co->T("Control mapping")))->OnClick.Add([this](UI::EventParams &e) { - screenManager()->push(new ControlMappingScreen(gamePath_)); + page->Header({ I18NCat::MAINSETTINGS, "Controls" }); + page->Choice("Control mapping").OnClick([host](UI::EventParams &e) { + host->PageScreenManager()->push(new ControlMappingScreen(host->PageGamePath())); }); - generalSettings->Add(new Choice(co->T("Calibrate analog stick")))->OnClick.Add([this](UI::EventParams &e) { - screenManager()->push(new AnalogCalibrationScreen(gamePath_)); + page->Choice("Calibrate analog stick").OnClick([host](UI::EventParams &e) { + host->PageScreenManager()->push(new AnalogCalibrationScreen(host->PageGamePath())); }); #if (PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP)) || (PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID)) - generalSettings->Add(new CheckBox(&g_Config.bSystemControls, co->T("Enable standard shortcut keys"))); + page->Setting(&g_Config.bSystemControls); #endif #if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) - generalSettings->Add(new CheckBox(&g_Config.bGamepadOnlyFocused, co->T("Ignore gamepads when not focused"))); + page->Setting(&g_Config.bGamepadOnlyFocused); #endif if (System_GetPropertyBool(SYSPROP_HAS_ACCELEROMETER)) { - // Show the tilt type on the item. - Choice *customizeTilt = generalSettings->Add(new ChoiceWithCallbackValueDisplay(co->T("Tilt control setup"), []() -> std::string { - if (g_Config.bTiltInputEnabled && (u32)g_Config.iTiltInputType < (u32)g_numTiltTypes) { - auto co = GetI18NCategory(I18NCat::CONTROLS); - return std::string(co->T(g_tiltTypes[g_Config.iTiltInputType])); - } else { - auto di = GetI18NCategory(I18NCat::DIALOG); - return std::string(di->T("Disabled")); - } - })); - customizeTilt->OnClick.Add([this](UI::EventParams &e) { - screenManager()->push(new TiltAnalogSettingsScreen(gamePath_)); + page->Custom("Tilt control setup", [host](UI::ViewGroup *parent) -> UI::View * { + // Show the tilt type on the item. + Choice *customizeTilt = parent->Add(new ChoiceWithCallbackValueDisplay(T(I18NCat::CONTROLS, "Tilt control setup"), []() -> std::string { + if (g_Config.bTiltInputEnabled && (u32)g_Config.iTiltInputType < (u32)g_numTiltTypes) { + auto co = GetI18NCategory(I18NCat::CONTROLS); + return std::string(co->T(g_tiltTypes[g_Config.iTiltInputType])); + } else { + auto di = GetI18NCategory(I18NCat::DIALOG); + return std::string(di->T("Disabled")); + } + })); + customizeTilt->OnClick.Add([host](UI::EventParams &e) { + host->PageScreenManager()->push(new TiltAnalogSettingsScreen(host->PageGamePath())); + }); + return customizeTilt; }); } else if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_VR) { // TODO: This seems like a regression - generalSettings->Add(new CheckBox(&g_Config.bHapticFeedback, co->T("HapticFeedback", "Haptic Feedback (vibration)"))); + page->Setting(&g_Config.bHapticFeedback); } - ChoiceGroup *analogSettings = controlsSettings->Add(new ChoiceGroup(co->T("Analog to digital mapping"))); - analogSettings->Add(new PopupSliderChoiceFloat(&g_Config.fAnalogTriggerThreshold, 0.02f, 0.98f, 0.75f, co->T("Analog trigger threshold"), screenManager())); - analogSettings->Add(new PopupSliderChoiceFloat(&g_Config.fAnalogStickThreshold, 0.2f, 0.98f, 0.75f, co->T("Analog stick threshold"), screenManager())); + page->Header("Analog to digital mapping"); + page->Setting(&g_Config.fAnalogTriggerThreshold); + page->Setting(&g_Config.fAnalogStickThreshold); // TVs don't have touch control, at least not yet. if ((deviceType != DEVICE_TYPE_TV) && (deviceType != DEVICE_TYPE_VR)) { - ChoiceGroup *touchSettings = controlsSettings->Add(new ChoiceGroup(co->T("On-screen touch controls"))); - touchSettings->Add(new CheckBox(&g_Config.bShowTouchControls, co->T("On-screen touch controls"))); - Choice *layoutEditorChoice = touchSettings->Add(new Choice(co->T("Edit touch control layout"))); - layoutEditorChoice->OnClick.Add([this](UI::EventParams &e) { - screenManager()->push(new TouchControlLayoutScreen(gamePath_)); - }); - layoutEditorChoice->SetEnabledPtr(&g_Config.bShowTouchControls); + page->Header("On-screen touch controls"); + page->Setting(&g_Config.bShowTouchControls); + page->Choice("Edit touch control layout").OnClick([host](UI::EventParams &e) { + host->PageScreenManager()->push(new TouchControlLayoutScreen(host->PageGamePath())); + }).Enabled(&g_Config.bShowTouchControls); + page->Choice("Gesture mapping").OnClick([host](UI::EventParams &e) { + host->PageScreenManager()->push(new GestureMappingScreen(host->PageGamePath())); + }).Enabled(&g_Config.bShowTouchControls); - Choice *gesture = touchSettings->Add(new Choice(co->T("Gesture mapping"))); - gesture->OnClick.Add([=](EventParams &e) { - screenManager()->push(new GestureMappingScreen(gamePath_)); - }); - gesture->SetEnabledPtr(&g_Config.bShowTouchControls); + page->Setting(&g_Config.iTouchButtonStyle).Enabled(&g_Config.bShowTouchControls); - static const char *touchControlStyles[] = { "Classic", "Thin borders", "Glowing borders" }; - View *style = touchSettings->Add(new PopupMultiChoice(&g_Config.iTouchButtonStyle, co->T("Button style"), touchControlStyles, 0, ARRAY_SIZE(touchControlStyles), I18NCat::CONTROLS, screenManager())); - style->SetEnabledPtr(&g_Config.bShowTouchControls); + page->Setting(&g_Config.iTouchButtonOpacity).Enabled(&g_Config.bShowTouchControls); + page->Setting(&g_Config.iTouchButtonHideSeconds).Enabled(&g_Config.bShowTouchControls); - PopupSliderChoice *opacity = touchSettings->Add(new PopupSliderChoice(&g_Config.iTouchButtonOpacity, 0, 100, 65, co->T("Button Opacity"), screenManager(), "%")); - opacity->SetEnabledPtr(&g_Config.bShowTouchControls); - opacity->SetFormat("%i%%"); - PopupSliderChoice *autoHide = touchSettings->Add(new PopupSliderChoice(&g_Config.iTouchButtonHideSeconds, 0, 300, 20, co->T("Auto-hide buttons after delay"), screenManager(), di->T("seconds, 0:off"))); - autoHide->SetEnabledPtr(&g_Config.bShowTouchControls); - autoHide->SetFormat(di->T("%d seconds")); - autoHide->SetZeroLabel(co->T("Off")); + page->Setting(&g_Config.bTouchGliding).Enabled(&g_Config.bShowTouchControls); - CheckBox *touchGliding = touchSettings->Add(new CheckBox(&g_Config.bTouchGliding, co->T("Keep first touched button pressed when dragging"))); - touchGliding->SetEnabledPtr(&g_Config.bShowTouchControls); - - TouchControlConfig &touch = g_Config.GetTouchControlsConfig(GetDeviceOrientation()); + TouchControlConfig &touch = g_Config.GetTouchControlsConfig(g_display.GetDeviceOrientation()); // Hide stick background, useful when increasing the size - CheckBox *hideStickBackground = touchSettings->Add(new CheckBox(&touch.bHideStickBackground, co->T("Hide touch analog stick background circle"))); - hideStickBackground->SetEnabledPtr(&g_Config.bShowTouchControls); + page->Setting(&touch.bHideStickBackground).Enabled(&g_Config.bShowTouchControls); // Sticky D-pad. - CheckBox *stickyDpad = touchSettings->Add(new CheckBox(&g_Config.bStickyTouchDPad, co->T("Sticky D-Pad"))); - stickyDpad->SetEnabledPtr(&g_Config.bShowTouchControls); - touchSettings->Add(new SettingHint(co->T("Easier sweeping movements"), stickyDpad)); + page->Setting(&g_Config.bStickyTouchDPad).Enabled(&g_Config.bShowTouchControls); + page->Hint("Easier sweeping movements"); // Re-centers itself to the touch location on touch-down. - CheckBox *floatingAnalog = touchSettings->Add(new CheckBox(&g_Config.bAutoCenterTouchAnalog, co->T("Auto-centering analog stick"))); - floatingAnalog->SetEnabledPtr(&g_Config.bShowTouchControls); + page->Setting(&g_Config.bAutoCenterTouchAnalog).Enabled(&g_Config.bShowTouchControls); if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_MOBILE) { - touchSettings->Add(new CheckBox(&g_Config.bHapticFeedback, co->T("HapticFeedback", "Haptic Feedback (vibration)"))); + page->Setting(&g_Config.bHapticFeedback); } // The pause button is now a regular on-screen button. - CheckBox *disableDiags = touchSettings->Add(new CheckBox(&g_Config.bDisableDpadDiagonals, co->T("Disable D-Pad diagonals (4-way touch)"))); - disableDiags->SetEnabledPtr(&g_Config.bShowTouchControls); + page->Setting(&g_Config.bDisableDpadDiagonals).Enabled(&g_Config.bShowTouchControls); } #if PPSSPP_PLATFORM(WINDOWS) - ChoiceGroup *inputSourceSettings = controlsSettings->Add(new ChoiceGroup(co->T("Control input sources"))); - inputSourceSettings->Add(new CheckBox(&g_Config.bAllowHIDInput, co->T("HID input"))); - inputSourceSettings->Add(new CheckBox(&g_Config.bAllowXInput, co->T("XInput"))); - inputSourceSettings->Add(new CheckBox(&g_Config.bAllowDInput, co->T("DirectInput"))); + page->Header("Control input sources"); + page->Setting(&g_Config.bAllowHIDInput); + page->Setting(&g_Config.bAllowXInput); + page->Setting(&g_Config.bAllowDInput); #endif if (deviceType != DEVICE_TYPE_VR) { - ChoiceGroup *keyboardSettings = controlsSettings->Add(new ChoiceGroup(co->T("Keyboard", "Keyboard Control Settings"))); + page->Header({ I18NCat::CONTROLS, "Keyboard", "Keyboard Control Settings" }); #if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) - keyboardSettings->Add(new CheckBox(&g_Config.bIgnoreWindowsKey, co->T("Ignore Windows Key"))); + page->Setting(&g_Config.bIgnoreWindowsKey); #endif - PopupSliderChoiceFloat *analogLimiter = new PopupSliderChoiceFloat(&g_Config.fAnalogLimiterDeadzone, 0.0f, 1.0f, 0.6f, co->T("Analog Limiter"), 0.10f, screenManager(), "/ 1.0"); - keyboardSettings->Add(analogLimiter); - keyboardSettings->Add(new SettingHint(co->T("AnalogLimiter Tip", "When the analog limiter button is pressed"), analogLimiter)); - keyboardSettings->Add(new PopupSliderChoice(&g_Config.iRapidFireInterval, 1, 10, 5, co->T("Rapid fire interval"), screenManager(), "frames")); + page->Setting(&g_Config.fAnalogLimiterDeadzone); + page->Hint({ I18NCat::CONTROLS, "AnalogLimiter Tip", "When the analog limiter button is pressed" }); + page->Setting(&g_Config.iRapidFireInterval); #if PPSSPP_PLATFORM(WINDOWS) || defined(SDL) || PPSSPP_PLATFORM(ANDROID) bool enableMouseSettings = true; #if PPSSPP_PLATFORM(ANDROID) @@ -958,22 +851,17 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings) #endif if (enableMouseSettings) { // The mousewheel button-release setting is independent of actual mouse delta control. - ChoiceGroup *mouseSettings = controlsSettings->Add(new ChoiceGroup(co->T("Mouse", "Mouse settings"))); - auto wheelUpDelaySlider = mouseSettings->Add(new PopupSliderChoice(&g_Config.iMouseWheelUpDelayMs, 10, 300, 1, co->T("Mouse wheel button-release delay"), screenManager())); - wheelUpDelaySlider->SetFormat(di->T("%d ms")); + page->Header({ I18NCat::CONTROLS, "Mouse", "Mouse settings" }); + page->Setting(&g_Config.iMouseWheelUpDelayMs); - CheckBox *mouseControl = mouseSettings->Add(new CheckBox(&g_Config.bMouseControl, co->T("Use Mouse Control"))); - mouseSettings->Add(new SettingHint(co->T("MouseControl Tip", "You can now map mouse in control mapping screen by pressing the 'M' icon."), mouseControl)); + page->Setting(&g_Config.bMouseControl); + page->Hint({ I18NCat::CONTROLS, "MouseControl Tip", "You can now map mouse in control mapping screen by pressing the 'M' icon." }); #if !PPSSPP_PLATFORM(ANDROID) - mouseSettings->Add(new CheckBox(&g_Config.bMouseConfine, co->T("Confine Mouse", "Trap mouse within window/display area")))->SetEnabledPtr(&g_Config.bMouseControl); + page->Setting(&g_Config.bMouseConfine).Enabled(&g_Config.bMouseControl); #endif - auto sensitivitySlider = mouseSettings->Add(new PopupSliderChoiceFloat(&g_Config.fMouseSensitivity, 0.01f, 1.0f, 0.1f, co->T("Mouse sensitivity"), 0.01f, screenManager(), "x")); - sensitivitySlider->SetEnabledPtr(&g_Config.bMouseControl); - sensitivitySlider->SetLiveUpdate(true); - auto smoothingSlider = mouseSettings->Add(new PopupSliderChoiceFloat(&g_Config.fMouseSmoothing, 0.0f, 0.95f, 0.9f, co->T("Mouse smoothing"), 0.05f, screenManager(), "x")); - smoothingSlider->SetEnabledPtr(&g_Config.bMouseControl); - smoothingSlider->SetLiveUpdate(true); + page->Setting(&g_Config.fMouseSensitivity).Enabled(&g_Config.bMouseControl).LiveUpdate(); + page->Setting(&g_Config.fMouseSmoothing).Enabled(&g_Config.bMouseControl).LiveUpdate(); } } } @@ -1022,15 +910,15 @@ MacAddressChooser::MacAddressChooser(RequesterToken token, Path gamePath, std::s } // The nickname is shown on both the Networking and System tabs. -UI::View *GameSettingsScreen::AddNicknameChoice(UI::ViewGroup *parent) { +static UI::View *AddNicknameChoice(SettingsPageHost *host, UI::ViewGroup *parent) { using namespace UI; auto sy = GetI18NCategory(I18NCat::SYSTEM); - PopupTextInputChoice *nickname = parent->Add(new PopupTextInputChoice(GetRequesterToken(), &g_Config.sNickName, sy->T("Nickname"), "", 32, screenManager())); + PopupTextInputChoice *nickname = parent->Add(new PopupTextInputChoice(host->PageRequesterToken(), &g_Config.sNickName, sy->T("Nickname"), "", 32, host->PageScreenManager())); if (g_Config.sNickName == "PPSSPP") { nickname->SetIconRight(ImageID("I_WARNING")); } - nickname->OnChange.Add([this](UI::EventParams &e) { + nickname->OnChange.Add([host](UI::EventParams &e) { // Copy to infrastructure name if valid and not already set. if (g_Config.sInfrastructureUsername.empty()) { if (g_Config.sNickName == SanitizeString(g_Config.sNickName, StringRestriction::AlphaNumDashUnderscore, 3, 16)) { @@ -1038,22 +926,18 @@ UI::View *GameSettingsScreen::AddNicknameChoice(UI::ViewGroup *parent) { } } // Updates the warning icon, and the username notice on the Networking tab. - RecreateViews(); + host->RecreatePageViews(); }); return nickname; } -void GameSettingsScreen::CreateNetworkingSettings(UI::ViewGroup *networkingSettings) { +static void BuildNetworkingSettings(SettingsPage *page) { using namespace UI; + SettingsPageHost *host = page->Host(); - auto n = GetI18NCategory(I18NCat::NETWORKING); - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - auto di = GetI18NCategory(I18NCat::DIALOG); + page->Header({ I18NCat::MAINSETTINGS, "Networking" }); - ChoiceGroup *generalSettings = networkingSettings->Add(new ChoiceGroup(ms->T("Networking"))); - - Choice *quickstart = generalSettings->Add(new Choice(n->T("Quick start guide for multiplayer"), ImageID("I_LINK_OUT"))); - quickstart->OnClick.Add([](EventParams &e) { + page->Choice("Quick start guide for multiplayer", ImageID("I_LINK_OUT")).OnClick([](EventParams &e) { auto n = GetI18NCategory(I18NCat::NETWORKING); std::string url(n->T("MultiplayerQuickStartURL", "https://www.ppsspp.org/docs/multiplayer/quickstart/")); System_LaunchUrl(LaunchUrlType::BROWSER_URL, url); @@ -1067,196 +951,210 @@ void GameSettingsScreen::CreateNetworkingSettings(UI::ViewGroup *networkingSetti } }; - generalSettings->Add(new CheckBox(&g_Config.bEnableWlan, n->T("Enable networking", "Enable networking/wlan (beta)")))->OnClick.Add(requestLocalNetwork); - generalSettings->Add(new MacAddressChooser(GetRequesterToken(), gamePath_, &g_Config.sMACAddress, n->T("MAC address"), screenManager())); + page->Setting(&g_Config.bEnableWlan).OnChange(requestLocalNetwork); + page->Custom("MAC address", [host](UI::ViewGroup *parent) -> UI::View * { + return parent->Add(new MacAddressChooser(host->PageRequesterToken(), host->PageGamePath(), &g_Config.sMACAddress, T(I18NCat::NETWORKING, "MAC address"), host->PageScreenManager())); + }); - ChoiceGroup *adhocSettings = networkingSettings->Add(new ChoiceGroup(n->T("Ad Hoc multiplayer"))); + page->Header("Ad Hoc multiplayer"); - auto launchAdhocServerScreen = [this](UI::EventParams &) { + page->Custom("Ad hoc server address", [host](UI::ViewGroup *parent) -> UI::View * { auto n = GetI18NCategory(I18NCat::NETWORKING); - screenManager()->push(new AdhocServerScreen(&g_Config.sProAdhocServer, n->T("Ad hoc server address"))); - }; + auto launchAdhocServerScreen = [host](UI::EventParams &) { + auto n = GetI18NCategory(I18NCat::NETWORKING); + host->PageScreenManager()->push(new AdhocServerScreen(&g_Config.sProAdhocServer, n->T("Ad hoc server address"))); + }; - if (AdhocServerNameIsCustom()) { - LinearLayout *serverRow = adhocSettings->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); - serverRow->SetSpacing(0.0f); - serverRow->Add(new ChoiceWithValueDisplay(&g_Config.sProAdhocServer, n->T("Ad hoc server address"), I18NCat::NONE, new LinearLayoutParams(1.0f)))->OnClick.Add(launchAdhocServerScreen); - serverRow->Add(new Choice(ImageID("I_EDIT_TEXT"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT)))->OnClick.Add([this](UI::EventParams &) { - AskToEditCurrentServer(GetRequesterToken(), screenManager()); - }); - } else if (!g_Config.sProAdhocServer.empty()) { - AdhocServerListEntry entry; - if (AdhocGetServerByHost(g_Config.sProAdhocServer, &entry)) { - adhocSettings->Add(new ChoiceWithFixedValueDisplay(ApplySafeSubstitutions("%1 (%2)", entry.name, entry.host), n->T("Ad hoc server address")))->OnClick.Add(launchAdhocServerScreen); - Choice *status = adhocSettings->Add(new Choice(ApplySafeSubstitutions(n->T("Server status: %1"), entry.name))); - status->SetIconLeft(ImageID("I_INFO")); - status->OnClick.Add([this, entry](UI::EventParams &) { - screenManager()->push(new AdhocServerInfoScreen(entry)); + if (AdhocServerNameIsCustom()) { + LinearLayout *serverRow = parent->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); + serverRow->SetSpacing(0.0f); + serverRow->Add(new ChoiceWithValueDisplay(&g_Config.sProAdhocServer, n->T("Ad hoc server address"), I18NCat::NONE, new LinearLayoutParams(1.0f)))->OnClick.Add(launchAdhocServerScreen); + serverRow->Add(new Choice(ImageID("I_EDIT_TEXT"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT)))->OnClick.Add([host](UI::EventParams &) { + AskToEditCurrentServer(host->PageRequesterToken(), host->PageScreenManager()); }); - } else { - // This case happens when updating from a previous version, as well as clicking on one of the local addresses - adhocSettings->Add(new ChoiceWithFixedValueDisplay(g_Config.sProAdhocServer, n->T("Ad hoc server address")))->OnClick.Add(launchAdhocServerScreen); + return serverRow; } - } else { - adhocSettings->Add(new ChoiceWithFixedValueDisplay(g_Config.sProAdhocServer, n->T("Ad hoc server address")))->OnClick.Add(launchAdhocServerScreen); + + AdhocServerListEntry entry; + if (!g_Config.sProAdhocServer.empty() && AdhocGetServerByHost(g_Config.sProAdhocServer, &entry)) { + Choice *choice = parent->Add(new ChoiceWithFixedValueDisplay(ApplySafeSubstitutions("%1 (%2)", entry.name, entry.host), n->T("Ad hoc server address"))); + choice->OnClick.Add(launchAdhocServerScreen); + return choice; + } + // This case happens when updating from a previous version, as well as clicking on one of the local addresses + Choice *choice = parent->Add(new ChoiceWithFixedValueDisplay(g_Config.sProAdhocServer, n->T("Ad hoc server address"))); + choice->OnClick.Add(launchAdhocServerScreen); + return choice; + }); + + AdhocServerListEntry serverEntry; + if (!AdhocServerNameIsCustom() && !g_Config.sProAdhocServer.empty() && AdhocGetServerByHost(g_Config.sProAdhocServer, &serverEntry)) { + page->Custom("Server status: %1", [host, serverEntry](UI::ViewGroup *parent) -> UI::View * { + Choice *status = parent->Add(new Choice(ApplySafeSubstitutions(T(I18NCat::NETWORKING, "Server status: %1"), serverEntry.name))); + status->SetIconLeft(ImageID("I_INFO")); + status->OnClick.Add([host, serverEntry](UI::EventParams &) { + host->PageScreenManager()->push(new AdhocServerInfoScreen(serverEntry)); + }); + return status; + }); } - static const char *relayModes[] = {"Auto", "Yes", "No"}; - PopupMultiChoice *relayModePopup = adhocSettings->Add(new PopupMultiChoice(&g_Config.iAdhocServerRelayMode, n->T("Try to use server-provided packet relay"), relayModes, 0, ARRAY_SIZE(relayModes), I18NCat::DIALOG, screenManager())); - relayModePopup->SetEnabled(!PSP_IsInited()); - adhocSettings->Add(new SettingHint(n->T("PacketRelayHint", "Available on servers that provide 'aemu_postoffice' packet relay, like socom.cc. Disable this for LAN or VPN play. Can be more reliable, but sometimes slower."), relayModePopup)); + page->Setting(&g_Config.iAdhocServerRelayMode).EnabledFunc([] { + return !PSP_IsInited(); + }); + page->Hint({ I18NCat::NETWORKING, "PacketRelayHint", "Available on servers that provide 'aemu_postoffice' packet relay, like socom.cc. Disable this for LAN or VPN play. Can be more reliable, but sometimes slower." }); - View *nicknameChoice = AddNicknameChoice(adhocSettings); - adhocSettings->Add(new SettingHint(n->T("Try to pick a unique nickname for ad hoc play"), nicknameChoice)); + page->Custom({ I18NCat::SYSTEM, "Nickname" }, [host](UI::ViewGroup *parent) { + return AddNicknameChoice(host, parent); + }); + page->Hint("Try to pick a unique nickname for ad hoc play"); - adhocSettings->Add(new CheckBox(&g_Config.bEnableAdhocServer, n->T("Enable built-in ad hoc server")))->OnClick.Add(requestLocalNetwork); + page->Setting(&g_Config.bEnableAdhocServer).OnChange(requestLocalNetwork); - CheckBox *relay_mode_enet = adhocSettings->Add(new CheckBox(&g_Config.bAdhocUseUdp, n->T("Connect to adhoc server using UDP (ENet) instead of TCP"))); - relay_mode_enet->SetEnabled(!PSP_IsInited()); - adhocSettings->Add(new SettingHint(n->T("Try switching between TCP/UDP if you suffer frequent adhoc server disconnects. Some network environments favor TCP and some UDP."), relay_mode_enet)); + page->Setting(&g_Config.bAdhocUseUdp).EnabledFunc([] { + return !PSP_IsInited(); + }); + page->Hint("Try switching between TCP/UDP if you suffer frequent adhoc server disconnects. Some network environments favor TCP and some UDP."); - CheckBox *relay_mode_enet_pdp_reliable = adhocSettings->Add(new CheckBox(&g_Config.bAdhocRelayModeEnetPdpReliable, n->T("Reliable datagram packet delivery on packet relay in UDP mode"))); - relay_mode_enet_pdp_reliable->SetEnabledFunc([] { + page->Setting(&g_Config.bAdhocRelayModeEnetPdpReliable).EnabledFunc([] { return !PSP_IsInited() && g_Config.bAdhocUseUdp; }); - adhocSettings->Add(new SettingHint(n->T("When disabled, packet loss on datagram adhoc sockets will be exposed to game's netcode. Keep this enabled if you don't know what that means."), relay_mode_enet_pdp_reliable)); + page->Hint("When disabled, packet loss on datagram adhoc sockets will be exposed to game's netcode. Keep this enabled if you don't know what that means."); - ChoiceGroup *infrastructureSettings = networkingSettings->Add(new ChoiceGroup(n->T("Infrastructure"))); + page->Header("Infrastructure"); if (g_Config.sInfrastructureUsername.empty()) { - infrastructureSettings->Add(new NoticeView(NoticeLevel::WARN, n->T("To play in Infrastructure Mode, you must enter a username"), ""))->SetWrapText(true); + page->Custom("To play in Infrastructure Mode, you must enter a username", [](UI::ViewGroup *parent) -> UI::View * { + NoticeView *notice = parent->Add(new NoticeView(NoticeLevel::WARN, T(I18NCat::NETWORKING, "To play in Infrastructure Mode, you must enter a username"), "")); + notice->SetWrapText(true); + return notice; + }); } - PopupTextInputChoice *usernameChoice = infrastructureSettings->Add(new PopupTextInputChoice(GetRequesterToken(), &g_Config.sInfrastructureUsername, di->T("Username"), "", 16, screenManager())); - usernameChoice->SetRestriction(StringRestriction::AlphaNumDashUnderscore, 3); - usernameChoice->OnChange.Add([this](UI::EventParams &e) { - RecreateViews(); + page->Custom({ I18NCat::DIALOG, "Username" }, [host](UI::ViewGroup *parent) -> UI::View * { + PopupTextInputChoice *usernameChoice = parent->Add(new PopupTextInputChoice(host->PageRequesterToken(), &g_Config.sInfrastructureUsername, T(I18NCat::DIALOG, "Username"), "", 16, host->PageScreenManager())); + usernameChoice->SetRestriction(StringRestriction::AlphaNumDashUnderscore, 3); + usernameChoice->OnChange.Add([host](UI::EventParams &e) { + host->RecreatePageViews(); + }); + return usernameChoice; }); - infrastructureSettings->Add(new CheckBox(&g_Config.bInfrastructureAutoDNS, n->T("Autoconfigure"))); - auto *dnsServer = infrastructureSettings->Add(new PopupTextInputChoice(GetRequesterToken(), &g_Config.sInfrastructureDNSServer, n->T("DNS server"), "", 32, screenManager())); - dnsServer->SetDisabledPtr(&g_Config.bInfrastructureAutoDNS); + page->Setting(&g_Config.bInfrastructureAutoDNS); + page->Custom("DNS server", [host](UI::ViewGroup *parent) -> UI::View * { + return parent->Add(new PopupTextInputChoice(host->PageRequesterToken(), &g_Config.sInfrastructureDNSServer, T(I18NCat::NETWORKING, "DNS server"), "", 32, host->PageScreenManager())); + }).Disabled(&g_Config.bInfrastructureAutoDNS); - ChoiceGroup *upnpSettings = networkingSettings->Add(new ChoiceGroup(n->T("UPnP (port-forwarding)"))); + page->Header("UPnP (port-forwarding)"); // Only togglable outside a game - sceNet latches settings like UPnPUseOriginalPort at boot, // and a game that's already mapped its ports wouldn't cope with them disappearing. - CheckBox *enableUPnP = upnpSettings->Add(new CheckBox(&g_Config.bEnableUPnP, n->T("Enable UPnP", "Enable UPnP (need a few seconds to detect)"))); - enableUPnP->SetEnabled(!PSP_IsInited()); - enableUPnP->OnClick.Add([requestLocalNetwork](UI::EventParams &e) { + page->Setting(&g_Config.bEnableUPnP).EnabledFunc([] { + return !PSP_IsInited(); + }).OnChange([requestLocalNetwork](UI::EventParams &e) { requestLocalNetwork(e); // Wake the UPnP service thread so it connects (or tears its mappings back down) right // away, instead of waiting for a port request that may never come. UPnP_Notify(); }); - auto *useOriPort = upnpSettings->Add(new CheckBox(&g_Config.bUPnPUseOriginalPort, n->T("UPnP use original port", "UPnP use original port (Enabled = PSP compatibility)"))); - upnpSettings->Add(new SettingHint(n->T("UseOriginalPort Tip", "May not work for all devices or games, see wiki."), useOriPort)); + page->Setting(&g_Config.bUPnPUseOriginalPort).Enabled(&g_Config.bEnableUPnP); + page->Hint({ I18NCat::NETWORKING, "UseOriginalPort Tip", "May not work for all devices or games, see wiki." }); - useOriPort->SetEnabledPtr(&g_Config.bEnableUPnP); + page->Header("Chat"); + page->Setting(&g_Config.bEnableNetworkChat); + page->Setting(&g_Config.iChatButtonPosition).Enabled(&g_Config.bEnableNetworkChat); + page->Setting(&g_Config.iChatScreenPosition).Enabled(&g_Config.bEnableNetworkChat); + page->Setting(&g_Config.bChatTimestamps).Enabled(&g_Config.bEnableNetworkChat); - ChoiceGroup *chatSettings = networkingSettings->Add(new ChoiceGroup(n->T("Chat"))); - chatSettings->Add(new CheckBox(&g_Config.bEnableNetworkChat, n->T("Enable network chat"))); - static const char *chatButtonPositions[] = { "Bottom Left", "Bottom Center", "Bottom Right", "Top Left", "Top Center", "Top Right", "Center Left", "Center Right", "None" }; - chatSettings->Add(new PopupMultiChoice(&g_Config.iChatButtonPosition, n->T("Chat Button Position"), chatButtonPositions, 0, ARRAY_SIZE(chatButtonPositions), I18NCat::DIALOG, screenManager()))->SetEnabledPtr(&g_Config.bEnableNetworkChat); - static const char *chatScreenPositions[] = { "Bottom Left", "Bottom Center", "Bottom Right", "Top Left", "Top Center", "Top Right" }; - chatSettings->Add(new PopupMultiChoice(&g_Config.iChatScreenPosition, n->T("Chat Screen Position"), chatScreenPositions, 0, ARRAY_SIZE(chatScreenPositions), I18NCat::DIALOG, screenManager()))->SetEnabledPtr(&g_Config.bEnableNetworkChat); - chatSettings->Add(new CheckBox(&g_Config.bChatTimestamps, n->T("Show timestamps in chat")))->SetEnabledPtr(&g_Config.bEnableNetworkChat); - - ChoiceGroup *quickChatSettings = networkingSettings->Add(new ChoiceGroup(n->T("Quick chat"))); - CheckBox *qc = quickChatSettings->Add(new CheckBox(&g_Config.bEnableQuickChat, n->T("Enable quick chat"))); - qc->SetEnabledPtr(&g_Config.bEnableNetworkChat); + page->Header("Quick chat"); + page->Setting(&g_Config.bEnableQuickChat).Enabled(&g_Config.bEnableNetworkChat); for (int i = 0; i < 5; i++) { - PopupTextInputChoice *qc1 = quickChatSettings->Add(new PopupTextInputChoice(GetRequesterToken(), &g_Config.sQuickChat[i], ApplySafeSubstitutions(n->T("Quick chat %1"), i + 1), "", 32, screenManager())); - qc1->SetEnabledFunc([] { return g_Config.bEnableQuickChat && g_Config.bEnableNetworkChat; }); + page->Custom("Quick chat %1", [host, i](UI::ViewGroup *parent) -> UI::View * { + return parent->Add(new PopupTextInputChoice(host->PageRequesterToken(), &g_Config.sQuickChat[i], ApplySafeSubstitutions(T(I18NCat::NETWORKING, "Quick chat %1"), i + 1), "", 32, host->PageScreenManager())); + }).EnabledFunc([] { + return g_Config.bEnableQuickChat && g_Config.bEnableNetworkChat; + }); } - ChoiceGroup *miscSettings = networkingSettings->Add(new ChoiceGroup(n->T("Misc", "Misc (default = compatibility)"))); - Choice *wiki = miscSettings->Add(new Choice(n->T("Open PPSSPP Multiplayer Wiki Page"), ImageID("I_LINK_OUT"))); - wiki->OnClick.Add([](EventParams &e) { + page->Header({ I18NCat::NETWORKING, "Misc", "Misc (default = compatibility)" }); + page->Choice("Open PPSSPP Multiplayer Wiki Page", ImageID("I_LINK_OUT")).OnClick([](EventParams &e) { auto n = GetI18NCategory(I18NCat::NETWORKING); std::string url(n->T("MultiplayerHowToURL", "https://github.com/hrydgard/ppsspp/wiki/How-to-play-multiplayer-games-with-PPSSPP")); System_LaunchUrl(LaunchUrlType::BROWSER_URL, url); }); static const char *wlanChannels[] = {"Auto", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"}; - auto wlanChannelChoice = miscSettings->Add(new PopupMultiChoice(&g_Config.iWlanAdhocChannel, n->T("WLAN Channel"), wlanChannels, 0, ARRAY_SIZE(wlanChannels), I18NCat::NETWORKING, screenManager())); - wlanChannelChoice->SetChoicesUntranslated(1, (int)ARRAY_SIZE(wlanChannels) - 1); - for (int i = 0; i < 4; i++) { - wlanChannelChoice->HideChoice(i + 2); - wlanChannelChoice->HideChoice(i + 7); - } - PopupSliderChoice *portOffset = miscSettings->Add(new PopupSliderChoice(&g_Config.iPortOffset, 0, 60000, 10000, n->T("Port offset"), 100, screenManager())); - miscSettings->Add(new SettingHint(n->T("PortOffsetHint"), portOffset)); - miscSettings->Add(new PopupSliderChoice(&g_Config.iMinTimeout, 0, 15000, 0, n->T("Minimum Timeout", "Minimum Timeout (override in ms, 0 = default)"), 50, screenManager()))->SetFormat(di->T("%d ms")); - miscSettings->Add(new CheckBox(&g_Config.bForcedFirstConnect, n->T("Forced First Connect", "Forced First Connect (faster Connect)"))); - miscSettings->Add(new CheckBox(&g_Config.bAllowSpeedControlWhileConnected, n->T("Allow speed control while connected (not recommended)"))); + page->Setting(&g_Config.iWlanAdhocChannel).Setup([](View *view) { + PopupMultiChoice *wlanChannelChoice = static_cast(view); + wlanChannelChoice->SetChoicesUntranslated(1, (int)ARRAY_SIZE(wlanChannels) - 1); + for (int i = 0; i < 4; i++) { + wlanChannelChoice->HideChoice(i + 2); + wlanChannelChoice->HideChoice(i + 7); + } + }); + page->Setting(&g_Config.iPortOffset); + page->Hint("PortOffsetHint"); + page->Setting(&g_Config.iMinTimeout); + page->Setting(&g_Config.bForcedFirstConnect); + page->Setting(&g_Config.bAllowSpeedControlWhileConnected); if (Discord::IsAvailable()) { - miscSettings->Add(new CheckBox(&g_Config.bDiscordRichPresence, n->T("Send Discord Presence information"))); + page->Setting(&g_Config.bDiscordRichPresence); } } -void GameSettingsScreen::CreateToolsSettings(UI::ViewGroup *tools) { +static void BuildToolsSettings(SettingsPage *page) { using namespace UI; + SettingsPageHost *host = page->Host(); - auto sa = GetI18NCategory(I18NCat::SAVEDATA); - auto sy = GetI18NCategory(I18NCat::SYSTEM); - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - auto dev = GetI18NCategory(I18NCat::DEVELOPER); - auto ri = GetI18NCategory(I18NCat::REMOTEISO); - - ChoiceGroup *toolsGroup = tools->Add(new ChoiceGroup(ms->T("Tools"))); + page->Header("Tools"); const bool showRetroAchievements = System_GetPropertyInt(SYSPROP_DEVICE_TYPE) != DEVICE_TYPE_VR; if (showRetroAchievements) { - auto retro = toolsGroup->Add(new Choice(sy->T("RetroAchievements"))); - retro->OnClick.Add([=](UI::EventParams &) -> void { - screenManager()->push(new RetroAchievementsSettingsScreen(gamePath_)); + page->Choice({ I18NCat::SYSTEM, "RetroAchievements" }).OnClick([host](UI::EventParams &) { + host->PageScreenManager()->push(new RetroAchievementsSettingsScreen(host->PageGamePath())); + }).Setup([](View *view) { + static_cast(view)->SetIconRight(ImageID("I_RETROACHIEVEMENTS_LOGO")); }); - retro->SetIconRight(ImageID("I_RETROACHIEVEMENTS_LOGO")); } // These were moved here so use the wrong translation objects, to avoid having to change all inis... This isn't a sustainable situation :P - toolsGroup->Add(new Choice(sa->T("Savedata Manager")))->OnClick.Add([=](UI::EventParams &) { - screenManager()->push(new SavedataScreen(gamePath_)); + page->Choice({ I18NCat::SAVEDATA, "Savedata Manager" }).OnClick([host](UI::EventParams &) { + host->PageScreenManager()->push(new SavedataScreen(host->PageGamePath())); }); - toolsGroup->Add(new Choice(dev->T("System Information")))->OnClick.Add([=](UI::EventParams &) { - screenManager()->push(new SystemInfoScreen(gamePath_)); + page->Choice({ I18NCat::DEVELOPER, "System Information" }).OnClick([host](UI::EventParams &) { + host->PageScreenManager()->push(new SystemInfoScreen(host->PageGamePath())); }); - toolsGroup->Add(new Choice(sy->T("Developer Tools")))->OnClick.Add([=](UI::EventParams &) { - screenManager()->push(new DeveloperToolsScreen(gamePath_)); + page->Link({ I18NCat::SYSTEM, "Developer Tools" }, I18NCat::DEVELOPER, &BuildDeveloperToolsPage); + page->Choice({ I18NCat::REMOTEISO, "Remote disc streaming" }).OnClick([host](UI::EventParams &) { + host->PageScreenManager()->push(new RemoteISOScreen(host->PageGamePath())); }); - toolsGroup->Add(new Choice(ri->T("Remote disc streaming")))->OnClick.Add([=](UI::EventParams &) { - screenManager()->push(new RemoteISOScreen(gamePath_)); - }); - toolsGroup->Add(new Choice(sy->T("PSP Firmware")))->OnClick.Add([=](UI::EventParams &) { - screenManager()->push(new FirmwareScreen(gamePath_)); + page->Choice({ I18NCat::SYSTEM, "PSP Firmware" }).OnClick([host](UI::EventParams &) { + host->PageScreenManager()->push(new FirmwareScreen(host->PageGamePath())); }); } -void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) { +void GameSettingsScreen::BuildSystemSettings(SettingsPage *page) { using namespace UI; - auto sy = GetI18NCategory(I18NCat::SYSTEM); - auto di = GetI18NCategory(I18NCat::DIALOG); - auto vr = GetI18NCategory(I18NCat::VR); - auto th = GetI18NCategory(I18NCat::THEMES); - auto psps = GetI18NCategory(I18NCat::PSPSETTINGS); // TODO: Should move more into this section. + page->Header("UI"); - ChoiceGroup *uiSettings = systemSettings->Add(new ChoiceGroup(sy->T("UI"))); + page->Custom("Language", [this](UI::ViewGroup *parent) -> UI::View * { + auto langCodeToName = [](std::string_view value) -> std::string { + auto &mapping = GetLangValuesMapping(); + auto iter = mapping.find(value); + if (iter != mapping.end()) { + return iter->second.first; + } + return std::string(value); + }; - auto langCodeToName = [](std::string_view value) -> std::string { - auto &mapping = GetLangValuesMapping(); - auto iter = mapping.find(value); - if (iter != mapping.end()) { - return iter->second.first; - } - return std::string(value); - }; - - uiSettings->Add(new ChoiceWithValueDisplay(&g_Config.sLanguageIni, sy->T("Language"), langCodeToName))->OnClick.Add([&](UI::EventParams &e) { - auto sy = GetI18NCategory(I18NCat::SYSTEM); - auto langScreen = new NewLanguageScreen(sy->T("Language")); - // The actual switching is handled in OnCompleted in NewLanguageScreen. - if (e.v) - langScreen->SetPopupOrigin(e.v); - screenManager()->push(langScreen); + Choice *language = parent->Add(new ChoiceWithValueDisplay(&g_Config.sLanguageIni, T(I18NCat::SYSTEM, "Language"), langCodeToName)); + language->OnClick.Add([this](UI::EventParams &e) { + auto sy = GetI18NCategory(I18NCat::SYSTEM); + auto langScreen = new NewLanguageScreen(sy->T("Language")); + // The actual switching is handled in OnCompleted in NewLanguageScreen. + if (e.v) + langScreen->SetPopupOrigin(e.v); + screenManager()->push(langScreen); + }); + return language; }); #if PPSSPP_PLATFORM(IOS) @@ -1264,15 +1162,12 @@ void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) { "Swipe once to switch app (indicator auto-hides)", "Swipe twice to switch app (indicator stays visible)" }; - PopupMultiChoice *switchMode = uiSettings->Add(new PopupMultiChoice(&g_Config.iAppSwitchMode, sy->T("App switching mode"), indicator, 0, ARRAY_SIZE(indicator), I18NCat::SYSTEM, screenManager())); - switchMode->OnChoice.Add([](EventParams &e) { - System_Notify(SystemNotification::APP_SWITCH_MODE_CHANGED); - }); + page->Setting(&g_Config.iAppSwitchMode); { // Note: On iPhone, iOS hides the status bar in landscape no matter what this is set to. DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation()); - uiSettings->Add(new CheckBox(&config.bImmersiveMode, sy->T("Hide status bar")))->OnClick.Add([](EventParams &e) { + page->Setting(&config.bImmersiveMode).OnChange([](EventParams &e) { System_Notify(SystemNotification::IMMERSIVE_MODE_CHANGE); }); } @@ -1282,14 +1177,14 @@ void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) { // Hide Immersive Mode on pre-kitkat Android if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 19) { DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation()); - uiSettings->Add(new CheckBox(&config.bImmersiveMode, sy->T("Hide navigation bar")))->OnClick.Handle(this, &GameSettingsScreen::OnImmersiveModeChange); + page->Setting(&config.bImmersiveMode).OnChange([this](EventParams &e) { + OnImmersiveModeChange(e); + }); } #endif - PopupSliderChoice *uiScale = uiSettings->Add(new PopupSliderChoice(&g_Config.iUIScaleFactor, -8, 8, 0, sy->T("UI size adjustment (DPI)"), screenManager())); - uiScale->SetZeroLabel(sy->T("Off")); UIContext *ctx = screenManager()->getUIContext(); - uiScale->OnChange.Add([ctx](UI::EventParams &e) { + page->Setting(&g_Config.iUIScaleFactor).OnChange([ctx](UI::EventParams &e) { const float dpiMul = UIScaleFactorToMultiplier(g_Config.iUIScaleFactor); g_display.Recalculate(-1, -1, -1, -1, dpiMul); ctx->InvalidateAtlas(); @@ -1298,54 +1193,52 @@ void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) { const Path bgPng = GetSysDirectory(DIRECTORY_SYSTEM) / "background.png"; const Path bgJpg = GetSysDirectory(DIRECTORY_SYSTEM) / "background.jpg"; - Choice *backgroundChoice = nullptr; if (File::Exists(bgPng) || File::Exists(bgJpg)) { - backgroundChoice = uiSettings->Add(new Choice(sy->T("Clear UI background"))); + page->Choice("Clear UI background").OnClick([this](UI::EventParams &e) { + OnChangeBackground(e); + }); } else if (System_GetPropertyBool(SYSPROP_HAS_IMAGE_BROWSER) || System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) { - backgroundChoice = uiSettings->Add(new Choice(sy->T("Set UI background..."))); - } - if (backgroundChoice) { - backgroundChoice->OnClick.Handle(this, &GameSettingsScreen::OnChangeBackground); + page->Choice("Set UI background...").OnClick([this](UI::EventParams &e) { + OnChangeBackground(e); + }); } - uiSettings->Add(new CheckBox(&g_Config.bTransparentBackground, sy->T("Transparent UI background"))); + page->Setting(&g_Config.bTransparentBackground); // Shared with achievements. - static const char *positions[] = { "None", "Bottom Left", "Bottom Center", "Bottom Right", "Top Left", "Top Center", "Top Right", "Center Left", "Center Right" }; - uiSettings->Add(new PopupMultiChoice(&g_Config.iNotificationPos, sy->T("Notification screen position"), positions, -1, ARRAY_SIZE(positions), I18NCat::DIALOG, screenManager())); + page->Setting(&g_Config.iNotificationPos); - static const char *backgroundAnimations[] = { "No animation", "Floating symbols", "Recent games", "Waves", "Moving background", "Bouncing icon", "Colored floating symbols" }; - uiSettings->Add(new PopupMultiChoice(&g_Config.iBackgroundAnimation, sy->T("UI background animation"), backgroundAnimations, 0, ARRAY_SIZE(backgroundAnimations), I18NCat::SYSTEM, screenManager())); + page->Setting(&g_Config.iBackgroundAnimation); - PopupMultiChoiceDynamic *theme = uiSettings->Add(new PopupMultiChoiceDynamic(&g_Config.sThemeName, sy->T("Theme"), GetThemeInfoNames(), I18NCat::THEMES, screenManager())); - theme->OnChoice.Add([](EventParams &e) { - UpdateTheme(); - // Reset the tint/saturation if the theme changed. - if (e.b) { - g_Config.fUITint = 0.0f; - g_Config.fUISaturation = 1.0f; - } + page->Custom("Theme", [this](UI::ViewGroup *parent) -> UI::View * { + PopupMultiChoiceDynamic *theme = parent->Add(new PopupMultiChoiceDynamic(&g_Config.sThemeName, T(I18NCat::SYSTEM, "Theme"), GetThemeInfoNames(), I18NCat::THEMES, screenManager())); + theme->OnChoice.Add([](EventParams &e) { + UpdateTheme(); + // Reset the tint/saturation if the theme changed. + if (e.b) { + g_Config.fUITint = 0.0f; + g_Config.fUISaturation = 1.0f; + } + }); + return theme; }); Draw::DrawContext *draw = screenManager()->getDrawContext(); if (!draw->GetBugs().Has(Draw::Bugs::RASPBERRY_SHADER_COMP_HANG)) { // We use shaders without tint capability on hardware with this driver bug. - PopupSliderChoiceFloat *tint = new PopupSliderChoiceFloat(&g_Config.fUITint, 0.0f, 1.0f, 0.0f, sy->T("Color tint"), 0.01f, screenManager()); - tint->SetHasDropShadow(false); - tint->SetLiveUpdate(true); - uiSettings->Add(tint); - PopupSliderChoiceFloat *saturation = new PopupSliderChoiceFloat(&g_Config.fUISaturation, 0.0f, 2.0f, 1.0f, sy->T("Color saturation"), 0.01f, screenManager()); - saturation->SetHasDropShadow(false); - saturation->SetLiveUpdate(true); - uiSettings->Add(saturation); + auto noDropShadow = [](View *view) { + static_cast(view)->SetHasDropShadow(false); + }; + page->Setting(&g_Config.fUITint).LiveUpdate().Setup(noDropShadow); + page->Setting(&g_Config.fUISaturation).LiveUpdate().Setup(noDropShadow); } - ChoiceGroup *memstickSettings = systemSettings->Add(new ChoiceGroup(sy->T("PSP Memory Stick"))); + page->Header("PSP Memory Stick"); if (System_GetPropertyBool(SYSPROP_HAS_OPEN_DIRECTORY)) { - memstickSettings->Add(new Choice(sy->T("Show Memory Stick folder")))->OnClick.Add([](UI::EventParams &p) { + page->Choice("Show Memory Stick folder").OnClick([](UI::EventParams &p) { System_LaunchUrl(LaunchUrlType::LOCAL_FOLDER, g_Config.memStickDirectory.ToString()); }); } @@ -1359,202 +1252,184 @@ void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) { } #endif if (showItHere) { - memstickSettings->Add(new Choice(sy->T("Set Memory Stick folder")))->OnClick.Add( - [=](UI::EventParams &) { - SetMemStickDirDarwin(GetRequesterToken()); - }); + page->Choice("Set Memory Stick folder").OnClick([this](UI::EventParams &) { + SetMemStickDirDarwin(GetRequesterToken()); + }); } #endif + auto addMemstickPath = [this, &page]() { + page->Custom("Memory Stick folder", [this](UI::ViewGroup *parent) -> UI::View * { + Choice *memstickPath = parent->Add(new ChoiceWithCallbackValueDisplay(T(I18NCat::SYSTEM, "Memory Stick folder"), []() { + return g_Config.memStickDirectory.ToVisualString(); + })); + memstickPath->OnClick.Handle(this, &GameSettingsScreen::OnShowMemstickScreen); + return memstickPath; + }).EnabledFunc([] { + return !PSP_IsInited(); + }); + }; + #if PPSSPP_PLATFORM(ANDROID) if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) != DEVICE_TYPE_VR) { - auto memstickPath = memstickSettings->Add(new ChoiceWithCallbackValueDisplay(sy->T("Memory Stick folder"), []() { - return g_Config.memStickDirectory.ToVisualString(); - })); - memstickPath->SetEnabled(!PSP_IsInited()); - memstickPath->OnClick.Handle(this, &GameSettingsScreen::OnShowMemstickScreen); + addMemstickPath(); } #elif defined(_WIN32) #if PPSSPP_PLATFORM(UWP) - auto memstickPath = memstickSettings->Add(new ChoiceWithCallbackValueDisplay(sy->T("Memory Stick folder"), []() { - return g_Config.memStickDirectory.ToVisualString(); - })); - memstickPath->SetEnabled(!PSP_IsInited()); - memstickPath->OnClick.Handle(this, &GameSettingsScreen::OnShowMemstickScreen); + addMemstickPath(); #else - CheckBox *myDocsChoice = memstickSettings->Add(new CheckBox(&installed_, sy->T("Memory Stick in My Documents"))); - myDocsChoice->SetEnabledPtr(&memstickMyDocEnabled_); - myDocsChoice->OnClick.Handle(this, &GameSettingsScreen::OnMemoryStickMyDoc); - CheckBox *otherChoice = memstickSettings->Add(new CheckBox(&otherinstalled_, sy->T("Memory Stick in installed.txt"))); - otherChoice->SetEnabledPtr(&memstickOtherEnabled_); - otherChoice->OnClick.Handle(this, &GameSettingsScreen::OnMemoryStickOther); + page->Bool(&installed_, "Memory Stick in My Documents").Enabled(&memstickMyDocEnabled_).OnChange([this](UI::EventParams &e) { + OnMemoryStickMyDoc(e); + }); + page->Bool(&otherinstalled_, "Memory Stick in installed.txt").Enabled(&memstickOtherEnabled_).OnChange([this](UI::EventParams &e) { + OnMemoryStickOther(e); + }); if (!System_GetPropertyBool(SYSPROP_CAN_GET_FREE_SPACE_FAST)) { // It might be slow, so use an option. - memstickSettings->Add(new CheckBox(&g_Config.bReportAccurateFreeStorageSpace, sy->T("Report free storage space accurately"))); + page->Setting(&g_Config.bReportAccurateFreeStorageSpace); } #endif #endif - memstickSettings->Add(new CheckBox(&g_Config.bMemStickInserted, sy->T("Memory Stick inserted"))); - UI::PopupSliderChoice *sizeChoice = memstickSettings->Add(new PopupSliderChoice(&g_Config.iMemStickSizeGB, 1, 32, 16, sy->T("Memory Stick size", "Memory Stick size"), screenManager(), "GB")); - sizeChoice->SetFormat("%d GB"); + page->Setting(&g_Config.bMemStickInserted); + page->Setting(&g_Config.iMemStickSizeGB); - ChoiceGroup *helpSettings = systemSettings->Add(new ChoiceGroup(sy->T("Help the PPSSPP team"))); - CheckBox *enableReportsCheckbox; - enableReportsCheckbox = new CheckBox(&enableReports_, sy->T("Enable Compatibility Server Reports")); - enableReportsCheckbox->SetEnabledFunc([]() { return Reporting::IsSupported(); }); - helpSettings->Add(enableReportsCheckbox); - - ChoiceGroup *emulationSettings = systemSettings->Add(new ChoiceGroup(sy->T("Emulation"))); - - emulationSettings->Add(new CheckBox(&g_Config.bFastMemory, sy->T("Fast Memory", "Fast Memory")))->OnClick.Handle(this, &GameSettingsScreen::OnJitAffectingSetting); - emulationSettings->Add(new CheckBox(&g_Config.bIgnoreBadMemAccess, sy->T("Ignore bad memory accesses"))); - - static const char *ioTimingMethods[] = { "Fast (lag on slow storage)", "Host (bugs, less lag)", "Simulate UMD delays", "Simulate UMD slow reading speed"}; - View *ioTimingMethod = emulationSettings->Add(new PopupMultiChoice(&g_Config.iIOTimingMethod, sy->T("I/O timing method"), ioTimingMethods, 0, ARRAY_SIZE(ioTimingMethods), I18NCat::SYSTEM, screenManager())); - emulationSettings->Add(new CheckBox(&g_Config.bForceLagSync, sy->T("Force real clock sync (slower, less lag)")))->SetDisabledPtr(&g_Config.bAutoFrameSkip); - PopupSliderChoice *lockedMhz = emulationSettings->Add(new PopupSliderChoice(&g_Config.iLockedCPUSpeed, 0, 1000, 0, sy->T("Change CPU Clock", "Change CPU Clock (unstable)"), screenManager(), sy->T("MHz, 0:default"))); - lockedMhz->SetZeroLabel(sy->T("Auto")); - - auto sa = GetI18NCategory(I18NCat::SAVEDATA); - - ChoiceGroup *saveStateSettings = systemSettings->Add(new ChoiceGroup(sa->T("Save states"))); // Borrow this string from the savedata manager - - saveStateSettings->Add(new CheckBox(&g_Config.bEnableStateUndo, sy->T("Savestate slot backups"))); - saveStateSettings->Add(new CheckBox(&g_Config.bConfirmLoadState, sy->T("Ask to confirm on load"))); - - PopupSliderChoice* savestateSlotCount = saveStateSettings->Add(new PopupSliderChoice(&g_Config.iSaveStateSlotCount, 1, 30, 5, sy->T("Savestate slot count"), screenManager())); - savestateSlotCount->OnChange.Add([](UI::EventParams &e) { - System_Notify(SystemNotification::UI); + page->Header("Help the PPSSPP team"); + page->Bool(&enableReports_, "Enable Compatibility Server Reports").EnabledFunc([]() { + return Reporting::IsSupported(); }); + page->Header("Emulation"); + + page->Setting(&g_Config.bFastMemory).OnChange([this](UI::EventParams &e) { + OnJitAffectingSetting(e); + }); + page->Setting(&g_Config.bIgnoreBadMemAccess); + + page->Setting(&g_Config.iIOTimingMethod); + page->Setting(&g_Config.bForceLagSync).Disabled(&g_Config.bAutoFrameSkip); + page->Setting(&g_Config.iLockedCPUSpeed); + + page->Header({ I18NCat::SAVEDATA, "Save states" }); // Borrow this string from the savedata manager + + page->Setting(&g_Config.bEnableStateUndo); + page->Setting(&g_Config.bConfirmLoadState); + + page->Setting(&g_Config.iSaveStateSlotCount); + // NOTE: We will soon support more states, but we'll keep this niche feature limited to the first five. - static const char *autoLoadSaveStateChoices[] = {"Off", "Oldest Save", "Newest Save", "Slot 1", "Slot 2", "Slot 3", "Slot 4", "Slot 5"}; - PopupMultiChoice *autoloadSaveState = saveStateSettings->Add(new PopupMultiChoice(&g_Config.iAutoLoadSaveState, sy->T("Auto load savestate"), autoLoadSaveStateChoices, 0, ARRAY_SIZE(autoLoadSaveStateChoices), I18NCat::SYSTEM, screenManager())); - if (g_Config.iAutoLoadSaveState != 1) { - autoloadSaveState->HideChoice(1); // Hide "Oldest Save" if not using that mode. It doesn't make sense. - } + page->Setting(&g_Config.iAutoLoadSaveState).Setup([](View *view) { + if (g_Config.iAutoLoadSaveState != 1) { + static_cast(view)->HideChoice(1); // Hide "Oldest Save" if not using that mode. It doesn't make sense. + } + }); - PopupSliderChoice *rewindInterval = saveStateSettings->Add(new PopupSliderChoice(&g_Config.iRewindSnapshotInterval, 0, 60, 0, sy->T("Rewind Snapshot Interval"), screenManager(), di->T("seconds, 0:off"))); - rewindInterval->SetFormat(di->T("%d seconds")); - rewindInterval->SetZeroLabel(sy->T("Off")); + page->Setting(&g_Config.iRewindSnapshotInterval); - ChoiceGroup *generalSettings = systemSettings->Add(new ChoiceGroup(sy->T("General"))); + page->Header("General"); - PopupSliderChoice *exitConfirmation = generalSettings->Add(new PopupSliderChoice(&g_Config.iAskForExitConfirmationAfterSeconds, 0, 1200, 300, sy->T("Ask for exit confirmation after seconds"), screenManager(), "s")); - exitConfirmation->SetZeroLabel(sy->T("Off")); + page->Setting(&g_Config.iAskForExitConfirmationAfterSeconds); if (System_GetPropertyBool(SYSPROP_CAN_RESTRICT_ORIENTATION)) { - auto co = GetI18NCategory(I18NCat::CONTROLS); - - // Display rotation - AddRotationPicker(screenManager(), generalSettings, true); + // Display rotation + page->Custom({ I18NCat::CONTROLS, "Screen Rotation" }, [this](UI::ViewGroup *parent) -> UI::View * { + AddRotationPicker(screenManager(), parent, true); + return parent->GetViewByIndex(parent->GetNumSubviews() - 1); + }); } if (System_GetPropertyBool(SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE)) { - generalSettings->Add(new CheckBox(&g_Config.bSustainedPerformanceMode, sy->T("Sustained performance mode")))->OnClick.Handle(this, &GameSettingsScreen::OnSustainedPerformanceModeChange); + page->Setting(&g_Config.bSustainedPerformanceMode).OnChange([this](UI::EventParams &e) { + OnSustainedPerformanceModeChange(e); + }); } - generalSettings->Add(new Choice(sy->T("Restore Default Settings")))->OnClick.Handle(this, &GameSettingsScreen::OnRestoreDefaultSettings); + page->Choice("Restore Default Settings").OnClick([this](UI::EventParams &e) { + OnRestoreDefaultSettings(e); + }); if (System_GetPropertyBool(SYSPROP_HAS_KEYBOARD)) - generalSettings->Add(new CheckBox(&g_Config.bBypassOSKWithKeyboard, sy->T("Use system native keyboard"))); + page->Setting(&g_Config.bBypassOSKWithKeyboard); if (System_GetPropertyBool(SYSPROP_ENOUGH_RAM_FOR_FULL_ISO)) { - generalSettings->Add(new CheckBox(&g_Config.bCacheFullIsoInRam, sy->T("Cache full ISO in RAM")))->SetEnabled(!PSP_IsInited()); + page->Setting(&g_Config.bCacheFullIsoInRam).EnabledFunc([] { + return !PSP_IsInited(); + }); } - CheckBox *checkForUpdate = generalSettings->Add(new CheckBox(&g_Config.bCheckForNewVersion, sy->T("VersionCheck", "Check for new versions of PPSSPP"))); - checkForUpdate->OnClick.Add([](UI::EventParams &e) { - // Reset the dismissed version so it will check again. - if (g_Config.bCheckForNewVersion) { - g_Config.sDismissedVersion.clear(); - } - }); - generalSettings->Add(new CheckBox(&g_Config.bScreenshotsAsPNG, sy->T("Screenshots as PNG"))); - static const char *screenshotModeChoices[] = { "Final processed image", "Raw game image" }; - generalSettings->Add(new PopupMultiChoice(&g_Config.iScreenshotMode, sy->T("Screenshot mode"), screenshotModeChoices, 0, ARRAY_SIZE(screenshotModeChoices), I18NCat::SYSTEM, screenManager())); + page->Setting(&g_Config.bCheckForNewVersion); + page->Setting(&g_Config.bScreenshotsAsPNG); + page->Setting(&g_Config.iScreenshotMode); // TODO: Make this setting available on Mac too. #if PPSSPP_PLATFORM(WINDOWS) || (defined (SDL) && !defined(MOBILE_DEVICE)) - generalSettings->Add(new CheckBox(&g_Config.bPauseOnLostFocus, sy->T("Pause when not focused"))); + page->Setting(&g_Config.bPauseOnLostFocus); #endif - auto gr = GetI18NCategory(I18NCat::GRAPHICS); std::vector cameraList = Camera::getDeviceList(); if (cameraList.size() >= 1) { - ChoiceGroup *cameraSettings = systemSettings->Add(new ChoiceGroup(gr->T("Camera"))); - PopupMultiChoiceDynamic *cameraChoice = cameraSettings->Add(new PopupMultiChoiceDynamic(&g_Config.sCameraDevice, gr->T("Camera Device"), cameraList, I18NCat::NONE, screenManager())); - cameraChoice->OnChoice.Add([](UI::EventParams &e) { - Camera::onCameraDeviceChange(); - }); + page->Header({ I18NCat::GRAPHICS, "Camera" }); + page->Setting(&g_Config.sCameraDevice, std::move(cameraList)); #if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) - cameraSettings->Add(new CheckBox(&g_Config.bCameraMirrorHorizontal, gr->T("Mirror camera image"))); + page->Setting(&g_Config.bCameraMirrorHorizontal); #endif } - ChoiceGroup *cheatSettings = systemSettings->Add(new ChoiceGroup(sy->T("Cheats", "Cheats"))); - cheatSettings->Add(new CheckBox(&g_Config.bEnableCheats, sy->T("Enable Cheats"))); - cheatSettings->Add(new CheckBox(&g_Config.bEnablePlugins, sy->T("Enable plugins"))); + page->Header("Cheats"); + page->Setting(&g_Config.bEnableCheats); + page->Setting(&g_Config.bEnablePlugins); - ChoiceGroup *pspSettings = systemSettings->Add(new ChoiceGroup(sy->T("PSP Settings"))); + page->Header("PSP Settings"); // The ordering here is simply mapping directly to PSP_SYSTEMPARAM_LANGUAGE_*. - static const char *defaultLanguages[] = { "Auto", "Japanese", "English", "French", "Spanish", "German", "Italian", "Dutch", "Portuguese", "Russian", "Korean", "Chinese (traditional)", "Chinese (simplified)" }; - pspSettings->Add(new PopupMultiChoice(&g_Config.iLanguage, psps->T("Game language"), defaultLanguages, -1, ARRAY_SIZE(defaultLanguages), I18NCat::PSPSETTINGS, screenManager())); - static const char *models[] = { "PSP-1000", "PSP-2000/3000" }; - pspSettings->Add(new PopupMultiChoice(&g_Config.iPSPModel, sy->T("PSP Model"), models, 0, ARRAY_SIZE(models), I18NCat::SYSTEM, screenManager()))->SetEnabled(!PSP_IsInited()); - UI::View *nicknameChoice = AddNicknameChoice(pspSettings); - pspSettings->Add(new CheckBox(&g_Config.bDayLightSavings, sy->T("Daylight savings"))); - static const char *dateFormat[] = { "YYYYMMDD", "MMDDYYYY", "DDMMYYYY" }; - pspSettings->Add(new PopupMultiChoice(&g_Config.iDateFormat, sy->T("Date Format"), dateFormat, 0, ARRAY_SIZE(dateFormat), I18NCat::SYSTEM, screenManager())); - static const char *timeFormat[] = { "24HR", "12HR" }; - pspSettings->Add(new PopupMultiChoice(&g_Config.iTimeFormat, sy->T("Time Format"), timeFormat, 0, ARRAY_SIZE(timeFormat), I18NCat::SYSTEM, screenManager())); - static const char *buttonPref[] = { "Use O to confirm", "Use X to confirm" }; - PopupMultiChoice *buttonPrefChoice = pspSettings->Add(new PopupMultiChoice(&g_Config.iButtonPreference, sy->T("Confirmation Button"), buttonPref, 0, ARRAY_SIZE(buttonPref), I18NCat::SYSTEM, screenManager())); - buttonPrefChoice->OnChoice.Add([](UI::EventParams &e) { - KeyMap::UpdateNativeMenuKeys(); + page->Setting(&g_Config.iLanguage); + page->Setting(&g_Config.iPSPModel).EnabledFunc([] { + return !PSP_IsInited(); }); + page->Custom("Nickname", [this](UI::ViewGroup *parent) { + return AddNicknameChoice(this, parent); + }); + page->Setting(&g_Config.bDayLightSavings); + page->Setting(&g_Config.iDateFormat); + page->Setting(&g_Config.iTimeFormat); + page->Setting(&g_Config.iButtonPreference); #if defined(_WIN32) - ChoiceGroup *recordingSettings = systemSettings->Add(new ChoiceGroup(sy->T("Recording"))); - recordingSettings->Add(new CheckBox(&g_Config.bDumpFrames, sy->T("Record Display"))); - recordingSettings->Add(new CheckBox(&g_Config.bUseFFV1, sy->T("Use Lossless Video Codec (FFV1)"))); - recordingSettings->Add(new CheckBox(&g_Config.bDumpVideoOutput, sy->T("Use output buffer (with overlay) for recording"))); - recordingSettings->Add(new CheckBox(&g_Config.bDumpAudio, sy->T("Record Audio"))); - recordingSettings->Add(new CheckBox(&g_Config.bSaveLoadResetsAVdumping, sy->T("Reset Recording on Save/Load State"))); + page->Header("Recording"); + page->Setting(&g_Config.bDumpFrames); + page->Setting(&g_Config.bUseFFV1); + page->Setting(&g_Config.bDumpVideoOutput); + page->Setting(&g_Config.bDumpAudio); + page->Setting(&g_Config.bSaveLoadResetsAVdumping); #endif } -void GameSettingsScreen::CreateVRSettings(UI::ViewGroup *vrSettings) { +static void BuildVRSettings(SettingsPage *page) { using namespace UI; - auto vr = GetI18NCategory(I18NCat::VR); int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE); if (deviceType == DEVICE_TYPE_VR) { - ChoiceGroup *vrGeneralSettings = vrSettings->Add(new ChoiceGroup(vr->T("Virtual reality"))); - vrGeneralSettings->Add(new CheckBox(&g_Config.bEnableVR, vr->T("Virtual reality"))); - vrGeneralSettings->Add(new CheckBox(&g_Config.bEnable6DoF, vr->T("6DoF movement"))); - vrGeneralSettings->Add(new CheckBox(&g_Config.bEnableStereo, vr->T("Stereoscopic vision (Experimental)"))); - vrGeneralSettings->Add(new CheckBox(&g_Config.bEnableImmersiveVR, vr->T("Enable immersive mode"))); + page->Header("Virtual reality"); + page->Setting(&g_Config.bEnableVR); + page->Setting(&g_Config.bEnable6DoF); + page->Setting(&g_Config.bEnableStereo); + page->Setting(&g_Config.bEnableImmersiveVR); if (IsPassthroughSupported()) { - vrGeneralSettings->Add(new CheckBox(&g_Config.bPassthrough, vr->T("Enable passthrough"))); + page->Setting(&g_Config.bPassthrough); } - vrGeneralSettings->Add(new CheckBox(&g_Config.bForce72Hz, vr->T("Force 72Hz update"))); + page->Setting(&g_Config.bForce72Hz); } - ChoiceGroup *vrCameraSettings = vrSettings->Add(new ChoiceGroup(vr->T("VR camera"))); + page->Header("VR camera"); if (deviceType == DEVICE_TYPE_VR) { - vrCameraSettings->Add(new PopupSliderChoiceFloat(&g_Config.fCanvasDistance, 1.0f, 15.0f, 12.0f, vr->T("Distance to 2D menus and scenes"), 1.0f, screenManager(), "")); - vrCameraSettings->Add(new PopupSliderChoiceFloat(&g_Config.fCanvas3DDistance, 1.0f, 15.0f, 3.0f, vr->T("Distance to 3D scenes when VR disabled"), 1.0f, screenManager(), "")); + page->Setting(&g_Config.fCanvasDistance); + page->Setting(&g_Config.fCanvas3DDistance); } - vrCameraSettings->Add(new PopupSliderChoiceFloat(&g_Config.fFieldOfViewPercentage, 100.0f, 200.0f, 100.0f, vr->T("Field of view scale"), 10.0f, screenManager(), vr->T("% of native FoV"))); - vrCameraSettings->Add(new CheckBox(&g_Config.bRescaleHUD, vr->T("Heads-up display detection"))); - PopupSliderChoiceFloat* vrHudScale = vrCameraSettings->Add(new PopupSliderChoiceFloat(&g_Config.fHeadUpDisplayScale, 0.0f, 1.5f, 0.3f, vr->T("Heads-up display scale"), 0.1f, screenManager(), "")); - vrHudScale->SetEnabledPtr(&g_Config.bRescaleHUD); - vrCameraSettings->Add(new CheckBox(&g_Config.bManualForceVR, vr->T("Manual switching between flat screen and VR using SCREEN key"))); + page->Setting(&g_Config.fFieldOfViewPercentage); + page->Setting(&g_Config.bRescaleHUD); + page->Setting(&g_Config.fHeadUpDisplayScale).Enabled(&g_Config.bRescaleHUD); + page->Setting(&g_Config.bManualForceVR); } - void GameSettingsScreen::OnImmersiveModeChange(UI::EventParams &e) { System_Notify(SystemNotification::IMMERSIVE_MODE_CHANGE); if (g_Config.iAndroidHwScale != 0) { @@ -1720,10 +1595,8 @@ void GameSettingsScreen::dialogFinished(const Screen *dialog, DialogResult resul recreate = true; } - // Show/hide the Analog Alternative Speed as appropriate - need to recreate views if this changed. - bool mapped = KeyMap::InputMappingsFromPspButton(VIRTKEY_SPEED_ANALOG, nullptr, true); - if (mapped != analogSpeedMapped_) { - analogSpeedMapped_ = mapped; + // The analog alternative speed is only shown when it's mapped. + if (equals(dialog->tag(), "ControlMapping")) { recreate = true; } @@ -1778,7 +1651,7 @@ void TriggerRestart(const char *why, bool editThenRestore, const Path &gamePath) // From PauseScreen std::string GetConfirmExitMessage(); -void GameSettingsScreen::TriggerRestartOrDo(std::function callback) { +void GameSettingsScreen::RestartOrDo(std::function callback) { auto di = GetI18NCategory(I18NCat::DIALOG); std::string confirmExitMessage = GetConfirmExitMessage(); @@ -1796,47 +1669,6 @@ void GameSettingsScreen::TriggerRestartOrDo(std::function callback) { })); } -void GameSettingsScreen::OnRenderingBackend(UI::EventParams &e) { - // It only makes sense to show the restart prompt if the backend was actually changed. - if (g_Config.iGPUBackend != (int)GetGPUBackend()) { - TriggerRestartOrDo([]() { - g_Config.iGPUBackend = (int)GetGPUBackend(); - }); - } -} - -void GameSettingsScreen::OnRenderingDevice(UI::EventParams &e) { - // It only makes sense to show the restart prompt if the device was actually changed. - std::string *deviceNameSetting = GPUDeviceNameSetting(); - if (deviceNameSetting && *deviceNameSetting != GetGPUBackendDevice()) { - auto di = GetI18NCategory(I18NCat::DIALOG); - TriggerRestartOrDo([this]() { - std::string *deviceNameSetting = GPUDeviceNameSetting(); - if (deviceNameSetting) - *deviceNameSetting = GetGPUBackendDevice(); - // Needed to redraw the setting. - RecreateViews(); - }); - } -} - -void GameSettingsScreen::OnInflightFramesChoice(UI::EventParams &e) { - if (g_Config.iInflightFrames != prevInflightFrames_) { - auto di = GetI18NCategory(I18NCat::DIALOG); - TriggerRestartOrDo([this]() { - g_Config.iInflightFrames = prevInflightFrames_; - }); - } -} - -void GameSettingsScreen::OnAudioDevice(UI::EventParams &e) { - auto a = GetI18NCategory(I18NCat::AUDIO); - if (g_Config.sAudioDevice == a->T("Auto")) { - g_Config.sAudioDevice.clear(); - } - System_Notify(SystemNotification::AUDIO_RESET_DEVICE); -} - void GameSettingsScreen::CallbackRestoreDefaults(bool yes) { if (yes) { g_Config.RestoreDefaults(RestoreSettingsBits::SETTINGS); diff --git a/UI/GameSettingsScreen.h b/UI/GameSettingsScreen.h index 2d04e0d1f8..a3010324c2 100644 --- a/UI/GameSettingsScreen.h +++ b/UI/GameSettingsScreen.h @@ -22,42 +22,52 @@ #include #include +#include "Common/Data/Text/I18n.h" #include "Common/UI/UIScreen.h" #include "Common/UI/PopupScreens.h" #include "Core/ConfigValues.h" #include "UI/BaseScreens.h" +#include "UI/SettingsPagesDialogScreen.h" #include "UI/TabbedDialogScreen.h" class Path; +class SettingsPage; // Per-game settings screen - enables you to configure graphic options, control options, etc // per game. -class GameSettingsScreen : public UITabbedBaseDialogScreen { +class GameSettingsScreen : public SettingsPagesDialogScreen { public: GameSettingsScreen(const Path &gamePath, std::string gameID = "", bool editThenRestore = false); ~GameSettingsScreen(); const char *tag() const override { return "GameSettings"; } + // Opens on the developer tools page. + void OpenDeveloperTools(); + protected: void CallbackRestoreDefaults(bool yes); void CallbackMemstickFolder(bool yes); void dialogFinished(const Screen *dialog, DialogResult result) override; void CreateTabs() override; - bool ShowSearchControls() const override { return true; } private: void PreCreateViews() override; - void CreateGraphicsSettings(UI::ViewGroup *graphicsSettings); - void CreateControlsSettings(UI::ViewGroup *tools); - void CreateAudioSettings(UI::ViewGroup *audioSettings); - void CreateNetworkingSettings(UI::ViewGroup *networkingSettings); - void CreateToolsSettings(UI::ViewGroup *tools); - void CreateSystemSettings(UI::ViewGroup *systemSettings); - void CreateVRSettings(UI::ViewGroup *vrSettings); - UI::View *AddNicknameChoice(UI::ViewGroup *parent); + // One per tab. + struct PageDef { + const char *tag; + const char *title; // In MAINSETTINGS. + const char *icon; + const char *helpTag; // If null, there's no help button. + I18NCat cat; + void (*build)(SettingsPage *page); // Null for the System tab, which the screen builds. + }; + static const std::vector &PageDefs(); + + void BuildSystemSettings(SettingsPage *page); + std::string gameID_; #if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) @@ -72,10 +82,6 @@ private: // Global settings handlers void OnChangeBackground(UI::EventParams &e); void OnRestoreDefaultSettings(UI::EventParams &e); - void OnRenderingBackend(UI::EventParams &e); - void OnRenderingDevice(UI::EventParams &e); - void OnInflightFramesChoice(UI::EventParams &e); - void OnAudioDevice(UI::EventParams &e); void OnJitAffectingSetting(UI::EventParams &e); void OnShowMemstickScreen(UI::EventParams &e); #if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) @@ -85,13 +91,11 @@ private: void OnImmersiveModeChange(UI::EventParams &e); void OnSustainedPerformanceModeChange(UI::EventParams &e); - void TriggerRestartOrDo(std::function callback); + void RestartOrDo(std::function callback) override; void ReadConfigTemporaries(); // Temporaries to convert setting types, cache enabled, etc. - int prevInflightFrames_ = -1; bool enableReports_ = false; - bool analogSpeedMapped_ = false; // edit the game-specific settings and restore the global settings after exiting bool editGameSpecificThenRestore_ = false; @@ -106,7 +110,6 @@ public: void CreateTabs() override; const char *tag() const override { return "GestureMapping"; } - bool ShowSearchControls() const override { return false; } protected: void CreateGestureTab(UI::LinearLayout *parent, int zoneIndex, bool portrait); }; diff --git a/UI/JitCompareScreen.h b/UI/JitCompareScreen.h index 67722194c0..8e3bbf9de0 100644 --- a/UI/JitCompareScreen.h +++ b/UI/JitCompareScreen.h @@ -12,7 +12,6 @@ public: const char *tag() const override { return "JitCompare"; } private: - bool ShowSearchControls() const override { return false; } void UpdateDisasm(); // Uses the current ListType diff --git a/UI/MiscViews.cpp b/UI/MiscViews.cpp index 3aa57bf0e2..2f5a85b37d 100644 --- a/UI/MiscViews.cpp +++ b/UI/MiscViews.cpp @@ -133,6 +133,10 @@ private: int textureHeight_ = 0; }; +UI::View *CreateGameIconView(const Path &gamePath, UI::LayoutParams *layoutParams) { + return new SimpleGameIconView(gamePath, layoutParams); +} + PaneTitleBar::PaneTitleBar(const Path &gamePath, std::string_view title, const std::string_view settingsCategory, UI::LayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams), gamePath_(gamePath) { using namespace UI; diff --git a/UI/MiscViews.h b/UI/MiscViews.h index bc719c6199..274e8f3555 100644 --- a/UI/MiscViews.h +++ b/UI/MiscViews.h @@ -48,6 +48,9 @@ private: bool animated_ = true; }; +// The icon of the game, for showing that settings are game-specific. +UI::View *CreateGameIconView(const Path &gamePath, UI::LayoutParams *layoutParams); + // Title at the top of a scrolling pane of settings. May later include a back button, but currently doesn't. // The gamePath_ is used to draw the icon of the game who's settings are currently being edited. // settingsCategory is used to build the help URL. diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 8dc508d686..af789df11d 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -143,7 +143,6 @@ #include "UI/EmuScreen.h" #include "UI/GameInfoCache.h" #include "UI/GameSettingsScreen.h" -#include "UI/DeveloperToolsScreen.h" #include "UI/GPUDriverTestScreen.h" #include "UI/MiscScreens.h" #include "UI/MemStickScreen.h" @@ -895,7 +894,9 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO g_screenManager->switchScreen(new LogoScreen(AfterLogoScreen::TO_GAME_SETTINGS)); } else if (equals(cmdLineOptions.startScreen.value(), "developertools")) { g_screenManager->switchScreen(new MainScreen()); - g_screenManager->push(new DeveloperToolsScreen(Path())); + GameSettingsScreen *settings = new GameSettingsScreen(Path()); + settings->OpenDeveloperTools(); + g_screenManager->push(settings); } else { WARN_LOG(Log::System, "Unknown start screen '%s'", cmdLineOptions.startScreen.value().c_str()); g_screenManager->switchScreen(new LogoScreen(AfterLogoScreen::DEFAULT)); diff --git a/UI/RemoteISOScreen.h b/UI/RemoteISOScreen.h index 74ffb1665a..aab2043ab7 100644 --- a/UI/RemoteISOScreen.h +++ b/UI/RemoteISOScreen.h @@ -38,7 +38,6 @@ protected: void CreateConnectTab(UI::ViewGroup *viewGroup); void CreateSettingsTab(UI::ViewGroup *viewGroup); - bool ShowSearchControls() const override { return false; } void update() override; diff --git a/UI/RetroAchievementScreens.h b/UI/RetroAchievementScreens.h index 3b4b7634fc..6cce2e58c2 100644 --- a/UI/RetroAchievementScreens.h +++ b/UI/RetroAchievementScreens.h @@ -22,7 +22,6 @@ public: void CreateTabs() override; protected: - bool ShowSearchControls() const override { return false; } private: void CreateLeaderboardsTab(UI::ViewGroup *viewGroup); @@ -42,7 +41,6 @@ public: void sendMessage(UIMessage message, const char *value) override; protected: - bool ShowSearchControls() const override { return false; } private: void CreateAccountTab(UI::ViewGroup *viewGroup); @@ -64,7 +62,6 @@ public: void update() override; protected: - bool ShowSearchControls() const override { return false; } private: void CreateLeaderboardTab(UI::LinearLayout *layout, const rc_client_leaderboard_t *leaderboard); diff --git a/UI/SavedataScreen.h b/UI/SavedataScreen.h index c9fe37effe..66fdce3463 100644 --- a/UI/SavedataScreen.h +++ b/UI/SavedataScreen.h @@ -105,7 +105,6 @@ protected: void CreateTabs() override; void CreateExtraButtons(UI::ViewGroup *verticalLayout, int margins) override; - bool ShowSearchControls() const override { return false; } private: void OnSavedataButtonClick(UI::EventParams &e); diff --git a/UI/SettingsPage.cpp b/UI/SettingsPage.cpp new file mode 100644 index 0000000000..01ba58a31b --- /dev/null +++ b/UI/SettingsPage.cpp @@ -0,0 +1,374 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include "Common/Log.h" +#include "Common/StringUtils.h" +#include "Common/UI/View.h" +#include "Common/UI/ViewGroup.h" +#include "Common/UI/PopupScreens.h" +#include "Common/UI/Root.h" +#include "Core/Config.h" +#include "Core/ConfigSettings.h" +#include "UI/MiscViews.h" +#include "UI/SettingsPage.h" + +std::string_view SettingText::Translate(I18NCat pageCat) const { + if (!key) { + return ""; + } + if (raw) { + return key; + } + return T(hasCat ? cat : pageCat, key, def ? def : ""); +} + +std::string_view SettingText::English() const { + return def ? def : key; +} + +SettingItem &SettingsPage::Add(SettingType type, SettingText text) { + SettingItem &item = items_.emplace_back(); + item.type = type; + item.text = text; + return item; +} + +SettingItem &SettingsPage::Header(SettingText text) { + return Add(SettingType::Header, text); +} + +// The info of the config setting that value points to. +static const ConfigSettingInfo &InfoFor(const void *value) { + static const ConfigSettingInfo none{}; + const ConfigSettingRef *ref = Config::FindSetting(value); + _dbg_assert_msg_(ref && ref->setting->Info().label, "Not a config setting, or it has no label"); + return (ref && ref->setting->Info().label) ? ref->setting->Info() : none; +} + +static SettingText LabelText(const ConfigSettingInfo &info) { + if (!info.label) { + return SettingText::Raw("?"); + } + return SettingText(info.cat, info.label, info.labelDefault); +} + +// Units that are symbols or short abbreviations, like %, x, ms and MHz, aren't translated. +static std::string TranslateUnits(I18NCat cat, const char *units) { + if (strlen(units) <= 3) { + return units; + } + for (const char *c = units; *c; c++) { + if (isalpha((unsigned char)*c)) { + return std::string(T(cat, units)); + } + } + return units; +} + +SettingItem &SettingsPage::Setting(bool *value) { + const ConfigSettingInfo &info = InfoFor(value); + SettingItem &item = Add(SettingType::Bool, LabelText(info)); + item.ptr = value; + item.configOnChanged = info.onChanged; + return item; +} + +SettingItem &SettingsPage::Setting(int *value) { + const ConfigSettingInfo &info = InfoFor(value); + if (info.HasChoices()) { + SettingItem &item = Add(SettingType::IntChoice, LabelText(info)); + item.ptr = value; + item.configOnChanged = info.onChanged; + item.choices = info.choices; + item.minVal = info.choiceMin; + item.numChoices = info.choiceCount; + return item; + } + + _dbg_assert_msg_(info.HasRange(), "Int setting '%s' has neither choices nor a range", info.label ? info.label : "?"); + SettingItem &item = Add(SettingType::IntSlider, LabelText(info)); + item.ptr = value; + item.configOnChanged = info.onChanged; + item.minValue = (int)info.minValue; + item.maxValue = (int)info.maxValue; + item.step = info.step >= 1.0f ? (int)info.step : 1; + if (info.units) { + item.units = TranslateUnits(info.cat, info.units); + item.format = item.units == "%" ? "%d%%" : "%d " + ReplaceAll(item.units, "%", "%%"); + } + if (info.zeroLabel) { + item.zeroLabel = SettingText(info.cat, info.zeroLabel); + } + if (info.negativeLabel) { + item.negativeLabel = SettingText(info.cat, info.negativeLabel); + } + return item; +} + +SettingItem &SettingsPage::Setting(float *value) { + const ConfigSettingInfo &info = InfoFor(value); + _dbg_assert_msg_(info.HasRange(), "Float setting '%s' has no range", info.label ? info.label : "?"); + SettingItem &item = Add(SettingType::FloatSlider, LabelText(info)); + item.ptr = value; + item.configOnChanged = info.onChanged; + item.fMinValue = info.minValue; + item.fMaxValue = info.maxValue; + item.fStep = info.step; + item.fDefaultValue = Config::GetDefaultValueFloat(value); + if (info.units) { + item.units = TranslateUnits(info.cat, info.units); + } + return item; +} + +SettingItem &SettingsPage::Setting(std::string *value, std::vector names, std::vector values) { + _dbg_assert_(values.empty() || values.size() == names.size()); + const ConfigSettingInfo &info = InfoFor(value); + SettingItem &item = Add(SettingType::StringChoice, LabelText(info)); + item.ptr = value; + item.configOnChanged = info.onChanged; + item.choiceNames = std::move(names); + item.choiceValues = std::move(values); + return item; +} + +SettingItem &SettingsPage::Bool(bool *value, SettingText text) { + SettingItem &item = Add(SettingType::Bool, text); + item.ptr = value; + return item; +} + +SettingItem &SettingsPage::Bool(SettingText text, std::function get, std::function set) { + SettingItem &item = Add(SettingType::Bool, text); + item.getBool = std::move(get); + item.setBool = std::move(set); + return item; +} + +SettingItem &SettingsPage::IntSlider(int *value, int minValue, int maxValue, SettingText text) { + SettingItem &item = Add(SettingType::IntSlider, text); + item.ptr = value; + item.minValue = minValue; + item.maxValue = maxValue; + return item; +} + +SettingItem &SettingsPage::Choice(SettingText text, ImageID image) { + SettingItem &item = Add(SettingType::Choice, text); + item.image = image; + return item; +} + +SettingItem &SettingsPage::Info(SettingText text, std::string_view rightText) { + SettingItem &item = Add(SettingType::Info, text); + item.rightText = rightText; + return item; +} + +SettingItem &SettingsPage::Hint(SettingText text) { + _dbg_assert_(!items_.empty()); + return Add(SettingType::Hint, text); +} + +SettingItem &SettingsPage::Link(SettingText text, I18NCat cat, std::function build) { + SettingItem &item = Add(SettingType::Link, text); + item.linkCat = cat; + item.linkBuild = std::move(build); + return item; +} + +SettingItem &SettingsPage::Custom(SettingText text, std::function create) { + SettingItem &item = Add(SettingType::Custom, text); + item.createCustom = std::move(create); + return item; +} + +namespace { + +// Shows and changes state through functions, instead of a bool it points to. +class CallbackCheckBox : public UI::CheckBox { +public: + CallbackCheckBox(std::string_view text, std::function get, std::function set) + : CheckBox(nullptr, text), get_(std::move(get)), set_(std::move(set)) {} + + void Toggle() override { + bool on = !get_(); + set_(on); + UI::PlayUISound(on ? UI::UISound::TOGGLE_ON : UI::UISound::TOGGLE_OFF); + } + bool Toggled() const override { + return get_(); + } + +private: + std::function get_; + std::function set_; +}; + +} // namespace + +// The config's reaction to the new value comes first, then the page's. +static std::function ChangeHandler(const SettingItem &item) { + if (!item.configOnChanged) { + return item.onChange; + } + void (*onChanged)() = item.configOnChanged; + std::function pageHandler = item.onChange; + return [onChanged, pageHandler](UI::EventParams &e) { + onChanged(); + if (pageHandler) { + pageHandler(e); + } + }; +} + +static UI::View *CreateSettingViewInner(const SettingItem &item, I18NCat pageCat, UI::View *prevView, UI::ViewGroup *parent, ScreenManager *screenManager) { + using namespace UI; + + std::string_view text = item.text.Translate(pageCat); + switch (item.type) { + case SettingType::Header: + return parent->Add(new ChoiceGroup(text)); + case SettingType::Bool: + { + CheckBox *checkBox = item.getBool ? new CallbackCheckBox(text, item.getBool, item.setBool) : new CheckBox((bool *)item.ptr, text); + parent->Add(checkBox); + if (std::function onChange = ChangeHandler(item)) { + checkBox->OnClick.Add(onChange); + } + return checkBox; + } + case SettingType::IntChoice: + { + I18NCat choiceCat = item.text.hasCat ? item.text.cat : pageCat; + PopupMultiChoice *choice = parent->Add(new PopupMultiChoice((int *)item.ptr, text, const_cast(item.choices), item.minVal, item.numChoices, choiceCat, screenManager)); + if (std::function onChange = ChangeHandler(item)) { + choice->OnChoice.Add(onChange); + } + return choice; + } + case SettingType::StringChoice: + { + // PopupMultiChoiceDynamic copies the values. + std::vector values = item.choiceValues; + PopupMultiChoiceDynamic *choice = parent->Add(new PopupMultiChoiceDynamic((std::string *)item.ptr, text, item.choiceNames, I18NCat::NONE, screenManager, values.empty() ? nullptr : &values)); + if (std::function onChange = ChangeHandler(item)) { + choice->OnChoice.Add(onChange); + } + return choice; + } + case SettingType::IntSlider: + { + int defaultValue = item.defaultValue; + if (!item.hasDefault) { + defaultValue = Config::GetDefaultValueInt((int *)item.ptr); + _dbg_assert_msg_(defaultValue != ::NO_DEFAULT_INT, "No default for slider '%s'", item.text.key); + } + PopupSliderChoice *slider = parent->Add(new PopupSliderChoice((int *)item.ptr, item.minValue, item.maxValue, defaultValue, text, item.step, screenManager, item.units)); + if (!item.format.empty()) { + slider->SetFormat(item.format); + } + if (!item.zeroLabel.empty()) { + slider->SetZeroLabel(item.zeroLabel.Translate(pageCat)); + } + if (!item.negativeLabel.empty()) { + slider->SetNegativeDisable(std::string(item.negativeLabel.Translate(pageCat))); + } + if (item.liveUpdate) { + slider->SetLiveUpdate(true); + } + if (item.restrictTo) { + slider->RestrictChoices(item.restrictTo, item.restrictCount); + } + if (std::function onChange = ChangeHandler(item)) { + slider->OnChange.Add(onChange); + } + return slider; + } + case SettingType::FloatSlider: + { + PopupSliderChoiceFloat *slider = parent->Add(new PopupSliderChoiceFloat((float *)item.ptr, item.fMinValue, item.fMaxValue, item.fDefaultValue, text, item.fStep, screenManager, item.units)); + if (item.liveUpdate) { + slider->SetLiveUpdate(true); + } + if (std::function onChange = ChangeHandler(item)) { + slider->OnChange.Add(onChange); + } + return slider; + } + case SettingType::Choice: + { + UI::Choice *choice = parent->Add(new UI::Choice(text, item.image)); + if (std::function onChange = ChangeHandler(item)) { + choice->OnClick.Add(onChange); + } + return choice; + } + case SettingType::Info: + return parent->Add(new InfoItem(text, item.rightText)); + case SettingType::Hint: + return parent->Add(new SettingHint(text, prevView)); + case SettingType::Custom: + return item.createCustom(parent); + case SettingType::Link: + { + // The screen showing the page handles the click. + UI::Choice *choice = parent->Add(new UI::Choice(text, item.image)); + choice->SetIconRight(ImageID("I_CHEVRON_RIGHT")); + return choice; + } + } + return nullptr; +} + +static UI::View *CreateSettingView(const SettingItem &item, I18NCat pageCat, UI::View *prevView, UI::ViewGroup *parent, ScreenManager *screenManager) { + UI::View *view = CreateSettingViewInner(item, pageCat, prevView, parent, screenManager); + if (!view) { + return nullptr; + } + if (item.enabledFunc) { + view->SetEnabledFunc(item.enabledFunc); + } else if (item.enabledPtr) { + view->SetEnabledPtr(item.enabledPtr); + } else if (item.disabledPtr) { + view->SetDisabledPtr(item.disabledPtr); + } + if (item.setup) { + item.setup(view); + } + return view; +} + +void CreateSettingViews(const SettingsPage &page, UI::ViewGroup *parent, ScreenManager *screenManager, std::vector *views) { + UI::View *prevView = nullptr; + // Each header is a group, which the items after it go in. Items before the first get one without a title. + UI::ViewGroup *group = nullptr; + for (const SettingItem &item : page.Items()) { + if (!group && item.type != SettingType::Header) { + group = parent->Add(new UI::ChoiceGroup()); + } + UI::View *view = CreateSettingView(item, page.DefaultCat(), prevView, item.type == SettingType::Header ? parent : group, screenManager); + if (views) { + views->push_back(view); + } + if (view && item.type == SettingType::Header) { + group = static_cast(view); + } else if (view) { + prevView = view; + } + } +} diff --git a/UI/SettingsPage.h b/UI/SettingsPage.h new file mode 100644 index 0000000000..a7a0e5b9cf --- /dev/null +++ b/UI/SettingsPage.h @@ -0,0 +1,214 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include +#include +#include +#include +#include + +#include "Common/Data/Text/I18n.h" +#include "Common/Render/TextureAtlas.h" + +namespace UI { +class View; +class ViewGroup; +struct EventParams; +} + +class Path; +class ScreenManager; +class SettingsPage; + +// What the pages' handlers can do on the screen showing them. Lives as long as the views do. +class SettingsPageHost { +public: + virtual ~SettingsPageHost() {} + virtual ScreenManager *PageScreenManager() = 0; + // For System_ requests (like file dialogs) whose results go to the views. + virtual int PageRequesterToken() = 0; + // Builds the page again, for when what's on it changed. + virtual void RecreatePageViews() = 0; + // The game whose settings are being edited, if any. + virtual const Path &PageGamePath() const = 0; + // For a setting that only takes effect after a restart: asks to restart, or else calls otherwise + // (which usually puts the setting back). + virtual void RestartOrDo(std::function otherwise) = 0; +}; + +// Translatable text, kept as a key so it can be looked up both in English and translated +// (for search) as well as when it's shown. +struct SettingText { + SettingText() {} + SettingText(const char *key) : key(key) {} + SettingText(I18NCat cat, const char *key, const char *def = nullptr) : cat(cat), key(key), def(def), hasCat(true) {} + // Shown as is, without translation (like module names). + static SettingText Raw(const char *text) { + SettingText t(text); + t.raw = true; + return t; + } + + // Texts without a category use the page's. + std::string_view Translate(I18NCat pageCat) const; + std::string_view English() const; + bool empty() const { return !key; } + + I18NCat cat = I18NCat::NONE; + const char *key = nullptr; + const char *def = nullptr; + bool hasCat = false; + bool raw = false; +}; + +enum class SettingType { + Header, + Bool, + IntChoice, + StringChoice, + IntSlider, + FloatSlider, + Choice, // A button. onChange is called on click. + Info, + Hint, // Explanatory text attached to the previous item. + Custom, + Link, // Opens another page. +}; + +// Describes one item in a settings page. Views are only created from this in CreateSettingViews, +// so a page can also be inspected without creating any (for search). +struct SettingItem { + SettingType type; + SettingText text; + void *ptr = nullptr; // bool *, int * or std::string *, depending on type. + + // IntChoice. The choices are translated in the same category as the text. + const char *const *choices = nullptr; + int minVal = 0; + int numChoices = 0; + + // StringChoice. If values is empty, the names are stored. + std::vector choiceNames; + std::vector choiceValues; + + // IntSlider. If defaultValue isn't set, it's taken from the config. + int minValue = 0; + int maxValue = 0; + int defaultValue = 0; + bool hasDefault = false; + int step = 1; + std::string format; + SettingText zeroLabel; + SettingText negativeLabel; + const int *restrictTo = nullptr; + size_t restrictCount = 0; + + // FloatSlider + float fMinValue = 0.0f; + float fMaxValue = 0.0f; + float fDefaultValue = 0.0f; + float fStep = 0.0f; + + // Both slider types. Already translated. + std::string units; + bool liveUpdate = false; + + // Choice + ImageID image; + + // Info + std::string rightText; + + // Custom + std::function createCustom; + + // Link: builds the page it opens, whose texts default to linkCat. helpTag is the docs page for it and the + // pages under it, if not the same as the linking page's. + std::function linkBuild; + I18NCat linkCat = I18NCat::NONE; + const char *helpTag = nullptr; + + // Bool, instead of ptr: for state that lives elsewhere, like whether a server runs. + std::function getBool; + std::function setBool; + + bool *enabledPtr = nullptr; + bool *disabledPtr = nullptr; + std::function enabledFunc; + // Called on click (Bool, Choice), choice (choices) or change (sliders). + std::function onChange; + // From the config, for settings described there. Called before onChange. + void (*configOnChanged)() = nullptr; + // For tweaks the description doesn't cover, called after the view is created. + std::function setup; + + SettingItem &Enabled(bool *ptr) { enabledPtr = ptr; return *this; } + SettingItem &Disabled(bool *ptr) { disabledPtr = ptr; return *this; } + SettingItem &EnabledFunc(std::function func) { enabledFunc = std::move(func); return *this; } + SettingItem &OnChange(std::function func) { onChange = std::move(func); return *this; } + SettingItem &OnClick(std::function func) { onChange = std::move(func); return *this; } + SettingItem &Setup(std::function func) { setup = std::move(func); return *this; } + SettingItem &Default(int value) { defaultValue = value; hasDefault = true; return *this; } + SettingItem &Step(int value) { step = value; return *this; } + SettingItem &Format(std::string_view fmt) { format = fmt; return *this; } + SettingItem &LiveUpdate() { liveUpdate = true; return *this; } + SettingItem &RestrictTo(const int *values, size_t count) { restrictTo = values; restrictCount = count; return *this; } +}; + +// A list of settings items. Texts without a category get the page's. +class SettingsPage { +public: + SettingsPage(I18NCat defaultCat, SettingsPageHost *host) : defaultCat_(defaultCat), host_(host) {} + + // Without a title, just starts a new group. + SettingItem &Header(SettingText text = SettingText()); + // Settings described in the config: the label, kind of view, range or choices, and default come from there. + SettingItem &Setting(bool *value); + SettingItem &Setting(int *value); + SettingItem &Setting(float *value); + // For string settings with choices that are only known at runtime, like devices. The names are shown as + // is, untranslated. If values is empty, the names are stored. + SettingItem &Setting(std::string *value, std::vector names, std::vector values = {}); + // For values that aren't config settings. + SettingItem &Bool(bool *value, SettingText text); + SettingItem &Bool(SettingText text, std::function get, std::function set); + SettingItem &IntSlider(int *value, int minValue, int maxValue, SettingText text); + SettingItem &Choice(SettingText text, ImageID image = ImageID::invalid()); + SettingItem &Info(SettingText text, std::string_view rightText); + SettingItem &Hint(SettingText text); + // For anything the other types can't describe. The text is only used for search. The view + // returned is the one a following Hint is attached to, and the one search results lead to. + SettingItem &Custom(SettingText text, std::function create); + // Opens another page, built by build, on top of this one. The text is the new page's title. + SettingItem &Link(SettingText text, I18NCat cat, std::function build); + + I18NCat DefaultCat() const { return defaultCat_; } + SettingsPageHost *Host() const { return host_; } + const std::deque &Items() const { return items_; } + +private: + SettingItem &Add(SettingType type, SettingText text); + + I18NCat defaultCat_; + SettingsPageHost *host_; + std::deque items_; // A deque so references stay valid while adding. +}; + +// If views is set, it gets the view created for each item (or null), in the same order. +void CreateSettingViews(const SettingsPage &page, UI::ViewGroup *parent, ScreenManager *screenManager, std::vector *views = nullptr); diff --git a/UI/SettingsPagesDialogScreen.cpp b/UI/SettingsPagesDialogScreen.cpp new file mode 100644 index 0000000000..94f1c3510f --- /dev/null +++ b/UI/SettingsPagesDialogScreen.cpp @@ -0,0 +1,594 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include "Common/Data/Text/I18n.h" +#include "Common/Input/KeyCodes.h" +#include "Common/StringUtils.h" +#include "Common/System/Request.h" +#include "Common/UI/Context.h" +#include "Common/UI/PopupScreens.h" +#include "Common/UI/Root.h" +#include "Common/UI/ScreenManager.h" +#include "Common/UI/ScrollView.h" +#include "Common/UI/TabHolder.h" +#include "Common/UI/ViewGroup.h" +#include "Core/Config.h" +#include "UI/MiscViews.h" +#include "UI/SettingsPage.h" +#include "UI/SettingsPagesDialogScreen.h" + +// Pages can link to each other, so search doesn't follow links forever. +static const int MAX_PAGE_DEPTH = 8; + +void SettingsPagesDialogScreen::AddSettingsTab(const char *tag, std::string_view title, ImageID imageId, I18NCat cat, std::function build, const char *helpTag) { + int index = (int)settingsTabs_.size(); + if (stacked_) { + settingsTabs_.push_back(SettingsTab{ index, tag, std::string(title), cat, std::move(build), helpTag ? helpTag : "", imageId }); + return; + } + settingsTabs_.push_back(SettingsTab{ (int)tabHolder_->GetTabContentViews().size(), tag, std::string(title), cat, std::move(build), helpTag ? helpTag : "", imageId }); + tabHolder_->AddTabDeferred(title, imageId, [this, index]() -> UI::ViewGroup * { + using namespace UI; + LinearLayout *contents = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); + contents->SetSpacing(0); + // Created on demand, outside CreateViews (see AddTab). + bool wasCreatingViews = IsCreatingViews(); + SetCreatingViews(true); + CreateSettingsTabContents(index, contents); + SetCreatingViews(wasCreatingViews); + return contents; + }); +} + +void SettingsPagesDialogScreen::OpenPage(const char *tabTag, const char *linkKey) { + openTabTag_ = tabTag; + openLinkKey_ = linkKey; +} + +std::unique_ptr SettingsPagesDialogScreen::BuildPage(const SettingsTab &tab, std::vector *path, std::vector *titles, std::string *helpTag) { + std::unique_ptr page = std::make_unique(tab.cat, this); + tab.build(page.get()); + titles->push_back(tab.title); + *helpTag = tab.helpTag; + for (size_t i = 0; i < path->size(); i++) { + int index = (*path)[i]; + if (index < 0 || index >= (int)page->Items().size() || page->Items()[index].type != SettingType::Link || i >= MAX_PAGE_DEPTH) { + path->resize(i); + break; + } + const SettingItem &link = page->Items()[index]; + titles->push_back(std::string(link.text.Translate(page->DefaultCat()))); + if (link.helpTag) { + *helpTag = link.helpTag; + } + std::unique_ptr next = std::make_unique(link.linkCat, page->Host()); + link.linkBuild(next.get()); + page = std::move(next); + } + return page; +} + +void SettingsPagesDialogScreen::CreateSettingsTabContents(int settingsTab, UI::LinearLayout *parent) { + using namespace UI; + SettingsTab &tab = settingsTabs_[settingsTab]; + std::vector &path = pagePaths_[tab.tag]; + std::vector titles; + std::string helpTag; + std::unique_ptr page = BuildPage(tab, &path, &titles, &helpTag); + if (settingsTab == stackRootTab_ && !path.empty()) { + // What tabs show when rotating to them. + if (currentTabSetting_) { + *currentTabSetting_ = path[0]; + } + pagePaths_[settingsTabs_[path[0]].tag].assign(path.begin() + 1, path.end()); + } + + if (!searchText_.empty()) { + auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); + titles = { std::string(ms->T("Search")) + ": " + searchText_ }; + } + CreateNavBar(parent, settingsTab, titles, helpTag); + + std::string pageKey = tab.tag; + for (int index : path) { + pageKey += "/" + std::to_string(index); + } + ScrollView *scroll = parent->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f))); + // Search results get their own tag, so their scroll position isn't restored onto the page when leaving them. + scroll->SetTag(searchText_.empty() ? pageKey : pageKey + "/search"); + LinearLayout *contents = scroll->Add(new ChoiceGroupList()); + + tab.views.clear(); + if (!searchText_.empty()) { + CreateSearchResults(contents); + return; + } + + // Each page remembers where it was scrolled to, for coming back to it. + scroll->RememberPosition(&scrollPositions_[pageKey]); + CreateSettingViews(*page, contents, screenManager(), &tab.views); + + const std::deque &items = page->Items(); + for (int i = 0; i < (int)items.size() && i < (int)tab.views.size(); i++) { + if (items[i].type == SettingType::Link && tab.views[i]) { + static_cast(tab.views[i])->OnClick.Add([this, settingsTab, i](UI::EventParams &) { + std::vector path = pagePaths_[settingsTabs_[settingsTab].tag]; + path.push_back(i); + ShowPage(settingsTab, path, 0); + }); + } + } + + if (pendingTab_ == settingsTab && pendingItem_ >= 0) { + for (int i = pendingItem_; i < (int)tab.views.size(); i++) { + if (tab.views[i] && tab.views[i]->CanBeFocused()) { + pendingFocusView_ = tab.views[i]; + break; + } + } + } + if (pendingTab_ == settingsTab) { + pendingTab_ = -1; + pendingItem_ = -1; + } +} + +// Where you are, with the earlier pages clickable to go back to them. Also has the search button. +void SettingsPagesDialogScreen::CreateNavBar(UI::ViewGroup *parent, int settingsTab, const std::vector &titles, const std::string &helpTag) { + using namespace UI; + const SettingsTab &tab = settingsTabs_[settingsTab]; + LinearLayout *bar = parent->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT, Margins(0, 0, 8, 0)))); + bar->SetPadding(Margins(12, 8, 12, 8)); + bar->SetSpacing(0.0f); + + // When stacked in portrait, there's no other back button, so it's always there, and leaves the screen from the root page. + if (stacked_ || !searchText_.empty() || !pagePaths_[tab.tag].empty()) { + Choice *backButton = bar->Add(new Choice(ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT))); + backButton->SetRoundCorners(RoundCorners::All); + backButton->OnClick.Add([this, settingsTab](UI::EventParams &) { + if (!GoBack(settingsTab)) { + TriggerFinish(DR_BACK); + } + }); + } + // Portrait has no room for the whole path, and the back button goes up it. + int firstShownTitle = stacked_ ? (int)titles.size() - 1 : 0; + for (int i = firstShownTitle; i < (int)titles.size(); i++) { + if (i > firstShownTitle) { + bar->Add(new TextView(">", new LinearLayoutParams(0.0f, Gravity::G_VCENTER, Margins(4, 0, 4, 0)))); + } + if (i == (int)titles.size() - 1) { + SimpleTextView *title = bar->Add(new SimpleTextView(titles[i], new LinearLayoutParams(0.0f, Gravity::G_VCENTER))); + title->SetBig(true); + continue; + } + Choice *crumb = bar->Add(new Choice(titles[i], new LinearLayoutParams(WRAP_CONTENT, ITEM_HEIGHT, 0.0f, Gravity::G_VCENTER))); + crumb->SetRoundCorners(RoundCorners::All); + crumb->OnClick.Add([this, settingsTab, i](UI::EventParams &) { + std::vector path = pagePaths_[settingsTabs_[settingsTab].tag]; + int focusItem = path[i]; // The link to the page we're leaving. + path.resize(i); + ShowPage(settingsTab, path, focusItem); + }); + } + + bar->Add(new Spacer(10.0f, new LinearLayoutParams(1.0f))); + + if (!gamePath_.empty() && g_Config.IsGameSpecific()) { + bar->Add(CreateGameIconView(gamePath_, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 0.0f, Gravity::G_VCENTER))); + } + + // While searching, the back button leaves the search, and this changes it. + if (CanShowSearch()) { + Choice *searchButton = bar->Add(new Choice(ImageID("I_SEARCH"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT, Margins(8, 0, 0, 0)))); + searchButton->SetRoundCorners(RoundCorners::All); + searchButton->OnClick.Add([this](UI::EventParams &) { + AskForSearchText(); + }); + } + + if (!helpTag.empty() && searchText_.empty()) { + std::string settingsUrl = join("https://www.ppsspp.org/docs/settings/", helpTag); + Choice *helpButton = bar->Add(new Choice(ImageID("I_LINK_OUT_QUESTION"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT, Margins(8, 0, 0, 0)))); + helpButton->SetRoundCorners(RoundCorners::All); + helpButton->OnClick.Add([settingsUrl](UI::EventParams &) { + System_LaunchUrl(LaunchUrlType::BROWSER_URL, settingsUrl); + }); + } +} + +void SettingsPagesDialogScreen::AskForSearchText() { + auto se = GetI18NCategory(I18NCat::SEARCH); + auto apply = [this](std::string_view text) { + SetSearchText(StripSpaces(text)); + }; + if (System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) { + System_InputBoxGetString(GetRequesterToken(), se->T("Find settings"), searchText_, false, [apply](std::string_view value, int) { + apply(value); + }); + return; + } + // The popup writes the text here before calling OnChange. + searchEditText_ = searchText_; + UI::TextEditPopupScreen *popup = new UI::TextEditPopupScreen(&searchEditText_, "", se->T("Find settings"), 64); + if (System_GetPropertyBool(SYSPROP_KEYBOARD_IS_SOFT)) { + popup->SetAlignTop(true); + } + popup->OnChange.Add([this, apply](UI::EventParams &) { + apply(searchEditText_); + }); + screenManager()->push(popup); +} + +// Every tab shows the search results while searching, so they all change. +void SettingsPagesDialogScreen::SetSearchText(std::string_view text) { + if (text == searchText_) { + return; + } + searchText_ = text; + pageChange_ = true; + RecreateViews(); +} + +// Recreating the views shows it. That waits for update, since this is usually called from a click on a view +// that will be deleted. +void SettingsPagesDialogScreen::ShowPage(int settingsTab, const std::vector &path, int itemIndex) { + if (stacked_ && settingsTab != stackRootTab_) { + std::vector rootPath = path; + rootPath.insert(rootPath.begin(), settingsTab); + ShowPage(stackRootTab_, rootPath, itemIndex); + return; + } + pagePaths_[settingsTabs_[settingsTab].tag] = path; + pageChange_ = true; + RecreateViews(); + pendingTab_ = settingsTab; + pendingItem_ = itemIndex; + pendingHighlight_ = false; +} + +bool SettingsPagesDialogScreen::GoBack(int settingsTab) { + if (!searchText_.empty()) { + SetSearchText(""); + return true; + } + if (settingsTab < 0) { + return false; + } + std::vector path = pagePaths_[settingsTabs_[settingsTab].tag]; + if (path.empty()) { + return false; + } + int focusItem = path.back(); // The link to the page we're leaving. + path.pop_back(); + ShowPage(settingsTab, path, focusItem); + return true; +} + +// Links to the tabs, like a tab strip would. +void SettingsPagesDialogScreen::BuildStackRootPage(SettingsPage *page) { + for (int t = 0; t < stackRootTab_; t++) { + const SettingsTab &tab = settingsTabs_[t]; + SettingItem &link = page->Link(SettingText::Raw(tab.title.c_str()), tab.cat, tab.build); + link.image = tab.icon; + if (!tab.helpTag.empty()) { + link.helpTag = tab.helpTag.c_str(); + } + } +} + +// A page asked for with OpenPage. +int SettingsPagesDialogScreen::OpenRequestedPage() { + if (openTabTag_.empty()) { + return -1; + } + int opened = -1; + for (int t = 0; t < (int)settingsTabs_.size(); t++) { + const SettingsTab &tab = settingsTabs_[t]; + if (tab.tag != openTabTag_) { + continue; + } + SettingsPage page(tab.cat, this); + tab.build(&page); + for (int i = 0; i < (int)page.Items().size(); i++) { + const SettingItem &item = page.Items()[i]; + if (item.type == SettingType::Link && item.text.key && openLinkKey_ == item.text.key) { + pagePaths_[tab.tag] = { i }; + pendingTab_ = t; + opened = t; + break; + } + } + } + openTabTag_.clear(); + openLinkKey_.clear(); + return opened; +} + +int SettingsPagesDialogScreen::CurrentSettingsTab() const { + if (stacked_) { + return stackRootTab_; + } + if (!tabHolder_) { + return -1; + } + int current = tabHolder_->GetCurrentTab(); + for (int t = 0; t < (int)settingsTabs_.size(); t++) { + if (settingsTabs_[t].tabIndex == current) { + return t; + } + } + return -1; +} + +bool SettingsPagesDialogScreen::CanShowSearch() { + if (!System_GetPropertyBool(SYSPROP_HAS_KEYBOARD) && !System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) { + return false; + } + return System_GetPropertyInt(SYSPROP_DEVICE_TYPE) != DEVICE_TYPE_VR; +} + +// Back closes the search, then goes back a page, before leaving the screen. +bool SettingsPagesDialogScreen::key(const KeyInput &key) { + if (key.flags & KeyInputFlags::DOWN) { + ClearHighlight(); + } + if ((key.flags & KeyInputFlags::DOWN) && UI::IsEscapeKey(key)) { + if (GoBack(CurrentSettingsTab())) { + UI::PlayUISound(UI::UISound::BACK); + return true; + } + } + return UIBaseDialogScreen::key(key); +} + +bool SettingsPagesDialogScreen::touch(const TouchInput &touch) { + if (touch.flags & TouchInputFlags::DOWN) { + ClearHighlight(); + } + return UIBaseDialogScreen::touch(touch); +} + +void SettingsPagesDialogScreen::ClearHighlight() { + if (highlightedView_) { + highlightedView_->SetHighlighted(false); + highlightedView_ = nullptr; + } +} + +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); +} + +// Lists the matching settings from all tabs and the pages they link to, each leading to where it is. +void SettingsPagesDialogScreen::CreateSearchResults(UI::ViewGroup *parent) { + using namespace UI; + auto se = GetI18NCategory(I18NCat::SEARCH); + + bool found = false; + for (int t = 0; t < (int)settingsTabs_.size(); t++) { + // It only links to the others. + if (t == stackRootTab_) { + continue; + } + const SettingsTab &tab = settingsTabs_[t]; + SettingsPage page(tab.cat, this); + tab.build(&page); + if (SearchPage(parent, t, {}, page, tab.title, 0)) { + found = true; + } + } + + if (!found) { + parent->Add(new TextView(ApplySafeSubstitutions(se->T("No settings matched '%1'"), searchText_), new LinearLayoutParams(Margins(20, 5)))); + } +} + +bool SettingsPagesDialogScreen::SearchPage(UI::ViewGroup *parent, int settingsTab, const std::vector &path, const SettingsPage &page, const std::string &title, int depth) { + using namespace UI; + 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; + } + } + + // The page title is the last part of title. + size_t lastSeparator = title.rfind(" > "); + std::string_view pageTitle = lastSeparator == std::string::npos ? std::string_view(title) : std::string_view(title).substr(lastSeparator + 3); + + bool found = false; + const SettingItem *header = nullptr; + // The section's results, created with its first. + ChoiceGroup *group = nullptr; + for (int i = 0; i < (int)items.size(); i++) { + const SettingItem &item = items[i]; + if (item.type == SettingType::Header) { + header = &item; + group = nullptr; + continue; + } + if (!show[i]) { + continue; + } + + if (!group) { + std::string headerTitle = title; + // Pages often start with a header repeating their title. + if (header && !header->text.empty() && header->text.Translate(cat) != pageTitle) { + headerTitle += " > "; + headerTitle += header->text.Translate(cat); + } + group = parent->Add(new ChoiceGroup(headerTitle)); + } + // Goes to the setting, highlighted, or opens the page that a link leads to. + Choice *result = group->Add(new Choice(item.text.Translate(cat))); + result->SetIconRight(ImageID("I_CHEVRON_RIGHT")); + std::vector pagePath = path; + int focusItem = i; + if (item.type == SettingType::Link) { + pagePath.push_back(i); + focusItem = -1; + } + result->OnClick.Add([this, settingsTab, pagePath, focusItem](UI::EventParams &) { + GoToPage(settingsTab, pagePath, focusItem); + }); + found = true; + } + + // And the pages this one links to. + if (depth < MAX_PAGE_DEPTH) { + for (int i = 0; i < (int)items.size(); i++) { + const SettingItem &item = items[i]; + if (item.type != SettingType::Link) { + continue; + } + SettingsPage linked(item.linkCat, page.Host()); + item.linkBuild(&linked); + std::vector linkPath = path; + linkPath.push_back(i); + if (SearchPage(parent, settingsTab, linkPath, linked, title + " > " + std::string(item.text.Translate(cat)), depth + 1)) { + found = true; + } + } + } + return found; +} + +// Leaves the search, and shows the page at path in its tab, with the item focused (in update, so it's +// highlighted and scrolled into view). +void SettingsPagesDialogScreen::GoToPage(int settingsTab, const std::vector &path, int itemIndex) { + SetSearchText(""); + if (!stacked_) { + // The tab strip keeps showing its tab when the views are recreated. This creates the tab if it wasn't + // already, so it has to come before ShowPage, or that would take the item to focus. + SetCurrentTab(settingsTabs_[settingsTab].tabIndex); + } + ShowPage(settingsTab, path, itemIndex); + pendingHighlight_ = true; +} + +void SettingsPagesDialogScreen::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) { + // Moving between pages only focuses with the keyboard (or a controller), which needs a focused view. + // A setting picked from search results is always shown, and with a mouse or touch, highlighted. + if (pendingHighlight_ || UI::IsFocusMovementEnabled()) { + UI::ScrollView *scrollView = UI::GetEnclosingScrollView(pendingFocusView_); + if (scrollView) { + scrollView->ScrollToCenter(pendingFocusView_); + } + if (UI::IsFocusMovementEnabled()) { + UI::SetFocusedView(pendingFocusView_, UI::FocusFlags::CAUSE_RESTORE); + } else if (UI::Clickable *clickable = dynamic_cast(pendingFocusView_)) { + ClearHighlight(); + clickable->SetHighlighted(true); + highlightedView_ = clickable; + } + } + pendingFocusView_ = nullptr; + pendingHighlight_ = false; + } +} + + +void SettingsPagesDialogScreen::CreateViews() { + settingsTabs_.clear(); + pendingFocusView_ = nullptr; + highlightedView_ = nullptr; + + bool wasStacked = stacked_; + stacked_ = GetDeviceOrientation() == DeviceOrientation::Portrait; + using namespace UI; + + if (stacked_) { + PreCreateViews(); + tabHolder_ = nullptr; + CreateTabs(); + const int requestedTab = OpenRequestedPage(); + + stackRootTab_ = (int)settingsTabs_.size(); + settingsTabs_.push_back(SettingsTab{ stackRootTab_, "SettingsRoot", std::string(T(I18NCat::DIALOG, "Settings")), I18NCat::DIALOG, [this](SettingsPage *page) { + BuildStackRootPage(page); + }, "", ImageID::invalid() }); + SettingsTab &rootTab = settingsTabs_[stackRootTab_]; + + // Show the tab that was asked for, or that was shown before rotating, under the page linking to it. + // (Pages shown from here are already on the root tab's path.) + int showTab = -1; + if (requestedTab >= 0) { + showTab = requestedTab; + pendingTab_ = stackRootTab_; + } else if (viewsCreated_ && !wasStacked && currentTabSetting_ && *currentTabSetting_ >= 0 && *currentTabSetting_ < stackRootTab_) { + showTab = *currentTabSetting_; + } + if (showTab >= 0) { + std::vector rootPath = pagePaths_[settingsTabs_[showTab].tag]; + rootPath.insert(rootPath.begin(), showTab); + pagePaths_[rootTab.tag] = rootPath; + } + + LinearLayout *contents = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(FILL_PARENT, FILL_PARENT)); + contents->SetSpacing(0.0f); + root_ = contents; + CreateSettingsTabContents(stackRootTab_, contents); + viewsCreated_ = true; + pageChange_ = false; + return; + } + stackRootTab_ = -1; + + UITabbedBaseDialogScreen::CreateViews(); + viewsCreated_ = true; + pageChange_ = false; +} + +// A page asked for with OpenPage is opened on its tab. +int SettingsPagesDialogScreen::InitialTab() { + OpenRequestedPage(); + return pendingTab_ >= 0 ? settingsTabs_[pendingTab_].tabIndex : -1; +} + +// Picking a tab means leaving the search for it. +void SettingsPagesDialogScreen::TabChanged(int tab) { + SetSearchText(""); +} diff --git a/UI/SettingsPagesDialogScreen.h b/UI/SettingsPagesDialogScreen.h new file mode 100644 index 0000000000..36d74fc7c4 --- /dev/null +++ b/UI/SettingsPagesDialogScreen.h @@ -0,0 +1,124 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Common/Data/Text/I18n.h" +#include "Common/Render/TextureAtlas.h" +#include "UI/SettingsPage.h" +#include "UI/TabbedDialogScreen.h" + +// Tabs described by SettingsPages, whose settings show up in search. Their pages can link to more pages, +// which open on top of them in the same tab, under a bar showing where you are (with search and help +// buttons). In portrait, instead of a strip of tabs, a page links to them, so all tabs must be settings tabs. +class SettingsPagesDialogScreen : public UITabbedBaseDialogScreen, public SettingsPageHost { +public: + SettingsPagesDialogScreen(const Path &gamePath, int *currentTabSetting = nullptr, TabDialogFlags flags = TabDialogFlags::Default) + : UITabbedBaseDialogScreen(gamePath, currentTabSetting, flags) {} + + // If helpTag is set, the bar links to the docs. 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, const char *helpTag = nullptr); + // When the views are created, opens the page that the root page of tab tabTag links to with the text key linkKey. + void OpenPage(const char *tabTag, const char *linkKey); + void CreateViews() override; + void update() override; + bool key(const KeyInput &key) override; + bool touch(const TouchInput &touch) override; + + ScreenManager *PageScreenManager() override { return screenManager(); } + int PageRequesterToken() override { return GetRequesterToken(); } + void RecreatePageViews() override { RecreateViews(); } + const Path &PageGamePath() const override { return gamePath_; } + +protected: + int InitialTab() override; + // While the views are recreated only to show another page or search results. Then what doesn't change + // with the page, like lists read from disk, doesn't need to be read again. + bool RecreatingForPageChange() const { return pageChange_; } + void TabChanged(int tab) override; + + // Whether there's a way to type a search. + static bool CanShowSearch(); + +private: + struct SettingsTab { + int tabIndex; + std::string tag; + std::string title; + I18NCat cat; + std::function build; + std::string helpTag; + ImageID icon; + std::vector views; // One per item of the page shown, filled in when the tab is created. + }; + + // Builds the page at path, which lists the links to follow from the tab's root page. Cuts the path + // where it stops leading to a page. titles gets the title of each page, starting with the tab's, and + // helpTag the page's docs. + std::unique_ptr BuildPage(const SettingsTab &tab, std::vector *path, std::vector *titles, std::string *helpTag); + void CreateSettingsTabContents(int settingsTab, UI::LinearLayout *parent); + void BuildStackRootPage(SettingsPage *page); + void CreateNavBar(UI::ViewGroup *parent, int settingsTab, const std::vector &titles, const std::string &helpTag); + void CreateSearchResults(UI::ViewGroup *parent); + bool SearchPage(UI::ViewGroup *parent, int settingsTab, const std::vector &path, const SettingsPage &page, const std::string &title, int depth); + // Returns the settings tab of the page, or -1 if none was asked for. + int OpenRequestedPage(); + void AskForSearchText(); + void SetSearchText(std::string_view text); + // Shows the page at path in the tab, and focuses itemIndex on it (if not -1). When stacked, that's under + // the page linking to the tabs. + void ShowPage(int settingsTab, const std::vector &path, int itemIndex); + // Closes the search, or else goes back a page. Returns false if there was nowhere to go back to. + bool GoBack(int settingsTab); + void GoToPage(int settingsTab, const std::vector &path, int itemIndex); + int CurrentSettingsTab() const; + void ClearHighlight(); + + std::vector settingsTabs_; + // The page shown in each tab, by tab tag, and each page's scroll position. Kept when the views are recreated, + // which is how a different page or the search results get shown. + std::map> pagePaths_; + std::map scrollPositions_; + std::string searchText_; + std::string searchEditText_; // For the text input popup. + + // In portrait there's no tab holder. Instead, the settings tab stackRootTab_ (added after the others) links + // to the others, and is the only one shown. Its link to each tab has the tab's index. + bool stacked_ = false; + bool viewsCreated_ = false; // So we know when rotating. + bool pageChange_ = false; + int stackRootTab_ = -1; + + // What to show when the views are next created. + std::string openTabTag_; + std::string openLinkKey_; + int pendingTab_ = -1; // In settingsTabs_. + int pendingItem_ = -1; // An item on that tab's page, to focus. + // Highlighted and scrolled to once laid out. + UI::View *pendingFocusView_ = nullptr; + bool pendingHighlight_ = false; // Show pendingFocusView_ even without keyboard navigation. + // Highlighted instead of focused, without keyboard navigation. Until the next touch or key. + UI::Clickable *highlightedView_ = nullptr; +}; diff --git a/UI/SystemInfoScreen.h b/UI/SystemInfoScreen.h index 37fc736353..2faf89fdc3 100644 --- a/UI/SystemInfoScreen.h +++ b/UI/SystemInfoScreen.h @@ -18,7 +18,6 @@ public: void resized() override { RecreateViews(); } protected: - bool ShowSearchControls() const override { return false; } private: void CreateDeviceInfoTab(UI::LinearLayout *deviceInfo); diff --git a/UI/TabbedDialogScreen.cpp b/UI/TabbedDialogScreen.cpp index a5501f26e4..e0cd34b111 100644 --- a/UI/TabbedDialogScreen.cpp +++ b/UI/TabbedDialogScreen.cpp @@ -3,8 +3,6 @@ #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" @@ -63,14 +61,6 @@ void UITabbedBaseDialogScreen::CreateViews() { // 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)); @@ -87,7 +77,7 @@ void UITabbedBaseDialogScreen::CreateViews() { }, nullptr)); }; } - tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 200, tabHolderFlags, filterNotice_, contextMenu, new LinearLayoutParams(1.0f)); + tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 200, tabHolderFlags, nullptr, contextMenu, new LinearLayoutParams(1.0f)); verticalLayout->Add(tabHolder_); if (!(flags_ & TabDialogFlags::ContextMenuInPortrait)) { CreateExtraButtons(verticalLayout, 0); @@ -98,7 +88,7 @@ void UITabbedBaseDialogScreen::CreateViews() { if (flags_ & TabDialogFlags::VerticalShowIcons) { tabHolderFlags |= TabHolderFlags::VerticalShowIcons; } - tabHolder_ = new TabHolder(ORIENT_VERTICAL, 300, tabHolderFlags, filterNotice_, nullptr); + tabHolder_ = new TabHolder(ORIENT_VERTICAL, 300, tabHolderFlags, nullptr, nullptr); CreateExtraButtons(tabHolder_->Container(), 10); tabHolder_->AddBack(this); root_ = tabHolder_; @@ -119,55 +109,20 @@ void UITabbedBaseDialogScreen::CreateViews() { if (currentTabSetting_) { *currentTabSetting_ = e.a; } + TabChanged(e.a); }); - if (System_GetPropertyBool(SYSPROP_HAS_KEYBOARD) || System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) { - // Hide search if screen is too small. - int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE); - if ((g_display.dp_xres < g_display.dp_yres || g_display.dp_yres >= 500) && (deviceType != DEVICE_TYPE_VR) && ShowSearchControls()) { - // Search - auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); - AddTab("GameSettingsSearch", ms->T("Search"), ImageID("I_SEARCH"), [this](UI::LinearLayout *searchSettings) { - auto se = GetI18NCategory(I18NCat::SEARCH); - - ChoiceGroup *searchGroup = searchSettings->Add(new ChoiceGroupList())->Add(new ChoiceGroup(se->T("Find settings"))); - searchGroup->Add(new PopupTextInputChoice(GetRequesterToken(), &searchFilter_, se->T("Filter"), "", 64, screenManager()))->OnChange.Add([this](UI::EventParams &e) { - System_PostUIMessage(UIMessage::GAMESETTINGS_SEARCH, StripSpaces(searchFilter_)); - }); - - clearSearchChoice_ = searchGroup->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)))); - }); + int initialTab = InitialTab(); + if (initialTab >= 0) { + if (currentTabSetting_) { + *currentTabSetting_ = initialTab; } - } - - // After the search tab, which the saved tab can be. - if (currentTabSetting_) { + tabHolder_->SetInitialTab(initialTab); + } else 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() { @@ -184,85 +139,3 @@ int UITabbedBaseDialogScreen::GetCurrentTab() const { void UITabbedBaseDialogScreen::SetCurrentTab(int tab) { tabHolder_->SetCurrentTab(tab); } - -// Hides the views in the group that don't match the filter, and returns whether any did. Groups that can't -// be focused (like ChoiceGroups) are searched recursively and stay visible if anything in them matches. -// Other views that can't be focused (headings, notices) are shown only when something after them matches. -static bool ApplySearchFilterToGroup(UI::ViewGroup *group, int firstIndex, const std::string &filter) { - using namespace UI; - bool anyMatch = false; - View *lastHeading = nullptr; - for (int i = firstIndex; i < group->GetNumSubviews(); ++i) { - View *v = group->GetViewByIndex(i); - bool match; - if (v->IsViewGroup() && !v->CanBeFocused()) { - match = ApplySearchFilterToGroup((ViewGroup *)v, 0, filter); - } else if (!v->CanBeFocused()) { - lastHeading = v; - v->SetVisibility(filter.empty() ? V_VISIBLE : V_GONE); - continue; - } else { - std::string label = v->DescribeText(); - std::transform(label.begin(), label.end(), label.begin(), tolower); - match = label.find(filter) != label.npos; - } - if (match && lastHeading) { - lastHeading->SetVisibility(V_VISIBLE); - } - v->SetVisibility((filter.empty() || match) ? V_VISIBLE : V_GONE); - anyMatch = anyMatch || match; - } - return anyMatch; -} - -void UITabbedBaseDialogScreen::ApplySearchFilter() { - using namespace UI; - // GAMESETTINGS_SEARCH is broadcast to every screen, including ones whose views don't exist yet. - if (!tabHolder_) { - 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 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; - } - - // The first view is the tab's title. - const bool tabMatches = ApplySearchFilterToGroup((ViewGroup *)tabContents, 1, searchFilter_) || searchFilter_.empty(); - 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); - } -} diff --git a/UI/TabbedDialogScreen.h b/UI/TabbedDialogScreen.h index 0e02f067d0..563c537123 100644 --- a/UI/TabbedDialogScreen.h +++ b/UI/TabbedDialogScreen.h @@ -43,29 +43,20 @@ protected: virtual void PreCreateViews() {} virtual void CreateTabs() = 0; virtual void CreateExtraButtons(UI::ViewGroup *verticalLayout, int margins) {} - virtual bool ShowSearchControls() const { return true; } virtual void EnsureTabs(); virtual bool UsePopupMenuInPortrait() const { return false; } virtual bool ForceHorizontalTabs() const { return false; } + // The tab to show first, after CreateTabs, if not the saved one (-1). + virtual int InitialTab() { return -1; } + virtual void TabChanged(int tab) {} + int GetCurrentTab() const; void SetCurrentTab(int tab); - void sendMessage(UIMessage message, const char *value) override; - -private: - void ApplySearchFilter(); - UI::TabHolder *tabHolder_ = nullptr; - UI::TextView *filterNotice_ = nullptr; - UI::Choice *clearSearchChoice_ = nullptr; - UI::TextView *noSearchResults_ = nullptr; - - // If we recreate the views while this is active we show it again - std::string oldSettingInfo_; - std::string searchFilter_; - int *currentTabSetting_ = nullptr; +private: TabDialogFlags flags_ = TabDialogFlags::Default; }; diff --git a/UI/UI.vcxproj b/UI/UI.vcxproj index 88b9804f53..93ce591691 100644 --- a/UI/UI.vcxproj +++ b/UI/UI.vcxproj @@ -77,6 +77,8 @@ + + @@ -140,6 +142,8 @@ + + diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters index 28df7acbed..b0db57a106 100644 --- a/UI/UI.vcxproj.filters +++ b/UI/UI.vcxproj.filters @@ -102,6 +102,12 @@ Screens + + Screens + + + Screens + Screens @@ -266,6 +272,12 @@ Screens + + Screens + + + Screens + Screens diff --git a/UWP/UI_UWP/UI_UWP.vcxproj b/UWP/UI_UWP/UI_UWP.vcxproj index 29566343cf..78af5186f3 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj +++ b/UWP/UI_UWP/UI_UWP.vcxproj @@ -125,6 +125,8 @@ + + @@ -188,6 +190,8 @@ + + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj.filters b/UWP/UI_UWP/UI_UWP.vcxproj.filters index 3058a7df82..69a9d96c42 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj.filters +++ b/UWP/UI_UWP/UI_UWP.vcxproj.filters @@ -131,6 +131,12 @@ Screens + + Screens + + + Screens + Screens @@ -287,6 +293,12 @@ Screens + + Screens + + + Screens + Screens diff --git a/android/jni/Android.mk b/android/jni/Android.mk index f61d4c1973..8c94abb2e2 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -1024,6 +1024,8 @@ LOCAL_SRC_FILES := \ $(SRC)/UI/GameBrowser.cpp \ $(SRC)/UI/MainScreen.cpp \ $(SRC)/UI/TabbedDialogScreen.cpp \ + $(SRC)/UI/SettingsPage.cpp \ + $(SRC)/UI/SettingsPagesDialogScreen.cpp \ $(SRC)/UI/AdhocServerScreen.cpp \ $(SRC)/UI/SimpleDialogScreen.cpp \ $(SRC)/UI/MemStickScreen.cpp \