From 7ad4d33094cbcb1f87baa520379addffffe4537e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 Oct 2025 10:18:38 +0200 Subject: [PATCH 1/5] Start refactoring the MainScreen code --- Core/Config.cpp | 3 ++ Core/Config.h | 5 +- UI/MainScreen.cpp | 129 +++++++++++++++++++++------------------------- UI/MainScreen.h | 5 +- 4 files changed, 70 insertions(+), 72 deletions(-) diff --git a/Core/Config.cpp b/Core/Config.cpp index ebac1e73a0..4687b2d358 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -245,6 +245,8 @@ static const ConfigSetting generalSettings[] = { ConfigSetting("CwCheatRefreshRate", &g_Config.iCwCheatRefreshIntervalMs, 77, CfgFlag::PER_GAME), ConfigSetting("CwCheatScrollPosition", &g_Config.fCwCheatScrollPosition, 0.0f, CfgFlag::PER_GAME), ConfigSetting("GameListScrollPosition", &g_Config.fGameListScrollPosition, 0.0f, CfgFlag::DEFAULT), + ConfigSetting("HomebrewScrollPosition", &g_Config.fHomebrewScrollPosition, 0.0f, CfgFlag::DEFAULT), + ConfigSetting("RemoteScrollPosition", &g_Config.fRemoteScrollPosition, 0.0f, CfgFlag::DEFAULT), ConfigSetting("DebugOverlay", &g_Config.iDebugOverlay, 0, CfgFlag::DONT_SAVE), ConfigSetting("DefaultTab", &g_Config.iDefaultTab, 0, CfgFlag::DEFAULT), ConfigSetting("DisableHLEFlags", &g_Config.iDisableHLE, 0, CfgFlag::PER_GAME), @@ -270,6 +272,7 @@ static const ConfigSetting generalSettings[] = { ConfigSetting("GridView1", &g_Config.bGridView1, true, CfgFlag::DEFAULT), ConfigSetting("GridView2", &g_Config.bGridView2, true, CfgFlag::DEFAULT), ConfigSetting("GridView3", &g_Config.bGridView3, false, CfgFlag::DEFAULT), + ConfigSetting("GridView4", &g_Config.bGridView4, false, CfgFlag::DEFAULT), ConfigSetting("RightAnalogUp", &g_Config.iRightAnalogUp, 0, CfgFlag::PER_GAME), ConfigSetting("RightAnalogDown", &g_Config.iRightAnalogDown, 0, CfgFlag::PER_GAME), ConfigSetting("RightAnalogLeft", &g_Config.iRightAnalogLeft, 0, CfgFlag::PER_GAME), diff --git a/Core/Config.h b/Core/Config.h index 718b2b5888..f61e06cb20 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -260,6 +260,8 @@ public: int iCwCheatRefreshIntervalMs; float fCwCheatScrollPosition; float fGameListScrollPosition; + float fHomebrewScrollPosition; + float fRemoteScrollPosition; int iBloomHack; //0 = off, 1 = safe, 2 = balanced, 3 = aggressive int iSkipGPUReadbackMode; // 0 = off, 1 = skip, 2 = to texture int iSplineBezierQuality; // 0 = low , 1 = Intermediate , 2 = High @@ -350,10 +352,11 @@ public: // 0 - no tilt, 1 - analog stick, 2 - D-Pad, 3 - Action Buttons (Tri, Cross, Square, Circle) int iTiltInputType; - // The three tabs. + // The four tabs (including Remote last) bool bGridView1; bool bGridView2; bool bGridView3; + bool bGridView4; // Right analog binding int iRightAnalogUp; diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 21e2bea382..e94295e0bf 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -1068,6 +1068,59 @@ MainScreen::~MainScreen() { g_BackgroundAudio.SetGame(Path()); } +#if PPSSPP_PLATFORM(IOS) +constexpr std::string_view getGamesUri = "https://www.ppsspp.org/getgames_ios"; +constexpr std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew_ios"; +#else +constexpr std::string_view getGamesUri = "https://www.ppsspp.org/getgames"; +constexpr std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew"; +#endif +constexpr std::string_view remoteGamesUri = "https://www.ppsspp.org/docs/reference/disc-streaming"; + +void MainScreen::CreateRecentTab() { + using namespace UI; + auto mm = GetI18NCategory(I18NCat::MAINMENU); + + ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); + scrollRecentGames->SetTag("MainScreenRecentGames"); + GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(), + Path("!RECENT"), BrowseFlags::NONE, &g_Config.bGridView1, screenManager(), "", "", + new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); + scrollRecentGames->Add(tabRecentGames); + gameBrowsers_.push_back(tabRecentGames); + + tabHolder_->AddTab(mm->T("Recent"), scrollRecentGames); + tabRecentGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant); + tabRecentGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected); + tabRecentGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight); +} + +GameBrowser *MainScreen::CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, bool *bGridView, float *scrollPos) { + using namespace UI; + auto mm = GetI18NCategory(I18NCat::MAINMENU); + + ScrollView *scrollView = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); + scrollView->SetTag(title); // Re-use title as tag, should be fine. + + GameBrowser *gameBrowser = new GameBrowser(GetRequesterToken(), path, browseFlags, bGridView, screenManager(), + mm->T(howToTitle), howToUri, + new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); + + scrollView->Add(gameBrowser); + gameBrowsers_.push_back(gameBrowser); + + tabHolder_->AddTab(mm->T(title), scrollView); + if (scrollPos) { + scrollView->RememberPosition(scrollPos); + } + + gameBrowser->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant); + gameBrowser->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected); + gameBrowser->OnHighlight.Handle(this, &MainScreen::OnGameHighlight); + + return gameBrowser; +} + void MainScreen::CreateViews() { // Information in the top left. // Back button to the bottom left. @@ -1094,81 +1147,18 @@ void MainScreen::CreateViews() { } if (showRecent) { - ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); - scrollRecentGames->SetTag("MainScreenRecentGames"); - GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(), - Path("!RECENT"), BrowseFlags::NONE, &g_Config.bGridView1, screenManager(), "", "", - new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); - scrollRecentGames->Add(tabRecentGames); - gameBrowsers_.push_back(tabRecentGames); - - tabHolder_->AddTab(mm->T("Recent"), scrollRecentGames); - tabRecentGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant); - tabRecentGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected); - tabRecentGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight); + CreateRecentTab(); } Button *focusButton = nullptr; if (hasStorageAccess) { - scrollAllGames_ = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); - scrollAllGames_->SetTag("MainScreenAllGames"); - ScrollView *scrollHomebrew = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); - scrollHomebrew->SetTag("MainScreenHomebrew"); - -#if PPSSPP_PLATFORM(IOS) - std::string_view getGamesUri = "https://www.ppsspp.org/getgames_ios"; - std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew_ios"; -#else - std::string_view getGamesUri = "https://www.ppsspp.org/getgames"; - std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew"; -#endif - - GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD, &g_Config.bGridView2, screenManager(), - mm->T("How to get games"), getGamesUri, - new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); - GameBrowser *tabHomebrew = new GameBrowser(GetRequesterToken(), GetSysDirectory(DIRECTORY_GAME), BrowseFlags::HOMEBREW_STORE, &g_Config.bGridView3, screenManager(), - mm->T("How to get homebrew & demos", "How to get homebrew && demos"), getHomebrewUri, - new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); - - scrollAllGames_->Add(tabAllGames); - gameBrowsers_.push_back(tabAllGames); - scrollHomebrew->Add(tabHomebrew); - gameBrowsers_.push_back(tabHomebrew); - - tabHolder_->AddTab(mm->T("Games"), scrollAllGames_); - tabHolder_->AddTab(mm->T("Homebrew & Demos"), scrollHomebrew); - scrollAllGames_->RememberPosition(&g_Config.fGameListScrollPosition); - - tabAllGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant); - tabHomebrew->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant); - - tabAllGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected); - tabHomebrew->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected); - - tabAllGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight); - tabHomebrew->OnHighlight.Handle(this, &MainScreen::OnGameHighlight); + CreateBrowserTab(Path(g_Config.currentDirectory), "Games", "How to get games", getGamesUri, BrowseFlags::STANDARD, &g_Config.bGridView2, &g_Config.fGameListScrollPosition); + CreateBrowserTab(GetSysDirectory(DIRECTORY_GAME), "Homebrew & Demos", "How to get homebrew & demos", getHomebrewUri, BrowseFlags::HOMEBREW_STORE, &g_Config.bGridView3, &g_Config.fHomebrewScrollPosition); if (g_Config.bRemoteTab && !g_Config.sLastRemoteISOServer.empty()) { - auto ri = GetI18NCategory(I18NCat::REMOTEISO); - - ScrollView *scrollRemote = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); - scrollRemote->SetTag("MainScreenRemote"); - Path remotePath(FormatRemoteISOUrl(g_Config.sLastRemoteISOServer.c_str(), g_Config.iLastRemoteISOPort, RemoteSubdir().c_str())); - - GameBrowser *tabRemote = new GameBrowser(GetRequesterToken(), remotePath, BrowseFlags::NAVIGATE, &g_Config.bGridView3, screenManager(), - ri->T("Remote disc streaming"), "https://www.ppsspp.org/docs/reference/disc-streaming", - new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); - tabRemote->SetHomePath(remotePath); - - scrollRemote->Add(tabRemote); - gameBrowsers_.push_back(tabRemote); - - tabHolder_->AddTab(ri->T("Remote disc streaming"), scrollRemote); - - tabRemote->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant); - tabRemote->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected); - tabRemote->OnHighlight.Handle(this, &MainScreen::OnGameHighlight); + GameBrowser *remoteBrowser = CreateBrowserTab(remotePath, "Remote disc streaming", "Remote disc streaming", remoteGamesUri, BrowseFlags::NAVIGATE, &g_Config.bGridView4, &g_Config.fRemoteScrollPosition); + remoteBrowser->SetHomePath(remotePath); } if (g_recentFiles.HasAny()) { @@ -1202,7 +1192,6 @@ void MainScreen::CreateViews() { leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f))); } } else { - scrollAllGames_ = nullptr; if (!showRecent) { leftColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f)); // Just so it's destroyed on recreate. @@ -1227,7 +1216,6 @@ void MainScreen::CreateViews() { ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL); LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); rightColumnItems->SetSpacing(0.0f); - rightColumn->Add(rightColumnItems); std::string versionString = PPSSPP_GIT_VERSION; // Strip the 'v' from the displayed version, and shorten the commit hash. @@ -1308,6 +1296,7 @@ void MainScreen::CreateViews() { // Officially, iOS apps should not have exit buttons. Remove it to maximize app store review chances. rightColumnChoices->Add(new Choice(mm->T("Exit")))->OnClick.Handle(this, &MainScreen::OnExit); #endif + rightColumn->Add(rightColumnItems); if (vertical) { root_ = new LinearLayout(ORIENT_VERTICAL); diff --git a/UI/MainScreen.h b/UI/MainScreen.h index 2f02794510..a812bc48aa 100644 --- a/UI/MainScreen.h +++ b/UI/MainScreen.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include "Common/File/Path.h" #include "Common/UI/UIScreen.h" @@ -131,6 +132,9 @@ public: protected: void CreateViews() override; + void CreateRecentTab(); + GameBrowser *CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, bool *bGridView, float *scrollPos); + void DrawBackground(UIContext &dc) override; void update() override; void sendMessage(UIMessage message, const char *value) override; @@ -165,7 +169,6 @@ protected: bool lockBackgroundAudio_ = false; bool lastVertical_ = false; bool confirmedTemporary_ = false; - UI::ScrollView *scrollAllGames_ = nullptr; bool searchKeyModifier_ = false; bool searchChanged_ = false; std::string searchFilter_; From 5b3c3bd189ea28c6afcea11fb4e9eafc321b3872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 Oct 2025 10:40:55 +0200 Subject: [PATCH 2/5] Replace some functions with lambdas --- UI/GameSettingsScreen.cpp | 48 ++++++++++++++++----------------------- UI/GameSettingsScreen.h | 7 ------ 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 7fc46e9e4d..08dea7a9ac 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -299,7 +299,13 @@ void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings) 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" }; resolutionChoice_ = graphicsSettings->Add(new PopupMultiChoice(&g_Config.iInternalResolution, gr->T("Rendering Resolution"), internalResolutions, 0, ARRAY_SIZE(internalResolutions), I18NCat::GRAPHICS, screenManager())); - resolutionChoice_->OnChoice.Handle(this, &GameSettingsScreen::OnResolutionChange); + resolutionChoice_->OnChoice.Add([](UI::EventParams &e) { + if (g_Config.iAndroidHwScale == 1) { + System_RecreateActivity(); + } + Reporting::UpdateConfig(); + System_PostUIMessage(UIMessage::GPU_RENDER_RESIZED); + }); resolutionChoice_->SetEnabledFunc([] { return !g_Config.bSoftwareRendering && !g_Config.bSkipBufferEffects; }); @@ -784,8 +790,12 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings) int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE); controlsSettings->Add(new ItemHeader(ms->T("Controls"))); - controlsSettings->Add(new Choice(co->T("Control Mapping")))->OnClick.Handle(this, &GameSettingsScreen::OnControlMapping); - controlsSettings->Add(new Choice(co->T("Calibrate Analog Stick")))->OnClick.Handle(this, &GameSettingsScreen::OnCalibrateAnalogs); + controlsSettings->Add(new Choice(co->T("Control Mapping")))->OnClick.Add([this](UI::EventParams &e) { + screenManager()->push(new ControlMappingScreen(gamePath_)); + }); + controlsSettings->Add(new Choice(co->T("Calibrate Analog Stick")))->OnClick.Add([this](UI::EventParams &e) { + screenManager()->push(new AnalogSetupScreen(gamePath_)); + }); controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fAnalogTriggerThreshold, 0.02f, 0.98f, 0.75f, co->T("Analog trigger threshold"), screenManager())); #if defined(USING_WIN_UI) || (PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID)) @@ -804,7 +814,9 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings) } return ""; })); - customizeTilt->OnClick.Handle(this, &GameSettingsScreen::OnTiltCustomize); + customizeTilt->OnClick.Add([this](UI::EventParams &e) { + screenManager()->push(new TouchControlLayoutScreen(gamePath_)); + }); } else if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_VR) { // TODO: This seems like a regression controlsSettings->Add(new CheckBox(&g_Config.bHapticFeedback, co->T("HapticFeedback", "Haptic Feedback (vibration)"))); } @@ -814,7 +826,9 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings) controlsSettings->Add(new ItemHeader(co->T("OnScreen", "On-Screen Touch Controls"))); controlsSettings->Add(new CheckBox(&g_Config.bShowTouchControls, co->T("OnScreen", "On-Screen Touch Controls"))); layoutEditorChoice_ = controlsSettings->Add(new Choice(co->T("Customize Touch Controls"))); - layoutEditorChoice_->OnClick.Handle(this, &GameSettingsScreen::OnTouchControlLayout); + layoutEditorChoice_->OnClick.Add([this](UI::EventParams &e) { + screenManager()->push(new TouchControlLayoutScreen(gamePath_)); + }); layoutEditorChoice_->SetEnabledPtr(&g_Config.bShowTouchControls); Choice *gesture = controlsSettings->Add(new Choice(co->T("Gesture mapping"))); @@ -1601,14 +1615,6 @@ void GameSettingsScreen::OnFullscreenMultiChange(UI::EventParams &e) { System_ToggleFullscreenState(g_Config.UseFullScreen() ? "1" : "0"); } -void GameSettingsScreen::OnResolutionChange(UI::EventParams &e) { - if (g_Config.iAndroidHwScale == 1) { - System_RecreateActivity(); - } - Reporting::UpdateConfig(); - System_PostUIMessage(UIMessage::GPU_RENDER_RESIZED); -} - void GameSettingsScreen::onFinish(DialogResult result) { Reporting::Enable(enableReports_, "report.ppsspp.org"); Reporting::UpdateConfig(); @@ -1806,22 +1812,6 @@ void GameSettingsScreen::OnTextureShaderChange(UI::EventParams &e) { g_Config.bTexHardwareScaling = g_Config.sTextureShaderName != "Off"; } -void GameSettingsScreen::OnControlMapping(UI::EventParams &e) { - screenManager()->push(new ControlMappingScreen(gamePath_)); -} - -void GameSettingsScreen::OnCalibrateAnalogs(UI::EventParams &e) { - screenManager()->push(new AnalogSetupScreen(gamePath_)); -} - -void GameSettingsScreen::OnTouchControlLayout(UI::EventParams &e) { - screenManager()->push(new TouchControlLayoutScreen(gamePath_)); -} - -void GameSettingsScreen::OnTiltCustomize(UI::EventParams &e) { - screenManager()->push(new TiltAnalogSettingsScreen(gamePath_)); -}; - void GameSettingsScreen::CallbackRestoreDefaults(bool yes) { if (yes) { g_Config.RestoreDefaults(RestoreSettingsBits::SETTINGS); diff --git a/UI/GameSettingsScreen.h b/UI/GameSettingsScreen.h index cc914e828f..8cec893d88 100644 --- a/UI/GameSettingsScreen.h +++ b/UI/GameSettingsScreen.h @@ -74,12 +74,6 @@ private: std::string memstickDisplay_; - // Event handlers - void OnControlMapping(UI::EventParams &e); - void OnCalibrateAnalogs(UI::EventParams &e); - void OnTouchControlLayout(UI::EventParams &e); - void OnTiltCustomize(UI::EventParams &e); - // Global settings handlers void OnAutoFrameskip(UI::EventParams &e); void OnTextureShader(UI::EventParams &e); @@ -92,7 +86,6 @@ private: void OnChangeBackground(UI::EventParams &e); void OnFullscreenChange(UI::EventParams &e); void OnFullscreenMultiChange(UI::EventParams &e); - void OnResolutionChange(UI::EventParams &e); void OnRestoreDefaultSettings(UI::EventParams &e); void OnRenderingBackend(UI::EventParams &e); void OnRenderingDevice(UI::EventParams &e); From b45d07d6cbcad8bbb0c1c124f5b8279da553ff80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 Oct 2025 11:19:20 +0200 Subject: [PATCH 3/5] Break out TabHolder from Common/UI/ViewGroup.cpp. A few new atlas images --- Common/Common.vcxproj | 2 + Common/Common.vcxproj.filters | 6 + Common/UI/TabHolder.cpp | 310 +++ Common/UI/TabHolder.h | 90 + Common/UI/View.cpp | 1 - Common/UI/View.h | 3 + Common/UI/ViewGroup.cpp | 296 --- Common/UI/ViewGroup.h | 78 - UI/DisplayLayoutScreen.cpp | 2 +- UI/DisplayLayoutScreen.h | 4 + UI/DriverManagerScreen.cpp | 4 +- UI/GPUDriverTestScreen.h | 1 + UI/GameSettingsScreen.cpp | 2 - UI/MainScreen.h | 2 + UI/RetroAchievementScreens.cpp | 3 +- UI/TabbedDialogScreen.cpp | 1 + UI/TabbedDialogScreen.h | 4 + UI/TouchControlLayoutScreen.h | 1 + UI/TouchControlVisibilityScreen.cpp | 1 + UI/UIAtlas.cpp | 5 + assets/ui_images/images.svg | 2894 +++++++++------------------ assets/ui_images/svg_sources.txt | 5 + 22 files changed, 1405 insertions(+), 2310 deletions(-) create mode 100644 Common/UI/TabHolder.cpp create mode 100644 Common/UI/TabHolder.h create mode 100644 assets/ui_images/svg_sources.txt diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj index 2836fe43cd..49600b911a 100644 --- a/Common/Common.vcxproj +++ b/Common/Common.vcxproj @@ -538,6 +538,7 @@ + @@ -983,6 +984,7 @@ + diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters index 69df3d4c50..8825a91d6f 100644 --- a/Common/Common.vcxproj.filters +++ b/Common/Common.vcxproj.filters @@ -698,6 +698,9 @@ ext\nanosvg + + UI + @@ -1302,6 +1305,9 @@ Render + + UI + diff --git a/Common/UI/TabHolder.cpp b/Common/UI/TabHolder.cpp new file mode 100644 index 0000000000..670c35debd --- /dev/null +++ b/Common/UI/TabHolder.cpp @@ -0,0 +1,310 @@ +#include "Common/Math/curves.h" +#include "Common/Data/Text/I18n.h" +#include "Common/UI/TabHolder.h" +#include "Common/UI/Root.h" +#include "Common/UI/ScrollView.h" +#include "Common/UI/ViewGroup.h" +#include "Common/UI/UIScreen.h" + +namespace UI { + +TabHolder::TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams) + : LinearLayout(Opposite(orientation), layoutParams) { + SetSpacing(0.0f); + if (orientation == ORIENT_HORIZONTAL) { + tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + tabStrip_->SetTopTabs(true); + tabScroll_ = new ScrollView(orientation, new LayoutParams(FILL_PARENT, WRAP_CONTENT)); + tabScroll_->Add(tabStrip_); + Add(tabScroll_); + } else { + tabContainer_ = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(stripSize, FILL_PARENT)); + tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(FILL_PARENT, FILL_PARENT)); + tabStrip_->SetTopTabs(true); + tabScroll_ = new ScrollView(orientation, new LinearLayoutParams(1.0f)); + tabScroll_->Add(tabStrip_); + tabContainer_->Add(tabScroll_); + Add(tabContainer_); + } + tabStrip_->OnChoice.Handle(this, &TabHolder::OnTabClick); + + Add(new Spacer(4.0f))->SetSeparator(); + + ViewGroup *contentHolder_ = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)); + if (bannerView) { + contentHolder_->Add(bannerView); + bannerView_ = bannerView; + } + contents_ = contentHolder_->Add(new AnchorLayout(new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f))); + contents_->SetClip(true); + Add(contentHolder_); +} + +void TabHolder::AddBack(UIScreen *parent) { + if (tabContainer_) { + auto di = GetI18NCategory(I18NCat::DIALOG); + tabContainer_->Add(new Choice(di->T("Back"), "", false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(0, 0, 10, 10))))->OnClick.Handle(parent, &UIScreen::OnBack); + } +} + +void TabHolder::AddTabContents(std::string_view title, ViewGroup *tabContents) { + tabs_.push_back(tabContents); + tabStrip_->AddChoice(title); + contents_->Add(tabContents); + if (tabs_.size() > 1) + tabContents->SetVisibility(V_GONE); + tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT)); + + // Will be filled in later. + tabTweens_.push_back(nullptr); + // This entry doesn't need one. + createFuncs_.push_back(nullptr); +} + +void TabHolder::AddTabDeferred(std::string_view title, std::function createCb) { + tabs_.push_back(nullptr); // marker + tabStrip_->AddChoice(title); + tabTweens_.push_back(nullptr); + createFuncs_.push_back(createCb); + + if (tabs_.size() == 1) { + EnsureTab(0); + } +} + +void TabHolder::EnsureAllCreated() { + for (int i = 0; i < createFuncs_.size(); i++) { + if (createFuncs_[i]) { + EnsureTab(i); + tabs_[i]->SetVisibility(i == currentTab_ ? V_VISIBLE : V_GONE); + } + } +} + +bool TabHolder::EnsureTab(int index) { + _dbg_assert_(index >= 0 && index < createFuncs_.size()); + + if (!tabs_[index]) { + _dbg_assert_(index < createFuncs_.size()); + _dbg_assert_(createFuncs_[index]); + std::function func; + createFuncs_[index].swap(func); + + ViewGroup *tabContents = func(); + tabs_[index] = tabContents; + contents_->Add(tabContents); + + tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT)); + return true; + } else { + return false; + } +} + +bool TabHolder::SetCurrentTab(int tab, bool skipTween) { + if (tab >= (int)tabs_.size()) { + // Ignore + return false; + } + + bool created = false; + + if (tab != currentTab_) { + _dbg_assert_(tabs_[currentTab_]); // we should always have a tab to switch *from*. + created = EnsureTab(tab); + } + + auto setupTween = [&](View *view, AnchorTranslateTween *&tween) { + _dbg_assert_(view != nullptr); + if (tween) + return; + + tween = new AnchorTranslateTween(0.15f, bezierEaseInOut); + tween->Finish.Add([&](EventParams &e) { + e.v->SetVisibility(tabs_[currentTab_] == e.v ? V_VISIBLE : V_GONE); + }); + view->AddTween(tween)->Persist(); + }; + + if (tab != currentTab_) { + Orientation orient = Opposite(orientation_); + // Direction from which the new tab will come. + float dir = tab < currentTab_ ? -1.0f : 1.0f; + + // First, setup any missing tweens. + setupTween(tabs_[currentTab_], tabTweens_[currentTab_]); + setupTween(tabs_[tab], tabTweens_[tab]); + + // Currently displayed, so let's reset it. + if (skipTween) { + tabs_[currentTab_]->SetVisibility(V_GONE); + tabTweens_[tab]->Reset(Point2D(0.0f, 0.0f)); + tabTweens_[tab]->Apply(tabs_[tab]); + } else { + tabTweens_[currentTab_]->Reset(Point2D(0.0f, 0.0f)); + + if (orient == ORIENT_HORIZONTAL) { + tabTweens_[tab]->Reset(Point2D(bounds_.w * dir, 0.0f)); + tabTweens_[currentTab_]->Divert(Point2D(bounds_.w * -dir, 0.0f)); + } else { + tabTweens_[tab]->Reset(Point2D(0.0f, bounds_.h * dir)); + tabTweens_[currentTab_]->Divert(Point2D(0.0f, bounds_.h * -dir)); + } + // Actually move it to the initial position now, just to avoid any flicker. + tabTweens_[tab]->Apply(tabs_[tab]); + tabTweens_[tab]->Divert(Point2D(0.0f, 0.0f)); + } + tabs_[tab]->SetVisibility(V_VISIBLE); + + currentTab_ = tab; + } + tabStrip_->SetSelection(tab, false); + + return created; +} + +void TabHolder::OnTabClick(EventParams &e) { + // We have e.b set when it was an explicit click action. + // In that case, we make the view gone and then visible - this scrolls scrollviews to the top. + if (e.b != 0) { + EnsureTab(e.a); + SetCurrentTab((int)e.a); + } +} + +void TabHolder::PersistData(PersistStatus status, std::string anonId, PersistMap &storage) { + ViewGroup::PersistData(status, anonId, storage); + + std::string tag = Tag(); + if (tag.empty()) { + tag = anonId; + } + + PersistBuffer &buffer = storage["TabHolder::" + tag]; + switch (status) { + case PERSIST_SAVE: + buffer.resize(1); + buffer[0] = currentTab_; + break; + + case PERSIST_RESTORE: + if (buffer.size() == 1) { + if (SetCurrentTab(buffer[0], true)) { + // Re-run PersistData. TODO: Only need to do it for the new tab. + ViewGroup::PersistData(status, anonId, storage); + } + } + break; + } +} + +void TabHolder::EnableTab(int tab, bool enabled) { + tabStrip_->EnableChoice(tab, enabled); +} + + +ChoiceStrip::ChoiceStrip(Orientation orientation, LayoutParams *layoutParams) + : LinearLayout(orientation, layoutParams) { + SetSpacing(0.0f); +} + +void ChoiceStrip::AddChoice(std::string_view title) { + StickyChoice *c = new StickyChoice(title, "", + orientation_ == ORIENT_HORIZONTAL ? + nullptr : + new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT)); + c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick); + Add(c); + if (selected_ == (int)views_.size() - 1) + c->Press(); +} + +void ChoiceStrip::AddChoice(ImageID buttonImage) { + StickyChoice *c = new StickyChoice(buttonImage, + orientation_ == ORIENT_HORIZONTAL ? + nullptr : + new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT)); + c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick); + Add(c); + if (selected_ == (int)views_.size() - 1) + c->Press(); +} + +void ChoiceStrip::OnChoiceClick(EventParams &e) { + // Unstick the other choices that weren't clicked. + for (int i = 0; i < (int)views_.size(); i++) { + if (views_[i] != e.v) { + Choice(i)->Release(); + } else { + selected_ = i; + } + } + + EventParams e2{}; + e2.v = views_[selected_]; + e2.a = selected_; + // Set to 1 to indicate an explicit click. + e2.b = 1; + // Dispatch immediately (we're already on the UI thread as we're in an event handler). + OnChoice.Dispatch(e2); +} + +void ChoiceStrip::SetSelection(int sel, bool triggerClick) { + int prevSelected = selected_; + StickyChoice *prevChoice = Choice(selected_); + if (prevChoice) + prevChoice->Release(); + selected_ = sel; + StickyChoice *newChoice = Choice(selected_); + if (newChoice) { + newChoice->Press(); + + if (topTabs_ && prevSelected != selected_) { + EventParams e{}; + e.v = views_[selected_]; + e.a = selected_; + // Set to 0 to indicate a selection change (not a click.) + e.b = triggerClick ? 1 : 0; + OnChoice.Trigger(e); + } + } +} + +void ChoiceStrip::EnableChoice(int choice, bool enabled) { + if (choice < (int)views_.size()) { + Choice(choice)->SetEnabled(enabled); + } +} + +bool ChoiceStrip::Key(const KeyInput &input) { + bool ret = false; + if (topTabs_ && (input.flags & KEY_DOWN)) { + if (IsTabLeftKey(input)) { + if (selected_ > 0) { + SetSelection(selected_ - 1, true); + UI::PlayUISound(UI::UISound::TOGGLE_OFF); // Maybe make specific sounds for this at some point? + } + ret = true; + } else if (IsTabRightKey(input)) { + if (selected_ < (int)views_.size() - 1) { + SetSelection(selected_ + 1, true); + UI::PlayUISound(UI::UISound::TOGGLE_ON); + } + ret = true; + } + } + return ret || ViewGroup::Key(input); +} + +std::string ChoiceStrip::DescribeText() const { + auto u = GetI18NCategory(I18NCat::UI_ELEMENTS); + return DescribeListUnordered(u->T("Choices:")); +} + +StickyChoice *ChoiceStrip::Choice(int index) { + if ((size_t)index < views_.size()) + return static_cast(views_[index]); + return nullptr; +} + +} // namespace UI diff --git a/Common/UI/TabHolder.h b/Common/UI/TabHolder.h new file mode 100644 index 0000000000..9f098c60ba --- /dev/null +++ b/Common/UI/TabHolder.h @@ -0,0 +1,90 @@ +#pragma once + +#include + +#include "Common/UI/View.h" +#include "Common/UI/ViewGroup.h" +#include "Common/UI/Tween.h" + +namespace UI { + +class ChoiceStrip; +class ScrollView; + +class TabHolder : public LinearLayout { +public: + TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams = 0); + + template + T *AddTab(std::string_view title, T *tabContents) { + AddTabContents(title, tabContents); + return tabContents; + } + void AddTabDeferred(std::string_view title, std::function createCb); + void EnableTab(int tab, bool enabled); + + void AddBack(UIScreen *parent); + + // Returns true if the tab wasn't created before (but is now). + bool SetCurrentTab(int tab, bool skipTween = false); + + int GetCurrentTab() const { return currentTab_; } + std::string DescribeLog() const override { return "TabHolder: " + View::DescribeLog(); } + + void PersistData(PersistStatus status, std::string anonId, PersistMap &storage) override; + + void EnsureAllCreated(); + + LinearLayout *Container() { return tabContainer_; } + + const std::vector &GetTabContentViews() const { + return tabs_; + } + +private: + void AddTabContents(std::string_view title, ViewGroup *tabContents); + void OnTabClick(EventParams &e); + bool EnsureTab(int index); // return true if it actually created a tab. + + View *bannerView_ = nullptr; + LinearLayout *tabContainer_ = nullptr; + ChoiceStrip *tabStrip_ = nullptr; + ScrollView *tabScroll_ = nullptr; + ViewGroup *contents_ = nullptr; + + int currentTab_ = 0; + std::vector tabs_; + std::vector tabTweens_; + std::vector> createFuncs_; +}; + +class ChoiceStrip : public LinearLayout { +public: + ChoiceStrip(Orientation orientation, LayoutParams *layoutParams = 0); + + void AddChoice(std::string_view title); + void AddChoice(ImageID buttonImage); + + int GetSelection() const { return selected_; } + void SetSelection(int sel, bool triggerClick); + + void EnableChoice(int choice, bool enabled); + + bool Key(const KeyInput &input) override; + + void SetTopTabs(bool tabs) { topTabs_ = tabs; } + + std::string DescribeLog() const override { return "ChoiceStrip: " + View::DescribeLog(); } + std::string DescribeText() const override; + + Event OnChoice; + +private: + StickyChoice *Choice(int index); + void OnChoiceClick(EventParams &e); + + int selected_ = 0; // Can be controlled with L/R. + bool topTabs_ = false; +}; + +} // namespace diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp index 7873615367..fd4da98669 100644 --- a/Common/UI/View.cpp +++ b/Common/UI/View.cpp @@ -21,7 +21,6 @@ namespace UI { -static constexpr Size ITEM_HEIGHT = 64.f; static constexpr float MIN_TEXT_SCALE = 0.8f; static constexpr float MAX_ITEM_SIZE = 65535.0f; diff --git a/Common/UI/View.h b/Common/UI/View.h index f617773491..439a28b384 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -139,6 +139,9 @@ typedef float Size; // can also be WRAP_CONTENT or FILL_PARENT. static constexpr Size WRAP_CONTENT = -1.0f; static constexpr Size FILL_PARENT = -2.0f; +// TODO: This needs to move to the theme. +static constexpr Size ITEM_HEIGHT = 64.f; + // Gravity enum Gravity { G_LEFT = 0, diff --git a/Common/UI/ViewGroup.cpp b/Common/UI/ViewGroup.cpp index 099e590d3e..2708b5cbc6 100644 --- a/Common/UI/ViewGroup.cpp +++ b/Common/UI/ViewGroup.cpp @@ -24,8 +24,6 @@ namespace UI { -static constexpr Size ITEM_HEIGHT = 64.f; - void ApplyGravity(const Bounds &outer, const Margins &margins, float w, float h, int gravity, Bounds &inner) { inner.w = w; inner.h = h; @@ -957,300 +955,6 @@ std::string GridLayoutList::DescribeText() const { return DescribeListOrdered(u->T("List:")); } -TabHolder::TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams) - : LinearLayout(Opposite(orientation), layoutParams) { - SetSpacing(0.0f); - if (orientation == ORIENT_HORIZONTAL) { - tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); - tabStrip_->SetTopTabs(true); - tabScroll_ = new ScrollView(orientation, new LayoutParams(FILL_PARENT, WRAP_CONTENT)); - tabScroll_->Add(tabStrip_); - Add(tabScroll_); - } else { - tabContainer_ = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(stripSize, FILL_PARENT)); - tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(FILL_PARENT, FILL_PARENT)); - tabStrip_->SetTopTabs(true); - tabScroll_ = new ScrollView(orientation, new LinearLayoutParams(1.0f)); - tabScroll_->Add(tabStrip_); - tabContainer_->Add(tabScroll_); - Add(tabContainer_); - } - tabStrip_->OnChoice.Handle(this, &TabHolder::OnTabClick); - - Add(new Spacer(4.0f))->SetSeparator(); - - ViewGroup *contentHolder_ = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)); - if (bannerView) { - contentHolder_->Add(bannerView); - bannerView_ = bannerView; - } - contents_ = contentHolder_->Add(new AnchorLayout(new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f))); - contents_->SetClip(true); - Add(contentHolder_); -} - -void TabHolder::AddBack(UIScreen *parent) { - if (tabContainer_) { - auto di = GetI18NCategory(I18NCat::DIALOG); - tabContainer_->Add(new Choice(di->T("Back"), "", false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(0, 0, 10, 10))))->OnClick.Handle(parent, &UIScreen::OnBack); - } -} - -void TabHolder::AddTabContents(std::string_view title, ViewGroup *tabContents) { - tabs_.push_back(tabContents); - tabStrip_->AddChoice(title); - contents_->Add(tabContents); - if (tabs_.size() > 1) - tabContents->SetVisibility(V_GONE); - tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT)); - - // Will be filled in later. - tabTweens_.push_back(nullptr); - // This entry doesn't need one. - createFuncs_.push_back(nullptr); -} - -void TabHolder::AddTabDeferred(std::string_view title, std::function createCb) { - tabs_.push_back(nullptr); // marker - tabStrip_->AddChoice(title); - tabTweens_.push_back(nullptr); - createFuncs_.push_back(createCb); - - if (tabs_.size() == 1) { - EnsureTab(0); - } -} - -void TabHolder::EnsureAllCreated() { - for (int i = 0; i < createFuncs_.size(); i++) { - if (createFuncs_[i]) { - EnsureTab(i); - tabs_[i]->SetVisibility(i == currentTab_ ? V_VISIBLE : V_GONE); - } - } -} - -bool TabHolder::EnsureTab(int index) { - _dbg_assert_(index >= 0 && index < createFuncs_.size()); - - if (!tabs_[index]) { - _dbg_assert_(index < createFuncs_.size()); - _dbg_assert_(createFuncs_[index]); - std::function func; - createFuncs_[index].swap(func); - - ViewGroup *tabContents = func(); - tabs_[index] = tabContents; - contents_->Add(tabContents); - - tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT)); - return true; - } else { - return false; - } -} - -bool TabHolder::SetCurrentTab(int tab, bool skipTween) { - if (tab >= (int)tabs_.size()) { - // Ignore - return false; - } - - bool created = false; - - if (tab != currentTab_) { - _dbg_assert_(tabs_[currentTab_]); // we should always have a tab to switch *from*. - created = EnsureTab(tab); - } - - auto setupTween = [&](View *view, AnchorTranslateTween *&tween) { - _dbg_assert_(view != nullptr); - if (tween) - return; - - tween = new AnchorTranslateTween(0.15f, bezierEaseInOut); - tween->Finish.Add([&](EventParams &e) { - e.v->SetVisibility(tabs_[currentTab_] == e.v ? V_VISIBLE : V_GONE); - }); - view->AddTween(tween)->Persist(); - }; - - if (tab != currentTab_) { - Orientation orient = Opposite(orientation_); - // Direction from which the new tab will come. - float dir = tab < currentTab_ ? -1.0f : 1.0f; - - // First, setup any missing tweens. - setupTween(tabs_[currentTab_], tabTweens_[currentTab_]); - setupTween(tabs_[tab], tabTweens_[tab]); - - // Currently displayed, so let's reset it. - if (skipTween) { - tabs_[currentTab_]->SetVisibility(V_GONE); - tabTweens_[tab]->Reset(Point2D(0.0f, 0.0f)); - tabTweens_[tab]->Apply(tabs_[tab]); - } else { - tabTweens_[currentTab_]->Reset(Point2D(0.0f, 0.0f)); - - if (orient == ORIENT_HORIZONTAL) { - tabTweens_[tab]->Reset(Point2D(bounds_.w * dir, 0.0f)); - tabTweens_[currentTab_]->Divert(Point2D(bounds_.w * -dir, 0.0f)); - } else { - tabTweens_[tab]->Reset(Point2D(0.0f, bounds_.h * dir)); - tabTweens_[currentTab_]->Divert(Point2D(0.0f, bounds_.h * -dir)); - } - // Actually move it to the initial position now, just to avoid any flicker. - tabTweens_[tab]->Apply(tabs_[tab]); - tabTweens_[tab]->Divert(Point2D(0.0f, 0.0f)); - } - tabs_[tab]->SetVisibility(V_VISIBLE); - - currentTab_ = tab; - } - tabStrip_->SetSelection(tab, false); - - return created; -} - -void TabHolder::OnTabClick(EventParams &e) { - // We have e.b set when it was an explicit click action. - // In that case, we make the view gone and then visible - this scrolls scrollviews to the top. - if (e.b != 0) { - EnsureTab(e.a); - SetCurrentTab((int)e.a); - } -} - -void TabHolder::PersistData(PersistStatus status, std::string anonId, PersistMap &storage) { - ViewGroup::PersistData(status, anonId, storage); - - std::string tag = Tag(); - if (tag.empty()) { - tag = anonId; - } - - PersistBuffer &buffer = storage["TabHolder::" + tag]; - switch (status) { - case PERSIST_SAVE: - buffer.resize(1); - buffer[0] = currentTab_; - break; - - case PERSIST_RESTORE: - if (buffer.size() == 1) { - if (SetCurrentTab(buffer[0], true)) { - // Re-run PersistData. TODO: Only need to do it for the new tab. - ViewGroup::PersistData(status, anonId, storage); - } - } - break; - } -} - -ChoiceStrip::ChoiceStrip(Orientation orientation, LayoutParams *layoutParams) - : LinearLayout(orientation, layoutParams) { - SetSpacing(0.0f); -} - -void ChoiceStrip::AddChoice(std::string_view title) { - StickyChoice *c = new StickyChoice(title, "", - orientation_ == ORIENT_HORIZONTAL ? - nullptr : - new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT)); - c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick); - Add(c); - if (selected_ == (int)views_.size() - 1) - c->Press(); -} - -void ChoiceStrip::AddChoice(ImageID buttonImage) { - StickyChoice *c = new StickyChoice(buttonImage, - orientation_ == ORIENT_HORIZONTAL ? - nullptr : - new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT)); - c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick); - Add(c); - if (selected_ == (int)views_.size() - 1) - c->Press(); -} - -void ChoiceStrip::OnChoiceClick(EventParams &e) { - // Unstick the other choices that weren't clicked. - for (int i = 0; i < (int)views_.size(); i++) { - if (views_[i] != e.v) { - Choice(i)->Release(); - } else { - selected_ = i; - } - } - - EventParams e2{}; - e2.v = views_[selected_]; - e2.a = selected_; - // Set to 1 to indicate an explicit click. - e2.b = 1; - // Dispatch immediately (we're already on the UI thread as we're in an event handler). - OnChoice.Dispatch(e2); -} - -void ChoiceStrip::SetSelection(int sel, bool triggerClick) { - int prevSelected = selected_; - StickyChoice *prevChoice = Choice(selected_); - if (prevChoice) - prevChoice->Release(); - selected_ = sel; - StickyChoice *newChoice = Choice(selected_); - if (newChoice) { - newChoice->Press(); - - if (topTabs_ && prevSelected != selected_) { - EventParams e{}; - e.v = views_[selected_]; - e.a = selected_; - // Set to 0 to indicate a selection change (not a click.) - e.b = triggerClick ? 1 : 0; - OnChoice.Trigger(e); - } - } -} - -void ChoiceStrip::EnableChoice(int choice, bool enabled) { - if (choice < (int)views_.size()) { - Choice(choice)->SetEnabled(enabled); - } -} - -bool ChoiceStrip::Key(const KeyInput &input) { - bool ret = false; - if (topTabs_ && (input.flags & KEY_DOWN)) { - if (IsTabLeftKey(input)) { - if (selected_ > 0) { - SetSelection(selected_ - 1, true); - UI::PlayUISound(UI::UISound::TOGGLE_OFF); // Maybe make specific sounds for this at some point? - } - ret = true; - } else if (IsTabRightKey(input)) { - if (selected_ < (int)views_.size() - 1) { - SetSelection(selected_ + 1, true); - UI::PlayUISound(UI::UISound::TOGGLE_ON); - } - ret = true; - } - } - return ret || ViewGroup::Key(input); -} - -std::string ChoiceStrip::DescribeText() const { - auto u = GetI18NCategory(I18NCat::UI_ELEMENTS); - return DescribeListUnordered(u->T("Choices:")); -} - -StickyChoice *ChoiceStrip::Choice(int index) { - if ((size_t)index < views_.size()) - return static_cast(views_[index]); - return nullptr; -} - CollapsibleSection::CollapsibleSection(std::string_view title, LayoutParams *layoutParams) : LinearLayout(ORIENT_VERTICAL, layoutParams) { open_ = &localOpen_; SetSpacing(0.0f); diff --git a/Common/UI/ViewGroup.h b/Common/UI/ViewGroup.h index 55fab65fd7..ac1ad27955 100644 --- a/Common/UI/ViewGroup.h +++ b/Common/UI/ViewGroup.h @@ -260,84 +260,6 @@ public: std::string DescribeText() const override; }; -class ChoiceStrip : public LinearLayout { -public: - ChoiceStrip(Orientation orientation, LayoutParams *layoutParams = 0); - - void AddChoice(std::string_view title); - void AddChoice(ImageID buttonImage); - - int GetSelection() const { return selected_; } - void SetSelection(int sel, bool triggerClick); - - void EnableChoice(int choice, bool enabled); - - bool Key(const KeyInput &input) override; - - void SetTopTabs(bool tabs) { topTabs_ = tabs; } - - std::string DescribeLog() const override { return "ChoiceStrip: " + View::DescribeLog(); } - std::string DescribeText() const override; - - Event OnChoice; - -private: - StickyChoice *Choice(int index); - void OnChoiceClick(EventParams &e); - - int selected_ = 0; // Can be controlled with L/R. - bool topTabs_ = false; -}; - -class TabHolder : public LinearLayout { -public: - TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams = 0); - - template - T *AddTab(std::string_view title, T *tabContents) { - AddTabContents(title, tabContents); - return tabContents; - } - void AddTabDeferred(std::string_view title, std::function createCb); - void EnableTab(int tab, bool enabled) { - tabStrip_->EnableChoice(tab, enabled); - } - - void AddBack(UIScreen *parent); - - // Returns true if the tab wasn't created before (but is now). - bool SetCurrentTab(int tab, bool skipTween = false); - - int GetCurrentTab() const { return currentTab_; } - std::string DescribeLog() const override { return "TabHolder: " + View::DescribeLog(); } - - void PersistData(PersistStatus status, std::string anonId, PersistMap &storage) override; - - void EnsureAllCreated(); - - LinearLayout *Container() { return tabContainer_; } - - const std::vector &GetTabContentViews() const { - return tabs_; - } - -private: - void AddTabContents(std::string_view title, ViewGroup *tabContents); - void OnTabClick(EventParams &e); - bool EnsureTab(int index); // return true if it actually created a tab. - - View *bannerView_ = nullptr; - LinearLayout *tabContainer_ = nullptr; - ChoiceStrip *tabStrip_ = nullptr; - ScrollView *tabScroll_ = nullptr; - ViewGroup *contents_ = nullptr; - - int currentTab_ = 0; - std::vector tabs_; - std::vector tabTweens_; - std::vector> createFuncs_; -}; - class CollapsibleHeader; class CollapsibleSection : public LinearLayout { diff --git a/UI/DisplayLayoutScreen.cpp b/UI/DisplayLayoutScreen.cpp index 07241bcb26..89cda23fc7 100644 --- a/UI/DisplayLayoutScreen.cpp +++ b/UI/DisplayLayoutScreen.cpp @@ -24,8 +24,8 @@ #include "Common/UI/Context.h" #include "Common/UI/View.h" #include "Common/UI/UIScreen.h" +#include "Common/UI/TabHolder.h" #include "Common/Math/math_util.h" -#include "Common/System/Display.h" #include "Common/System/NativeApp.h" #include "Common/VR/PPSSPPVR.h" #include "Common/StringUtils.h" diff --git a/UI/DisplayLayoutScreen.h b/UI/DisplayLayoutScreen.h index 09a6be40f9..be6e875770 100644 --- a/UI/DisplayLayoutScreen.h +++ b/UI/DisplayLayoutScreen.h @@ -25,6 +25,10 @@ #include "MiscScreens.h" +namespace UI { +class ChoiceStrip; +} + class DisplayLayoutScreen : public UIDialogScreenWithGameBackground { public: DisplayLayoutScreen(const Path &filename); diff --git a/UI/DriverManagerScreen.cpp b/UI/DriverManagerScreen.cpp index 65be97bd9e..3454139aeb 100644 --- a/UI/DriverManagerScreen.cpp +++ b/UI/DriverManagerScreen.cpp @@ -7,7 +7,7 @@ #include "Core/Config.h" #include "Core/System.h" -#include "UI/View.h" +#include "Common/UI/View.h" #include "UI/DriverManagerScreen.h" #include "UI/GameSettingsScreen.h" // for triggerrestart #include "UI/OnScreenDisplay.h" @@ -79,8 +79,6 @@ public: std::string name_; }; -static constexpr UI::Size ITEM_HEIGHT = 64.f; - DriverChoice::DriverChoice(const std::string &driverName, bool current, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_VERTICAL, layoutParams), name_(driverName) { using namespace UI; SetSpacing(2.0f); diff --git a/UI/GPUDriverTestScreen.h b/UI/GPUDriverTestScreen.h index 6f27f855be..7421603de8 100644 --- a/UI/GPUDriverTestScreen.h +++ b/UI/GPUDriverTestScreen.h @@ -4,6 +4,7 @@ #include "Common/UI/Context.h" #include "Common/UI/View.h" #include "Common/UI/ViewGroup.h" +#include "Common/UI/TabHolder.h" #include "Common/Log.h" #include "UI/MiscScreens.h" diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 08dea7a9ac..1a683133d6 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -936,8 +936,6 @@ public: MacAddressChooser(RequesterToken token, Path gamePath, std::string *value, std::string_view title, ScreenManager *screenManager, UI::LayoutParams *layoutParams = nullptr); }; -static constexpr UI::Size ITEM_HEIGHT = 64.f; - MacAddressChooser::MacAddressChooser(RequesterToken token, Path gamePath_, std::string *value, std::string_view title, ScreenManager *screenManager, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_HORIZONTAL, layoutParams) { using namespace UI; SetSpacing(5.0f); diff --git a/UI/MainScreen.h b/UI/MainScreen.h index a812bc48aa..9c31fbcbee 100644 --- a/UI/MainScreen.h +++ b/UI/MainScreen.h @@ -23,6 +23,7 @@ #include "Common/File/Path.h" #include "Common/UI/UIScreen.h" #include "Common/UI/ViewGroup.h" +#include "Common/UI/TabHolder.h" #include "UI/MiscScreens.h" #include "Common/File/PathBrowser.h" @@ -134,6 +135,7 @@ protected: void CreateViews() override; void CreateRecentTab(); GameBrowser *CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, bool *bGridView, float *scrollPos); + UI::ViewGroup *CreateButtons(bool vertical); void DrawBackground(UIContext &dc) override; void update() override; diff --git a/UI/RetroAchievementScreens.cpp b/UI/RetroAchievementScreens.cpp index eaefaa8204..40f2bf7560 100644 --- a/UI/RetroAchievementScreens.cpp +++ b/UI/RetroAchievementScreens.cpp @@ -2,6 +2,7 @@ #include "Common/System/Request.h" #include "Common/UI/View.h" #include "Common/UI/ViewGroup.h" +#include "Common/UI/TabHolder.h" #include "Common/UI/Context.h" #include "Common/Data/Text/I18n.h" #include "Common/UI/IconCache.h" @@ -26,8 +27,6 @@ public: UI::UISound sound_; }; -static constexpr UI::Size ITEM_HEIGHT = 64.f; - AudioFileChooser::AudioFileChooser(RequesterToken token, std::string *value, std::string_view title, UI::UISound sound, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_HORIZONTAL, layoutParams), sound_(sound) { using namespace UI; SetSpacing(2.0f); diff --git a/UI/TabbedDialogScreen.cpp b/UI/TabbedDialogScreen.cpp index e34c06f444..07346a2c78 100644 --- a/UI/TabbedDialogScreen.cpp +++ b/UI/TabbedDialogScreen.cpp @@ -4,6 +4,7 @@ #include "Common/System/NativeApp.h" #include "Common/System/Request.h" #include "Common/System/Display.h" +#include "Common/UI/TabHolder.h" #include "UI/TabbedDialogScreen.h" void TabbedUIDialogScreenWithGameBackground::AddTab(const char *tag, std::string_view title, std::function createCallback, bool isSearch) { diff --git a/UI/TabbedDialogScreen.h b/UI/TabbedDialogScreen.h index 436bb45ffa..a856acbf38 100644 --- a/UI/TabbedDialogScreen.h +++ b/UI/TabbedDialogScreen.h @@ -8,6 +8,10 @@ #include "Core/ConfigValues.h" #include "UI/MiscScreens.h" +namespace UI { +class TabHolder; +} + class TabbedUIDialogScreenWithGameBackground : public UIDialogScreenWithGameBackground { public: TabbedUIDialogScreenWithGameBackground(const Path &gamePath) : UIDialogScreenWithGameBackground(gamePath) {} diff --git a/UI/TouchControlLayoutScreen.h b/UI/TouchControlLayoutScreen.h index eb33a0799e..105463a69f 100644 --- a/UI/TouchControlLayoutScreen.h +++ b/UI/TouchControlLayoutScreen.h @@ -19,6 +19,7 @@ #include "Common/UI/View.h" #include "Common/UI/ViewGroup.h" +#include "Common/UI/TabHolder.h" #include "MiscScreens.h" class ControlLayoutView; diff --git a/UI/TouchControlVisibilityScreen.cpp b/UI/TouchControlVisibilityScreen.cpp index b6b3ed71ac..4ad7c9a67a 100644 --- a/UI/TouchControlVisibilityScreen.cpp +++ b/UI/TouchControlVisibilityScreen.cpp @@ -15,6 +15,7 @@ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. +#include "Common/UI/TabHolder.h" #include "Common/Render/TextureAtlas.h" #include "Common/Data/Text/I18n.h" #include "Common/StringUtils.h" diff --git a/UI/UIAtlas.cpp b/UI/UIAtlas.cpp index f468b66137..854acafce5 100644 --- a/UI/UIAtlas.cpp +++ b/UI/UIAtlas.cpp @@ -129,6 +129,11 @@ static const ImageMeta imageIDs[] = { {"I_PIN", false}, {"I_UNPIN", false}, {"I_FOLDER_PINNED", false}, + {"I_FILLED_CIRCLE_1", false}, + {"I_FILLED_CIRCLE_2", false}, + {"I_FILLED_CIRCLE_3", false}, + {"I_FILLED_CIRCLE_4", false}, + {"I_FILLED_CIRCLE_5", false}, }; static std::string PNGNameFromID(std::string_view id) { diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg index 7a9db667e4..00ccb9713a 100644 --- a/assets/ui_images/images.svg +++ b/assets/ui_images/images.svg @@ -7,14 +7,14 @@ viewBox="0 0 158.75 158.75" version="1.1" id="svg1" - inkscape:version="1.4 (86a8ad7, 2024-10-11)" + inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)" sodipodi:docname="images.svg" + xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + +Layer 1 diff --git a/assets/ui_images/svg_sources.txt b/assets/ui_images/svg_sources.txt new file mode 100644 index 0000000000..6a34956285 --- /dev/null +++ b/assets/ui_images/svg_sources.txt @@ -0,0 +1,5 @@ +Most of the vectors in images.svg are custom drawn by hrydgard and NABN00B. + +Some are derived from public domain SVGs from the following sites: + +www.svgrepo.com From 7b743071d212a2b45fbf4b976e4d3564440a4d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 Oct 2025 12:46:07 +0200 Subject: [PATCH 4/5] Prepare for adding more sensible back buttons to TabbedDialogScreen portrait mode --- CMakeLists.txt | 2 + Common/UI/TabHolder.cpp | 2 +- Common/UI/TabHolder.h | 8 ++- UI/DevScreens.cpp | 4 +- UI/GPUDriverTestScreen.cpp | 2 +- UI/GameSettingsScreen.cpp | 2 +- UI/MainScreen.cpp | 4 +- UI/RemoteISOScreen.cpp | 2 +- UI/TabbedDialogScreen.cpp | 4 +- UI/TouchControlVisibilityScreen.cpp | 4 +- UI/UIAtlas.cpp | 3 + UWP/CommonUWP/CommonUWP.vcxproj | 2 + UWP/CommonUWP/CommonUWP.vcxproj.filters | 6 ++ android/jni/Android.mk | 1 + assets/ui_images/images.svg | 77 +++++++++++++++++-------- 15 files changed, 86 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e4bd4a582a..7efddc6b3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -884,6 +884,8 @@ add_library(Common STATIC Common/UI/ViewGroup.h Common/UI/ScrollView.cpp Common/UI/ScrollView.h + Common/UI/TabHolder.cpp + Common/UI/TabHolder.h Common/UI/PopupScreens.cpp Common/UI/PopupScreens.h Common/BitScan.h diff --git a/Common/UI/TabHolder.cpp b/Common/UI/TabHolder.cpp index 670c35debd..43605d99f2 100644 --- a/Common/UI/TabHolder.cpp +++ b/Common/UI/TabHolder.cpp @@ -8,7 +8,7 @@ namespace UI { -TabHolder::TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams) +TabHolder::TabHolder(Orientation orientation, float stripSize, TabHolderFlags flags, View *bannerView, LayoutParams *layoutParams) : LinearLayout(Opposite(orientation), layoutParams) { SetSpacing(0.0f); if (orientation == ORIENT_HORIZONTAL) { diff --git a/Common/UI/TabHolder.h b/Common/UI/TabHolder.h index 9f098c60ba..00c4e70120 100644 --- a/Common/UI/TabHolder.h +++ b/Common/UI/TabHolder.h @@ -11,9 +11,15 @@ namespace UI { class ChoiceStrip; class ScrollView; +enum class TabHolderFlags { + Default = 0, + BackButton = 1, +}; +ENUM_CLASS_BITOPS(TabHolderFlags); + class TabHolder : public LinearLayout { public: - TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams = 0); + TabHolder(Orientation orientation, float stripSize, TabHolderFlags flags, View *bannerView, LayoutParams *layoutParams); template T *AddTab(std::string_view title, T *tabContents) { diff --git a/UI/DevScreens.cpp b/UI/DevScreens.cpp index 6711f1c0a9..234562993d 100644 --- a/UI/DevScreens.cpp +++ b/UI/DevScreens.cpp @@ -474,7 +474,7 @@ void ShaderListScreen::CreateViews() { LinearLayout *layout = new LinearLayout(ORIENT_VERTICAL); root_ = layout; - tabs_ = new TabHolder(ORIENT_HORIZONTAL, 40, nullptr, new LinearLayoutParams(1.0)); + tabs_ = new TabHolder(ORIENT_HORIZONTAL, 40, TabHolderFlags::Default, nullptr, new LinearLayoutParams(1.0)); tabs_->SetTag("DevShaderList"); layout->Add(tabs_); layout->Add(new Button(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack); @@ -549,7 +549,7 @@ void FrameDumpTestScreen::CreateViews() { auto di = GetI18NCategory(I18NCat::DIALOG); TabHolder *tabHolder; - tabHolder = new TabHolder(ORIENT_VERTICAL, 200, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false)); + tabHolder = new TabHolder(ORIENT_VERTICAL, 200, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false)); root_->Add(tabHolder); tabHolder->AddBack(this); tabHolder->SetTag("DumpTypes"); diff --git a/UI/GPUDriverTestScreen.cpp b/UI/GPUDriverTestScreen.cpp index 9277271dc3..b9993af4ea 100644 --- a/UI/GPUDriverTestScreen.cpp +++ b/UI/GPUDriverTestScreen.cpp @@ -301,7 +301,7 @@ void GPUDriverTestScreen::CreateViews() { AnchorLayout *anchor = new AnchorLayout(); root_ = anchor; - tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 30.0f, nullptr, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT, false)); + tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 30.0f, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT, false)); anchor->Add(tabHolder_); tabHolder_->AddTab("Discard", new LinearLayout(ORIENT_VERTICAL)); tabHolder_->AddTab("Shader", new LinearLayout(ORIENT_VERTICAL)); diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 1a683133d6..8252943932 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -2043,7 +2043,7 @@ void GestureMappingScreen::CreateViews() { root_ = new AnchorLayout(new LayoutParams(FILL_PARENT, FILL_PARENT)); AddStandardBack(root_); - TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, 200, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false)); + TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, 200, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false)); root_->Add(tabHolder); ScrollView *rightPanel = new ScrollView(ORIENT_VERTICAL); tabHolder->AddTab(co->T("Gesture"), rightPanel); diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index e94295e0bf..ccb655f860 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -1131,7 +1131,7 @@ void MainScreen::CreateViews() { auto mm = GetI18NCategory(I18NCat::MAINMENU); - tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 64, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f)); + tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 64, TabHolderFlags::Default, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f)); ViewGroup *leftColumn = tabHolder_; tabHolder_->SetTag("MainScreenGames"); gameBrowsers_.clear(); @@ -1568,7 +1568,7 @@ void UmdReplaceScreen::CreateViews() { auto mm = GetI18NCategory(I18NCat::MAINMENU); auto di = GetI18NCategory(I18NCat::DIALOG); - TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0)); + TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, TabHolderFlags::Default, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0)); leftColumn->SetTag("UmdReplace"); leftColumn->SetClip(true); diff --git a/UI/RemoteISOScreen.cpp b/UI/RemoteISOScreen.cpp index 6ccf857118..76a4fa84d2 100644 --- a/UI/RemoteISOScreen.cpp +++ b/UI/RemoteISOScreen.cpp @@ -595,7 +595,7 @@ void RemoteISOBrowseScreen::CreateViews() { using namespace UI; - TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); + TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, TabHolderFlags::Default, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); tabHolder_ = leftColumn; tabHolder_->SetTag("RemoteGames"); gameBrowsers_.clear(); diff --git a/UI/TabbedDialogScreen.cpp b/UI/TabbedDialogScreen.cpp index 07346a2c78..09617ea8cc 100644 --- a/UI/TabbedDialogScreen.cpp +++ b/UI/TabbedDialogScreen.cpp @@ -40,13 +40,13 @@ void TabbedUIDialogScreenWithGameBackground::CreateViews() { if (vertical) { auto di = GetI18NCategory(I18NCat::DIALOG); LinearLayout *verticalLayout = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(FILL_PARENT, FILL_PARENT)); - tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 200, filterNotice_, new LinearLayoutParams(1.0f)); + tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 200, TabHolderFlags::BackButton, filterNotice_, new LinearLayoutParams(1.0f)); verticalLayout->Add(tabHolder_); CreateExtraButtons(verticalLayout, 0); verticalLayout->Add(new Choice(di->T("Back"), "", false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(10, 0))))->OnClick.Handle(this, &UIScreen::OnBack); root_->Add(verticalLayout); } else { - tabHolder_ = new TabHolder(ORIENT_VERTICAL, 200, filterNotice_, new AnchorLayoutParams(10, 0, 10, 0, false)); + tabHolder_ = new TabHolder(ORIENT_VERTICAL, 200, TabHolderFlags::Default, filterNotice_, new AnchorLayoutParams(10, 0, 10, 0, false)); CreateExtraButtons(tabHolder_->Container(), 10); tabHolder_->AddBack(this); root_->Add(tabHolder_); diff --git a/UI/TouchControlVisibilityScreen.cpp b/UI/TouchControlVisibilityScreen.cpp index 4ad7c9a67a..161f287b5a 100644 --- a/UI/TouchControlVisibilityScreen.cpp +++ b/UI/TouchControlVisibilityScreen.cpp @@ -58,7 +58,7 @@ void TouchControlVisibilityScreen::CreateViews() { Choice *toggleAll = new Choice(di->T("Toggle All"), "", false, new AnchorLayoutParams(leftColumnWidth - 10, WRAP_CONTENT, 10, NONE, NONE, 84)); root_->Add(toggleAll)->OnClick.Handle(this, &TouchControlVisibilityScreen::OnToggleAll); - TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, leftColumnWidth, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false)); + TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, leftColumnWidth, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false)); tabHolder->SetTag("TouchControlVisibility"); root_->Add(tabHolder); ScrollView *rightPanel = new ScrollView(ORIENT_VERTICAL); @@ -143,7 +143,7 @@ void RightAnalogMappingScreen::CreateViews() { root_ = new AnchorLayout(new LayoutParams(FILL_PARENT, FILL_PARENT)); Choice *back = new Choice(di->T("Back"), "", false, new AnchorLayoutParams(leftColumnWidth - 10, WRAP_CONTENT, 10, NONE, NONE, 10)); root_->Add(back)->OnClick.Handle(this, &UIScreen::OnBack); - TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, leftColumnWidth, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false)); + TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, leftColumnWidth, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false)); root_->Add(tabHolder); ScrollView *rightPanel = new ScrollView(ORIENT_VERTICAL); tabHolder->AddTab(co->T("Binds"), rightPanel); diff --git a/UI/UIAtlas.cpp b/UI/UIAtlas.cpp index 854acafce5..1ddceec2fb 100644 --- a/UI/UIAtlas.cpp +++ b/UI/UIAtlas.cpp @@ -134,6 +134,9 @@ static const ImageMeta imageIDs[] = { {"I_FILLED_CIRCLE_3", false}, {"I_FILLED_CIRCLE_4", false}, {"I_FILLED_CIRCLE_5", false}, + {"I_SETTINGS_DISPLAY", false}, + {"I_NAVIGATE_BACK", false}, + {"I_NAVIGATE_FORWARD", false}, }; static std::string PNGNameFromID(std::string_view id) { diff --git a/UWP/CommonUWP/CommonUWP.vcxproj b/UWP/CommonUWP/CommonUWP.vcxproj index 7825745660..16413f3310 100644 --- a/UWP/CommonUWP/CommonUWP.vcxproj +++ b/UWP/CommonUWP/CommonUWP.vcxproj @@ -221,6 +221,7 @@ + @@ -384,6 +385,7 @@ + diff --git a/UWP/CommonUWP/CommonUWP.vcxproj.filters b/UWP/CommonUWP/CommonUWP.vcxproj.filters index a6315e5801..01e33d6961 100644 --- a/UWP/CommonUWP/CommonUWP.vcxproj.filters +++ b/UWP/CommonUWP/CommonUWP.vcxproj.filters @@ -523,6 +523,9 @@ Render + + UI + @@ -1005,6 +1008,9 @@ Render + + UI + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index c74c4c410b..519895951a 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -365,6 +365,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Common/UI/View.cpp \ $(SRC)/Common/UI/ViewGroup.cpp \ $(SRC)/Common/UI/ScrollView.cpp \ + $(SRC)/Common/UI/TabHolder.cpp \ $(SRC)/Common/UI/PopupScreens.cpp \ $(SRC)/Common/Serialize/Serializer.cpp \ $(SRC)/Common/ArmCPUDetect.cpp \ diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg index 00ccb9713a..b119c3eb35 100644 --- a/assets/ui_images/images.svg +++ b/assets/ui_images/images.svg @@ -24,9 +24,9 @@ inkscape:pagecheckerboard="true" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="2.0000001" - inkscape:cx="467.24999" - inkscape:cy="319.24999" + inkscape:zoom="8.0000002" + inkscape:cx="328.24999" + inkscape:cy="324.43749" inkscape:window-width="3840" inkscape:window-height="2071" inkscape:window-x="-9" @@ -34,7 +34,39 @@ inkscape:window-maximized="1" inkscape:current-layer="layer1" showgrid="false" /> + id="I_SETTINGS_DISPLAY" + transform="matrix(1.7225478,0,0,1.7225478,-72.335968,31.982012)"> Layer 1 + id="title1">Layer 1 From 1514df8e3a66b80a4376d44457656bbb1f51eab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 23 Oct 2025 12:59:40 +0200 Subject: [PATCH 5/5] Add a much more usable back button to Settings screen in portrait mode --- Common/UI/Root.cpp | 13 +++++++--- Common/UI/Root.h | 5 ++-- Common/UI/Screen.h | 1 + Common/UI/TabHolder.cpp | 52 ++++++++++++++++++++++--------------- Common/UI/TabHolder.h | 3 ++- Common/UI/UIScreen.cpp | 5 +++- Common/UI/View.h | 8 +++--- UI/TabbedDialogScreen.cpp | 1 - assets/ui_images/images.svg | 15 +++++++---- 9 files changed, 65 insertions(+), 38 deletions(-) diff --git a/Common/UI/Root.cpp b/Common/UI/Root.cpp index 1054d75cbf..2007652855 100644 --- a/Common/UI/Root.cpp +++ b/Common/UI/Root.cpp @@ -31,7 +31,8 @@ void EventTriggered(Event *e, EventParams params) { g_dispatchQueue.push_front(item); } -void DispatchEvents() { +DialogResult DispatchEvents() { + DialogResult result = DR_NONE; while (!g_dispatchQueue.empty()) { DispatchQueueItem item; if (g_dispatchQueue.empty()) @@ -40,8 +41,12 @@ void DispatchEvents() { g_dispatchQueue.pop_back(); if (item.e) { item.e->Dispatch(item.params); + if (item.params.bubbleResult != DR_NONE) { + result = item.params.bubbleResult; + } } } + return result; } void RemoveQueuedEventsByView(View *view) { @@ -363,13 +368,13 @@ restart: } } -void UpdateViewHierarchy(ViewGroup *root) { +DialogResult UpdateViewHierarchy(ViewGroup *root) { ProcessHeldKeys(root); frameCount++; if (!root) { ERROR_LOG(Log::UI, "Tried to update a view hierarchy from a zero pointer root"); - return; + return DR_NONE; } if (focusMoves.size()) { @@ -402,7 +407,7 @@ void UpdateViewHierarchy(ViewGroup *root) { } root->Update(); - DispatchEvents(); + return DispatchEvents(); } } // namespace UI diff --git a/Common/UI/Root.h b/Common/UI/Root.h index 625204b774..90c50811ae 100644 --- a/Common/UI/Root.h +++ b/Common/UI/Root.h @@ -4,6 +4,7 @@ #include "Common/UI/Context.h" #include "Common/Input/InputState.h" +#include "Common/UI/Screen.h" namespace UI { @@ -17,12 +18,12 @@ void RemoveQueuedEventsByEvent(Event *e); void RemoveQueuedEventsByView(View * v); void EventTriggered(Event *e, EventParams params); -void DispatchEvents(); +DialogResult DispatchEvents(); class ViewGroup; void LayoutViewHierarchy(const UIContext &dc, ViewGroup *root, bool ignoreInsets); -void UpdateViewHierarchy(ViewGroup *root); +DialogResult UpdateViewHierarchy(ViewGroup *root); enum class KeyEventResult { IGNORE_KEY, // Don't let it be processed. diff --git a/Common/UI/Screen.h b/Common/UI/Screen.h index 447e86ac1a..b7486e6e3f 100644 --- a/Common/UI/Screen.h +++ b/Common/UI/Screen.h @@ -28,6 +28,7 @@ namespace UI { } enum DialogResult { + DR_NONE, DR_OK, DR_CANCEL, DR_YES, diff --git a/Common/UI/TabHolder.cpp b/Common/UI/TabHolder.cpp index 43605d99f2..073df5b3b9 100644 --- a/Common/UI/TabHolder.cpp +++ b/Common/UI/TabHolder.cpp @@ -12,8 +12,12 @@ TabHolder::TabHolder(Orientation orientation, float stripSize, TabHolderFlags fl : LinearLayout(Opposite(orientation), layoutParams) { SetSpacing(0.0f); if (orientation == ORIENT_HORIZONTAL) { + // This orientation supports adding a back button. tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); tabStrip_->SetTopTabs(true); + if (flags & TabHolderFlags::BackButton) { + tabStrip_->AddBackButton(); + } tabScroll_ = new ScrollView(orientation, new LayoutParams(FILL_PARENT, WRAP_CONTENT)); tabScroll_->Add(tabStrip_); Add(tabScroll_); @@ -215,7 +219,8 @@ void ChoiceStrip::AddChoice(std::string_view title) { new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT)); c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick); Add(c); - if (selected_ == (int)views_.size() - 1) + choices_.push_back(c); + if (selected_ == (int)choices_.size() - 1) c->Press(); } @@ -226,22 +231,33 @@ void ChoiceStrip::AddChoice(ImageID buttonImage) { new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT)); c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick); Add(c); - if (selected_ == (int)views_.size() - 1) + choices_.push_back(c); + if (selected_ == (int)choices_.size() - 1) c->Press(); } +void ChoiceStrip::AddBackButton() { + Choice *c = new Choice(ImageID("I_NAVIGATE_BACK")); + c->OnClick.Add([](EventParams &e) { + e.bubbleResult = DR_BACK; + }); + // c->OnClick.Handle(this, &UIScreen::OnBack); + Add(c); + Add(new Spacer(5.0f)); +} + void ChoiceStrip::OnChoiceClick(EventParams &e) { // Unstick the other choices that weren't clicked. - for (int i = 0; i < (int)views_.size(); i++) { - if (views_[i] != e.v) { - Choice(i)->Release(); + for (int i = 0; i < (int)choices_.size(); i++) { + if (choices_[i] != e.v) { + choices_[i]->Release(); } else { selected_ = i; } } EventParams e2{}; - e2.v = views_[selected_]; + e2.v = selected_ < choices_.size() ? choices_[selected_] : nullptr; e2.a = selected_; // Set to 1 to indicate an explicit click. e2.b = 1; @@ -251,17 +267,17 @@ void ChoiceStrip::OnChoiceClick(EventParams &e) { void ChoiceStrip::SetSelection(int sel, bool triggerClick) { int prevSelected = selected_; - StickyChoice *prevChoice = Choice(selected_); - if (prevChoice) + if (selected_ < choices_.size()) { + StickyChoice *prevChoice = choices_[selected_]; prevChoice->Release(); + } selected_ = sel; - StickyChoice *newChoice = Choice(selected_); - if (newChoice) { + if (selected_ < choices_.size()) { + StickyChoice *newChoice = choices_[selected_]; newChoice->Press(); - if (topTabs_ && prevSelected != selected_) { EventParams e{}; - e.v = views_[selected_]; + e.v = choices_[selected_]; e.a = selected_; // Set to 0 to indicate a selection change (not a click.) e.b = triggerClick ? 1 : 0; @@ -271,8 +287,8 @@ void ChoiceStrip::SetSelection(int sel, bool triggerClick) { } void ChoiceStrip::EnableChoice(int choice, bool enabled) { - if (choice < (int)views_.size()) { - Choice(choice)->SetEnabled(enabled); + if (choice < (int)choices_.size()) { + choices_[choice]->SetEnabled(enabled); } } @@ -286,7 +302,7 @@ bool ChoiceStrip::Key(const KeyInput &input) { } ret = true; } else if (IsTabRightKey(input)) { - if (selected_ < (int)views_.size() - 1) { + if (selected_ < (int)choices_.size() - 1) { SetSelection(selected_ + 1, true); UI::PlayUISound(UI::UISound::TOGGLE_ON); } @@ -301,10 +317,4 @@ std::string ChoiceStrip::DescribeText() const { return DescribeListUnordered(u->T("Choices:")); } -StickyChoice *ChoiceStrip::Choice(int index) { - if ((size_t)index < views_.size()) - return static_cast(views_[index]); - return nullptr; -} - } // namespace UI diff --git a/Common/UI/TabHolder.h b/Common/UI/TabHolder.h index 00c4e70120..1e6c6496e3 100644 --- a/Common/UI/TabHolder.h +++ b/Common/UI/TabHolder.h @@ -70,6 +70,7 @@ public: void AddChoice(std::string_view title); void AddChoice(ImageID buttonImage); + void AddBackButton(); int GetSelection() const { return selected_; } void SetSelection(int sel, bool triggerClick); @@ -86,9 +87,9 @@ public: Event OnChoice; private: - StickyChoice *Choice(int index); void OnChoiceClick(EventParams &e); + std::vector choices_; int selected_ = 0; // Can be controlled with L/R. bool topTabs_ = false; }; diff --git a/Common/UI/UIScreen.cpp b/Common/UI/UIScreen.cpp index c67bb709b9..e0fa4b483f 100644 --- a/Common/UI/UIScreen.cpp +++ b/Common/UI/UIScreen.cpp @@ -149,7 +149,10 @@ void UIScreen::update() { DoRecreateViews(); if (root_) { - UpdateViewHierarchy(root_); + DialogResult result = UpdateViewHierarchy(root_); + if (result != DR_NONE) { + TriggerFinish(result); + } } while (true) { diff --git a/Common/UI/View.h b/Common/UI/View.h index 439a28b384..22c429d9eb 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -22,6 +22,7 @@ #include "Common/Input/KeyCodes.h" #include "Common/Common.h" +#include "Common/UI/Screen.h" // for DialogResult #undef small @@ -243,10 +244,11 @@ struct MeasureSpec { // Should cover all bases. struct EventParams { - View *v; - uint32_t a, b, x, y; - float f; + View *v = nullptr; + uint32_t a = 0, b = 0, x = 0, y = 0; + float f = 0.0f; std::string s; + DialogResult bubbleResult = DialogResult::DR_NONE; }; typedef std::function EventCallback; diff --git a/UI/TabbedDialogScreen.cpp b/UI/TabbedDialogScreen.cpp index 09617ea8cc..2b5c142c50 100644 --- a/UI/TabbedDialogScreen.cpp +++ b/UI/TabbedDialogScreen.cpp @@ -43,7 +43,6 @@ void TabbedUIDialogScreenWithGameBackground::CreateViews() { tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 200, TabHolderFlags::BackButton, filterNotice_, new LinearLayoutParams(1.0f)); verticalLayout->Add(tabHolder_); CreateExtraButtons(verticalLayout, 0); - verticalLayout->Add(new Choice(di->T("Back"), "", false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(10, 0))))->OnClick.Handle(this, &UIScreen::OnBack); root_->Add(verticalLayout); } else { tabHolder_ = new TabHolder(ORIENT_VERTICAL, 200, TabHolderFlags::Default, filterNotice_, new AnchorLayoutParams(10, 0, 10, 0, false)); diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg index b119c3eb35..b65c4eaaa9 100644 --- a/assets/ui_images/images.svg +++ b/assets/ui_images/images.svg @@ -24,9 +24,9 @@ inkscape:pagecheckerboard="true" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="8.0000002" - inkscape:cx="328.24999" - inkscape:cy="324.43749" + inkscape:zoom="22.627418" + inkscape:cx="265.69537" + inkscape:cy="320.8276" inkscape:window-width="3840" inkscape:window-height="2071" inkscape:window-x="-9" @@ -4186,9 +4186,14 @@ id="title1">Layer 1 + d="m 77.896652,80.722753 2.657871,2.704643 -2.552633,2.645661 -0.727756,-0.714513 1.41025,-1.409734 h -3.050491 c -1.344769,0 -2.424142,1.079372 -2.424142,2.424142 v 0.304891 c 0,1.27712 1.024032,2.300634 2.301153,2.300634 h 3.379638 v 1.042314 h -3.379638 c -1.840515,0 -3.342949,-1.502434 -3.342949,-3.342948 v -0.304891 c 0,-1.908164 1.557775,-3.467489 3.465938,-3.467489 h 3.049974 l -1.444812,-1.491584 z" + sodipodi:nodetypes="cccccssssccssssccc" />