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 \