From 3f766c2545ec3d024b2d08830a303755e818fa80 Mon Sep 17 00:00:00 2001 From: Eld Axe Date: Fri, 23 Oct 2026 15:30:49 +0700 Subject: [PATCH] Refactor and extend touch-control customization - Consolidate touch-control configuration and rendering for SVG quality, pressed styles, opacity, and icon shadows. - Add an option to preserve original SVG colors by disabling theme tinting in both idle and pressed states, including analog controls. - Add touch-layout switching with a dedicated virtual key and input-release handling. - Add custom-button summary labels with configurable position, margins, sizing, and editor preview behavior that follows auto-hide. - Integrate global and per-game settings plus English and Indonesian translations. - Update the combined feature branch's build/test configuration. --- .github/workflows/manual_generate_apk.yml | 8 +- Common/UI/Context.cpp | 32 ++- Common/UI/Context.h | 1 + Core/Config.cpp | 140 ++++++++++++ Core/Config.h | 43 +++- Core/KeyMap.cpp | 1 + Core/KeyMap.h | 1 + UI/CustomButtonMappingScreen.cpp | 21 +- UI/EmuScreen.cpp | 18 +- UI/GameSettingsScreen.cpp | 34 ++- UI/GamepadEmu.cpp | 259 +++++++++++++++++----- UI/GamepadEmu.h | 11 +- UI/TouchControlLayoutScreen.cpp | 167 +++++++++++--- UI/TouchControlLayoutScreen.h | 3 + UI/TouchControlVisibilityScreen.cpp | 118 +++++++++- UI/TouchControlVisibilityScreen.h | 4 + UI/UIAtlas.cpp | 61 +++-- assets/lang/en_US.ini | 13 ++ assets/lang/id_ID.ini | 13 ++ 19 files changed, 817 insertions(+), 131 deletions(-) diff --git a/.github/workflows/manual_generate_apk.yml b/.github/workflows/manual_generate_apk.yml index c2a06e947421..4a95b355193d 100644 --- a/.github/workflows/manual_generate_apk.yml +++ b/.github/workflows/manual_generate_apk.yml @@ -55,10 +55,10 @@ jobs: # with: # ndk-version: r21e - - name: Test androidGitVersion - run: | - echo "count=${{steps.valid-tags.outputs.count}}" - gradle --quiet androidGitVersion + #- name: Test androidGitVersion + # run: | + # echo "count=${{steps.valid-tags.outputs.count}}" + # gradle --quiet androidGitVersion - name: Assemble APK run: ./gradlew assemble${{ github.event.inputs.buildVariant }} --stacktrace diff --git a/Common/UI/Context.cpp b/Common/UI/Context.cpp index 54001a09a054..41af57706c9d 100644 --- a/Common/UI/Context.cpp +++ b/Common/UI/Context.cpp @@ -3,6 +3,7 @@ #include #include "Common/System/Display.h" +#include "Core/Config.h" #include "Common/System/System.h" #include "Common/UI/UI.h" #include "Common/UI/View.h" @@ -42,15 +43,32 @@ void UIContext::Init(Draw::DrawContext *thin3d, Draw::Pipeline *uipipe, Draw::Pi void UIContext::BeginFrame() { frameStartTime_ = time_now_d(); + // Per-game config is loaded when a game starts, after UIContext may already + // have created its atlas. Detect the setting change here so the first game + // frame rebuilds the atlas without requiring the user to revisit Settings. + if (lastTouchButtonAtlasScale_ != g_Config.iTouchButtonAtlasScale) { + lastTouchButtonAtlasScale_ = g_Config.iTouchButtonAtlasScale; + atlasInvalid_ = true; + } if (atlasInvalid_ || !uitexture_) { - if (uitexture_) { - uitexture_->Release(); - } + // Build the replacement before releasing the current texture. Atlas generation may + // fail on devices with limited texture memory, especially at high SVG quality. AtlasData data = atlasProvider_(draw_, AtlasChoice::General, 1.0f / g_display.dpi_scale_x, atlasInvalid_); - uitexture_ = data.texture; - _dbg_assert_(uitexture_); - ui_draw2d.SetAtlas(data.atlas); - atlasInvalid_ = false; + if (!data.texture || !data.atlas) { + WARN_LOG(Log::G3D, "Failed to create UI atlas texture; keeping the previous atlas if available"); + if (!uitexture_) { + // There is no valid atlas to draw with yet. Avoid binding a null texture. + return; + } + atlasInvalid_ = false; + } else { + if (uitexture_) { + uitexture_->Release(); + } + uitexture_ = data.texture; + ui_draw2d.SetAtlas(data.atlas); + atlasInvalid_ = false; + } } if (!fontTexture_) { diff --git a/Common/UI/Context.h b/Common/UI/Context.h index 02726f28cd5d..0e2182207d79 100644 --- a/Common/UI/Context.h +++ b/Common/UI/Context.h @@ -164,4 +164,5 @@ class UIContext { UIAtlasProviderFunc atlasProvider_{}; bool atlasInvalid_ = false; + int lastTouchButtonAtlasScale_ = -1; }; diff --git a/Core/Config.cpp b/Core/Config.cpp index 8818631e604a..78667374ac86 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -1013,6 +1013,86 @@ static const ConfigSetting controlSettings[] = { ConfigSetting("Custom17Mapping", "Custom17Image", "Custom17Shape", "Custom17Toggle", "Custom17Repeat", SETTING_IDX(g_Config, CustomButton, 17), {0, 2, 9, false, false}, CfgFlag::PER_GAME), ConfigSetting("Custom18Mapping", "Custom18Image", "Custom18Shape", "Custom18Toggle", "Custom18Repeat", SETTING_IDX(g_Config, CustomButton, 18), {0, 3, 9, false, false}, CfgFlag::PER_GAME), ConfigSetting("Custom19Mapping", "Custom19Image", "Custom19Shape", "Custom19Toggle", "Custom19Repeat", SETTING_IDX(g_Config, CustomButton, 19), {0, 4, 9, false, false}, CfgFlag::PER_GAME), + ConfigSetting("Custom0Summary", SETTING(g_Config, bCustomButtonSummary[0]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom1Summary", SETTING(g_Config, bCustomButtonSummary[1]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom2Summary", SETTING(g_Config, bCustomButtonSummary[2]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom3Summary", SETTING(g_Config, bCustomButtonSummary[3]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom4Summary", SETTING(g_Config, bCustomButtonSummary[4]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom5Summary", SETTING(g_Config, bCustomButtonSummary[5]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom6Summary", SETTING(g_Config, bCustomButtonSummary[6]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom7Summary", SETTING(g_Config, bCustomButtonSummary[7]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom8Summary", SETTING(g_Config, bCustomButtonSummary[8]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom9Summary", SETTING(g_Config, bCustomButtonSummary[9]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom10Summary", SETTING(g_Config, bCustomButtonSummary[10]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom11Summary", SETTING(g_Config, bCustomButtonSummary[11]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom12Summary", SETTING(g_Config, bCustomButtonSummary[12]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom13Summary", SETTING(g_Config, bCustomButtonSummary[13]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom14Summary", SETTING(g_Config, bCustomButtonSummary[14]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom15Summary", SETTING(g_Config, bCustomButtonSummary[15]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom16Summary", SETTING(g_Config, bCustomButtonSummary[16]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom17Summary", SETTING(g_Config, bCustomButtonSummary[17]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom18Summary", SETTING(g_Config, bCustomButtonSummary[18]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom19Summary", SETTING(g_Config, bCustomButtonSummary[19]), false, CfgFlag::PER_GAME), + ConfigSetting("Custom0SummaryText", SETTING(g_Config, sCustomButtonSummaryText[0]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom1SummaryText", SETTING(g_Config, sCustomButtonSummaryText[1]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom2SummaryText", SETTING(g_Config, sCustomButtonSummaryText[2]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom3SummaryText", SETTING(g_Config, sCustomButtonSummaryText[3]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom4SummaryText", SETTING(g_Config, sCustomButtonSummaryText[4]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom5SummaryText", SETTING(g_Config, sCustomButtonSummaryText[5]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom6SummaryText", SETTING(g_Config, sCustomButtonSummaryText[6]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom7SummaryText", SETTING(g_Config, sCustomButtonSummaryText[7]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom8SummaryText", SETTING(g_Config, sCustomButtonSummaryText[8]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom9SummaryText", SETTING(g_Config, sCustomButtonSummaryText[9]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom10SummaryText", SETTING(g_Config, sCustomButtonSummaryText[10]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom11SummaryText", SETTING(g_Config, sCustomButtonSummaryText[11]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom12SummaryText", SETTING(g_Config, sCustomButtonSummaryText[12]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom13SummaryText", SETTING(g_Config, sCustomButtonSummaryText[13]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom14SummaryText", SETTING(g_Config, sCustomButtonSummaryText[14]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom15SummaryText", SETTING(g_Config, sCustomButtonSummaryText[15]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom16SummaryText", SETTING(g_Config, sCustomButtonSummaryText[16]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom17SummaryText", SETTING(g_Config, sCustomButtonSummaryText[17]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom18SummaryText", SETTING(g_Config, sCustomButtonSummaryText[18]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom19SummaryText", SETTING(g_Config, sCustomButtonSummaryText[19]), "", CfgFlag::PER_GAME), + ConfigSetting("Custom0SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[0]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom1SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[1]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom2SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[2]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom3SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[3]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom4SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[4]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom5SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[5]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom6SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[6]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom7SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[7]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom8SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[8]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom9SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[9]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom10SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[10]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom11SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[11]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom12SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[12]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom13SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[13]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom14SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[14]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom15SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[15]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom16SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[16]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom17SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[17]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom18SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[18]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom19SummaryPos", SETTING(g_Config, iCustomButtonSummaryPosition[19]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom0SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[0]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom1SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[1]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom2SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[2]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom3SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[3]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom4SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[4]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom5SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[5]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom6SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[6]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom7SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[7]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom8SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[8]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom9SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[9]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom10SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[10]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom11SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[11]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom12SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[12]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom13SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[13]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom14SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[14]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom15SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[15]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom16SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[16]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom17SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[17]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom18SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[18]), 0, CfgFlag::PER_GAME), + ConfigSetting("Custom19SummaryMargin", SETTING(g_Config, iCustomButtonSummaryMargin[19]), 0, CfgFlag::PER_GAME), ConfigSetting("TiltBaseAngleY", SETTING(g_Config, fTiltBaseAngleY), 0.9f, CfgFlag::PER_GAME), ConfigSetting("TiltInvertX", SETTING(g_Config, bInvertTiltX), false, CfgFlag::PER_GAME), @@ -1029,7 +1109,15 @@ static const ConfigSetting controlSettings[] = { ConfigSetting("DisableDpadDiagonals", SETTING(g_Config, bDisableDpadDiagonals), false, CfgFlag::PER_GAME), ConfigSetting("GamepadOnlyFocused", SETTING(g_Config, bGamepadOnlyFocused), false, CfgFlag::PER_GAME), ConfigSetting("TouchButtonStyle", SETTING(g_Config, iTouchButtonStyle), 1, CfgFlag::PER_GAME), + ConfigSetting("TouchButtonPressedStyle", SETTING(g_Config, iTouchButtonPressedStyle), 0, CfgFlag::PER_GAME), ConfigSetting("TouchButtonOpacity", SETTING(g_Config, iTouchButtonOpacity), 65, CfgFlag::PER_GAME), + ConfigSetting("TouchButtonSurfaceAlpha", SETTING(g_Config, iTouchButtonSurfaceAlpha), 25, CfgFlag::PER_GAME), + ConfigSetting("TouchAnalogHeadOpacity", SETTING(g_Config, iTouchAnalogHeadOpacity), 65, CfgFlag::PER_GAME), + ConfigSetting("TouchButtonEnlargePercent", SETTING(g_Config, iTouchButtonEnlargePercent), 150, CfgFlag::PER_GAME), + ConfigSetting("TouchButtonShrinkPercent", SETTING(g_Config, iTouchButtonShrinkPercent), 67, CfgFlag::PER_GAME), + ConfigSetting("TouchButtonAtlasScale", SETTING(g_Config, iTouchButtonAtlasScale), 3, CfgFlag::PER_GAME), + ConfigSetting("DisableTouchButtonShadows", SETTING(g_Config, bDisableTouchButtonShadows), false, CfgFlag::PER_GAME), + ConfigSetting("DisableTouchButtonThemeColor", SETTING(g_Config, bDisableTouchButtonThemeColor), false, CfgFlag::PER_GAME), ConfigSetting("TouchButtonHideSeconds", SETTING(g_Config, iTouchButtonHideSeconds), 8, CfgFlag::PER_GAME), ConfigSetting("AutoCenterTouchAnalog", SETTING(g_Config, bAutoCenterTouchAnalog), false, CfgFlag::PER_GAME), ConfigSetting("StickyTouchDPad", SETTING(g_Config, bStickyTouchDPad), false, CfgFlag::PER_GAME), @@ -1064,6 +1152,7 @@ static const ConfigSetting controlSettings[] = { ConfigSetting("SystemControls", SETTING(g_Config, bSystemControls), true, CfgFlag::DEFAULT), ConfigSetting("RapidFileInterval", SETTING(g_Config, iRapidFireInterval), 5, CfgFlag::DEFAULT), + ConfigSetting("TouchLayoutSelection", SETTING(g_Config, iTouchLayoutSelectionSaved), 1, CfgFlag::PER_GAME), #if PPSSPP_PLATFORM(WINDOWS) ConfigSetting("AllowHIDInput", SETTING(g_Config, bAllowHIDInput), true, CfgFlag::DEFAULT), @@ -1207,6 +1296,8 @@ static const ConfigSectionMeta g_sectionMeta[] = { { &g_Config.displayLayoutPortrait, displayLayoutSettings, ARRAY_SIZE(displayLayoutSettings), "DisplayLayout.Portrait"}, // These we don't want to read from the old settings, since for most people, those settings will be bad. { &g_Config.touchControlsLandscape, touchControlSettings, ARRAY_SIZE(touchControlSettings), "TouchControls.Landscape", "Control" }, // We read the old settings from [Control], since most people played in landscape before. { &g_Config.touchControlsPortrait, touchControlSettings, ARRAY_SIZE(touchControlSettings), "TouchControls.Portrait"}, // These we don't want to read from the old settings, since for most people, those settings will be bad. + { &g_Config.touchControlsLandscapeLayout2, touchControlSettings, ARRAY_SIZE(touchControlSettings), "TouchControls.LandscapeLayout2" }, + { &g_Config.touchControlsPortraitLayout2, touchControlSettings, ARRAY_SIZE(touchControlSettings), "TouchControls.PortraitLayout2" }, { &g_Config.gestureControls[0], gestureControlSettings, ARRAY_SIZE(gestureControlSettings), "GestureControls.Left", "General"}, // We read the old settings from [General], since most of them used to be there (except the analog stuff). { &g_Config.gestureControls[1], gestureControlSettings, ARRAY_SIZE(gestureControlSettings), "GestureControls.Right", "General"}, // We read the old settings from [General], since most of them used to be there (except the analog stuff). }; @@ -1469,7 +1560,9 @@ void Config::Load(const char *iniFileName, const char *controllerIniFilename) { // but these configs shouldn't contain older versions anyhow _dbg_assert_(!IsGameSpecific()); + EnsureSecondaryLayoutsInitialized(); PostLoadCleanup(); + g_Config.iTouchLayoutSelection = g_Config.iTouchLayoutSelectionSaved; INFO_LOG(Log::Config, "Config loaded: '%s' (%0.1f ms)", iniFilename_.c_str(), (time_now_d() - startTime) * 1000.0); } @@ -2003,6 +2096,7 @@ bool Config::LoadGameConfig(const std::string &gameId) { PostLoadCleanup(); DEBUG_LOG(Log::Config, "Game-specific config loaded: %s", gameId_.c_str()); + g_Config.iTouchLayoutSelection = g_Config.iTouchLayoutSelectionSaved; return true; } @@ -2185,3 +2279,49 @@ int MultiplierToVolume100(float multiplier) { float UIScaleFactorToMultiplier(int factor) { return powf(2.0f, (float)factor / 8.0f); } +void Config::SwapTouchControlsLayouts() { + // Instead of swapping the large layout structs themselves, + // just toggle the selection between layout 1 and 2. + // This is much safer: avoids invalidating any references/pointers + // to the layout structs, and is atomic. + if (iTouchLayoutSelection == 1) { + iTouchLayoutSelection = 2; + } else { + iTouchLayoutSelection = 1; + } + // Note: do NOT auto-save here. Caller should persist if desired. +} + +void Config::EnsureSecondaryLayoutsInitialized() { + auto IsConfigured = [](const TouchControlConfig &c) { + // If any position has been set (x >= 0), or any custom button is visible, + // consider the layout configured. + auto posSet = [](const ConfigTouchPos &p) { return p.x >= 0.0f || p.y >= 0.0f; }; + + if (posSet(c.touchActionButtonCenter) || posSet(c.touchDpad) || posSet(c.touchStartKey) || + posSet(c.touchSelectKey) || posSet(c.touchFastForwardKey) || posSet(c.touchLKey) || + posSet(c.touchRKey) || posSet(c.touchAnalogStick) || posSet(c.touchRightAnalogStick) || + posSet(c.touchPauseKey)) { + return true; + } + + for (size_t i = 0; i < TouchControlConfig::CUSTOM_BUTTON_COUNT; ++i) { + if (c.touchCustom[i].show) + return true; + if (posSet(c.touchCustom[i])) + return true; + } + + return false; + }; + + if (!IsConfigured(touchControlsPortraitLayout2)) { + // Use preset defaults for the secondary layout instead of copying + // the primary layout. This avoids unintentionally duplicating + // user-customized primary layouts into layout 2. + touchControlsPortraitLayout2.ResetToDefault("TouchControls.PortraitLayout2"); + } + if (!IsConfigured(touchControlsLandscapeLayout2)) { + touchControlsLandscapeLayout2.ResetToDefault("TouchControls.LandscapeLayout2"); + } +} diff --git a/Core/Config.h b/Core/Config.h index 0e3f7b35fa7e..6c8b34ed69a2 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -503,9 +503,17 @@ struct Config : public ConfigBlock { bool bDisableDpadDiagonals; bool bGamepadOnlyFocused; - // Control Style + // On-screen button appearance and pressed feedback. int iTouchButtonStyle; + int iTouchButtonPressedStyle; int iTouchButtonOpacity; + int iTouchButtonSurfaceAlpha; + int iTouchAnalogHeadOpacity; + int iTouchButtonEnlargePercent; + int iTouchButtonShrinkPercent; + int iTouchButtonAtlasScale; + bool bDisableTouchButtonShadows = false; + bool bDisableTouchButtonThemeColor = false; int iTouchButtonHideSeconds; // Snap touch control position @@ -524,8 +532,18 @@ struct Config : public ConfigBlock { TouchControlConfig touchControlsLandscape; TouchControlConfig touchControlsPortrait; + // Secondary touch layout used by the Swap Layout virtual key. + TouchControlConfig touchControlsLandscapeLayout2; + TouchControlConfig touchControlsPortraitLayout2; + int iTouchLayoutSelection = 1; + int iTouchLayoutSelectionSaved = 1; + // These are shared between portrait and landscape, just the positions aren't. ConfigCustomButton CustomButton[TouchControlConfig::CUSTOM_BUTTON_COUNT]; + bool bCustomButtonSummary[TouchControlConfig::CUSTOM_BUTTON_COUNT]{}; + std::string sCustomButtonSummaryText[TouchControlConfig::CUSTOM_BUTTON_COUNT]; + int iCustomButtonSummaryPosition[TouchControlConfig::CUSTOM_BUTTON_COUNT]{}; + int iCustomButtonSummaryMargin[TouchControlConfig::CUSTOM_BUTTON_COUNT]{}; // Ignored on iOS and other platforms that lack pause. bool bShowTouchPause; @@ -794,7 +812,30 @@ struct Config : public ConfigBlock { return orientation == DeviceOrientation::Portrait ? touchControlsPortrait : touchControlsLandscape; } + // Get the touched control config based on current layout selection + const TouchControlConfig &GetCurrentTouchControlsConfig(DeviceOrientation orientation) const { + if (iTouchLayoutSelection == 2) { + return orientation == DeviceOrientation::Portrait ? touchControlsPortraitLayout2 : touchControlsLandscapeLayout2; + } + return orientation == DeviceOrientation::Portrait ? touchControlsPortrait : touchControlsLandscape; + } + TouchControlConfig &GetCurrentTouchControlsConfig(DeviceOrientation orientation) { + if (iTouchLayoutSelection == 2) { + return orientation == DeviceOrientation::Portrait ? touchControlsPortraitLayout2 : touchControlsLandscapeLayout2; + } + return orientation == DeviceOrientation::Portrait ? touchControlsPortrait : touchControlsLandscape; + } + + // Exchange layout selections between mode 1 and 2 + void SwapTouchControlsLayouts(); + + // Ensure layout2 is initialized as a copy of layout1 if empty + void EnsureSecondaryLayoutsInitialized(); + static int GetDefaultValueInt(int *configSetting); + // Initialize Layout 2 as copy of Layout 1 if not already configured + void InitializeLayout2IfNeeded(); + void DoNotSaveSetting(void *configSetting) { settingsNotToSave_.push_back(configSetting); diff --git a/Core/KeyMap.cpp b/Core/KeyMap.cpp index 16f8c4ff8088..f55a5bb97419 100644 --- a/Core/KeyMap.cpp +++ b/Core/KeyMap.cpp @@ -507,6 +507,7 @@ const KeyMap_IntStrPair psp_button_names[] = { #endif {VIRTKEY_TOGGLE_DEBUGGER, "Toggle Debugger"}, {VIRTKEY_TOGGLE_TILT, "Toggle tilt control"}, + {VIRTKEY_SWAP_LAYOUT, "Swap layout"}, {VIRTKEY_OPENCHAT, "OpenChat" }, diff --git a/Core/KeyMap.h b/Core/KeyMap.h index 77484215c1f2..6a11a09867df 100644 --- a/Core/KeyMap.h +++ b/Core/KeyMap.h @@ -83,6 +83,7 @@ enum VirtKey : u32 { VIRTKEY_TOGGLE_TILT = 0x40000035, VIRTKEY_AXIS_SWAP_HOLD = 0x40000036, VIRTKEY_CREATE_FRAME_DUMP = 0x40000037, + VIRTKEY_SWAP_LAYOUT = 0x40000038, VIRTKEY_LAST, VIRTKEY_COUNT = VIRTKEY_LAST - VIRTKEY_FIRST }; diff --git a/UI/CustomButtonMappingScreen.cpp b/UI/CustomButtonMappingScreen.cpp index 08c479d8d05f..89173ad312f9 100644 --- a/UI/CustomButtonMappingScreen.cpp +++ b/UI/CustomButtonMappingScreen.cpp @@ -132,15 +132,25 @@ void CustomButtonMappingScreen::CreateDialogViews(UI::ViewGroup *parent) { LinearLayout *leftColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(120, FILL_PARENT)); auto di = GetI18NCategory(I18NCat::DIALOG); - TouchControlConfig &touch = g_Config.GetTouchControlsConfig(deviceOrientation_); + TouchControlConfig &touch = g_Config.GetCurrentTouchControlsConfig(deviceOrientation_); + static const char *summaryPositionLabels[] = { "Center", "Left", "Down", "Right", "Up" }; + static const char *summaryMarginLabels[] = { "None", "Small", "Medium", "High" }; ConfigCustomButton *cfg = nullptr; bool *show = nullptr; + bool *summaryShow = nullptr; + std::string *summaryText = nullptr; + int *summaryPosition = nullptr; + int *summaryMargin = nullptr; cfg = &g_Config.CustomButton[id_]; // This screen is reachable from the main menu, so neither GamepadEmu nor the layout screen // need have sanitized these yet - and everything below indexes the tables with them. CustomKeyData::Sanitize(*cfg); show = &touch.touchCustom[id_].show; + summaryShow = &g_Config.bCustomButtonSummary[id_]; + summaryText = &g_Config.sCustomButtonSummaryText[id_]; + summaryPosition = &g_Config.iCustomButtonSummaryPosition[id_]; + summaryMargin = &g_Config.iCustomButtonSummaryMargin[id_]; // Only load once, since the views can be recreated (on rotation, for example) before the array is saved. if (!arrayLoaded_) { for (int i = 0; i < ARRAY_SIZE(g_customKeyList); i++) { @@ -185,6 +195,15 @@ void CustomButtonMappingScreen::CreateDialogViews(UI::ViewGroup *parent) { screenManager()->push(shape); }); + vertLayout->Add(new CheckBox(summaryShow, co->T("Show summary"))); + vertLayout->Add(new PopupTextInputChoice(GetRequesterToken(), summaryText, co->T("Summary text"), co->T("Summary text"), 48, screenManager())); + PopupMultiChoice *summaryPos = vertLayout->Add(new PopupMultiChoice(summaryPosition, co->T("Summary position"), summaryPositionLabels, 0, ARRAY_SIZE(summaryPositionLabels), I18NCat::CONTROLS, screenManager())); + summaryPos->SetEnabledPtr(summaryShow); + PopupMultiChoice *summaryMarginChoice = vertLayout->Add(new PopupMultiChoice(summaryMargin, co->T("Summary margin"), summaryMarginLabels, 0, ARRAY_SIZE(summaryMarginLabels), I18NCat::CONTROLS, screenManager())); + summaryMarginChoice->SetEnabledFunc([summaryShow, summaryPosition]() { + return *summaryShow && *summaryPosition != 0; + }); + vertLayout->Add(new ItemHeader(co->T("Button Binding"))); vertLayout->Add(new CheckBox(&(cfg->toggle), co->T("Toggle mode"))); vertLayout->Add(new CheckBox(&(cfg->repeat), co->T("Repeat mode"))); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index e401c974f136..004e4f6e16ed 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -801,6 +801,22 @@ void EmuScreen::ProcessVKey(VirtKey virtKey, bool down) { } } break; + case VIRTKEY_SWAP_LAYOUT: + if (down) { + // Switch the active layout first, then release all inputs from the + // current view tree before it is recreated. RELEASE_ALL is not tied + // to the touch position, so a pointer still over a button won't keep + // the previous control latched during the layout transition. + g_Config.SwapTouchControlsLayouts(); + + TouchInput releaseTouch{}; + releaseTouch.id = 0; + releaseTouch.flags = TouchInputFlags::RELEASE_ALL; + UIScreen::touch(releaseTouch); + + System_PostUIMessage(UIMessage::RECREATE_VIEWS); + } + break; case VIRTKEY_OPENCHAT: if (down) { if (g_Config.bEnableNetworkChat && !g_Config.bShowImDebugger) { @@ -1249,7 +1265,7 @@ void EmuScreen::CreateViews() { const DeviceOrientation deviceOrientation = GetDeviceOrientation(); - TouchControlConfig &touch = g_Config.GetTouchControlsConfig(deviceOrientation); + TouchControlConfig &touch = g_Config.GetCurrentTouchControlsConfig(deviceOrientation); const Bounds &bounds = GetLayoutBounds(*screenManager()->getUIContext()); diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 87baaed7fa60..ce9d91c54c95 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -869,13 +869,43 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings) }); gesture->SetEnabledPtr(&g_Config.bShowTouchControls); - static const char *touchControlStyles[] = { "Classic", "Thin borders", "Glowing borders" }; + static const char *touchControlStyles[] = { "Classic", "Border" }; View *style = controlsSettings->Add(new PopupMultiChoice(&g_Config.iTouchButtonStyle, co->T("Button style"), touchControlStyles, 0, ARRAY_SIZE(touchControlStyles), I18NCat::CONTROLS, screenManager())); style->SetEnabledPtr(&g_Config.bShowTouchControls); + static const char *touchAtlasScales[] = { "1x (Low)", "2x (Medium)", "3x (High)", "4x (Very High)" }; + PopupMultiChoice *atlasScale = new PopupMultiChoice(&g_Config.iTouchButtonAtlasScale, co->T("Button icon quality"), touchAtlasScales, 1, ARRAY_SIZE(touchAtlasScales), I18NCat::CONTROLS, screenManager()); + controlsSettings->Add(atlasScale); + atlasScale->SetEnabledPtr(&g_Config.bShowTouchControls); + atlasScale->OnChoice.Add([this](UI::EventParams &e) { screenManager()->getUIContext()->InvalidateAtlas(); }); + CheckBox *disableTouchShadows = controlsSettings->Add(new CheckBox(&g_Config.bDisableTouchButtonShadows, co->T("Disable icon shadow"))); + disableTouchShadows->SetEnabledPtr(&g_Config.bShowTouchControls); + disableTouchShadows->OnClick.Add([this](UI::EventParams &e) { screenManager()->getUIContext()->InvalidateAtlas(); }); + CheckBox *disableTouchThemeColor = controlsSettings->Add(new CheckBox(&g_Config.bDisableTouchButtonThemeColor, co->T("Disable theme color on touch controls"))); + disableTouchThemeColor->SetEnabledPtr(&g_Config.bShowTouchControls); + + static const char *touchButtonPressedStyles[] = { "Glow", "Enlarge", "Shrink", "Ripple" }; + View *pressedStyle = controlsSettings->Add(new PopupMultiChoice(&g_Config.iTouchButtonPressedStyle, co->T("Button pressed style"), touchButtonPressedStyles, 0, ARRAY_SIZE(touchButtonPressedStyles), I18NCat::CONTROLS, screenManager())); + pressedStyle->SetEnabledPtr(&g_Config.bShowTouchControls); + PopupSliderChoice *opacity = controlsSettings->Add(new PopupSliderChoice(&g_Config.iTouchButtonOpacity, 0, 100, 65, co->T("Button Opacity"), screenManager(), "%")); opacity->SetEnabledPtr(&g_Config.bShowTouchControls); opacity->SetFormat("%i%%"); + + PopupSliderChoice *surfaceAlpha = controlsSettings->Add(new PopupSliderChoice(&g_Config.iTouchButtonSurfaceAlpha, 0, 100, 25, co->T("Button fill opacity"), screenManager(), "%")); + surfaceAlpha->SetEnabledPtr(&g_Config.bShowTouchControls); + surfaceAlpha->SetFormat("%i%%"); + + PopupSliderChoice *analogHeadOpacity = controlsSettings->Add(new PopupSliderChoice(&g_Config.iTouchAnalogHeadOpacity, 0, 100, 65, co->T("Analog head opacity"), screenManager(), "%")); + analogHeadOpacity->SetEnabledPtr(&g_Config.bShowTouchControls); + analogHeadOpacity->SetFormat("%i%%"); + + PopupSliderChoice *enlargeScale = controlsSettings->Add(new PopupSliderChoice(&g_Config.iTouchButtonEnlargePercent, 100, 500, 150, co->T("Enlarge size"), screenManager(), "%")); + enlargeScale->SetFormat("%i%%"); + enlargeScale->SetEnabledFunc([] { return g_Config.bShowTouchControls && g_Config.iTouchButtonPressedStyle == 1; }); + PopupSliderChoice *shrinkScale = controlsSettings->Add(new PopupSliderChoice(&g_Config.iTouchButtonShrinkPercent, 1, 100, 67, co->T("Shrink size"), screenManager(), "%")); + shrinkScale->SetFormat("%i%%"); + shrinkScale->SetEnabledFunc([] { return g_Config.bShowTouchControls && g_Config.iTouchButtonPressedStyle == 2; }); PopupSliderChoice *autoHide = controlsSettings->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")); @@ -884,7 +914,7 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings) CheckBox *touchGliding = controlsSettings->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.GetCurrentTouchControlsConfig(GetDeviceOrientation()); // Hide stick background, useful when increasing the size CheckBox *hideStickBackground = controlsSettings->Add(new CheckBox(&touch.bHideStickBackground, co->T("Hide touch analog stick background circle"))); diff --git a/UI/GamepadEmu.cpp b/UI/GamepadEmu.cpp index 79833e70d4be..8fb1b40aa03e 100644 --- a/UI/GamepadEmu.cpp +++ b/UI/GamepadEmu.cpp @@ -16,6 +16,7 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include +#include #include "Common/Data/Color/RGBAUtil.h" #include "Common/Data/Text/I18n.h" @@ -36,6 +37,16 @@ const float TOUCH_SCALE_FACTOR = 1.5f; +static float GetConfiguredEnlargeScale(float scale) { + const float multiplier = std::clamp(g_Config.iTouchButtonEnlargePercent / 100.0f, 1.0f, 5.0f); + return scale * multiplier; +} + +static float GetConfiguredShrinkScale(float scale) { + const float multiplier = std::clamp(g_Config.iTouchButtonShrinkPercent / 100.0f, 0.01f, 1.0f); + return scale * multiplier; +} + static uint32_t usedPointerMask = 0; static uint32_t analogPointerMask = 0; @@ -86,10 +97,51 @@ float GamepadGetOpacity() { return g_gamepadOpacity; } -static u32 GetButtonColor() { +static u32 GetButtonColor(const UIContext &dc) { + // White modulation preserves original SVG colors when theme tinting is disabled. + return g_Config.bDisableTouchButtonThemeColor ? 0xFFFFFFFF : dc.GetTheme().itemStyle.fgColor; +} + +static u32 GetPressedColor(const UIContext &dc) { + // Keep original SVG colors while theme tinting is disabled, including while pressed. + return g_Config.bDisableTouchButtonThemeColor ? 0xFFFFFFFF : dc.GetTheme().itemDownStyle.fgColor; +} + +// Match the touch-control editor's Classic fill and Border outline colors. +static u32 GetButtonSurfaceColor() { return g_Config.iTouchButtonStyle != 0 ? 0xFFFFFF : 0xc0b080; } + +static int GetPressedStyle() { + // Keep rendering safe if a config contains an unsupported value. + return g_Config.iTouchButtonPressedStyle >= 0 && g_Config.iTouchButtonPressedStyle <= 3 ? g_Config.iTouchButtonPressedStyle : 1; +} + +static float GetPressedScale(float scale) { + switch (GetPressedStyle()) { + case 0: // Glow + case 3: return scale; // Ripple + case 2: return GetConfiguredShrinkScale(scale); + default: return GetConfiguredEnlargeScale(scale); + } +} + +static float GetPressedOpacity(float opacity) { + return GetPressedStyle() == 0 ? opacity * 1.35f : opacity; +} + +// Draws a softly expanding silhouette while a touch control is held. +static void DrawPressedRipple(UIContext &dc, ImageID image, float x, float y, float scale, float opacity, float angle = 0.0f, bool flip = false) { + if (GetPressedStyle() != 3) + return; + + float phase = (float)std::fmod(time_now_d() * 1.5, 1.0); + float rippleScale = scale * (1.0f + phase * 0.55f); + float rippleOpacity = opacity * (1.0f - phase) * 0.22f; + dc.Draw()->DrawImageRotated(image, x, y, rippleScale, angle, colorAlpha(GetPressedColor(dc), rippleOpacity), flip); +} + GamepadComponent::GamepadComponent(std::string_view key, UI::LayoutParams *layoutParams) : UI::View(layoutParams), key_(key) {} static void rotateTouchHelper(float &dx, float &dy) { @@ -173,6 +225,10 @@ bool MultiTouchButton::Touch(const TouchInput &input) { if (input.flags & TouchInputFlags::RELEASE_ALL) { pointerDownMask_ = 0; usedPointerMask = 0; + // RELEASE_ALL represents every active pointer, not just input.id. + for (int i = 0; i < TOUCH_MAX_POINTERS; ++i) { + primaryButton[i] = nullptr; + } } return retval; } @@ -182,27 +238,28 @@ void MultiTouchButton::Draw(UIContext &dc) { if (opacity <= 0.0f) return; - float scale = scale_; - if (IsDownVisually()) { - if (g_Config.iTouchButtonStyle == 2) { - opacity *= 1.35f; - } else { - scale *= TOUCH_SCALE_FACTOR; - opacity *= 1.15f; - } - } + const bool isDown = IsDownVisually(); + float scale = isDown ? GetPressedScale(scale_) : scale_; + if (isDown) + opacity = GetPressedOpacity(opacity); - uint32_t colorBg = colorAlpha(GetButtonColor(), opacity); - uint32_t downBg = colorAlpha(0xFFFFFF, opacity * 0.5f); - uint32_t color = colorAlpha(0xFFFFFF, opacity); + uint32_t colorBg = colorAlpha(GetButtonSurfaceColor(), g_Config.iTouchButtonSurfaceAlpha / 100.0f); + uint32_t downBg = colorAlpha(GetPressedColor(dc), g_Config.iTouchButtonSurfaceAlpha / 100.0f * 0.5f); + uint32_t color = colorAlpha(isDown ? GetPressedColor(dc) : GetButtonColor(dc), opacity); - if (IsDownVisually() && g_Config.iTouchButtonStyle == 2) { - if (bgImg_ != bgDownImg_) - dc.Draw()->DrawImageRotated(bgDownImg_, bounds_.centerX(), bounds_.centerY(), scale, bgAngle_ * (M_PI * 2 / 360.0f), downBg, flipImageH_); + if (isDown && GetPressedStyle() == 3) { + DrawPressedRipple(dc, bgImg_, bounds_.centerX(), bounds_.centerY(), scale_, opacity, bgAngle_ * (M_PI * 2 / 360.0f), flipImageH_); } dc.Draw()->DrawImageRotated(bgImg_, bounds_.centerX(), bounds_.centerY(), scale, bgAngle_ * (M_PI * 2 / 360.0f), colorBg, flipImageH_); + // Draw the pressed background on top of the normal background. This must + // also run when both ImageIDs are identical (for example, the PSP face + // buttons in Classic style), otherwise Glow has no visible pressed effect. + if (isDown && GetPressedStyle() == 0) { + dc.Draw()->DrawImageRotated(bgDownImg_, bounds_.centerX(), bounds_.centerY(), scale, bgAngle_ * (M_PI * 2 / 360.0f), downBg, flipImageH_); + } + float x = bounds_.centerX(); float y = bounds_.centerY(); // Hack round the fact that the center of the rectangular picture the triangle is contained in @@ -271,12 +328,55 @@ void CustomButton::GetContentDimensions(const UIContext &dc, float &w, float &h) } } +std::string CustomButton::DescribeText() const { + if (summaryShow_ && !summaryText_.empty()) + return summaryText_; + return MultiTouchButton::DescribeText(); +} + +void CustomButton::Draw(UIContext &dc) { + MultiTouchButton::Draw(dc); + if (!summaryShow_ || summaryText_.empty()) + return; + // Match the button's visibility and fade-out, including auto-hide after inactivity. + const float opacity = GamepadGetOpacity(); + if (opacity <= 0.0f) + return; + static constexpr float marginSizes[] = { 0.0f, 6.0f, 12.0f, 20.0f }; + const int position = std::clamp(summaryPosition_, 0, 4); + const int margin = std::clamp(summaryMargin_, 0, 3); + const float offset = marginSizes[margin] * scale_; + const uint32_t textColor = colorAlpha(0xFFFFFF, opacity); + switch (position) { + case 0: dc.DrawTextShadow(summaryText_, bounds_.centerX(), bounds_.centerY(), textColor, ALIGN_HCENTER | ALIGN_VCENTER | FLAG_WRAP_TEXT); break; + case 1: dc.DrawTextShadow(summaryText_, bounds_.x - offset, bounds_.centerY(), textColor, ALIGN_RIGHT | ALIGN_VCENTER | FLAG_WRAP_TEXT); break; + case 2: dc.DrawTextShadow(summaryText_, bounds_.centerX(), bounds_.y2() + offset, textColor, ALIGN_HCENTER | ALIGN_TOP | FLAG_WRAP_TEXT); break; + case 3: dc.DrawTextShadow(summaryText_, bounds_.x2() + offset, bounds_.centerY(), textColor, ALIGN_LEFT | ALIGN_VCENTER | FLAG_WRAP_TEXT); break; + case 4: dc.DrawTextShadow(summaryText_, bounds_.centerX(), bounds_.y - offset, textColor, ALIGN_HCENTER | ALIGN_BOTTOM | FLAG_WRAP_TEXT); break; + } +} + bool CustomButton::Touch(const TouchInput &input) { using namespace CustomKeyData; bool lastDown = pointerDownMask_ != 0; bool retval = MultiTouchButton::Touch(input); bool down = pointerDownMask_ != 0; + if (input.flags & TouchInputFlags::RELEASE_ALL) { + // Layout swaps must release custom mappings too, including toggle and + // repeat buttons whose key-up is not handled by the normal pointer-up path. + if (on_ || lastDown || pressedFrames_ > 0) { + for (int i = 0; i < ARRAY_SIZE(g_customKeyList); i++) { + if (pspButtonBit_ & (1ULL << i)) { + controlMapper_->PSPKey(DEVICE_ID_TOUCH, g_customKeyList[i].c, KeyInputFlags::UP); + } + } + } + on_ = false; + pressedFrames_ = 0; + return retval; + } + if (down && !lastDown) { if (g_Config.bHapticFeedback) System_Vibrate(HAPTIC_VIRTUAL_KEY); @@ -333,7 +433,9 @@ void CustomButton::Update() { } bool PSPButton::IsDownVisually() const { - return (__CtrlPeekButtonsVisual() & pspButtonBit_) != 0; + // Keep pressed visuals responsive to direct touch, even when the visual + // button state from the core hasn't caught up yet. + return pointerDownMask_ != 0 || (__CtrlPeekButtonsVisual() & pspButtonBit_) != 0; } PSPDpad::PSPDpad(ImageID arrowIndex, std::string_view key, ImageID arrowDownIndex, ImageID overlayIndex, float scale, float spacing, UI::LayoutParams *layoutParams) @@ -360,6 +462,15 @@ bool PSPDpad::Touch(const TouchInput &input) { bool retval = GamepadComponent::Touch(input); + if (input.flags & TouchInputFlags::RELEASE_ALL) { + // A layout swap destroys this view without individual pointer-up events. + // Release any directions held by the old D-pad before it is recreated. + dragPointerId_ = -1; + usedPointerMask = 0; + ProcessTouch(bounds_.centerX(), bounds_.centerY(), false, false); + return retval; + } + if (input.flags & TouchInputFlags::DOWN) { if (dragPointerId_ == -1 && bounds_.Contains(input.x, input.y)) { dragPointerId_ = input.id; @@ -485,20 +596,22 @@ void PSPDpad::Draw(UIContext &dc) { float x2 = bounds_.centerX() + xoff[i] * (r + 10.f * scale_); float y2 = bounds_.centerY() + yoff[i] * (r + 10.f * scale_); float angle = i * (M_PI / 2.0f); - float imgScale = isDown ? scale_ * TOUCH_SCALE_FACTOR : scale_; - float imgOpacity = opacity; + float imgScale = isDown ? GetPressedScale(scale_) : scale_; + float imgOpacity = isDown ? GetPressedOpacity(opacity) : opacity; + + if (isDown && GetPressedStyle() == 3) { + DrawPressedRipple(dc, arrowIndex_, x, y, scale_, imgOpacity, angle + PI, false); + } - if (isDown && g_Config.iTouchButtonStyle == 2) { - imgScale = scale_; - imgOpacity *= 1.35f; + if (isDown && GetPressedStyle() == 0) { - uint32_t downBg = colorAlpha(0x00FFFFFF, imgOpacity * 0.5f); + uint32_t downBg = colorAlpha(GetPressedColor(dc), g_Config.iTouchButtonSurfaceAlpha / 100.0f * 0.5f); if (arrowIndex_ != arrowDownIndex_) dc.Draw()->DrawImageRotated(arrowDownIndex_, x, y, imgScale, angle + PI, downBg, false); } - uint32_t colorBg = colorAlpha(GetButtonColor(), imgOpacity); - uint32_t color = colorAlpha(0xFFFFFF, imgOpacity); + uint32_t colorBg = colorAlpha(GetButtonSurfaceColor(), g_Config.iTouchButtonSurfaceAlpha / 100.0f); + uint32_t color = colorAlpha(isDown ? GetPressedColor(dc) : GetButtonColor(dc), imgOpacity); dc.Draw()->DrawImageRotated(arrowIndex_, x, y, imgScale, angle + PI, colorBg, false); if (overlayIndex_.isValid()) @@ -522,12 +635,20 @@ void PSPStick::Draw(UIContext &dc) { if (opacity <= 0.0f) return; - if (dragPointerId_ != -1 && g_Config.iTouchButtonStyle == 2) { - opacity *= 1.35f; - } + if (dragPointerId_ != -1) + opacity = GetPressedOpacity(opacity); - uint32_t colorBg = colorAlpha(GetButtonColor(), opacity); - uint32_t downBg = colorAlpha(0x00FFFFFF, opacity * 0.5f); + // Analog head opacity is independent of Button Opacity and surface opacity. + float headOpacity = g_Config.iTouchAnalogHeadOpacity / 100.0f; + if (dragPointerId_ != -1) + headOpacity = GetPressedOpacity(headOpacity); + + // Use white as the modulation color so built-in and custom SVG RGB colors remain intact. + uint32_t colorBg = colorAlpha(0xFFFFFFFF, headOpacity); + // Keep the analog base readable; surface alpha is for button artwork, + // while the analog base follows the configured control opacity. + uint32_t surfaceBgColor = colorAlpha(GetButtonSurfaceColor(), g_Config.iTouchButtonSurfaceAlpha / 100.0f); + uint32_t downBg = colorAlpha(GetPressedColor(dc), headOpacity * 0.5f); if (centerX_ < 0.0f) { centerX_ = bounds_.centerX(); @@ -544,11 +665,14 @@ void PSPStick::Draw(UIContext &dc) { const TouchControlConfig &config = g_Config.GetTouchControlsConfig(g_display.GetDeviceOrientation()); if (!config.bHideStickBackground) - dc.Draw()->DrawImage(bgImg_, stickX, stickY, 1.0f * scale_, colorBg, ALIGN_CENTER); + dc.Draw()->DrawImage(bgImg_, stickX, stickY, 1.0f * scale_, surfaceBgColor, ALIGN_CENTER); float headScale = stick_ ? config.fRightStickHeadScale : config.fLeftStickHeadScale; - if (dragPointerId_ != -1 && g_Config.iTouchButtonStyle == 2 && stickDownImg_ != stickImageIndex_) - dc.Draw()->DrawImage(stickDownImg_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * scale_ * headScale, downBg, ALIGN_CENTER); - dc.Draw()->DrawImage(stickImageIndex_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * scale_ * headScale, colorBg, ALIGN_CENTER); + float pressedScale = dragPointerId_ != -1 ? GetPressedScale(scale_) : scale_; + if (dragPointerId_ != -1 && GetPressedStyle() == 3) + DrawPressedRipple(dc, stickImageIndex_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, pressedScale * headScale, headOpacity); + if (dragPointerId_ != -1 && GetPressedStyle() == 0 && stickDownImg_ != stickImageIndex_) + dc.Draw()->DrawImage(stickDownImg_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * pressedScale * headScale, downBg, ALIGN_CENTER); + dc.Draw()->DrawImage(stickImageIndex_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * pressedScale * headScale, colorBg, ALIGN_CENTER); } bool PSPStick::Touch(const TouchInput &input) { @@ -568,7 +692,7 @@ bool PSPStick::Touch(const TouchInput &input) { return retval; } if (input.flags & TouchInputFlags::DOWN) { - const TouchControlConfig &config = g_Config.GetTouchControlsConfig(g_display.GetDeviceOrientation()); + const TouchControlConfig &config = g_Config.GetCurrentTouchControlsConfig(g_display.GetDeviceOrientation()); float fac = 0.5f * (stick_ ? config.fRightStickHeadScale : config.fLeftStickHeadScale)-0.5f; if (dragPointerId_ == -1 && bounds_.Expand(bounds_.w*fac, bounds_.h*fac).Contains(input.x, input.y)) { if (g_Config.bAutoCenterTouchAnalog) { @@ -640,12 +764,20 @@ void PSPCustomStick::Draw(UIContext &dc) { if (opacity <= 0.0f) return; - if (dragPointerId_ != -1 && g_Config.iTouchButtonStyle == 2) { - opacity *= 1.35f; - } + if (dragPointerId_ != -1) + opacity = GetPressedOpacity(opacity); + + // Analog head opacity is independent of Button Opacity and surface opacity. + float headOpacity = g_Config.iTouchAnalogHeadOpacity / 100.0f; + if (dragPointerId_ != -1) + headOpacity = GetPressedOpacity(headOpacity); - uint32_t colorBg = colorAlpha(GetButtonColor(), opacity); - uint32_t downBg = colorAlpha(0x00FFFFFF, opacity * 0.5f); + // Use white as the modulation color so built-in and custom SVG RGB colors remain intact. + uint32_t colorBg = colorAlpha(0xFFFFFFFF, headOpacity); + // Keep the analog base readable; surface alpha is for button artwork, + // while the analog base follows the configured control opacity. + uint32_t surfaceBgColor = colorAlpha(GetButtonSurfaceColor(), g_Config.iTouchButtonSurfaceAlpha / 100.0f); + uint32_t downBg = colorAlpha(GetPressedColor(dc), headOpacity * 0.5f); if (centerX_ < 0.0f) { centerX_ = bounds_.centerX(); @@ -662,10 +794,13 @@ void PSPCustomStick::Draw(UIContext &dc) { const TouchControlConfig &config = g_Config.GetTouchControlsConfig(g_display.GetDeviceOrientation()); const float headScale = config.fRightStickHeadScale; if (!config.bHideStickBackground) - dc.Draw()->DrawImage(bgImg_, stickX, stickY, 1.0f * scale_, colorBg, ALIGN_CENTER); - if (dragPointerId_ != -1 && g_Config.iTouchButtonStyle == 2 && stickDownImg_ != stickImageIndex_) - dc.Draw()->DrawImage(stickDownImg_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * scale_ * headScale, downBg, ALIGN_CENTER); - dc.Draw()->DrawImage(stickImageIndex_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * scale_ * headScale, colorBg, ALIGN_CENTER); + dc.Draw()->DrawImage(bgImg_, stickX, stickY, 1.0f * scale_, surfaceBgColor, ALIGN_CENTER); + float pressedScale = dragPointerId_ != -1 ? GetPressedScale(scale_) : scale_; + if (dragPointerId_ != -1 && GetPressedStyle() == 3) + DrawPressedRipple(dc, stickImageIndex_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, pressedScale * headScale, headOpacity); + if (dragPointerId_ != -1 && GetPressedStyle() == 0 && stickDownImg_ != stickImageIndex_) + dc.Draw()->DrawImage(stickDownImg_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * pressedScale * headScale, downBg, ALIGN_CENTER); + dc.Draw()->DrawImage(stickImageIndex_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * pressedScale * headScale, colorBg, ALIGN_CENTER); } bool PSPCustomStick::Touch(const TouchInput &input) { @@ -685,7 +820,7 @@ bool PSPCustomStick::Touch(const TouchInput &input) { return false; } if (input.flags & TouchInputFlags::DOWN) { - const TouchControlConfig &config = g_Config.GetTouchControlsConfig(g_display.GetDeviceOrientation()); + const TouchControlConfig &config = g_Config.GetCurrentTouchControlsConfig(g_display.GetDeviceOrientation()); float fac = 0.5f * config.fRightStickHeadScale - 0.5f; if (dragPointerId_ == -1 && bounds_.Expand(bounds_.w*fac, bounds_.h*fac).Contains(input.x, input.y)) { if (g_Config.bAutoCenterTouchAnalog) { @@ -1021,11 +1156,13 @@ GamepadEmuView::GamepadEmuView(const TouchControlConfig &config, float xres, flo const int halfW = xres / 2; - const ImageID roundImage = g_Config.iTouchButtonStyle ? ImageID("I_ROUND_LINE") : ImageID("I_ROUND"); - const ImageID rectImage = g_Config.iTouchButtonStyle ? ImageID("I_RECT_LINE") : ImageID("I_RECT"); - const ImageID shoulderImage = g_Config.iTouchButtonStyle ? ImageID("I_SHOULDER_LINE") : ImageID("I_SHOULDER"); - const ImageID stickImage = g_Config.iTouchButtonStyle ? ImageID("I_STICK_LINE") : ImageID("I_STICK"); - const ImageID stickBg = g_Config.iTouchButtonStyle ? ImageID("I_STICK_BG_LINE") : ImageID("I_STICK_BG"); + // Border uses outline assets; Classic uses the original filled assets. + const ImageID roundImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_ROUND_LINE") : ImageID("I_ROUND"); + const ImageID roundDownImage = ImageID("I_ROUND"); + const ImageID rectImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_RECT_LINE") : ImageID("I_RECT"); + const ImageID shoulderImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_SHOULDER_LINE") : ImageID("I_SHOULDER"); + const ImageID stickImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_STICK_LINE") : ImageID("I_STICK"); + const ImageID stickBg = g_Config.iTouchButtonStyle == 1 ? ImageID("I_STICK_BG_LINE") : ImageID("I_STICK_BG"); auto addPSPButton = [this, buttonLayoutParams](int buttonBit, const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, const ConfigTouchPos &touch, ButtonOffset off = { 0, 0 }) -> PSPButton * { if (touch.show) { @@ -1039,15 +1176,15 @@ GamepadEmuView::GamepadEmuView(const TouchControlConfig &config, float xres, flo } return nullptr; }; - auto addCustomButton = [this, buttonLayoutParams, controlMapper](const ConfigCustomButton& cfg, const char *key, const ConfigTouchPos &touch) -> CustomButton * { + auto addCustomButton = [this, buttonLayoutParams, controlMapper](const ConfigCustomButton& cfg, bool summaryShow, const std::string &summaryText, int summaryPosition, int summaryMargin, const char *key, const ConfigTouchPos &touch) -> CustomButton * { using namespace CustomKeyData; if (touch.show) { _dbg_assert_(cfg.shape < ARRAY_SIZE(customKeyShapes)); _dbg_assert_(cfg.image < ARRAY_SIZE(customKeyImages)); // Note: cfg.shape and cfg.image are bounds-checked elsewhere. - auto aux = Add(new CustomButton(cfg.key, key, cfg.toggle, cfg.repeat, controlMapper, - g_Config.iTouchButtonStyle == 0 ? customKeyShapes[cfg.shape].i : customKeyShapes[cfg.shape].l, customKeyShapes[cfg.shape].i, + auto aux = Add(new CustomButton(cfg.key, key, cfg.toggle, cfg.repeat, summaryShow, summaryText, summaryPosition, summaryMargin, controlMapper, + g_Config.iTouchButtonStyle == 1 ? customKeyShapes[cfg.shape].l : customKeyShapes[cfg.shape].i, customKeyShapes[cfg.shape].i, customKeyImages[cfg.image].i, touch.scale, customKeyShapes[cfg.shape].d, buttonLayoutParams(touch))); aux->SetAngle(customKeyImages[cfg.image].r, customKeyShapes[cfg.shape].r); aux->FlipImageH(customKeyShapes[cfg.shape].f); @@ -1057,7 +1194,7 @@ GamepadEmuView::GamepadEmuView(const TouchControlConfig &config, float xres, flo }; if (config.touchPauseKey.show) { - auto button = addBoolButton(pause, "Pause button", roundImage, ImageID("I_ROUND"), ImageID("I_HAMBURGER"), config.touchPauseKey); + auto button = addBoolButton(pause, "Pause button", roundImage, roundImage, ImageID("I_HAMBURGER"), config.touchPauseKey); if (button) { // The user is not allowed to hide this completely on some platforms - it must be findable. button->SetMinimumAlpha(0.1f); @@ -1066,13 +1203,13 @@ GamepadEmuView::GamepadEmuView(const TouchControlConfig &config, float xres, flo // touchActionButtonCenter.show will always be true, since that's the default. if (config.bShowTouchCircle) - addPSPButton(CTRL_CIRCLE, "Circle button", roundImage, ImageID("I_ROUND"), ImageID("I_CIRCLE"), config.touchActionButtonCenter, circleOffset); + addPSPButton(CTRL_CIRCLE, "Circle button", roundImage, roundDownImage, ImageID("I_CIRCLE"), config.touchActionButtonCenter, circleOffset); if (config.bShowTouchCross) - addPSPButton(CTRL_CROSS, "Cross button", roundImage, ImageID("I_ROUND"), ImageID("I_CROSS"), config.touchActionButtonCenter, crossOffset); + addPSPButton(CTRL_CROSS, "Cross button", roundImage, roundDownImage, ImageID("I_CROSS"), config.touchActionButtonCenter, crossOffset); if (config.bShowTouchTriangle) - addPSPButton(CTRL_TRIANGLE, "Triangle button", roundImage, ImageID("I_ROUND"), ImageID("I_TRIANGLE"), config.touchActionButtonCenter, triangleOffset); + addPSPButton(CTRL_TRIANGLE, "Triangle button", roundImage, roundDownImage, ImageID("I_TRIANGLE"), config.touchActionButtonCenter, triangleOffset); if (config.bShowTouchSquare) - addPSPButton(CTRL_SQUARE, "Square button", roundImage, ImageID("I_ROUND"), ImageID("I_SQUARE"), config.touchActionButtonCenter, squareOffset); + addPSPButton(CTRL_SQUARE, "Square button", roundImage, roundDownImage, ImageID("I_SQUARE"), config.touchActionButtonCenter, squareOffset); addPSPButton(CTRL_START, "Start button", rectImage, ImageID("I_RECT"), ImageID("I_START"), config.touchStartKey); addPSPButton(CTRL_SELECT, "Select button", rectImage, ImageID("I_RECT"), ImageID("I_SELECT"), config.touchSelectKey); @@ -1092,7 +1229,7 @@ GamepadEmuView::GamepadEmuView(const TouchControlConfig &config, float xres, flo rTrigger->FlipImageH(true); if (config.touchDpad.show) { - const ImageID dirImage = g_Config.iTouchButtonStyle ? ImageID("I_DIR_LINE") : ImageID("I_DIR"); + const ImageID dirImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_DIR_LINE") : ImageID("I_DIR"); Add(new PSPDpad(dirImage, "D-pad", ImageID("I_DIR"), ImageID("I_ARROW"), config.touchDpad.scale, config.fDpadSpacing, buttonLayoutParams(config.touchDpad))); } @@ -1112,7 +1249,7 @@ GamepadEmuView::GamepadEmuView(const TouchControlConfig &config, float xres, flo char temp[64]; snprintf(temp, sizeof(temp), "Custom %d button", i + 1); - addCustomButton(g_Config.CustomButton[i], temp, config.touchCustom[i]); + addCustomButton(g_Config.CustomButton[i], g_Config.bCustomButtonSummary[i], g_Config.sCustomButtonSummaryText[i], g_Config.iCustomButtonSummaryPosition[i], g_Config.iCustomButtonSummaryMargin[i], temp, config.touchCustom[i]); } // Add the two gesture zones. @@ -1260,7 +1397,7 @@ void GestureGamepad::Draw(UIContext &dc) { if (opacity <= 0.0f) return; - uint32_t colorBg = colorAlpha(GetButtonColor(), opacity); + uint32_t colorBg = colorAlpha(GetButtonSurfaceColor(), g_Config.iTouchButtonSurfaceAlpha / 100.0f); if (GetZone().bAnalogGesture && dragPointerId_ != -1) { dc.Draw()->DrawImage(ImageID("I_CIRCLE"), downX_, downY_, 0.7f, colorBg, ALIGN_CENTER); diff --git a/UI/GamepadEmu.h b/UI/GamepadEmu.h index ce8463c10024..2002243e83d3 100644 --- a/UI/GamepadEmu.h +++ b/UI/GamepadEmu.h @@ -187,11 +187,13 @@ const int baseActionButtonSpacing = 60; // Customizable buttons, press a combination of buttons specified by pspButtonBit. class CustomButton : public MultiTouchButton { public: - CustomButton(uint64_t pspButtonBit, std::string_view key, bool toggle, bool repeat, ControlMapper* controllMapper, ImageID bgImg, ImageID bgDownImg, ImageID img, float scale, bool invertedContentDimension, UI::LayoutParams *layoutParams) - : MultiTouchButton(key, bgImg, bgDownImg, img, scale, layoutParams), pspButtonBit_(pspButtonBit), toggle_(toggle), repeat_(repeat), controlMapper_(controllMapper), on_(false), invertedContentDimension_(invertedContentDimension) { + CustomButton(uint64_t pspButtonBit, std::string_view key, bool toggle, bool repeat, bool summaryShow, std::string_view summaryText, int summaryPosition, int summaryMargin, ControlMapper* controllMapper, ImageID bgImg, ImageID bgDownImg, ImageID img, float scale, bool invertedContentDimension, UI::LayoutParams *layoutParams) + : MultiTouchButton(key, bgImg, bgDownImg, img, scale, layoutParams), pspButtonBit_(pspButtonBit), toggle_(toggle), repeat_(repeat), controlMapper_(controllMapper), on_(false), invertedContentDimension_(invertedContentDimension), summaryShow_(summaryShow), summaryText_(summaryText), summaryPosition_(summaryPosition), summaryMargin_(summaryMargin) { } bool Touch(const TouchInput &input) override; void Update() override; + void Draw(UIContext &dc) override; + std::string DescribeText() const override; bool IsDownVisually() const override; // For visual purpose bool IsDownByTouch() const override; @@ -205,6 +207,10 @@ class CustomButton : public MultiTouchButton { ControlMapper* controlMapper_; bool on_; bool invertedContentDimension_; // Swap width and height + bool summaryShow_; + std::string summaryText_; + int summaryPosition_; + int summaryMargin_; }; struct GestureControlConfig; @@ -364,6 +370,7 @@ namespace CustomKeyData { { ImageID::invalid(), VIRTKEY_TOGGLE_DEBUGGER }, { ImageID::invalid(), VIRTKEY_PAUSE_NO_MENU }, { ImageID::invalid(), VIRTKEY_TOGGLE_TILT }, + { ImageID::invalid(), VIRTKEY_SWAP_LAYOUT }, // IMPORTANT: Only add at the end! }; static_assert(ARRAY_SIZE(g_customKeyList) <= 64, "Too many key for a uint64_t bit mask"); diff --git a/UI/TouchControlLayoutScreen.cpp b/UI/TouchControlLayoutScreen.cpp index b50ad967332d..11e6e4ed4475 100644 --- a/UI/TouchControlLayoutScreen.cpp +++ b/UI/TouchControlLayoutScreen.cpp @@ -16,6 +16,8 @@ // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include +#include +#include #include #include "Common/Data/Color/RGBAUtil.h" @@ -42,11 +44,20 @@ static u32 GetButtonColor() { return g_Config.iTouchButtonStyle != 0 ? 0xFFFFFF : 0xc0b080; } +// Match gameplay's Button fill opacity for the background only. +static u32 GetEditorButtonFillColor(float opacity) { + const float fillOpacity = g_Config.iTouchButtonStyle == 0 + ? opacity * (g_Config.iTouchButtonSurfaceAlpha / 100.0f) + : opacity; + return colorAlpha(GetButtonColor(), fillOpacity); +} + class DragDropButton : public MultiTouchButton { public: - DragDropButton(ConfigTouchPos &pos, const char *key, ImageID bgImg, ImageID img, const Bounds &screenBounds) + DragDropButton(ConfigTouchPos &pos, const char *key, ImageID bgImg, ImageID img, const Bounds &screenBounds, + bool summaryShow = false, std::string summaryText = {}, int summaryPosition = 0, int summaryMargin = 0) : MultiTouchButton(key, bgImg, bgImg, img, pos.scale, new UI::AnchorLayoutParams(pos.x * screenBounds.w, pos.y * screenBounds.h, UI::NONE, UI::NONE, UI::Centering::Both)), - x_(pos.x), y_(pos.y), theScale_(pos.scale), screenBounds_(screenBounds) { + x_(pos.x), y_(pos.y), theScale_(pos.scale), screenBounds_(screenBounds), summaryShow_(summaryShow), summaryText_(std::move(summaryText)), summaryPosition_(summaryPosition), summaryMargin_(summaryMargin) { scale_ = theScale_; } @@ -60,6 +71,34 @@ class DragDropButton : public MultiTouchButton { void Draw(UIContext &dc) override { scale_ = theScale_*layoutAreaScale; // Scale down just for rendering MultiTouchButton::Draw(dc); + + if (summaryShow_ && !summaryText_.empty()) { + static constexpr float marginSizes[] = { 0.0f, 6.0f, 12.0f, 20.0f }; + const int position = std::clamp(summaryPosition_, 0, 4); + const int margin = std::clamp(summaryMargin_, 0, 3); + const float offset = marginSizes[margin] * scale_; + // Keep summary labels fully visible while editing, independent of gameplay auto-hide. + const uint32_t textColor = 0xFFFFFFFF; + + switch (position) { + case 0: + dc.DrawTextShadow(summaryText_, bounds_.centerX(), bounds_.centerY(), textColor, ALIGN_HCENTER | ALIGN_VCENTER | FLAG_WRAP_TEXT); + break; + case 1: + dc.DrawTextShadow(summaryText_, bounds_.x - offset, bounds_.centerY(), textColor, ALIGN_RIGHT | ALIGN_VCENTER | FLAG_WRAP_TEXT); + break; + case 2: + dc.DrawTextShadow(summaryText_, bounds_.centerX(), bounds_.y2() + offset, textColor, ALIGN_HCENTER | ALIGN_TOP | FLAG_WRAP_TEXT); + break; + case 3: + dc.DrawTextShadow(summaryText_, bounds_.x2() + offset, bounds_.centerY(), textColor, ALIGN_LEFT | ALIGN_VCENTER | FLAG_WRAP_TEXT); + break; + case 4: + dc.DrawTextShadow(summaryText_, bounds_.centerX(), bounds_.y - offset, textColor, ALIGN_HCENTER | ALIGN_BOTTOM | FLAG_WRAP_TEXT); + break; + } + } + scale_ = theScale_/layoutAreaScale; // is this is needed? } @@ -70,11 +109,13 @@ class DragDropButton : public MultiTouchButton { } virtual float GetScale() const { return theScale_; } + virtual float *GetScalePtr() { return &theScale_; } virtual void SetScale(float s) { theScale_ = s; scale_ = s; } virtual float GetSpacing() const { return 1.0f; } virtual void SetSpacing(float s) { } + virtual bool Contains(float x, float y) { const float thresholdFactor = 0.25f; const float thresholdW = thresholdFactor * bounds_.w; @@ -88,6 +129,41 @@ class DragDropButton : public MultiTouchButton { const Bounds &screenBounds_; float &theScale_; float &x_, &y_; + bool summaryShow_; + std::string summaryText_; + int summaryPosition_; + int summaryMargin_; +}; + +class TouchControlSizeSliderScreen : public UI::PopupScreen { +public: + TouchControlSizeSliderScreen(float *scale, float size) + : PopupScreen("Set size", "OK", "Cancel"), scale_(scale), size_(size) { + } + + void CreatePopupContents(UI::ViewGroup *parent) override { + using namespace UI; + SliderFloat *slider = parent->Add(new SliderFloat(&size_, 0.5f, 5.0f, + new LinearLayoutParams(Margins(12.0f, 16.0f)))); + slider->OnChange.Add([this](EventParams &) { + size_ = std::clamp(size_, 0.5f, 5.0f); + }); + } + + const char *tag() const override { return "TouchControlSizeSlider"; } + +protected: + void OnCompleted(DialogResult result) override { + if (result != DR_OK || !scale_) { + return; + } + *scale_ = std::clamp(size_, 0.5f, 5.0f); + g_Config.Save("TouchControlLayoutScreen::setControlSize"); + } + +private: + float *scale_; + float size_; }; class PSPActionButtons : public DragDropButton { @@ -95,7 +171,7 @@ class PSPActionButtons : public DragDropButton { PSPActionButtons(ConfigTouchPos &pos, const char *key, float &spacing, const Bounds &screenBounds) : DragDropButton(pos, key, ImageID::invalid(), ImageID::invalid(), screenBounds), spacing_(spacing) { using namespace UI; - roundId_ = g_Config.iTouchButtonStyle ? ImageID("I_ROUND_LINE") : ImageID("I_ROUND"); + roundId_ = g_Config.iTouchButtonStyle == 1 ? ImageID("I_ROUND_LINE") : ImageID("I_ROUND"); }; void setCircleVisibility(bool visible) { @@ -117,7 +193,7 @@ class PSPActionButtons : public DragDropButton { void Draw(UIContext &dc) override { scale_ = theScale_*layoutAreaScale; float opacity = GamepadGetOpacity(); - uint32_t colorBg = colorAlpha(GetButtonColor(), opacity); + uint32_t colorBg = GetEditorButtonFillColor(opacity); uint32_t color = colorAlpha(0xFFFFFF, opacity); int centerX = bounds_.centerX(); @@ -246,13 +322,13 @@ class PSPDPadButtons : public DragDropButton { void Draw(UIContext &dc) override { scale_ = theScale_*layoutAreaScale; float opacity = GamepadGetOpacity(); - uint32_t colorBg = colorAlpha(GetButtonColor(), opacity); + uint32_t colorBg = GetEditorButtonFillColor(opacity); uint32_t color = colorAlpha(0xFFFFFF, opacity); static const float xoff[4] = {1, 0, -1, 0}; static const float yoff[4] = {0, 1, 0, -1}; - ImageID dirImage = g_Config.iTouchButtonStyle ? ImageID("I_DIR_LINE") : ImageID("I_DIR"); + ImageID dirImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_DIR_LINE") : ImageID("I_DIR"); for (int i = 0; i < 4; i++) { float r = D_pad_Radius * spacing_ * layoutAreaScale; @@ -291,10 +367,10 @@ class PSPStickDragDrop : public DragDropButton { scale_ = theScale_ * layoutAreaScale; float opacity = GamepadGetOpacity(); - uint32_t colorBg = colorAlpha(GetButtonColor(), opacity); + uint32_t colorBg = GetEditorButtonFillColor(opacity); - const ImageID stickImage = g_Config.iTouchButtonStyle ? ImageID("I_STICK_LINE") : ImageID("I_STICK"); - const ImageID stickBg = g_Config.iTouchButtonStyle ? ImageID("I_STICK_BG_LINE") : ImageID("I_STICK_BG"); + const ImageID stickImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_STICK_LINE") : ImageID("I_STICK"); + const ImageID stickBg = g_Config.iTouchButtonStyle == 1 ? ImageID("I_STICK_BG_LINE") : ImageID("I_STICK_BG"); dc.Draw()->DrawImage(stickBg, bounds_.centerX(), bounds_.centerY(), scale_, colorBg, ALIGN_CENTER); dc.Draw()->DrawImage(stickImage, bounds_.centerX(), bounds_.centerY(), scale_ * spacing_, colorBg, ALIGN_CENTER); @@ -350,8 +426,8 @@ class DragDropButton; class ControlLayoutView : public UI::AnchorLayout { public: - ControlLayoutView(DeviceOrientation orientation, UI::LayoutParams *layoutParams) - : UI::AnchorLayout(layoutParams), deviceOrientation_(orientation) { + ControlLayoutView(DeviceOrientation orientation, bool *setSizeMode, ScreenManager *screenManager, UI::LayoutParams *layoutParams) + : UI::AnchorLayout(layoutParams), deviceOrientation_(orientation), setSizeMode_(setSizeMode), screenManager_(screenManager) { SetClip(true); } @@ -379,6 +455,8 @@ class ControlLayoutView : public UI::AnchorLayout { float startSpacing_ = -1.0f; DeviceOrientation deviceOrientation_; + bool *setSizeMode_; + ScreenManager *screenManager_; }; static Point2D ClampTo(const Point2D &p, const Bounds &b) { @@ -439,6 +517,11 @@ bool ControlLayoutView::Touch(const TouchInput &touch) { } if ((touch.flags & TouchInputFlags::DOWN) && pickedControl_ == 0) { pickedControl_ = getPickedControl(touch.x, touch.y); + if (pickedControl_ && mode_ == 1 && setSizeMode_ && *setSizeMode_) { + screenManager_->push(new TouchControlSizeSliderScreen(pickedControl_->GetScalePtr(), pickedControl_->GetScale())); + pickedControl_ = nullptr; + return true; + } if (pickedControl_) { startDragX_ = touch.x; startDragY_ = touch.y; @@ -458,12 +541,15 @@ bool ControlLayoutView::Touch(const TouchInput &touch) { } void ControlLayoutView::Draw(UIContext& dc) { - float opacity = g_Config.iTouchButtonOpacity / 100.0f; - GamepadUpdateOpacity(std::max(0.5f, opacity)); + // Use the configured button fill opacity in the editor, just like normal gameplay. + const float savedOpacity = GamepadGetOpacity(); + const float opacity = g_Config.iTouchButtonOpacity / 100.0f; + GamepadUpdateOpacity(opacity); using namespace UI; dc.FillRect(Drawable(0x80000000), bounds_); dc.Flush(); UI::AnchorLayout::Draw(dc); + GamepadUpdateOpacity(savedOpacity); } void ControlLayoutView::CreateViews() { @@ -476,7 +562,7 @@ void ControlLayoutView::CreateViews() { } // Create all the subviews. - TouchControlConfig &touch = g_Config.GetTouchControlsConfig(deviceOrientation_); + TouchControlConfig &touch = g_Config.GetCurrentTouchControlsConfig(deviceOrientation_); if (touch.bShowTouchCircle || touch.bShowTouchCross || touch.bShowTouchTriangle || touch.bShowTouchSquare) { PSPActionButtons *actionButtons = new PSPActionButtons(touch.touchActionButtonCenter, "Action buttons", touch.fActionButtonSpacing, bounds); @@ -487,11 +573,11 @@ void ControlLayoutView::CreateViews() { controls_.push_back(actionButtons); } - ImageID rectImage = g_Config.iTouchButtonStyle ? ImageID("I_RECT_LINE") : ImageID("I_RECT"); - ImageID roundImage = g_Config.iTouchButtonStyle ? ImageID("I_ROUND_LINE") : ImageID("I_ROUND"); - ImageID shoulderImage = g_Config.iTouchButtonStyle ? ImageID("I_SHOULDER_LINE") : ImageID("I_SHOULDER"); - ImageID stickImage = g_Config.iTouchButtonStyle ? ImageID("I_STICK_LINE") : ImageID("I_STICK"); - ImageID stickBg = g_Config.iTouchButtonStyle ? ImageID("I_STICK_BG_LINE") : ImageID("I_STICK_BG"); + ImageID rectImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_RECT_LINE") : ImageID("I_RECT"); + ImageID roundImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_ROUND_LINE") : ImageID("I_ROUND"); + ImageID shoulderImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_SHOULDER_LINE") : ImageID("I_SHOULDER"); + ImageID stickImage = g_Config.iTouchButtonStyle == 1 ? ImageID("I_STICK_LINE") : ImageID("I_STICK"); + ImageID stickBg = g_Config.iTouchButtonStyle == 1 ? ImageID("I_STICK_BG_LINE") : ImageID("I_STICK_BG"); auto addDragDropButton = [&](ConfigTouchPos &pos, const char *key, ImageID bgImg, ImageID img) { DragDropButton *b = nullptr; @@ -524,10 +610,11 @@ void ControlLayoutView::CreateViews() { controls_.push_back(new PSPStickDragDrop(touch.touchRightAnalogStick, "Right analog stick", stickBg, stickImage, bounds, touch.fRightStickHeadScale)); } - auto addDragCustomKey = [&](ConfigTouchPos &pos, const char *key, const ConfigCustomButton& cfg) { + auto addDragCustomKey = [&](ConfigTouchPos &pos, const char *key, const ConfigCustomButton& cfg, bool summaryShow, const std::string &summaryText, int summaryPosition, int summaryMargin) { DragDropButton *b = nullptr; if (pos.show) { - b = new DragDropButton(pos, key, g_Config.iTouchButtonStyle == 0 ? customKeyShapes[cfg.shape].i : customKeyShapes[cfg.shape].l, customKeyImages[cfg.image].i, bounds); + // Only Border uses outline shapes; Classic uses filled shapes. + b = new DragDropButton(pos, key, g_Config.iTouchButtonStyle == 1 ? customKeyShapes[cfg.shape].l : customKeyShapes[cfg.shape].i, customKeyImages[cfg.image].i, bounds); b->FlipImageH(customKeyShapes[cfg.shape].f); b->SetAngle(customKeyImages[cfg.image].r, customKeyShapes[cfg.shape].r); controls_.push_back(b); @@ -541,7 +628,7 @@ void ControlLayoutView::CreateViews() { char temp[64]; snprintf(temp, sizeof(temp), "Custom %d button", i); - addDragCustomKey(touch.touchCustom[i], temp, g_Config.CustomButton[i]); + addDragCustomKey(touch.touchCustom[i], temp, g_Config.CustomButton[i], g_Config.bCustomButtonSummary[i], g_Config.sCustomButtonSummaryText[i], g_Config.iCustomButtonSummaryPosition[i], g_Config.iCustomButtonSummaryMargin[i]); } for (size_t i = 0; i < controls_.size(); i++) { @@ -590,12 +677,23 @@ void TouchControlLayoutScreen::OnReset(UI::EventParams &e) { const Bounds &bounds = GetLayoutBounds(*screenManager()->getUIContext()); const DeviceOrientation orientation = GetDeviceOrientation(); - TouchControlConfig &touch = g_Config.GetTouchControlsConfig(orientation); + TouchControlConfig &touch = g_Config.GetCurrentTouchControlsConfig(orientation); touch.ResetLayout(); InitPadLayout(&touch, orientation, bounds.w, bounds.h); RecreateViews(); }; +void TouchControlLayoutScreen::OnLayoutSelection(UI::EventParams &e) { + const DeviceOrientation orientation = GetDeviceOrientation(); + const int selection = e.a + 1; + g_Config.iTouchLayoutSelection = selection; + g_Config.iTouchLayoutSelectionSaved = selection; + + const Bounds &bounds = GetLayoutBounds(*screenManager()->getUIContext()); + InitPadLayout(&g_Config.GetCurrentTouchControlsConfig(orientation), orientation, bounds.w, bounds.h); + RecreateViews(); +} + void TouchControlLayoutScreen::dialogFinished(const Screen *dialog, DialogResult result) { UIBaseDialogScreen::dialogFinished(dialog, result); RecreateViews(); @@ -621,7 +719,7 @@ void TouchControlLayoutScreen::CreateViews() { // setup g_Config for button layout const Bounds &bounds = GetLayoutBounds(*screenManager()->getUIContext()); const DeviceOrientation orientation = GetDeviceOrientation(); - InitPadLayout(&g_Config.GetTouchControlsConfig(orientation), orientation, bounds.w, bounds.h); + InitPadLayout(&g_Config.GetCurrentTouchControlsConfig(orientation), orientation, bounds.w, bounds.h); // const bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait; @@ -642,22 +740,34 @@ void TouchControlLayoutScreen::CreateViews() { mode_ = new ChoiceStrip(ORIENT_VERTICAL); mode_->AddChoice(di->T("Move"), ImageID("I_MOVE")); mode_->AddChoice(di->T("Resize"), ImageID("I_RESIZE")); - mode_->SetSelection(0, false); + mode_->SetSelection(selectedMode_, false); mode_->OnChoice.Add([this](UI::EventParams &e) { - int mode = mode_->GetSelection(); + selectedMode_ = mode_->GetSelection(); if (layoutView_) { - layoutView_->mode_ = mode; + layoutView_->mode_ = selectedMode_; } }); + ChoiceStrip *layoutSelectionStrip = new ChoiceStrip(ORIENT_VERTICAL); + layoutSelectionStrip->AddChoice("Layout 1"); + layoutSelectionStrip->AddChoice("Layout 2"); + layoutSelectionStrip->SetSelection(g_Config.iTouchLayoutSelection - 1, false); + layoutSelectionStrip->OnChoice.Handle(this, &TouchControlLayoutScreen::OnLayoutSelection); + CheckBox *snap = new CheckBox(&g_Config.bTouchSnapToGrid, di->T("Snap")); PopupSliderChoice *gridSize = new PopupSliderChoice(&g_Config.iTouchSnapGridSize, 2, 256, 64, di->T("Grid"), screenManager(), ""); gridSize->SetEnabledPtr(&g_Config.bTouchSnapToGrid); leftColumn->Add(mode_); + leftColumn->Add(new CheckBox(&setSizeMode_, di->T("Set size")))->SetEnabledFunc([this]() { + return mode_ && mode_->GetSelection() == 1; + }); leftColumn->Add(new Choice(co->T("Customize")))->OnClick.Add([this](UI::EventParams &e) { screenManager()->push(new TouchControlVisibilityScreen(gamePath_)); }); + leftColumn->Add(new Spacer(8.0f)); + leftColumn->Add(new TextView(di->T("Layout:")))->SetTextSize(TextSize::Small); + leftColumn->Add(layoutSelectionStrip); leftColumn->Add(snap); leftColumn->Add(gridSize); leftColumn->Add(new Choice(di->T("Reset")))->OnClick.Handle(this, &TouchControlLayoutScreen::OnReset); @@ -670,5 +780,6 @@ void TouchControlLayoutScreen::CreateViews() { rightColumn->Add(new TextView(co->T(DeviceOrientationToString(orientation))))->SetTextSize(TextSize::Small); rightColumn->Add(new Spacer(new LinearLayoutParams(1.0))); float previewHeight = bounds.h * layoutAreaScale; - layoutView_ = rightColumn->Add(new ControlLayoutView(GetDeviceOrientation(), new LinearLayoutParams(FILL_PARENT, previewHeight))); + layoutView_ = rightColumn->Add(new ControlLayoutView(GetDeviceOrientation(), &setSizeMode_, screenManager(), new LinearLayoutParams(FILL_PARENT, previewHeight))); + layoutView_->mode_ = selectedMode_; } diff --git a/UI/TouchControlLayoutScreen.h b/UI/TouchControlLayoutScreen.h index 528738773711..690f84a58540 100644 --- a/UI/TouchControlLayoutScreen.h +++ b/UI/TouchControlLayoutScreen.h @@ -40,8 +40,11 @@ class TouchControlLayoutScreen : public UIBaseDialogScreen { ViewLayoutMode LayoutMode() const override { return ViewLayoutMode::ApplyInsets; } void OnReset(UI::EventParams &e); void OnMode(UI::EventParams &e); + void OnLayoutSelection(UI::EventParams &e); private: UI::ChoiceStrip *mode_ = nullptr; ControlLayoutView *layoutView_ = nullptr; + bool setSizeMode_ = false; + int selectedMode_ = 0; }; diff --git a/UI/TouchControlVisibilityScreen.cpp b/UI/TouchControlVisibilityScreen.cpp index fd9a5c4d0c41..708861c50087 100644 --- a/UI/TouchControlVisibilityScreen.cpp +++ b/UI/TouchControlVisibilityScreen.cpp @@ -23,6 +23,8 @@ #include "Common/UI/PopupScreens.h" #include "Common/UI/ScreenManager.h" +#include + #include "Core/Config.h" #include "UI/TouchControlVisibilityScreen.h" @@ -30,6 +32,58 @@ static const int leftColumnWidth = 140; +class CustomKeySizeSliderScreen : public UI::PopupScreen { +public: + explicit CustomKeySizeSliderScreen(float size) + : PopupScreen("Set size to all Custom Keys", "OK", "Cancel"), size_(size) { + } + + void CreatePopupContents(UI::ViewGroup *parent) override { + using namespace UI; + SliderFloat *slider = parent->Add(new SliderFloat(&size_, 0.5f, 3.0f, + new LinearLayoutParams(Margins(12.0f, 16.0f)))); + slider->OnChange.Add([this](EventParams &) { + size_ = std::clamp(size_, 0.5f, 3.0f); + }); + } + + const char *tag() const override { return "CustomKeySizeSlider"; } + +protected: + void OnCompleted(DialogResult result) override { + if (result != DR_OK) { + return; + } + const float size = std::clamp(size_, 0.5f, 3.0f); + for (int i = 0; i < TouchControlConfig::CUSTOM_BUTTON_COUNT; ++i) { + g_Config.touchControlsLandscape.touchCustom[i].scale = size; + g_Config.touchControlsPortrait.touchCustom[i].scale = size; + } + g_Config.Save("TouchControlVisibilityScreen::setCustomKeySize"); + } + +private: + float size_; +}; + +class CustomKeySizeChoice : public UI::Choice { +public: + CustomKeySizeChoice(float size, std::string_view text, ScreenManager *screenManager) + : Choice(text), size_(size), screenManager_(screenManager) { + OnClick.Add([this](UI::EventParams &e) { + auto popup = new CustomKeySizeSliderScreen(size_); + if (e.v) { + popup->SetPopupOrigin(e.v); + } + screenManager_->push(popup); + }); + } + +private: + float size_; + ScreenManager *screenManager_; +}; + class CheckBoxChoice : public UI::Choice { public: CheckBoxChoice(std::string_view text, UI::CheckBox *checkbox, UI::LayoutParams *lp) @@ -65,6 +119,10 @@ void TouchControlVisibilityScreen::CreateContextMenu(UI::ViewGroup *parent) { } nextToggleAll_ = !nextToggleAll_; }); + + parent->Add(new CustomKeySizeChoice( + std::clamp(g_Config.touchControlsLandscape.touchCustom[0].scale, 0.5f, 3.0f), + "Set size to all Custom Keys", screenManager())); } void TouchControlVisibilityScreen::CreateDialogViews(UI::ViewGroup *parent) { @@ -81,7 +139,7 @@ void TouchControlVisibilityScreen::CreateDialogViews(UI::ViewGroup *parent) { gridsettings.fillCells = true; GridLayout *grid = parent->Add(new GridLayoutList(gridsettings, new LayoutParams(FILL_PARENT, WRAP_CONTENT))); - TouchControlConfig &touch = g_Config.GetTouchControlsConfig(GetDeviceOrientation()); + TouchControlConfig &touch = g_Config.GetCurrentTouchControlsConfig(GetDeviceOrientation()); toggles_.clear(); toggles_.push_back({ "Circle", &touch.bShowTouchCircle, ImageID("I_CIRCLE"), nullptr }); @@ -118,15 +176,27 @@ void TouchControlVisibilityScreen::CreateDialogViews(UI::ViewGroup *parent) { Choice *choice; if (toggle.handle) { - // Handle custom button strings differently, and hackily. But will extend to arbitrary button counts. - char translated[256]; - int i = 0; - if (sscanf(toggle.key.c_str(), "Custom %d", &i) == 1) { - snprintf(translated, sizeof(translated), mc->T_cstr("Custom %d"), i); + // Custom buttons use their summary text when one is configured. + // Otherwise show the same icon used by the ComboKey itself. + int customIndex = -1; + if (sscanf(toggle.key.c_str(), "Custom %d", &customIndex) == 1) { + --customIndex; + if (customIndex >= 0 && customIndex < TouchControlConfig::CUSTOM_BUTTON_COUNT) { + const std::string &summary = g_Config.sCustomButtonSummaryText[customIndex]; + if (!summary.empty()) { + CustomKeyData::Sanitize(g_Config.CustomButton[customIndex]); + const ImageID icon = CustomKeyData::customKeyImages[g_Config.CustomButton[customIndex].image].i; + choice = new Choice(summary, icon, new LinearLayoutParams(1.0f)); + } else { + CustomKeyData::Sanitize(g_Config.CustomButton[customIndex]); + choice = new Choice(CustomKeyData::customKeyImages[g_Config.CustomButton[customIndex].image].i, new LinearLayoutParams(1.0f)); + } + } else { + choice = new Choice(mc->T(toggle.key), "", new LinearLayoutParams(1.0f)); + } } else { - truncate_cpy(translated, sizeof(translated), mc->T(toggle.key)); + choice = new Choice(mc->T(toggle.key), "", new LinearLayoutParams(1.0f)); } - choice = new Choice(std::string(translated) + " (" + std::string(mc->T("tap to customize")) + ")", "", new LinearLayoutParams(1.0f)); choice->OnClick.Add(toggle.handle); } else if (toggle.img.isValid()) { choice = new CheckBoxChoice(toggle.img, checkbox, new LinearLayoutParams(1.0f)); @@ -146,7 +216,37 @@ void TouchControlVisibilityScreen::CreateDialogViews(UI::ViewGroup *parent) { } } +void TouchControlVisibilityScreen::dialogFinished(const Screen *dialog, DialogResult result) { + UISimpleBaseDialogScreen::dialogFinished(dialog, result); + // Rebuild the choices after a custom button icon or summary text is edited. + RecreateViews(); +} + void TouchControlVisibilityScreen::onFinish(DialogResult result) { + // Refresh current layout pointer and sync visibility settings + TouchControlConfig &touch = g_Config.GetCurrentTouchControlsConfig(GetDeviceOrientation()); + for (size_t i = 0; i < toggles_.size(); i++) { + if (toggles_[i].show) { + bool val = *toggles_[i].show; + if (i == 0) touch.bShowTouchCircle = val; + else if (i == 1) touch.bShowTouchCross = val; + else if (i == 2) touch.bShowTouchSquare = val; + else if (i == 3) touch.bShowTouchTriangle = val; + else if (i == 4) touch.touchLKey.show = val; + else if (i == 5) touch.touchRKey.show = val; + else if (i == 6) touch.touchStartKey.show = val; + else if (i == 7) touch.touchSelectKey.show = val; + else if (i == 8) touch.touchDpad.show = val; + else if (i == 9) touch.touchAnalogStick.show = val; + else if (i == 10) touch.touchRightAnalogStick.show = val; + else if (i == 11) touch.touchFastForwardKey.show = val; + else if (i == 12) touch.touchPauseKey.show = val; + else if (i >= 13 && i < (size_t)(13 + TouchControlConfig::CUSTOM_BUTTON_COUNT)) { + int idx = i - 13; + touch.touchCustom[idx].show = val; + } + } + } g_Config.Save("TouchControlVisibilityScreen::onFinish"); } @@ -162,7 +262,7 @@ void RightAnalogMappingScreen::CreateDialogViews(UI::ViewGroup *parent) { auto co = GetI18NCategory(I18NCat::CONTROLS); auto mc = GetI18NCategory(I18NCat::MAPPABLECONTROLS); - TouchControlConfig &touch = g_Config.GetTouchControlsConfig(GetDeviceOrientation()); + TouchControlConfig &touch = g_Config.GetCurrentTouchControlsConfig(GetDeviceOrientation()); static const char *rightAnalogButton[] = {"None", "L", "R", "Square", "Triangle", "Circle", "Cross", "D-pad up", "D-pad down", "D-pad left", "D-pad right", "Start", "Select", "RightAn.Up", "RightAn.Down", "RightAn.Left", "RightAn.Right", "An.Up", "An.Down", "An.Left", "An.Right"}; diff --git a/UI/TouchControlVisibilityScreen.h b/UI/TouchControlVisibilityScreen.h index 9aab91f16dd5..05c6480a80f9 100644 --- a/UI/TouchControlVisibilityScreen.h +++ b/UI/TouchControlVisibilityScreen.h @@ -41,6 +41,10 @@ class TouchControlVisibilityScreen : public UISimpleBaseDialogScreen { void CreateContextMenu(UI::ViewGroup *parent) override; void onFinish(DialogResult result) override; +protected: + void dialogFinished(const Screen *dialog, DialogResult result) override; + +public: const char *tag() const override { return "TouchControlVisibility"; } std::string_view GetTitle() const override; diff --git a/UI/UIAtlas.cpp b/UI/UIAtlas.cpp index 3430114e9541..14c65f36c012 100644 --- a/UI/UIAtlas.cpp +++ b/UI/UIAtlas.cpp @@ -15,6 +15,8 @@ #include "Common/Log.h" #include "Common/Data/Convert/ColorConv.h" #include "Core/Util/PathUtil.h" +#include "Core/System.h" +#include "Core/Config.h" #include "UI/UIAtlas.h" @@ -334,6 +336,8 @@ static bool RasterizeSVG(std::string_view filename, float dpiScale, int maxTextu int sy = minY + y; const u32 *src = (u32 *)svgImg + (sy * svgWidth + sx); u32 col = *src; + // Preserve the SVG's original colors for analog heads, including the built-in SVG. + // Head opacity is applied at draw time without replacing the original RGB colors. img.set1(x, y, col); } } @@ -365,7 +369,7 @@ static bool RasterizeSVG(std::string_view filename, float dpiScale, int maxTextu return true; } -static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int maxTextureSize, const ImageMeta *imageIDs, size_t imageCount) { +static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, float touchScaleMultiplier, Image *dest, int maxTextureSize, const ImageMeta *imageIDs, size_t imageCount) { Bucket bucket; #ifdef _DEBUG @@ -382,24 +386,24 @@ static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int if (!RasterizeSVG("ui_images/images.svg", dpiScale, maxTextureSize, imageIDs, imageCount, &images)) { return false; } + // Apply the selected touch SVG multiplier to both user and built-in buttons.svg. + // RasterizeSVG stores the actual raster scale in Image::scale, so logical UI size is preserved. + const float touchDpiScale = dpiScale * touchScaleMultiplier; Path customButtons = GetSysDirectory(DIRECTORY_SYSTEM) / "buttons.svg"; - if (File::Exists(customButtons)) { - if (!RasterizeSVG(customButtons.c_str(), dpiScale, maxTextureSize, imageIDs, imageCount, &images)) { - return false; - } - } else { - if (!RasterizeSVG("ui_images/buttons.svg", dpiScale, maxTextureSize, imageIDs, imageCount, &images)) { - return false; - } + const char *buttonsSVG = File::Exists(customButtons) ? customButtons.c_str() : "ui_images/buttons.svg"; + INFO_LOG(Log::G3D, "Touch button SVG quality: %.0fx (base scale %.2f, requested raster scale %.2f)", touchScaleMultiplier, dpiScale, touchDpiScale); + if (!RasterizeSVG(buttonsSVG, touchDpiScale, maxTextureSize, imageIDs, imageCount, &images)) { + return false; } Instant shadowStart = Instant::Now(); // We can trivially parallelize shadowing/extension of the images. - ParallelRangeLoop(&g_threadManager, [&images, imageIDs](int start, int end) { + const bool disableTouchButtonShadows = g_Config.bDisableTouchButtonShadows; + ParallelRangeLoop(&g_threadManager, [&images, imageIDs, disableTouchButtonShadows](int start, int end) { for (int i = start; i < end; i++) { // Here we could exclude some images from the drop shadow, if desired. if (!images[i].IsEmpty()) { - if (imageIDs[i].addShadow) { + if (imageIDs[i].addShadow && !disableTouchButtonShadows) { // DEBUG_LOG(Log::G3D, "Adding drop shadow to %.*s", STR_VIEW(imageIDs[i].id)); AddDropShadow(images[i], 3, 0.66f); } else { @@ -464,6 +468,13 @@ static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int Instant bucketStart = Instant::Now(); bucket.Pack2(imageWidth); INFO_LOG(Log::G3D, " - Packed in %.2f ms (image size: %dx%d)", bucketStart.ElapsedMs(), bucket.w, bucket.h); + // The SVG's source canvas can fit maxTextureSize while the combined atlas does not. + // Reject oversized atlases before allocating/copying the final texture, so the caller + // can retry at a lower touch-button raster scale. + if (maxTextureSize > 0 && (bucket.w > maxTextureSize || bucket.h > maxTextureSize)) { + WARN_LOG(Log::G3D, "UI atlas %dx%d exceeds device texture limit %d; retrying at lower touch SVG quality", bucket.w, bucket.h, maxTextureSize); + return false; + } Instant resolveStart = Instant::Now(); std::vector results = bucket.Resolve(dest); @@ -493,21 +504,41 @@ static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int static Image g_cachedUIAtlasImage; static float g_cachedDpiScale = 0.0f; +static int g_cachedTouchAtlasScale = -1; +static bool g_cachedDisableTouchButtonShadows = false; // The caller must cache the Atlas. Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas, float dpiScale, bool invalidate) { - if (g_cachedUIAtlasImage.IsEmpty() || dpiScale != g_cachedDpiScale || invalidate) { + if (g_cachedUIAtlasImage.IsEmpty() || dpiScale != g_cachedDpiScale || g_Config.iTouchButtonAtlasScale != g_cachedTouchAtlasScale || g_Config.bDisableTouchButtonShadows != g_cachedDisableTouchButtonShadows || invalidate) { INFO_LOG(Log::G3D, "Regenerating atlas (empty: %s). Dpi scale (changed: %s): %0.2f (invalidate=%d)", g_cachedUIAtlasImage.IsEmpty() ? "true" : "false", dpiScale != g_cachedDpiScale ? "true" : "false", dpiScale, invalidate); - g_cachedUIAtlasImage.clear(); - if (!GenerateUIAtlasImage(atlas, dpiScale, &g_cachedUIAtlasImage, draw->GetDeviceCaps().maxTextureSize, g_uiImageIDs, ARRAY_SIZE(g_uiImageIDs))) { - ERROR_LOG(Log::G3D, "Failed to generate UI atlas!"); + // Quality levels are user-facing values 1-4, with 3 as the default. + static constexpr float kTouchAtlasMultipliers[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + const int requestedQuality = std::clamp(g_Config.iTouchButtonAtlasScale, 1, (int)ARRAY_SIZE(kTouchAtlasMultipliers)); + const int maxTextureSize = draw->GetDeviceCaps().maxTextureSize; + int effectiveQuality = requestedQuality; + bool generated = false; + for (; effectiveQuality >= 1; --effectiveQuality) { + g_cachedUIAtlasImage.clear(); + if (GenerateUIAtlasImage(atlas, dpiScale, kTouchAtlasMultipliers[effectiveQuality - 1], &g_cachedUIAtlasImage, maxTextureSize, g_uiImageIDs, ARRAY_SIZE(g_uiImageIDs))) { + generated = true; + break; + } + WARN_LOG(Log::G3D, "Retrying UI atlas with touch SVG quality %dx", (int)kTouchAtlasMultipliers[std::max(1, effectiveQuality - 1) - 1]); + } + if (!generated) { + ERROR_LOG(Log::G3D, "Failed to generate UI atlas at every supported touch SVG quality"); return nullptr; } + if (effectiveQuality < requestedQuality) { + WARN_LOG(Log::G3D, "Touch SVG quality requested %dx but atlas limits require %dx", (int)kTouchAtlasMultipliers[requestedQuality - 1], (int)kTouchAtlasMultipliers[effectiveQuality - 1]); + } } g_cachedDpiScale = dpiScale; + g_cachedTouchAtlasScale = g_Config.iTouchButtonAtlasScale; + g_cachedDisableTouchButtonShadows = g_Config.bDisableTouchButtonShadows; // Create the texture. Draw::TextureDesc desc{}; diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index 09581d5c3c7b..4accc1b7c3e2 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -199,6 +199,19 @@ Screen Rotation = Screen rotation Sensitivity = Sensitivity Sensitivity (scale) = Sensitivity (scale) Shape = Shape +Summary = Summary +Summary text = Summary text +Summary position = Summary position +Summary margin = Summary margin +Center = Center +Left = Left +Down = Down +Right = Right +Up = Up +None = None +Small = Small +Medium = Medium +High = High Sticky D-Pad = Sticky D-Pad Swipe = Swipe Swipe sensitivity = Swipe sensitivity diff --git a/assets/lang/id_ID.ini b/assets/lang/id_ID.ini index 3d7b4354b8b3..80362206bac5 100644 --- a/assets/lang/id_ID.ini +++ b/assets/lang/id_ID.ini @@ -176,6 +176,19 @@ Screen Rotation = Rotasi layar Sensitivity (scale) = Sensitivitas (skala) Sensitivity = Sensitivitas Shape = Bentuk +Summary = Ringkasan +Summary text = Teks ringkasan +Summary position = Posisi ringkasan +Summary margin = Jarak ringkasan +Center = Tengah +Left = Kiri +Down = Bawah +Right = Kanan +Up = Atas +None = Tidak ada +Small = Kecil +Medium = Sedang +High = Tinggi Sticky D-Pad = Sticky D-Pad Swipe = Geser Swipe sensitivity = Sensitivitas gerakan geser