From 3c398657df8e89ad0e38248604f978f3be2a17a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 26 Oct 2026 16:04:47 +0100 Subject: [PATCH] ImDebugger: Route the soft keyboard through the System request layer Instead of a hook and four Android_* functions declared in ImDebugger.cpp, the debugger calls System_SetSoftKeyboardForced(), and the platform reports a closed keyboard with UIMessage::SOFT_KEYBOARD_HIDDEN. "Keep keyboard after switching apps" is passed along with the request, and the activity's onStop ends a keyboard that shouldn't persist, so the native side no longer tracks app pauses. The debugger is now also told when it's hidden. Frame() isn't called then, so hiding it with the hotkey used to leave the keyboard up. The keyboard button and its Misc menu options are only shown when SYSPROP_KEYBOARD_IS_SOFT is set, which Android now reports too. iOS and UWP on Xbox report it as well, but don't implement the request yet. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/System/Request.cpp | 1 + Common/System/Request.h | 8 ++ Common/System/System.h | 3 + UI/ImDebugger/ImDebugger.cpp | 101 +++++------------- UI/ImDebugger/ImDebugger.h | 18 ++-- UI/NativeApp.cpp | 8 ++ android/jni/app-android.cpp | 36 ++----- .../org/ppsspp/ppsspp/NativeSurfaceView.java | 19 ++-- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 13 +-- 9 files changed, 83 insertions(+), 124 deletions(-) diff --git a/Common/System/Request.cpp b/Common/System/Request.cpp index 858cf688f5..fa003fea9b 100644 --- a/Common/System/Request.cpp +++ b/Common/System/Request.cpp @@ -50,6 +50,7 @@ const char *RequestTypeAsString(SystemRequestType type) { case SystemRequestType::MOVE_TO_TRASH: return "MOVE_TO_TRASH"; case SystemRequestType::IAP_RESTORE_PURCHASES: return "IAP_RESTORE_PURCHASES"; case SystemRequestType::IAP_MAKE_PURCHASE: return "IAP_MAKE_PURCHASE"; + case SystemRequestType::SET_SOFT_KEYBOARD_FORCED: return "SET_SOFT_KEYBOARD_FORCED"; default: return "N/A"; } } diff --git a/Common/System/Request.h b/Common/System/Request.h index d55a450e81..82ca7f7556 100644 --- a/Common/System/Request.h +++ b/Common/System/Request.h @@ -208,6 +208,14 @@ enum class TextInputUser { }; void System_SetTextInputWanted(TextInputUser user, bool wanted); +// Keeps the soft keyboard up (typing as key and char events) until called with false. With keepAfterAppSwitch, +// it comes back after switching to another app and back, otherwise the platform drops it then. When the platform +// closes it, it posts UIMessage::SOFT_KEYBOARD_HIDDEN. Only Android implements this so far, though other platforms +// with SYSPROP_KEYBOARD_IS_SOFT should. +inline void System_SetSoftKeyboardForced(bool forced, bool keepAfterAppSwitch) { + g_requestManager.MakeSystemRequest(SystemRequestType::SET_SOFT_KEYBOARD_FORCED, NO_REQUESTER_TOKEN, nullptr, nullptr, "", "", (int64_t)forced, (int64_t)keepAfterAppSwitch); +} + inline void System_SetKeepScreenBright(bool keepScreenBright) { g_requestManager.MakeSystemRequest(SystemRequestType::SET_KEEP_SCREEN_BRIGHT, NO_REQUESTER_TOKEN, nullptr, nullptr, "", "", (int64_t)keepScreenBright); } diff --git a/Common/System/System.h b/Common/System/System.h index eaa90e19b2..085bc947f0 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -105,6 +105,8 @@ enum class SystemRequestType { IAP_MAKE_PURCHASE, OPEN_DISPLAY_SETTINGS, + + SET_SOFT_KEYBOARD_FORCED, }; // Run a closure on the main thread. Used to safely implement UI that runs on another thread. @@ -324,6 +326,7 @@ enum class UIMessage { SAVE_FRAME_DUMP, ADHOC_SERVER_LIST_CHANGED, SAVESTATE_SLOT_SAVED, // A slot's save state file was written. + SOFT_KEYBOARD_HIDDEN, // The platform closed a soft keyboard forced by System_SetSoftKeyboardForced. }; std::string System_GetProperty(SystemProperty prop); diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index 019f85995d..9636ebbac4 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -1,5 +1,4 @@ #include -#include #include #include "ppsspp_config.h" @@ -87,28 +86,6 @@ static constexpr bool kIsMobilePlatform = true; static constexpr bool kIsMobilePlatform = false; #endif -// Defined further down (see ImDebugger_SetSoftKeyboardHook). -static std::function g_softKeyboardHook; - -#if PPSSPP_PLATFORM(ANDROID) -void Android_SetKeyboardForced(bool forced); // Defined in android/jni/app-android.cpp. -void Android_SetKeyboardPersist(bool persist); // Whether the keyboard comes back after an app switch. -bool Android_ConsumeAppPaused(); // True once after the app was paused (switched away from). -bool Android_ConsumeKeyboardDismissed(); // True once after the user closed the keyboard with Back. -#endif - -// Shows or hides the soft keyboard. A hook set through ImDebugger_SetSoftKeyboardHook takes priority; otherwise -// Android uses its built-in implementation, and other platforms do nothing. -static void ApplySoftKeyboard(bool show) { - if (g_softKeyboardHook) { - g_softKeyboardHook(show); - return; - } -#if PPSSPP_PLATFORM(ANDROID) - Android_SetKeyboardForced(show); -#endif -} - void ShowInMemoryViewerMenuItem(uint32_t addr, ImControl &control) { if (ImGui::BeginMenu("Show in memory viewer")) { for (int i = 0; i < 4; i++) { @@ -2688,12 +2665,27 @@ ImDebugger::ImDebugger() { } ImDebugger::~ImDebugger() { - if (keyboardShown_) { - ApplySoftKeyboard(false); - } + UpdateSoftKeyboard(false); SaveConfig(); } +// Called every frame. The platform is only told about changes. +void ImDebugger::UpdateSoftKeyboard(bool debuggerVisible) { + // Hiding the toggle turns the keyboard off too, otherwise there'd be no way to dismiss it. + const bool show = debuggerVisible && cfg_.forceKeyboard && cfg_.showKeyboardToggle; + if (show == keyboardShown_ && (!show || cfg_.keyboardPersist == keyboardPersistSent_)) { + return; + } + keyboardShown_ = show; + keyboardPersistSent_ = cfg_.keyboardPersist; + System_SetSoftKeyboardForced(show, cfg_.keyboardPersist); +} + +void ImDebugger::OnHidden() { + cfg_.forceKeyboard = false; // So the keyboard doesn't pop back up when the debugger is reopened. + UpdateSoftKeyboard(false); +} + void ImDebugger::SaveConfig() { cfg_.SaveConfig(ConfigPath()); } @@ -2703,15 +2695,6 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUCommon *gpuDebug, Draw: const CoreState coreState = ::coreState; const bool showDebugger = g_Config.bShowImDebugger && PSP_IsInited(); // This will be separated from showing the IM UI in general. - // The debugger got hidden some other way (hotkey, settings...): drop the keyboard so it's off when reopened. - if (!g_Config.bShowImDebugger && cfg_.forceKeyboard) { - cfg_.forceKeyboard = false; - if (keyboardShown_) { - keyboardShown_ = false; - ApplySoftKeyboard(false); - } - } - const bool allowSaveStates = !Achievements::HardcoreModeActive() && PSP_IsInited() && (!IsNetworkConnected() || g_Config.bAllowSavestateWhileConnected); if (Achievements::HardcoreModeActive()) { @@ -2756,6 +2739,7 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUCommon *gpuDebug, Draw: const float em = ImGui::GetFontSize(); const bool compactMenu = cfg_.compactMenu || ImGui::GetIO().DisplaySize.x < em * 46.0f; const float rowExtra = compactMenu ? em * 0.2f : 0.0f; // Slightly taller rows for fingers. + const bool hasSoftKeyboard = System_GetPropertyBool(SYSPROP_KEYBOARD_IS_SOFT); auto beginMenu = [&](const char *label) -> bool { if (!ImGui::BeginMenu(label)) { @@ -2771,11 +2755,6 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUCommon *gpuDebug, Draw: }; auto closeDebugger = [&]() { g_Config.bShowImDebugger = false; - cfg_.forceKeyboard = false; // So the keyboard doesn't pop back up when the debugger is reopened. - if (keyboardShown_) { - keyboardShown_ = false; - ApplySoftKeyboard(false); - } }; auto menuFile = [&]() { @@ -3048,8 +3027,10 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUCommon *gpuDebug, Draw: ImGui::MenuItem("Dear ImGui Style editor", nullptr, &cfg_.styleEditorOpen); ImGui::Separator(); ImGui::MenuItem("Quick scroll bar", nullptr, &cfg_.quickScrollEnabled); - ImGui::MenuItem("Show keyboard toggle", nullptr, &cfg_.showKeyboardToggle); - ImGui::MenuItem("Keep keyboard after switching apps", nullptr, &cfg_.keyboardPersist); + if (hasSoftKeyboard) { + ImGui::MenuItem("Show keyboard toggle", nullptr, &cfg_.showKeyboardToggle); + ImGui::MenuItem("Keep keyboard after switching apps", nullptr, &cfg_.keyboardPersist); + } ImGui::MenuItem("Compact menu", nullptr, &cfg_.compactMenu); ImGui::SetNextItemWidth(em * 10.0f); ImGui::SliderInt("Bar left margin", &cfg_.barLeftMargin, 0, 600, "%d%%"); // Percent of the text height. @@ -3120,38 +3101,14 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUCommon *gpuDebug, Draw: } // Soft keyboard toggle (like the keyboard button in Winlator). - if (cfg_.showKeyboardToggle) { + if (hasSoftKeyboard && cfg_.showKeyboardToggle) { ImGui::MenuItem(cfg_.forceKeyboard ? "Hide Keyboard" : "Show Keyboard", nullptr, &cfg_.forceKeyboard); } ImGui::EndMainMenuBar(); } - // Force the soft keyboard to show while the debugger is open (the platform side is a hook, see ImDebugger.h). - { -#if PPSSPP_PLATFORM(ANDROID) - // If the keyboard shouldn't survive an app switch, turn it off once we've been away. - if (Android_ConsumeAppPaused() && !cfg_.keyboardPersist) { - cfg_.forceKeyboard = false; - } - // The keyboard was closed from the system (Back button): keep the toggle in sync. - if (Android_ConsumeKeyboardDismissed()) { - cfg_.forceKeyboard = false; - } - const int persist = cfg_.keyboardPersist ? 1 : 0; - if (persist != keyboardPersistSent_) { - keyboardPersistSent_ = persist; - Android_SetKeyboardPersist(persist != 0); - } -#endif - // Hiding the toggle turns the keyboard off too, otherwise there'd be no way to dismiss it. - const bool wantKeyboard = cfg_.forceKeyboard && cfg_.showKeyboardToggle && g_Config.bShowImDebugger; - if (wantKeyboard != keyboardShown_) { - keyboardShown_ = wantKeyboard; - ApplySoftKeyboard(wantKeyboard); - } - } - + UpdateSoftKeyboard(true); if (cfg_.demoOpen) { ImGui::ShowDemoWindow(&cfg_.demoOpen); @@ -3382,17 +3339,11 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUCommon *gpuDebug, Draw: luaConsole_.Draw(cfg_); } - if (g_Config.bShowImDebugger && cfg_.quickScrollEnabled) { + if (cfg_.quickScrollEnabled) { DrawQuickScroll(); } } -// Platform hook for forcing the soft keyboard. Left empty by default (no-op), so this builds on every platform. -// Android: set it from native code to call InputMethodManager.showSoftInput / hideSoftInputFromWindow via the activity. -void ImDebugger_SetSoftKeyboardHook(std::function hook) { - g_softKeyboardHook = std::move(hook); -} - // ---- Quick scroll strip ---- // The window to scroll, or the nearest parent that actually has something to scroll. diff --git a/UI/ImDebugger/ImDebugger.h b/UI/ImDebugger/ImDebugger.h index 3ae68e1292..bf9a437bd4 100644 --- a/UI/ImDebugger/ImDebugger.h +++ b/UI/ImDebugger/ImDebugger.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "ext/imgui/imgui.h" @@ -135,7 +134,7 @@ struct ImConfig { // Touch / mobile helpers. bool quickScrollEnabled = true; // Show the quick scroll strip on the right edge. bool quickScrollCollapsed = false; // Strip collapsed to a single small button. - bool forceKeyboard = false; // Keyboard currently active (toggled from the bar). Asks the platform to show the soft keyboard (see ImDebugger_SetSoftKeyboardHook). + bool forceKeyboard = false; // Keyboard currently active (toggled from the bar). Asks the platform to keep the soft keyboard up. bool showKeyboardToggle = true; // Show the keyboard button in the bar. Hiding it also turns the keyboard off. bool keyboardPersist = true; // Keep the keyboard on after switching to another app and back. int barLeftMargin = 0; // Empty space left of the first bar button, in percent of the text height (keeps it clear of rounded screen corners). @@ -202,9 +201,17 @@ public: void DeviceLost(); void SaveConfig(); + // Call every frame the debugger isn't shown, instead of Frame(). + void OnHidden(); + // The platform closed the forced soft keyboard (Back, or an app switch without "keep keyboard"). + void OnSoftKeyboardHidden() { + cfg_.forceKeyboard = false; + } + private: Path ConfigPath(); void DrawQuickScroll(); + void UpdateSoftKeyboard(bool debuggerVisible); RequesterToken reqToken_; @@ -236,14 +243,9 @@ private: ImGuiID quickScrollTarget_ = 0; // Whether we last asked the platform to show the soft keyboard. bool keyboardShown_ = false; - int keyboardPersistSent_ = -1; // Last "keep keyboard after app switch" value sent to the platform. + bool keyboardPersistSent_ = false; }; -// Optional override for the "force keyboard" toggle. The callback is called with true to show the soft keyboard and -// false to hide it. Android already has a built-in implementation (Android_SetKeyboardForced in app-android.cpp), -// so this is only needed on other platforms. -void ImDebugger_SetSoftKeyboardHook(std::function hook); - // Simple custom controls and utilities. void ImClickableValue(const char *id, uint32_t value, ImControl &control, ImCmd cmd); void ImClickableValueFloat(const char *id, float value); diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 0bdce0d71f..aa04513b1c 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -440,6 +440,9 @@ void runImDebugger(Draw::DrawContext *draw) { } else { uiContext->SetOverrideScreenFrame(nullptr); SetOverrideScreenFrame(nullptr); + if (imDebugger_) { + imDebugger_->OnHidden(); + } } // Enable text input (needed for KeyInputFlags::CHAR events) only while an imgui text field is active. @@ -1519,6 +1522,11 @@ bool HandleGlobalMessage(UIMessage message, const std::string &value) { } else if (message == UIMessage::SAVE_FRAME_DUMP) { SaveFrameDump(); return true; + } else if (message == UIMessage::SOFT_KEYBOARD_HIDDEN) { + if (imDebugger_) { + imDebugger_->OnSoftKeyboardHidden(); + } + return true; } else { return false; } diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index bda0f747f4..4778870450 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -313,31 +313,6 @@ void System_ShowKeyboard() { PushCommand("showKeyboard", ""); } -// Used by the ImDebugger "Kbd" toggle. Unlike System_ShowKeyboard, this keeps the keyboard up until turned off, -// and lets it type into the game view (see NativeSurfaceView.setKeyboardForced). -void Android_SetKeyboardForced(bool forced) { - PushCommand("forceKeyboard", forced ? "1" : "0"); -} - -// Whether the forced keyboard comes back by itself after switching to another app and returning. -void Android_SetKeyboardPersist(bool persist) { - PushCommand("keyboardPersist", persist ? "1" : "0"); -} - -// Set by NativeApp.pause(), read (and cleared) by the ImDebugger so it can drop the keyboard after an app switch. -static std::atomic g_appWasPaused{false}; - -bool Android_ConsumeAppPaused() { - return g_appWasPaused.exchange(false); -} - -// Set when the user closed the soft keyboard with the Back button, so the debugger can untick its toggle. -static std::atomic g_keyboardDismissed{false}; - -bool Android_ConsumeKeyboardDismissed() { - return g_keyboardDismissed.exchange(false); -} - void System_Vibrate(int length_ms) { char temp[32]; snprintf(temp, sizeof(temp), "%d", length_ms); @@ -509,6 +484,9 @@ bool System_GetPropertyBool(SystemProperty prop) { } case SYSPROP_HAS_KEYBOARD: return deviceType != DEVICE_TYPE_VR; + case SYSPROP_KEYBOARD_IS_SOFT: + // Even with a hardware keyboard connected, the soft one can still be shown. + return true; case SYSPROP_HAS_ACCELEROMETER: // A headset has the sensors, but tilting it isn't tilt control. return g_hasAccelerometer && deviceType != DEVICE_TYPE_VR; @@ -944,7 +922,6 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_resume(JNIEnv *, jclass) { extern "C" void Java_org_ppsspp_ppsspp_NativeApp_pause(JNIEnv *, jclass) { INFO_LOG(Log::System, "NativeApp.pause() - begin"); - g_appWasPaused = true; AndroidAudio_Pause(g_audioState); INFO_LOG(Log::System, "NativeApp.pause() - end"); } @@ -1221,6 +1198,9 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string // The MIME type first, as a path could contain the separator. PushCommand("share_file", param2 + ":@:" + param1); return true; + case SystemRequestType::SET_SOFT_KEYBOARD_FORCED: + PushCommand("forceKeyboard", !param3 ? "off" : (param4 ? "persist" : "on")); + return true; case SystemRequestType::SET_KEEP_SCREEN_BRIGHT: PushCommand("set_keep_screen_bright", param3 ? "on" : "off"); return true; @@ -1469,8 +1449,8 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendMessageFromJava(JNI } else if (msg == "permission_granted") { SetPermissionStatusFromJava(prm, PERMISSION_STATUS_GRANTED); System_PostUIMessage(UIMessage::PERMISSION_GRANTED, prm); - } else if (msg == "keyboardDismissed") { - g_keyboardDismissed = true; + } else if (msg == "softKeyboardHidden") { + System_PostUIMessage(UIMessage::SOFT_KEYBOARD_HIDDEN); } else if (msg == "sustained_perf_supported") { sustainedPerfSupported = true; } else if (msg == "safe_insets") { diff --git a/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java b/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java index 59b21469a2..06d518d5b1 100644 --- a/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java +++ b/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java @@ -22,7 +22,7 @@ public class NativeSurfaceView extends SurfaceView { // (a plain SurfaceView is ignored by showSoftInput), and typed text is passed on to the native side. private boolean keyboardForced = false; // Bring the keyboard back by itself when the app regains focus (after an app switch). - private boolean keyboardPersist = true; + private boolean keyboardPersist = false; public NativeSurfaceView(Context context) { super(context); @@ -38,13 +38,10 @@ public class NativeSurfaceView extends SurfaceView { return true; } - public void setKeyboardPersist(boolean persist) { - keyboardPersist = persist; - } - // Must be called on the UI thread. - public void setKeyboardForced(boolean forced) { + public void setKeyboardForced(boolean forced, boolean persist) { keyboardForced = forced; + keyboardPersist = persist; InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); if (imm == null) { return; @@ -88,11 +85,19 @@ public class NativeSurfaceView extends SurfaceView { @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { if (keyboardForced && keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { - NativeApp.sendMessageFromJava("keyboardDismissed", ""); + NativeApp.sendMessageFromJava("softKeyboardHidden", ""); } return super.onKeyPreIme(keyCode, event); } + // Called from the activity's onStop. Without persistence, switching to another app ends the forced keyboard. + public void onActivityStopped() { + if (keyboardForced && !keyboardPersist) { + setKeyboardForced(false, false); + NativeApp.sendMessageFromJava("softKeyboardHidden", ""); + } + } + @Override public boolean onCheckIsTextEditor() { return keyboardForced; diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index effd12aea6..006866e06c 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -1210,6 +1210,10 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList mSensorManager.unregisterListener(this); loseAudioFocus(this.audioManager, this.audioFocusChangeListener); + if (mSurfaceView != null) { + mSurfaceView.onActivityStopped(); + } + // After a finish() and a quick relaunch, this comes after the new activity's onStart, and the audio and // the location updates are its now. if (checkCurrent("pausing audio and location in onStop")) { @@ -1845,12 +1849,9 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList inputMethodManager.hideSoftInputFromWindow(surfView.getWindowToken(), 0); return true; } else if (command.equals("forceKeyboard") && mSurfaceView != null) { - // "1" = keep the soft keyboard up (and let it type into the game), "0" = hide it again. - mSurfaceView.setKeyboardForced(params.equals("1")); - return true; - } else if (command.equals("keyboardPersist") && mSurfaceView != null) { - // "1" = bring the forced keyboard back after an app switch, "0" = don't. - mSurfaceView.setKeyboardPersist(params.equals("1")); + // "on" = keep the soft keyboard up (and let it type into the game), "persist" = also bring it back after + // an app switch, "off" = hide it again. + mSurfaceView.setKeyboardForced(!params.equals("off"), params.equals("persist")); return true; } else if (command.equals("inputbox")) { String title = "Input";