From f3123564a217292b83525df483b811c641e5e8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Nov 2026 07:57:43 +0100 Subject: [PATCH 1/8] UI: Stop scrolling gestures from turning into clicks - A drag that only shows up in the UP event (a quick flick with few or no MOVE events in between) now cancels the press instead of clicking. - A press that stops a moving list only stops it, as in Android's lists. - Android's ACTION_CANCEL is passed on as UP | CANCEL rather than a plain UP, so a gesture the system takes over doesn't click wherever the finger was. Co-Authored-By: Claude Opus 5.5 --- Common/Input/GestureDetector.cpp | 23 ++++++++++++++++++-- Common/UI/ScrollView.cpp | 9 ++++++++ android/src/org/ppsspp/ppsspp/NativeApp.java | 4 ++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Common/Input/GestureDetector.cpp b/Common/Input/GestureDetector.cpp index bf33013c21..a76ffa6ec4 100644 --- a/Common/Input/GestureDetector.cpp +++ b/Common/Input/GestureDetector.cpp @@ -20,6 +20,8 @@ TouchInput GestureDetector::Update(const TouchInput &touch, const Bounds &bounds } // Mouse / 1-finger-touch control. Pointer &p = pointers[touch.id]; + const float dragThreshold = 5.0f / g_display.dpi_scale_y; + bool cancelUp = false; if ((touch.flags & TouchInputFlags::DOWN) && bounds.Contains(touch.x, touch.y)) { p.down = true; p.downTime = time_now_d(); @@ -32,6 +34,20 @@ TouchInput GestureDetector::Update(const TouchInput &touch, const Bounds &bounds p.estimatedInertiaX = 0.0f; p.estimatedInertiaY = 0.0f; } else if (touch.flags & TouchInputFlags::UP) { + // The finger can move between the last MOVE and the UP (a quick flick may not have a MOVE at all), + // so a drag can first show up here. It's too late to scroll, but the press mustn't become a click. + if (p.down && !p.active) { + p.distanceX += fabsf(touch.x - p.lastX); + p.distanceY += fabsf(touch.y - p.lastY); + p.lastX = touch.x; + p.lastY = touch.y; + if ((gestureMask_ & GESTURE_DRAG_VERTICAL) && p.distanceY > p.distanceX && p.distanceY > dragThreshold) { + cancelUp = true; + } + if ((gestureMask_ & GESTURE_DRAG_HORIZONTAL) && p.distanceX > p.distanceY && p.distanceX > dragThreshold) { + cancelUp = true; + } + } p.down = false; } else { p.distanceX += fabsf(touch.x - p.lastX); @@ -46,8 +62,6 @@ TouchInput GestureDetector::Update(const TouchInput &touch, const Bounds &bounds p.lastY = touch.y; } - const float dragThreshold = 5.0f / g_display.dpi_scale_y; - if ((gestureMask_ & GESTURE_DRAG_VERTICAL) && p.distanceY > p.distanceX) { if (p.down) { double timeDown = time_now_d() - p.downTime; @@ -78,6 +92,11 @@ TouchInput GestureDetector::Update(const TouchInput &touch, const Bounds &bounds } } + if (cancelUp) { + TouchInput inp2 = touch; + inp2.flags |= TouchInputFlags::CANCEL; + return inp2; + } return touch; } diff --git a/Common/UI/ScrollView.cpp b/Common/UI/ScrollView.cpp index 8eaa09d6c2..e391b38a74 100644 --- a/Common/UI/ScrollView.cpp +++ b/Common/UI/ScrollView.cpp @@ -167,6 +167,8 @@ bool ScrollView::Key(const KeyInput &input) { const float friction = 0.92f; const float stop_threshold = 0.1f; +// Scroll speed (per frame) above which a press only stops the list. +const float flingStopMinSpeed = 1.0f; bool ScrollView::Touch(const TouchInput &input) { if (input.flags & TouchInputFlags::MOUSE) { @@ -180,7 +182,11 @@ bool ScrollView::Touch(const TouchInput &input) { return ViewGroup::Touch(input); } + bool stopsFling = false; if ((input.flags & TouchInputFlags::DOWN) && scrollTouchId_ == -1 && bounds_.Contains(input.x, input.y)) { + // A press that stops a moving list only stops it, as in Android's lists, so it's kept from the children. + // The second check skips a list that is pushing against its end, which doesn't visibly move. + stopsFling = fabsf(inertia_) > flingStopMinSpeed && ClampedScrollPos(scrollPos_ - inertia_) != scrollPos_; if (orientation_ == ORIENT_VERTICAL) { Bob bob = ComputeBob(); float internalY = input.y - bounds_.y; @@ -241,6 +247,9 @@ bool ScrollView::Touch(const TouchInput &input) { scrollToTarget_ = false; } + if (stopsFling) { + return true; + } if (!(input.flags & TouchInputFlags::DOWN) || bounds_.Contains(input.x, input.y)) { return ViewGroup::Touch(input2); } else { diff --git a/android/src/org/ppsspp/ppsspp/NativeApp.java b/android/src/org/ppsspp/ppsspp/NativeApp.java index 8484ee5c35..dfe419ca22 100644 --- a/android/src/org/ppsspp/ppsspp/NativeApp.java +++ b/android/src/org/ppsspp/ppsspp/NativeApp.java @@ -195,9 +195,9 @@ public class NativeApp { case MotionEvent.ACTION_CANCEL: // The whole gesture is over (the system took it, for example), so every pointer in the // event is, not just the one at the action index. Otherwise the rest stay held down - // on the native side. Handle like ACTION_UP for now. + // on the native side. UP | CANCEL, so the UI lets go without clicking. Log.i(TAG, "ACTION_CANCEL"); - code = 4; + code = 4 | 8; break; case MotionEvent.ACTION_MOVE: { code = 1; From f7497827716855ab21b547f366139beb7cd9986f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Nov 2026 08:16:59 +0100 Subject: [PATCH 2/8] Fix struct alignment warning --- Core/HLE/sceParseUri.h | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Core/HLE/sceParseUri.h b/Core/HLE/sceParseUri.h index 5d7b677b2c..1aaabf6b5c 100644 --- a/Core/HLE/sceParseUri.h +++ b/Core/HLE/sceParseUri.h @@ -23,14 +23,8 @@ #pragma once -#ifdef _MSC_VER -#define PACK // on MSVC we use #pragma pack() instead so let's kill this. -#pragma pack(push, 1) -#else -#define PACK __attribute__((packed)) -#endif - -typedef struct PSPParsedUri { +// This used to have pragma pack, but useless as all members are naturally aligned. +struct PSPParsedUri { s32 noSlash; s32 schemeAddr; s32 userInfoUserNameAddr; @@ -41,10 +35,8 @@ typedef struct PSPParsedUri { s32 fragmentAddr; u16 port; u8 unknown[10]; // padding might be included here? -} PACK PSPParsedUri; +}; -#ifdef _MSC_VER -#pragma pack(pop) -#endif +static_assert(sizeof(PSPParsedUri) == 44, "PSPParsedUri size mismatch"); void Register_sceParseUri(); From a0fb1d53cfba9311b2049549f1530c0d39e5cf48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Nov 2026 08:17:29 +0100 Subject: [PATCH 3/8] Remove "Settings" from UmdReplaceScreen. Fix some round corners in UI. --- UI/MainScreen.cpp | 7 +------ UI/MiscViews.cpp | 3 ++- UI/MiscViews.h | 2 +- UI/PauseScreen.cpp | 5 ++--- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index c6f9ab964f..f3ce5d1f7a 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -952,9 +952,8 @@ void UmdReplaceScreen::CreateViews() { leftColumn->SetClip(true); ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(270, FILL_PARENT, actionMenuMargins)); - LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); + LinearLayout *rightColumnItems = rightColumn->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); rightColumnItems->SetSpacing(0.0f); - rightColumn->Add(rightColumnItems); if (g_Config.iMaxRecent > 0) { ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); @@ -993,10 +992,6 @@ void UmdReplaceScreen::CreateViews() { } rightColumnItems->Add(new Choice(di->T("Cancel")))->OnClick.Handle(this, &UIScreen::OnCancel); - rightColumnItems->Add(new Spacer()); - rightColumnItems->Add(new Choice(mm->T("Game Settings")))->OnClick.Add([this](UI::EventParams &e) { - screenManager()->push(new GameSettingsScreen(Path())); - }); if (g_recentFiles.HasAny()) { leftColumn->SetCurrentTab(0, true); diff --git a/UI/MiscViews.cpp b/UI/MiscViews.cpp index a95bc6fa30..e91b4a2983 100644 --- a/UI/MiscViews.cpp +++ b/UI/MiscViews.cpp @@ -250,7 +250,7 @@ void GameImageView::Draw(UIContext &dc) { dc.RebindTexture(); } -void AddRotationPicker(ScreenManager *screenManager, UI::ViewGroup *parent, bool text) { +UI::View *AddRotationPicker(ScreenManager *screenManager, UI::ViewGroup *parent, bool text) { using namespace UI; static const char *screenRotation[] = { "Auto", "Landscape", "Portrait", "Landscape Reversed", "N/A", "Landscape Auto"}; static const std::map screenRotationIcons{ @@ -277,6 +277,7 @@ void AddRotationPicker(ScreenManager *screenManager, UI::ViewGroup *parent, bool INFO_LOG(Log::System, "New display rotation: %d", g_Config.iScreenRotation); System_Notify(SystemNotification::ROTATE_UPDATED); }); + return rot; } void GameInfoBGView::Draw(UIContext &dc) { diff --git a/UI/MiscViews.h b/UI/MiscViews.h index e0b20fa2b3..d001fc346f 100644 --- a/UI/MiscViews.h +++ b/UI/MiscViews.h @@ -105,7 +105,7 @@ private: UI::View *setting_; }; -void AddRotationPicker(ScreenManager *screenManager, UI::ViewGroup *parent, bool text); +UI::View *AddRotationPicker(ScreenManager *screenManager, UI::ViewGroup *parent, bool text); class SearchBar : public UI::InertView { public: diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index 6354d3c78c..3dd72888a7 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -834,7 +834,6 @@ void GamePauseScreen::CreateViews() { g_Config.bRunBehindPauseMenu = !g_Config.bRunBehindPauseMenu; playButton_->SetIconLeft(g_Config.bRunBehindPauseMenu ? ImageID("I_PAUSE_LINE") : ImageID("I_PLAY_LINE")); }); - playButton_->SetRoundCorners(); bool mustRunBehind = MustRunBehind(); playButton_->SetEnabled(!mustRunBehind); @@ -843,7 +842,6 @@ void GamePauseScreen::CreateViews() { infoButton->OnClick.Add([this](UI::EventParams &e) { screenManager()->push(new GameScreen(gamePath_, true)); }); - infoButton->SetRoundCorners(); if (System_GetPropertyBool(SYSPROP_CAN_RESTRICT_ORIENTATION)) { AddRotationPicker(screenManager(), middleColumn, false); @@ -854,8 +852,9 @@ void GamePauseScreen::CreateViews() { menuButton->OnClick.Add([this, menuButton, portrait](UI::EventParams &e) { ShowContextMenu(menuButton, portrait); }); - menuButton->SetRoundCorners(); } + + middleColumn->ApplyRoundCorners(); } else { playButton_ = nullptr; } From 7a22fe43a590d70141ba095e17698e7d9e3c5776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Nov 2026 08:19:40 +0100 Subject: [PATCH 4/8] UI: Base UmdReplaceScreen on the two-pane dialog screen So it gets a usable portrait layout: a title bar with a back button, the game list, and the Load button below it. Landscape keeps its old shape, with Back replacing the Cancel button. Co-Authored-By: Claude Opus 5.5 --- UI/MainScreen.cpp | 49 +++++++++++++++++++++++------------------------ UI/MainScreen.h | 10 +++++++--- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index f3ce5d1f7a..5596a3171e 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -939,21 +939,21 @@ void MainScreen::dialogFinished(const Screen *dialog, DialogResult result) { } } -void UmdReplaceScreen::CreateViews() { +std::string_view UmdReplaceScreen::GetTitle() const { + auto pa = GetI18NCategory(I18NCat::PAUSE); + return insert_ ? pa->T("Insert UMD") : pa->T("Switch UMD"); +} + +void UmdReplaceScreen::CreateContentViews(UI::ViewGroup *parent) { using namespace UI; - Margins actionMenuMargins(0, 100, 15, 0); auto mm = GetI18NCategory(I18NCat::MAINMENU); - auto di = GetI18NCategory(I18NCat::DIALOG); const bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait; - TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, TabHolderFlags::Default, nullptr, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0)); - leftColumn->SetTag("UmdReplace"); - leftColumn->SetClip(true); - - ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(270, FILL_PARENT, actionMenuMargins)); - LinearLayout *rightColumnItems = rightColumn->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT))); - rightColumnItems->SetSpacing(0.0f); + TabHolder *tabs = new TabHolder(ORIENT_HORIZONTAL, 64, TabHolderFlags::Default, nullptr, nullptr, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)); + tabs->SetTag("UmdReplace"); + tabs->SetClip(true); + parent->Add(tabs); if (g_Config.iMaxRecent > 0) { ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); @@ -962,7 +962,7 @@ void UmdReplaceScreen::CreateViews() { Path("!RECENT"), BrowseFlags::NONE, portrait, &g_Config.iGameBrowserView1, screenManager(), "", "", new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); scrollRecentGames->Add(tabRecentGames); - leftColumn->AddTab(mm->T("Recent"), ImageID::invalid(), scrollRecentGames); + tabs->AddTab(mm->T("Recent"), ImageID::invalid(), scrollRecentGames); tabRecentGames->OnChoice.Handle(this, &UmdReplaceScreen::OnGameSelected); tabRecentGames->OnHoldChoice.Handle(this, &UmdReplaceScreen::OnGameSelected); } @@ -975,14 +975,25 @@ void UmdReplaceScreen::CreateViews() { scrollAllGames->Add(tabAllGames); - leftColumn->AddTab(mm->T("Games"), ImageID::invalid(), scrollAllGames); + tabs->AddTab(mm->T("Games"), ImageID::invalid(), scrollAllGames); tabAllGames->OnChoice.Handle(this, &UmdReplaceScreen::OnGameSelected); tabAllGames->OnHoldChoice.Handle(this, &UmdReplaceScreen::OnGameSelected); + if (g_recentFiles.HasAny()) { + tabs->SetCurrentTab(0, true); + } else if (g_Config.iMaxRecent > 0) { + tabs->SetCurrentTab(1, true); + } +} + +void UmdReplaceScreen::CreateSettingsViews(UI::ViewGroup *parent) { + using namespace UI; + auto mm = GetI18NCategory(I18NCat::MAINMENU); + if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) { - rightColumnItems->Add(new Choice(mm->T("Load", "Load...")))->OnClick.Add([&](UI::EventParams &e) { + parent->Add(new Choice(mm->T("Load", "Load...")))->OnClick.Add([this](UI::EventParams &e) { auto mm = GetI18NCategory(I18NCat::MAINMENU); System_BrowseForFile(GetRequesterToken(), mm->T("Load"), BrowseFileType::BOOTABLE, [this](std::string_view value, int) { UseDisc(Path(value)); @@ -990,18 +1001,6 @@ void UmdReplaceScreen::CreateViews() { }); }); } - - rightColumnItems->Add(new Choice(di->T("Cancel")))->OnClick.Handle(this, &UIScreen::OnCancel); - - if (g_recentFiles.HasAny()) { - leftColumn->SetCurrentTab(0, true); - } else if (g_Config.iMaxRecent > 0) { - leftColumn->SetCurrentTab(1, true); - } - - root_ = new LinearLayout(ORIENT_HORIZONTAL); - root_->Add(leftColumn); - root_->Add(rightColumn); } void UmdReplaceScreen::update() { diff --git a/UI/MainScreen.h b/UI/MainScreen.h index 7a61a2e250..df0dc11f61 100644 --- a/UI/MainScreen.h +++ b/UI/MainScreen.h @@ -26,6 +26,7 @@ #include "Common/UI/TabHolder.h" #include "Common/UI/PopupScreens.h" #include "UI/BaseScreens.h" +#include "UI/SimpleDialogScreen.h" #include "Common/File/PathBrowser.h" #include "UI/GameBrowser.h" @@ -103,14 +104,17 @@ private: void InstantHighlight(const Path &path); }; -class UmdReplaceScreen : public UIBaseDialogScreen { +class UmdReplaceScreen : public UITwoPaneBaseDialogScreen { public: // insert: put a disc into the empty drive (the VSH) rather than swap the one a game is running from. - explicit UmdReplaceScreen(bool insert = false) : insert_(insert) {} + explicit UmdReplaceScreen(bool insert = false) + : UITwoPaneBaseDialogScreen(Path(), TwoPaneFlags::SettingsToTheRight | TwoPaneFlags::NoTopbarInLandscape), insert_(insert) {} const char *tag() const override { return "UmdReplace"; } protected: - void CreateViews() override; + std::string_view GetTitle() const override; + void CreateSettingsViews(UI::ViewGroup *parent) override; + void CreateContentViews(UI::ViewGroup *parent) override; void update() override; private: From 990378df083c276f827aa85815ec188cd420cada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Nov 2026 10:46:24 +0100 Subject: [PATCH 5/8] Tweak the slider popup dialogs. Update an URL. --- Common/UI/PopupScreens.cpp | 21 ++++++++++++++------- Core/Config.cpp | 2 +- UI/FirmwareScreen.cpp | 2 +- UI/MainScreen.cpp | 4 +++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Common/UI/PopupScreens.cpp b/Common/UI/PopupScreens.cpp index 4867fcc616..4a27d80fe0 100644 --- a/Common/UI/PopupScreens.cpp +++ b/Common/UI/PopupScreens.cpp @@ -662,13 +662,13 @@ void SliderPopupScreen::CreatePopupContents(UI::ViewGroup *parent) { LinearLayout *lin = vert->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(UI::Margins(10, 10)))); Button *decrease = lin->Add(new Button("", ImageID("I_MINUS"), new LayoutParams(60.0f, 60.0f))); decrease->OnClick.Handle(this, &SliderPopupScreen::OnDecrease); - decrease->SetRoundCorners(); Button *increase = lin->Add(new Button("", ImageID("I_PLUS"), new LayoutParams(60.0f, 60.0f))); increase->OnClick.Handle(this, &SliderPopupScreen::OnIncrease); - increase->SetRoundCorners(); edit_ = new TextEdit("", Title(), "", new LinearLayoutParams(1.0f)); edit_->SetMaxLen(16); + edit_->SetTextColor(dc.GetTheme().itemStyle.fgColor); + edit_->SetTextAlign(FLAG_DYNAMIC_ASCII); edit_->OnTextChange.Handle(this, &SliderPopupScreen::OnTextChange); changing_ = true; UpdateTextBox(); @@ -676,7 +676,7 @@ void SliderPopupScreen::CreatePopupContents(UI::ViewGroup *parent) { lin->Add(edit_); if (!units_.empty()) - lin->Add(new TextView(units_)); + lin->Add(new TextView(units_, new LinearLayoutParams(0.0f, Gravity::G_VCENTER))); if (defaultValue_ != NO_DEFAULT_FLOAT) { auto di = GetI18NCategory(I18NCat::DIALOG); @@ -687,12 +687,13 @@ void SliderPopupScreen::CreatePopupContents(UI::ViewGroup *parent) { UpdateTextBox(); changing_ = false; }); - reset->SetRoundCorners(); } if (!negativeLabel_.empty()) vert->Add(new CheckBox(&disabled_, negativeLabel_)); + lin->ApplyRoundCorners(); + if (IsFocusMovementEnabled()) UI::SetFocusedView(slider_, FocusFlags::CAUSE_SCREEN_CHANGE); } @@ -708,8 +709,10 @@ void SliderFloatPopupScreen::CreatePopupContents(UI::ViewGroup *parent) { vert->Add(slider_); LinearLayout *lin = vert->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(UI::Margins(10, 10)))); - lin->Add(new Button("", ImageID("I_MINUS"), new LayoutParams(60.0f, 60.0f)))->OnClick.Handle(this, &SliderFloatPopupScreen::OnDecrease); - lin->Add(new Button("", ImageID("I_PLUS"), new LayoutParams(60.0f, 60.0f)))->OnClick.Handle(this, &SliderFloatPopupScreen::OnIncrease); + Button *decrease = lin->Add(new Button("", ImageID("I_MINUS"), new LayoutParams(60.0f, 60.0f))); + decrease->OnClick.Handle(this, &SliderFloatPopupScreen::OnDecrease); + Button *increase = lin->Add(new Button("", ImageID("I_PLUS"), new LayoutParams(60.0f, 60.0f))); + increase->OnClick.Handle(this, &SliderFloatPopupScreen::OnIncrease); edit_ = new TextEdit("", Title(), "", new LinearLayoutParams(1.0f)); edit_->SetMaxLen(16); @@ -720,18 +723,22 @@ void SliderFloatPopupScreen::CreatePopupContents(UI::ViewGroup *parent) { UpdateTextBox(); changing_ = false; lin->Add(edit_); + if (!units_.empty()) lin->Add(new TextView(units_))->SetTextColor(dc.GetTheme().itemStyle.fgColor); if (defaultValue_ != NO_DEFAULT_FLOAT) { auto di = GetI18NCategory(I18NCat::DIALOG); - lin->Add(new Button(di->T("Reset"), new LayoutParams(WRAP_CONTENT, 60.0f)))->OnClick.Add([=](UI::EventParams &) { + Button *reset = lin->Add(new Button(di->T("Reset"), new LayoutParams(WRAP_CONTENT, 60.0f))); + reset->OnClick.Add([=](UI::EventParams &) { sliderValue_ = defaultValue_; if (liveUpdate_) { *value_ = defaultValue_; } }); + reset->SetRoundCorners(); } + lin->ApplyRoundCorners(); // slider_ = parent->Add(new SliderFloat(&sliderValue_, minValue_, maxValue_, new LinearLayoutParams(UI::Margins(10, 5)))); if (IsFocusMovementEnabled()) diff --git a/Core/Config.cpp b/Core/Config.cpp index 71759454f1..a56536f6cb 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -458,7 +458,7 @@ static constexpr ConfigSetting generalSettings[] = { ConfigSetting("RemoteISOSharedDir", SETTING(g_Config, sRemoteISOSharedDir), "", CfgFlag::DEFAULT), ConfigSetting("RemoteISOShareType", SETTING(g_Config, iRemoteISOShareType), (int)RemoteISOShareType::RECENT, CfgFlag::DEFAULT), ConfigSetting("AskForExitConfirmationAfterSeconds", SETTING(g_Config, iAskForExitConfirmationAfterSeconds), 300, CfgFlag::PER_GAME) - .Label(I18NCat::SYSTEM, "Ask for exit confirmation after seconds").Range(0, 1200).Units("s").ZeroLabel("Off"), + .Label(I18NCat::SYSTEM, "Ask for exit confirmation after seconds").Range(0, 1200).Units("seconds").ZeroLabel("Off"), #if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS) ConfigSetting("ScreenRotation", SETTING(g_Config, iScreenRotation), ROTATION_AUTO, CfgFlag::DEFAULT), diff --git a/UI/FirmwareScreen.cpp b/UI/FirmwareScreen.cpp index 6890b6a34a..3aa16c5c04 100644 --- a/UI/FirmwareScreen.cpp +++ b/UI/FirmwareScreen.cpp @@ -209,7 +209,7 @@ void FirmwareScreen::CreateSettingsViews(UI::ViewGroup *parent) { Choice *moreInfo = groups->Add(new ChoiceGroup())->Add(new Choice(di->T("More info"))); moreInfo->OnClick.Add([](UI::EventParams &) { - System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/docs/getting-started/how-to-run-the-vsh/"); + System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/docs/getting-started/how-to-run-the-xmb/"); }); moreInfo->SetIconRight(ImageID("I_LINK_OUT")); diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 5596a3171e..044a1a1957 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -993,7 +993,9 @@ void UmdReplaceScreen::CreateSettingsViews(UI::ViewGroup *parent) { auto mm = GetI18NCategory(I18NCat::MAINMENU); if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) { - parent->Add(new Choice(mm->T("Load", "Load...")))->OnClick.Add([this](UI::EventParams &e) { + ChoiceGroupList *settings = parent->Add(new ChoiceGroupList()); + ChoiceGroup *choices = settings->Add(new ChoiceGroup()); + choices->Add(new Choice(mm->T("Load", "Load..."), ImageID("I_FOLDER_OPEN")))->OnClick.Add([this](UI::EventParams &e) { auto mm = GetI18NCategory(I18NCat::MAINMENU); System_BrowseForFile(GetRequesterToken(), mm->T("Load"), BrowseFileType::BOOTABLE, [this](std::string_view value, int) { UseDisc(Path(value)); From b7d7cf43feedabe04d17c7df9593a731ea627a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Nov 2026 10:49:20 +0100 Subject: [PATCH 6/8] Update the SWAENET listing --- assets/adhoc-servers.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/adhoc-servers.json b/assets/adhoc-servers.json index f6ddbcc8d5..992b65cba6 100644 --- a/assets/adhoc-servers.json +++ b/assets/adhoc-servers.json @@ -239,13 +239,13 @@ "name": "SWAENET", "host": "132.226.189.224", "discord": "https://discord.gg/CgZxfHA8ZC", - "web": "http://132.226.189.224:8888/", - "location": "Mumbai,India", + "web": "https://132.226.189.224/", + "location": "Mumbai, India", "location-emoji": "🇮🇳", - "description": "Indian adhoc relay from SWAEYERS community", + "description": "SWAENET PPSSPP Relay – Mumbai, India – part of the SWAEYERS community, offering low‑latency, reliable gaming connectivity.", "data_mode": "AemuPostoffice", - "status_data_json": "http://132.226.189.224:8888/data.json", - "icon": "http://132.226.189.224:8888/assets/favicon.svg" + "status_data_json": "https://132.226.189.224/data.json", + "icon": "https://132.226.189.224/assets/favicon.svg" } ] } From 6fb7be45175fedb2dda67f5aeedbd3222a5cc616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Nov 2026 10:51:10 +0100 Subject: [PATCH 7/8] Move SWAENET up one step in the listing. --- assets/adhoc-servers.json | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/assets/adhoc-servers.json b/assets/adhoc-servers.json index 992b65cba6..3deb6e16b1 100644 --- a/assets/adhoc-servers.json +++ b/assets/adhoc-servers.json @@ -212,6 +212,18 @@ "status_xml": "", "icon": "https://raw.githubusercontent.com/MainPiyushHoon/Alpha-Community-Webpage/refs/heads/main/src/images/ALPHA.webp" }, + { + "name": "SWAENET", + "host": "132.226.189.224", + "discord": "https://discord.gg/CgZxfHA8ZC", + "web": "https://132.226.189.224/", + "location": "Mumbai, India", + "location-emoji": "🇮🇳", + "description": "SWAENET PPSSPP Relay – Mumbai, India – part of the SWAEYERS community, offering low‑latency, reliable gaming connectivity.", + "data_mode": "AemuPostoffice", + "status_data_json": "https://132.226.189.224/data.json", + "icon": "https://132.226.189.224/assets/favicon.svg" + }, { "name": "aemu_postoffice test server", "host": "185.183.157.234", @@ -228,24 +240,11 @@ "name": "Andexplaystream", "host": "psp.andexplaystream.online", "discord": "", - "web": "", "location": "Jakarta, Indonesia", "location-emoji": "🇮🇩", "description": "For players looking to play any games", "data_mode": "AemuPostoffice", "status_data_json": "http://psp.andexplaystream.online/data.json" - }, - { - "name": "SWAENET", - "host": "132.226.189.224", - "discord": "https://discord.gg/CgZxfHA8ZC", - "web": "https://132.226.189.224/", - "location": "Mumbai, India", - "location-emoji": "🇮🇳", - "description": "SWAENET PPSSPP Relay – Mumbai, India – part of the SWAEYERS community, offering low‑latency, reliable gaming connectivity.", - "data_mode": "AemuPostoffice", - "status_data_json": "https://132.226.189.224/data.json", - "icon": "https://132.226.189.224/assets/favicon.svg" } ] } From 8ed244ee658eaa169d34557facea18247a8aae76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 7 Nov 2026 10:57:46 +0100 Subject: [PATCH 8/8] UI: Only list disc images on the UMD switch screen Both its lists offered everything bootable, the XMB, homebrew and PS1 games included, though only what UmdMount takes can go in the drive. The new UMDS_ONLY browse flag keeps PSP disc images and disc folders. ISOs and CSOs are trusted by extension so big folders stay quick to list; CHDs and PBPs are identified, since they can be PS1 games. Co-Authored-By: Claude Opus 5.5 --- UI/GameBrowser.cpp | 43 ++++++++++++++++++++++++++++++++++++++++--- UI/GameBrowser.h | 1 + UI/MainScreen.cpp | 4 ++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index d752af6314..b40a08612d 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -38,6 +38,7 @@ #include "Core/HLE/sceCtrl.h" #include "Core/ELF/PBPReader.h" #include "Core/FileLoaders/PS1EbootFileLoader.h" +#include "Core/Loaders.h" #include "Core/MemMap.h" #include "UI/GameBrowser.h" @@ -277,11 +278,42 @@ void GameBrowser::PinToggleClick(UI::EventParams &e) { refreshPending_ = true; } +// Whether UmdMount takes it: a PSP disc image (also a UMD video, or a PSN game's ISO in an EBOOT.PBP), +// or a disc's files in a directory. +static bool IsUmdPath(const Path &path, bool isDirectory) { + if (!isDirectory) { + const std::string ext = path.GetFileExtension(); + // Trusted by name, since opening every image would make big folders slow to list. + if (ext == ".iso" || ext == ".cso") { + return true; + } + // A CHD can hold a PS1 disc, and a PBP can be homebrew or a PS1 game. + if (ext != ".chd" && ext != ".pbp") { + return false; + } + } + std::unique_ptr loader(ConstructFileLoader(path)); + std::string error; + switch (Identify_File(loader.get(), &error)) { + case IdentifiedFileType::PSP_ISO: + case IdentifiedFileType::PSP_ISO_NP: + case IdentifiedFileType::PSP_DISC_DIRECTORY: + case IdentifiedFileType::PSP_UMD_VIDEO_ISO: + return true; + default: + return false; + } +} + bool GameBrowser::HasSpecialFiles(std::vector &filenames) { if (path_.GetPath().ToString() == "!RECENT") { filenames.clear(); for (auto &str : g_recentFiles.GetRecentFiles()) { - filenames.emplace_back(str); + Path path(str); + if ((browseFlags_ & BrowseFlags::UMDS_ONLY) && !IsUmdPath(path, File::IsDirectory(path))) { + continue; + } + filenames.push_back(path); } return true; } @@ -524,8 +556,9 @@ void GameBrowser::Refresh() { gameButtons.push_back(new GameButton(GameButtonType::Game, filenames[i], lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); } } else if (!listingPending_) { + const bool umdsOnly = browseFlags_ & BrowseFlags::UMDS_ONLY; std::vector fileInfo; - path_.GetListing(fileInfo, "iso:cso:chd:pbp:elf:prx:ppdmp:cue:ccd:mds:bin:img:"); + path_.GetListing(fileInfo, umdsOnly ? "iso:cso:chd:pbp:" : "iso:cso:chd:pbp:elf:prx:ppdmp:cue:ccd:mds:bin:img:"); // PS1 discs: a .bin or .img shows up as its cue sheet or .ccd, when it has one. RemoveDescribedDiscImages(&fileInfo); // Drives and servers aren't games, and probing a server that's gone away can take seconds. @@ -543,6 +576,10 @@ void GameBrowser::Refresh() { else if (!isGame && File::Exists(path_.GetPath() / fileInfo[i].name / "PARAM.SFO")) isSaveData = true; + if (umdsOnly && (isSaveData || (isGame && !IsUmdPath(fileInfo[i].fullName, fileInfo[i].isDirectory)))) { + continue; + } + if (!isGame && !isSaveData) { if (browseFlags_ & BrowseFlags::NAVIGATE) { dirButtons.push_back(new GameButton(GameButtonType::Dir, fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); @@ -558,7 +595,7 @@ void GameBrowser::Refresh() { // Put RAR/ZIP files at the end to get them out of the way. Game update packages go here // too - they're not something to boot, they're something to install. // We do support unpacking some of them automatically. - if (browseFlags_ & BrowseFlags::ARCHIVES) { + if ((browseFlags_ & BrowseFlags::ARCHIVES) && !umdsOnly) { fileInfo.clear(); path_.GetListing(fileInfo, "zip:rar:r00:r01:7z:pkg:"); if (!fileInfo.empty()) { diff --git a/UI/GameBrowser.h b/UI/GameBrowser.h index 0f429ac243..49c4f87229 100644 --- a/UI/GameBrowser.h +++ b/UI/GameBrowser.h @@ -28,6 +28,7 @@ enum class BrowseFlags { PIN = 8, HOMEBREW_STORE = 16, UPLOAD_BUTTON = 32, + UMDS_ONLY = 64, // Only what can go in the UMD drive (see UmdMount). STANDARD = 1 | 2 | 4 | 8 | 32, }; ENUM_CLASS_BITOPS(BrowseFlags); diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 044a1a1957..169521831d 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -959,7 +959,7 @@ void UmdReplaceScreen::CreateContentViews(UI::ViewGroup *parent) { ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); scrollRecentGames->SetTag("UmdReplaceRecentGames"); GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(), - Path("!RECENT"), BrowseFlags::NONE, portrait, &g_Config.iGameBrowserView1, screenManager(), "", "", + Path("!RECENT"), BrowseFlags::UMDS_ONLY, portrait, &g_Config.iGameBrowserView1, screenManager(), "", "", new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); scrollRecentGames->Add(tabRecentGames); tabs->AddTab(mm->T("Recent"), ImageID::invalid(), scrollRecentGames); @@ -969,7 +969,7 @@ void UmdReplaceScreen::CreateContentViews(UI::ViewGroup *parent) { ScrollView *scrollAllGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); scrollAllGames->SetTag("UmdReplaceAllGames"); - GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD, portrait, &g_Config.iGameBrowserView2, screenManager(), + GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD | BrowseFlags::UMDS_ONLY, portrait, &g_Config.iGameBrowserView2, screenManager(), mm->T("How to get games"), "https://www.ppsspp.org/getgames.html", new LinearLayoutParams(FILL_PARENT, FILL_PARENT));