From ef3c7985ee9b419d838826eaedfe2dfb062bdf7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Nov 2026 10:35:23 +0100 Subject: [PATCH] UI: Draw setting hints inside their Choice or CheckBox ClickableItem gets a hint text, drawn small under the item's own content, so it highlights and clicks along with it instead of being a separate SettingHint view below. Settings pages attach it with .Hint() on the item it belongs to, and search matches it as part of that item. SettingHint stays for the few hints that have no item to go in: custom settings views that aren't ClickableItems, and cheat comments under a title. Co-Authored-By: Claude Opus 5.5 --- Common/UI/PopupScreens.cpp | 12 ++++--- Common/UI/View.cpp | 62 +++++++++++++++++++++++++------- Common/UI/View.h | 22 ++++++++++++ UI/CwCheatScreen.cpp | 14 ++++++-- UI/Settings/AudioPage.cpp | 9 ++--- UI/Settings/ControlsPage.cpp | 9 ++--- UI/Settings/GraphicsPage.cpp | 21 ++++------- UI/Settings/NetworkingPage.cpp | 18 ++++------ UI/SettingsPage.cpp | 26 +++++++------- UI/SettingsPage.h | 7 ++-- UI/SettingsPagesDialogScreen.cpp | 12 +++---- UI/TiltAnalogSettingsScreen.cpp | 3 +- 12 files changed, 130 insertions(+), 85 deletions(-) diff --git a/Common/UI/PopupScreens.cpp b/Common/UI/PopupScreens.cpp index e7a301ebb8..65e5406ebb 100644 --- a/Common/UI/PopupScreens.cpp +++ b/Common/UI/PopupScreens.cpp @@ -1088,26 +1088,28 @@ void AbstractChoiceWithValueDisplay::Draw(UIContext &dc) { textPadding_.right = w + paddingX; Choice::Draw(dc); + const Bounds bounds = ContentBounds(); int imagePadding = 0; if (rightIconImage_.isValid()) { - imagePadding = bounds_.h; + imagePadding = bounds.h; } dc.SetFontScale(scale, scale); - Bounds valueBounds(bounds_.x2() - textPadding_.right - imagePadding, bounds_.y, w, bounds_.h); + Bounds valueBounds(bounds.x2() - textPadding_.right - imagePadding, bounds.y, w, bounds.h); dc.DrawTextRect(valueText, valueBounds, color, ALIGN_RIGHT | ALIGN_VCENTER | FLAG_WRAP_TEXT); dc.SetFontScale(1.0f, 1.0f); } else { Choice::Draw(dc); + const Bounds bounds = ContentBounds(); if (iconOnly_) { // In this case we only display the image of the choice. Useful for small buttons spawning a popup. - dc.Draw()->DrawImageRotated(ValueImage(), bounds_.centerX(), bounds_.centerY(), imgScale_, imgRot_, style.fgColor, imgFlipH_); + dc.Draw()->DrawImageRotated(ValueImage(), bounds.centerX(), bounds.centerY(), imgScale_, imgRot_, style.fgColor, imgFlipH_); return; } - float scale = CalculateValueScale(dc, valueText, bounds_.w); + float scale = CalculateValueScale(dc, valueText, bounds.w); dc.SetFontScale(scale, scale); - dc.DrawTextRect(valueText, bounds_.Expand(-paddingX, 0.0f), color, ALIGN_LEFT | ALIGN_VCENTER | FLAG_WRAP_TEXT); + dc.DrawTextRect(valueText, bounds.Expand(-paddingX, 0.0f), color, ALIGN_LEFT | ALIGN_VCENTER | FLAG_WRAP_TEXT); dc.SetFontScale(1.0f, 1.0f); } } diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp index 0f7b8140a6..1c9920aade 100644 --- a/Common/UI/View.cpp +++ b/Common/UI/View.cpp @@ -439,6 +439,36 @@ void ClickableItem::Draw(UIContext &dc) { DrawBG(dc, style, dc.GetTheme().choiceRoundRadius); } +static constexpr float HINT_PADDING_X = 12.0f; +static constexpr float HINT_PADDING_BOTTOM = 8.0f; + +void ClickableItem::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) { + Clickable::Measure(dc, horiz, vert); + hintHeight_ = 0.0f; + if (hintText_.empty()) { + return; + } + float availWidth = measuredWidth_ - HINT_PADDING_X * 2; + if (availWidth <= 0.0f) { + availWidth = MAX_ITEM_SIZE; + } + float w, h; + dc.MeasureTextRect(dc.GetTheme().uiFontTiny, 1.0f, 1.0f, hintText_, availWidth, &w, &h, FLAG_WRAP_TEXT); + hintHeight_ = h + HINT_PADDING_BOTTOM; + measuredHeight_ += hintHeight_; +} + +void ClickableItem::DrawHint(UIContext &dc, const Style &style) const { + if (hintText_.empty()) { + return; + } + dc.SetFontStyle(dc.GetTheme().uiFontTiny); + dc.SetFontScale(1.0f, 1.0f); + Bounds hintBounds(bounds_.x + HINT_PADDING_X, bounds_.y2() - hintHeight_, bounds_.w - HINT_PADDING_X * 2, hintHeight_ - HINT_PADDING_BOTTOM); + dc.DrawTextRect(hintText_, hintBounds, style.fgColor, ALIGN_TOPLEFT | FLAG_WRAP_TEXT); + dc.SetFontStyle(dc.GetTheme().uiFont); +} + void Choice::ClickInternal() { ClickableItem::ClickInternal(); UI::PlayUISound(UI::UISound::CONFIRM); @@ -494,10 +524,12 @@ void Choice::Draw(UIContext &dc) { DrawBG(dc, style, dc.GetTheme().choiceRoundRadius); + const Bounds bounds = ContentBounds(); + if (image_.isValid() && text_.empty()) { - dc.Draw()->DrawImageRotated(image_, bounds_.centerX(), bounds_.centerY(), imgScale_, imgRot_, style.fgColor, imgFlipH_); + dc.Draw()->DrawImageRotated(image_, bounds.centerX(), bounds.centerY(), imgScale_, imgRot_, style.fgColor, imgFlipH_); if (shine_) { - Bounds b = Bounds::FromCenter(bounds_.centerX(), bounds_.centerY(), bounds_.h * 0.4f); + Bounds b = Bounds::FromCenter(bounds.centerX(), bounds.centerY(), bounds.h * 0.4f); DrawIconShine(dc, b.Inset(5.0f, 5.0f), 0.65f, false); } } else if (!text_.empty() && !hideTitle_) { @@ -512,9 +544,9 @@ void Choice::Draw(UIContext &dc) { if (image) { _dbg_assert_(image); // TODO: Use scale rotation and flip here as well (DrawImageRotated is always ALIGN_CENTER for now) - dc.Draw()->DrawImage(image_, bounds_.x + bounds_.h * 0.5f + paddingLeft, bounds_.centerY(), 1.0f, style.fgColor, ALIGN_CENTER); + dc.Draw()->DrawImage(image_, bounds.x + bounds.h * 0.5f + paddingLeft, bounds.centerY(), 1.0f, style.fgColor, ALIGN_CENTER); - paddingLeft += bounds_.h; + paddingLeft += bounds.h; } } @@ -524,30 +556,32 @@ void Choice::Draw(UIContext &dc) { paddingRight = std::max(ITEM_HEIGHT, imgW); } - float availWidth = bounds_.w - (paddingLeft + paddingRight + textPadding_.horiz()); + float availWidth = bounds.w - (paddingLeft + paddingRight + textPadding_.horiz()); if (centered_) { - dc.DrawTextRectSqueeze(text_, bounds_, style.fgColor, ALIGN_CENTER | FLAG_WRAP_TEXT | drawTextFlags_); + dc.DrawTextRectSqueeze(text_, bounds, style.fgColor, ALIGN_CENTER | FLAG_WRAP_TEXT | drawTextFlags_); } else { if (rightIconImage_.isValid()) { uint32_t col = rightIconKeepColor_ ? 0xffffffff : style.fgColor; // Don't apply theme to gold icon - float iconX = bounds_.x2() - paddingRight + ITEM_HEIGHT * 0.5f; + float iconX = bounds.x2() - paddingRight + ITEM_HEIGHT * 0.5f; if (shine_) { - Bounds b = Bounds::FromCenter(iconX, bounds_.centerY(), bounds_.h * 0.4f); + Bounds b = Bounds::FromCenter(iconX, bounds.centerY(), bounds.h * 0.4f); DrawIconShine(dc, b.Inset(5.0f, 5.0f), 0.65f, false); } - dc.Draw()->DrawImageRotated(rightIconImage_, iconX, bounds_.centerY(), rightIconScale_, rightIconRot_, col, rightIconFlipH_); + dc.Draw()->DrawImageRotated(rightIconImage_, iconX, bounds.centerY(), rightIconScale_, rightIconRot_, col, rightIconFlipH_); } - Bounds textBounds(bounds_.x + paddingLeft + textPadding_.left, bounds_.y, availWidth, bounds_.h); + Bounds textBounds(bounds.x + paddingLeft + textPadding_.left, bounds.y, availWidth, bounds.h); dc.DrawTextRectSqueeze(text_, textBounds, style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT | drawTextFlags_); } dc.SetFontScale(1.0f, 1.0f); } + DrawHint(dc, style); + if (selected_) { - dc.Draw()->DrawImage(ImageID("I_CHECKMARK"), bounds_.x2() - 30, bounds_.centerY(), 1.0f, style.fgColor, ALIGN_CENTER); + dc.Draw()->DrawImage(ImageID("I_CHECKMARK"), bounds.x2() - 30, bounds.centerY(), 1.0f, style.fgColor, ALIGN_CENTER); } } @@ -832,11 +866,13 @@ void CheckBox::Draw(UIContext &dc) { // Padding right of the checkbox image too. const float availWidth = bounds_.w - paddingX * 2 - imageW - paddingX; + const Bounds bounds = ContentBounds(); if (!text_.empty()) { - Bounds textBounds(bounds_.x + paddingX, bounds_.y, availWidth, bounds_.h); + Bounds textBounds(bounds.x + paddingX, bounds.y, availWidth, bounds.h); dc.DrawTextRectSqueeze(text_, textBounds, style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); } - dc.Draw()->DrawImage(image, bounds_.x2() - paddingX, bounds_.centerY(), 1.0f, style.fgColor, ALIGN_RIGHT | ALIGN_VCENTER); + dc.Draw()->DrawImage(image, bounds.x2() - paddingX, bounds.centerY(), 1.0f, style.fgColor, ALIGN_RIGHT | ALIGN_VCENTER); + DrawHint(dc, style); dc.SetFontScale(1.0f, 1.0f); } diff --git a/Common/UI/View.h b/Common/UI/View.h index 70fc2508d9..d9a552b852 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -750,9 +750,31 @@ class ClickableItem : public Clickable { public: ClickableItem(LayoutParams *layoutParams); void GetContentDimensions(const UIContext &dc, float &w, float &h) const override; + void Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) override; // Draws the item background. void Draw(UIContext &dc) override; + + // Small explanatory text under the item, part of it (highlighted and clicked along with it). + // Drawn by Choice and CheckBox. + void SetHintText(std::string_view text) { + hintText_ = text; + } + const std::string &GetHintText() const { + return hintText_; + } + +protected: + // The item's bounds without the hint below. + Bounds ContentBounds() const { + Bounds b = bounds_; + b.h -= hintHeight_; + return b; + } + void DrawHint(UIContext &dc, const Style &style) const; + + std::string hintText_; + float hintHeight_ = 0.0f; }; // Use to trigger something or open a submenu screen. diff --git a/UI/CwCheatScreen.cpp b/UI/CwCheatScreen.cpp index b98f92a0bc..b48004d8b3 100644 --- a/UI/CwCheatScreen.cpp +++ b/UI/CwCheatScreen.cpp @@ -307,7 +307,7 @@ void CwCheatScreen::CreateContentViews(UI::ViewGroup *parent) { rightColumn->Add(new ItemHeader(cw->T("Cheats"))); bool prevIsTitle = false; - View *prev = nullptr; + CheckBox *prev = nullptr; for (size_t i = 0; i < fileInfo_.size(); ++i) { std::string_view text; if (fileInfo_[i].IsTitle(&text)) { @@ -318,7 +318,17 @@ void CwCheatScreen::CreateContentViews(UI::ViewGroup *parent) { prevIsTitle = true; prev = nullptr; } else if (fileInfo_[i].IsPostComment(&text)) { - rightColumn->Add(new SettingHint(text, prev)); + if (prev) { + // A cheat's comment lines go in its checkbox. + std::string hint = prev->GetHintText(); + if (!hint.empty()) { + hint += '\n'; + } + hint += text; + prev->SetHintText(hint); + } else { + rightColumn->Add(new SettingHint(text, nullptr)); + } prevIsTitle = false; } else { // Regular cheat code. diff --git a/UI/Settings/AudioPage.cpp b/UI/Settings/AudioPage.cpp index 7b16ac84be..9206301cea 100644 --- a/UI/Settings/AudioPage.cpp +++ b/UI/Settings/AudioPage.cpp @@ -103,8 +103,7 @@ void BuildAudioSettings(SettingsPage *page) { sdlAudio = true; static const int bufferSizes[] = {128, 256, 512, 1024, 2048}; - page->IntSlider(&g_Config.iSDLAudioBufferSize, 0, 2048, "Buffer size").Default(256).RestrictTo(bufferSizes, ARRAY_SIZE(bufferSizes)); - page->Hint({ I18NCat::DIALOG, "This change will not take effect until PPSSPP is restarted." }); + page->IntSlider(&g_Config.iSDLAudioBufferSize, 0, 2048, "Buffer size").Default(256).RestrictTo(bufferSizes, ARRAY_SIZE(bufferSizes)).Hint({ I18NCat::DIALOG, "This change will not take effect until PPSSPP is restarted." }); #endif #if PPSSPP_PLATFORM(WINDOWS) @@ -155,13 +154,11 @@ void BuildAudioSettings(SettingsPage *page) { }; // AAudio isn't stable before Android 8.1, so there it's always OpenSL ES. if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 27) { - page->Setting(&g_Config.iAndroidAudioApi).OnChange(restartAudio); - page->Hint({ I18NCat::AUDIO, "AudioApiHint", "Try the other one if you have no sound or other audio problems." }); + page->Setting(&g_Config.iAndroidAudioApi).OnChange(restartAudio).Hint({ I18NCat::AUDIO, "AudioApiHint", "Try the other one if you have no sound or other audio problems." }); page->Setting(&g_Config.bAndroidAudioMMap).OnChange(restartAudio).EnabledFunc([]() { return g_Config.iAndroidAudioApi == (int)AndroidAudioApi::AAUDIO; - }); - page->Hint({ I18NCat::AUDIO, "AudioMMapHint", "Lowest audio delay. On some devices, it causes silence over Bluetooth or other connected audio devices. Turn it off if you get no sound." }); + }).Hint({ I18NCat::AUDIO, "AudioMMapHint", "Lowest audio delay. On some devices, it causes silence over Bluetooth or other connected audio devices. Turn it off if you get no sound." }); } // Show Oboe debug info const std::string audioErrorStr = AndroidAudio_GetErrorString(g_audioState); diff --git a/UI/Settings/ControlsPage.cpp b/UI/Settings/ControlsPage.cpp index 7c68dc3bbd..0fe7a8edd2 100644 --- a/UI/Settings/ControlsPage.cpp +++ b/UI/Settings/ControlsPage.cpp @@ -103,8 +103,7 @@ void BuildControlsSettings(SettingsPage *page) { page->Setting(&touch.bHideStickBackground).Enabled(&g_Config.bShowTouchControls); // Sticky D-pad. - page->Setting(&g_Config.bStickyTouchDPad).Enabled(&g_Config.bShowTouchControls); - page->Hint("Easier sweeping movements"); + page->Setting(&g_Config.bStickyTouchDPad).Enabled(&g_Config.bShowTouchControls).Hint("Easier sweeping movements"); // Re-centers itself to the touch location on touch-down. page->Setting(&g_Config.bAutoCenterTouchAnalog).Enabled(&g_Config.bShowTouchControls); @@ -130,8 +129,7 @@ void BuildControlsSettings(SettingsPage *page) { #if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) page->Setting(&g_Config.bIgnoreWindowsKey); #endif - page->Setting(&g_Config.fAnalogLimiterDeadzone); - page->Hint({ I18NCat::CONTROLS, "AnalogLimiter Tip", "When the analog limiter button is pressed" }); + page->Setting(&g_Config.fAnalogLimiterDeadzone).Hint({ I18NCat::CONTROLS, "AnalogLimiter Tip", "When the analog limiter button is pressed" }); page->Setting(&g_Config.iRapidFireInterval); #if PPSSPP_PLATFORM(WINDOWS) || defined(SDL) || PPSSPP_PLATFORM(ANDROID) bool enableMouseSettings = true; @@ -148,8 +146,7 @@ void BuildControlsSettings(SettingsPage *page) { page->Header({ I18NCat::CONTROLS, "Mouse", "Mouse settings" }); page->Setting(&g_Config.iMouseWheelUpDelayMs); - page->Setting(&g_Config.bMouseControl); - page->Hint({ I18NCat::CONTROLS, "MouseControl Tip", "You can now map mouse in control mapping screen by pressing the 'M' icon." }); + page->Setting(&g_Config.bMouseControl).Hint({ I18NCat::CONTROLS, "MouseControl Tip", "You can now map mouse in control mapping screen by pressing the 'M' icon." }); #if !PPSSPP_PLATFORM(ANDROID) page->Setting(&g_Config.bMouseConfine).Enabled(&g_Config.bMouseControl); diff --git a/UI/Settings/GraphicsPage.cpp b/UI/Settings/GraphicsPage.cpp index 428f87fa37..9fed98791e 100644 --- a/UI/Settings/GraphicsPage.cpp +++ b/UI/Settings/GraphicsPage.cpp @@ -113,8 +113,7 @@ static void BuildSpeedHacksPage(SettingsPage *page) { page->Header(); - page->Setting(&g_Config.bSkipBufferEffects).Disabled(&g_Config.bSoftwareRendering); - page->Hint({I18NCat::GRAPHICS, "RenderingMode NonBuffered Tip", "Faster, but graphics may be missing in some games"}); + page->Setting(&g_Config.bSkipBufferEffects).Disabled(&g_Config.bSoftwareRendering).Hint({I18NCat::GRAPHICS, "RenderingMode NonBuffered Tip", "Faster, but graphics may be missing in some games"}); page->Setting(&g_Config.iSkipGPUReadbackMode).Disabled(&g_Config.bSoftwareRendering).Setup([](View *view) { static_cast(view)->SetDefault(0); @@ -302,8 +301,7 @@ void BuildGraphicsSettings(SettingsPage *page) { } page->Setting(&g_Config.bRenderDuplicateFrames).EnabledFunc([] { return !g_Config.bSkipBufferEffects && g_Config.iFrameSkip == 0; - }); - page->Hint({I18NCat::GRAPHICS, "RenderDuplicateFrames Tip", "Can make framerate smoother in games that run at lower framerates"}); + }).Hint({I18NCat::GRAPHICS, "RenderDuplicateFrames Tip", "Can make framerate smoother in games that run at lower framerates"}); page->Header("Frame Rate Control"); @@ -314,14 +312,12 @@ void BuildGraphicsSettings(SettingsPage *page) { } page->Header(); - page->Link({I18NCat::GRAPHICS, "Speed Hacks"}, I18NCat::GRAPHICS, &BuildSpeedHacksPage); - page->Hint({I18NCat::GRAPHICS, "SpeedHacksHint", "These settings can cause rendering errors"}); + page->Link({I18NCat::GRAPHICS, "Speed Hacks"}, I18NCat::GRAPHICS, &BuildSpeedHacksPage).Hint({I18NCat::GRAPHICS, "SpeedHacksHint", "These settings can cause rendering errors"}); page->Header(); page->Setting(&g_Config.iBloomHack).EnabledFunc([] { return !g_Config.bSoftwareRendering && g_Config.iInternalResolution != 1; - }); - page->Hint("Reduces artifacts"); + }).Hint("Reduces artifacts"); page->Header("Latency"); @@ -333,8 +329,7 @@ void BuildGraphicsSettings(SettingsPage *page) { g_Config.iInflightFrames = previous; }); } - }); - page->Hint("Faster, input lag"); // TODO: This hint could use improvement. + }).Hint("Faster, input lag"); // TODO: This hint could use improvement. } page->Header("Texture upscaling"); @@ -364,13 +359,11 @@ void BuildGraphicsSettings(SettingsPage *page) { }); page->Setting(&g_Config.iTexScalingLevel).EnabledFunc([]() { return !g_Config.bSoftwareRendering && !UsingHardwareTextureScaling(); - }); - page->Hint({ I18NCat::GRAPHICS, "UpscaleLevel Tip", "CPU heavy - some scaling may be delayed to avoid stutter" }); + }).Hint({ I18NCat::GRAPHICS, "UpscaleLevel Tip", "CPU heavy - some scaling may be delayed to avoid stutter" }); page->Setting(&g_Config.bTexDeposterize).EnabledFunc([]() { return !g_Config.bSoftwareRendering && !UsingHardwareTextureScaling(); - }); - page->Hint({ I18NCat::GRAPHICS, "Deposterize Tip", "Fixes visual banding glitches in upscaled textures" }); + }).Hint({ I18NCat::GRAPHICS, "Deposterize Tip", "Fixes visual banding glitches in upscaled textures" }); page->Header("Texture Filtering"); diff --git a/UI/Settings/NetworkingPage.cpp b/UI/Settings/NetworkingPage.cpp index 82786adf06..89e8b602ca 100644 --- a/UI/Settings/NetworkingPage.cpp +++ b/UI/Settings/NetworkingPage.cpp @@ -208,25 +208,21 @@ void BuildNetworkingSettings(SettingsPage *page) { page->Setting(&g_Config.iAdhocServerRelayMode).EnabledFunc([] { return !PSP_IsInited(); - }); - page->Hint({ I18NCat::NETWORKING, "PacketRelayHint", "Available on servers that provide 'aemu_postoffice' packet relay, like socom.cc. Disable this for LAN or VPN play. Can be more reliable, but sometimes slower." }); + }).Hint({ I18NCat::NETWORKING, "PacketRelayHint", "Available on servers that provide 'aemu_postoffice' packet relay, like socom.cc. Disable this for LAN or VPN play. Can be more reliable, but sometimes slower." }); page->Custom({ I18NCat::SYSTEM, "Nickname" }, [host](UI::ViewGroup *parent) { return AddNicknameChoice(host, parent); - }); - page->Hint("Try to pick a unique nickname for ad hoc play"); + }).Hint("Try to pick a unique nickname for ad hoc play"); page->Setting(&g_Config.bEnableAdhocServer).OnChange(requestLocalNetwork); page->Setting(&g_Config.bAdhocUseUdp).EnabledFunc([] { return !PSP_IsInited(); - }); - page->Hint("Try switching between TCP/UDP if you suffer frequent adhoc server disconnects. Some network environments favor TCP and some UDP."); + }).Hint("Try switching between TCP/UDP if you suffer frequent adhoc server disconnects. Some network environments favor TCP and some UDP."); page->Setting(&g_Config.bAdhocRelayModeEnetPdpReliable).EnabledFunc([] { return !PSP_IsInited() && g_Config.bAdhocUseUdp; - }); - page->Hint("When disabled, packet loss on datagram adhoc sockets will be exposed to the game's netcode. Keep this enabled if you don't know what that means."); + }).Hint("When disabled, packet loss on datagram adhoc sockets will be exposed to the game's netcode. Keep this enabled if you don't know what that means."); page->Header("Infrastructure"); if (g_Config.sInfrastructureUsername.empty()) { @@ -261,8 +257,7 @@ void BuildNetworkingSettings(SettingsPage *page) { // away, instead of waiting for a port request that may never come. UPnP_Notify(); }); - page->Setting(&g_Config.bUPnPUseOriginalPort).Enabled(&g_Config.bEnableUPnP); - page->Hint({ I18NCat::NETWORKING, "UseOriginalPort Tip", "May not work for all devices or games, see wiki." }); + page->Setting(&g_Config.bUPnPUseOriginalPort).Enabled(&g_Config.bEnableUPnP).Hint({ I18NCat::NETWORKING, "UseOriginalPort Tip", "May not work for all devices or games, see wiki." }); page->Header(); page->Link({I18NCat::NETWORKING, "Chat"}, I18NCat::NETWORKING, &BuildChatSettingsPage); @@ -282,8 +277,7 @@ void BuildNetworkingSettings(SettingsPage *page) { wlanChannelChoice->HideChoice(i + 7); } }); - page->Setting(&g_Config.iPortOffset); - page->Hint("PortOffsetHint"); + page->Setting(&g_Config.iPortOffset).Hint("PortOffsetHint"); page->Setting(&g_Config.iMinTimeout); page->Setting(&g_Config.bForcedFirstConnect); page->Setting(&g_Config.bAllowSpeedControlWhileConnected); diff --git a/UI/SettingsPage.cpp b/UI/SettingsPage.cpp index ad35cc9ca9..883b4547a2 100644 --- a/UI/SettingsPage.cpp +++ b/UI/SettingsPage.cpp @@ -181,11 +181,6 @@ SettingItem &SettingsPage::Info(SettingText text, std::string_view rightText) { return item; } -SettingItem &SettingsPage::Hint(SettingText text) { - _dbg_assert_(!items_.empty()); - return Add(SettingType::Hint, text); -} - SettingItem &SettingsPage::Description(SettingText text) { return Add(SettingType::Description, text); } @@ -279,7 +274,7 @@ static std::function ChangeHandler(const SettingItem &i }; } -static UI::View *CreateSettingViewInner(const SettingItem &item, I18NCat pageCat, UI::View *prevView, UI::ViewGroup *parent, ScreenManager *screenManager) { +static UI::View *CreateSettingViewInner(const SettingItem &item, I18NCat pageCat, UI::ViewGroup *parent, ScreenManager *screenManager) { using namespace UI; std::string_view text = item.text.Translate(pageCat); @@ -363,8 +358,6 @@ static UI::View *CreateSettingViewInner(const SettingItem &item, I18NCat pageCat } case SettingType::Info: return parent->Add(new InfoItem(text, item.rightText)); - case SettingType::Hint: - return parent->Add(new SettingHint(text, prevView)); case SettingType::Description: { TextView *description = parent->Add(new TextView(text, FLAG_WRAP_TEXT, false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(0, 4)))); @@ -386,8 +379,8 @@ static UI::View *CreateSettingViewInner(const SettingItem &item, I18NCat pageCat return nullptr; } -static UI::View *CreateSettingView(const SettingItem &item, I18NCat pageCat, UI::View *prevView, UI::ViewGroup *parent, ScreenManager *screenManager) { - UI::View *view = CreateSettingViewInner(item, pageCat, prevView, parent, screenManager); +static UI::View *CreateSettingView(const SettingItem &item, I18NCat pageCat, UI::ViewGroup *parent, ScreenManager *screenManager) { + UI::View *view = CreateSettingViewInner(item, pageCat, parent, screenManager); if (!view) { return nullptr; } @@ -403,6 +396,14 @@ static UI::View *CreateSettingView(const SettingItem &item, I18NCat pageCat, UI: choice->SetIconRight(ImageID("I_CHEVRON_RIGHT")); } } + if (!item.hint.empty()) { + if (UI::ClickableItem *clickable = dynamic_cast(view)) { + clickable->SetHintText(item.hint.Translate(pageCat)); + } else { + // Custom views that can't show a hint get it below them. + parent->Add(new SettingHint(item.hint.Translate(pageCat), view)); + } + } if (item.setup) { item.setup(view); } @@ -410,7 +411,6 @@ static UI::View *CreateSettingView(const SettingItem &item, I18NCat pageCat, UI: } void CreateSettingViews(const SettingsPage &page, UI::ViewGroup *parent, ScreenManager *screenManager, std::vector *views) { - UI::View *prevView = nullptr; // Each header is a group, which the items after it go in. Items before the first get one without a title. UI::ViewGroup *group = nullptr; for (const SettingItem &item : page.Items()) { @@ -419,7 +419,7 @@ void CreateSettingViews(const SettingsPage &page, UI::ViewGroup *parent, ScreenM if (!group && !outsideGroups) { group = parent->Add(new UI::ChoiceGroup()); } - UI::View *view = CreateSettingView(item, page.DefaultCat(), prevView, outsideGroups ? parent : group, screenManager); + UI::View *view = CreateSettingView(item, page.DefaultCat(), outsideGroups ? parent : group, screenManager); if (views) { views->push_back(view); } @@ -430,8 +430,6 @@ void CreateSettingViews(const SettingsPage &page, UI::ViewGroup *parent, ScreenM group = nullptr; } else if (view && item.type == SettingType::Header) { group = static_cast(view); - } else if (view) { - prevView = view; } } } diff --git a/UI/SettingsPage.h b/UI/SettingsPage.h index 54a1fbfdee..8655895ea9 100644 --- a/UI/SettingsPage.h +++ b/UI/SettingsPage.h @@ -86,7 +86,6 @@ enum class SettingType { FloatSlider, Choice, // A button. onChange is called on click. Info, - Hint, // Explanatory text attached to the previous item. Description, // Text about the page or a part of it, outside the groups. Custom, Link, // Opens another page. @@ -97,6 +96,8 @@ enum class SettingType { struct SettingItem { SettingType type; SettingText text; + // Explanatory text, shown under the item. Searched along with the text. + SettingText hint; void *ptr = nullptr; // bool *, int * or std::string *, depending on type. // IntChoice. The choices are translated in the same category as the text. @@ -169,6 +170,7 @@ struct SettingItem { SettingItem &OnChange(std::function func) { onChange = std::move(func); return *this; } SettingItem &OnClick(std::function func) { onChange = std::move(func); return *this; } SettingItem &Setup(std::function func) { setup = std::move(func); return *this; } + SettingItem &Hint(SettingText text) { hint = text; return *this; } SettingItem &OpensScreen() { opensScreen = true; return *this; } SettingItem &Default(int value) { defaultValue = value; hasDefault = true; return *this; } SettingItem &Step(int value) { step = value; return *this; } @@ -197,11 +199,10 @@ public: SettingItem &IntSlider(int *value, int minValue, int maxValue, SettingText text); SettingItem &Choice(SettingText text, ImageID image = ImageID::invalid()); SettingItem &Info(SettingText text, std::string_view rightText); - SettingItem &Hint(SettingText text); // The items after it start a new group. Not searched. SettingItem &Description(SettingText text); // For anything the other types can't describe. The text is only used for search. The view - // returned is the one a following Hint is attached to, and the one search results lead to. + // returned is the one its Hint is shown in, and the one search results lead to. SettingItem &Custom(SettingText text, std::function create); // Opens another page, built by build, on top of this one. The text is the new page's title. SettingItem &Link(SettingText text, I18NCat cat, std::function build); diff --git a/UI/SettingsPagesDialogScreen.cpp b/UI/SettingsPagesDialogScreen.cpp index 4e3b7f53ff..f723c7b4fa 100644 --- a/UI/SettingsPagesDialogScreen.cpp +++ b/UI/SettingsPagesDialogScreen.cpp @@ -409,10 +409,9 @@ bool SettingsPagesDialogScreen::SearchPage(UI::ViewGroup *parent, int settingsTa const I18NCat cat = page.DefaultCat(); const std::deque &items = page.Items(); - // A matching header shows its whole section, and a matching hint the setting it belongs to. + // A matching header shows its whole section. std::vector show(items.size()); bool sectionMatches = false; - int lastSetting = -1; for (int i = 0; i < (int)items.size(); i++) { const SettingItem &item = items[i]; bool matches = SettingTextMatches(item.text, cat, searchText_); @@ -420,15 +419,12 @@ bool SettingsPagesDialogScreen::SearchPage(UI::ViewGroup *parent, int settingsTa case SettingType::Header: sectionMatches = matches; break; - case SettingType::Hint: - if (matches && lastSetting >= 0) { - show[lastSetting] = true; - } - break; case SettingType::Description: break; default: - lastSetting = i; + if (!item.hint.empty() && SettingTextMatches(item.hint, cat, searchText_)) { + matches = true; + } show[i] = matches || sectionMatches; break; } diff --git a/UI/TiltAnalogSettingsScreen.cpp b/UI/TiltAnalogSettingsScreen.cpp index 1f4584e852..59d5cda727 100644 --- a/UI/TiltAnalogSettingsScreen.cpp +++ b/UI/TiltAnalogSettingsScreen.cpp @@ -124,9 +124,8 @@ void TiltAnalogSettingsScreen::CreateSettingsViews(UI::ViewGroup *settings) { Choice *calibrate = new Choice(co->T("Calibrate")); calibrate->OnClick.Handle(this, &TiltAnalogSettingsScreen::OnCalibrate); calibrate->SetEnabledPtr(&g_Config.bTiltInputEnabled); + calibrate->SetHintText(co->T("To Calibrate", "Hold device at your preferred angle and press Calibrate.")); calibrationSettings->Add(calibrate); - SettingHint *calibrationInfo = new SettingHint(co->T("To Calibrate", "Hold device at your preferred angle and press Calibrate."), calibrate); - calibrationSettings->Add(calibrationInfo); ChoiceGroup *sensitivitySettings = groups->Add(new ChoiceGroup(co->T("Sensitivity"))); if (TiltTypeIsAnalog(g_Config.iTiltInputType)) {