diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index fd254fdf5a..019f85995d 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "ppsspp_config.h" @@ -86,6 +87,28 @@ 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++) { @@ -2665,6 +2688,9 @@ ImDebugger::ImDebugger() { } ImDebugger::~ImDebugger() { + if (keyboardShown_) { + ApplySoftKeyboard(false); + } SaveConfig(); } @@ -2677,6 +2703,15 @@ 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()) { @@ -2736,6 +2771,11 @@ 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 = [&]() { @@ -3008,6 +3048,8 @@ 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); 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. @@ -3077,9 +3119,40 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUCommon *gpuDebug, Draw: break; } + // Soft keyboard toggle (like the keyboard button in Winlator). + if (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); + } + } + + if (cfg_.demoOpen) { ImGui::ShowDemoWindow(&cfg_.demoOpen); } @@ -3314,6 +3387,12 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUCommon *gpuDebug, Draw: } } +// 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. @@ -3599,6 +3678,9 @@ void ImConfig::SyncConfig(IniFile *ini, bool save) { sync.Sync("realtimePixelPreview", &realtimePixelPreview, false); sync.Sync("quickScrollEnabled", &quickScrollEnabled, true); sync.Sync("quickScrollCollapsed", &quickScrollCollapsed, false); + sync.Sync("forceKeyboard", &forceKeyboard, false); + sync.Sync("showKeyboardToggle", &showKeyboardToggle, true); + sync.Sync("keyboardPersist", &keyboardPersist, true); sync.Sync("compactMenu", &compactMenu, kIsMobilePlatform); sync.Sync("barLeftMargin", &barLeftMargin, kIsMobilePlatform ? 200 : 0); } diff --git a/UI/ImDebugger/ImDebugger.h b/UI/ImDebugger/ImDebugger.h index d71b1fb7b6..3ae68e1292 100644 --- a/UI/ImDebugger/ImDebugger.h +++ b/UI/ImDebugger/ImDebugger.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "ext/imgui/imgui.h" @@ -134,6 +135,9 @@ 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 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). bool compactMenu = false; // Always use the single "Menu" dropdown (it's also chosen automatically on narrow screens). @@ -230,8 +234,16 @@ private: // Quick scroll strip: the last debugger window the user touched (ImGui window ID). 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. }; +// 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/android/jni/app-android.cpp b/android/jni/app-android.cpp index 6b7e1538c8..bda0f747f4 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -313,6 +313,31 @@ 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); @@ -919,6 +944,7 @@ 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"); } @@ -1443,6 +1469,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 == "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 5e8f314a59..59b21469a2 100644 --- a/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java +++ b/android/src/org/ppsspp/ppsspp/NativeSurfaceView.java @@ -5,11 +5,25 @@ package org.ppsspp.ppsspp; // Used by the Vulkan backend. import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; +import android.os.SystemClock; +import android.text.InputType; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceView; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; public class NativeSurfaceView extends SurfaceView { + // When set, the view pretends to be a text editor so Android will really show the soft keyboard for it + // (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; + public NativeSurfaceView(Context context) { super(context); } @@ -23,4 +37,112 @@ public class NativeSurfaceView extends SurfaceView { NativeApp.processTouchEvent(ev); return true; } + + public void setKeyboardPersist(boolean persist) { + keyboardPersist = persist; + } + + // Must be called on the UI thread. + public void setKeyboardForced(boolean forced) { + keyboardForced = forced; + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) { + return; + } + if (forced) { + setFocusable(true); + setFocusableInTouchMode(true); + requestFocus(); + imm.restartInput(this); + showKeyboardSoon(); + } else { + imm.hideSoftInputFromWindow(getWindowToken(), 0); + imm.restartInput(this); + clearFocus(); + setFocusableInTouchMode(false); + setFocusable(false); + } + } + + private void showKeyboardSoon() { + post(() -> { + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null && keyboardForced) { + imm.showSoftInput(this, 0); + } + }); + } + + // Android hides the keyboard when the window loses focus (app switch, dialogs). Bring it back. + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (hasWindowFocus && keyboardForced && keyboardPersist) { + requestFocus(); + showKeyboardSoon(); + } + } + + // Back is offered to the view before the keyboard sees it, but only while the keyboard is up. Tell the native side + // so its keyboard toggle follows (the keyboard itself is hidden by the system as usual). + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyboardForced && keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { + NativeApp.sendMessageFromJava("keyboardDismissed", ""); + } + return super.onKeyPreIme(keyCode, event); + } + + @Override + public boolean onCheckIsTextEditor() { + return keyboardForced; + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + if (!keyboardForced) { + return null; + } + // TYPE_NULL asks the keyboard for plain key events instead of treating this as a text field. + outAttrs.inputType = InputType.TYPE_NULL; + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_NONE; + return new BaseInputConnection(this, false) { + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + for (int i = 0; i < text.length(); ) { + int cp = Character.codePointAt(text, i); + i += Character.charCount(cp); + if (cp == '\n') { + sendKeyPress(KeyEvent.KEYCODE_ENTER); + } else if (!Character.isISOControl(cp)) { + NativeApp.keyChar(NativeApp.DEVICE_ID_KEYBOARD, cp); + } + } + return true; + } + + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + for (int i = 0; i < beforeLength; i++) { + sendKeyPress(KeyEvent.KEYCODE_DEL); + } + for (int i = 0; i < afterLength; i++) { + sendKeyPress(KeyEvent.KEYCODE_FORWARD_DEL); + } + return true; + } + }; + } + + // Goes through the activity's normal key handling, same as a hardware keyboard would. + private void sendKeyPress(int keyCode) { + Context context = getContext(); + if (!(context instanceof Activity)) { + return; + } + Activity activity = (Activity) context; + long now = SystemClock.uptimeMillis(); + activity.dispatchKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0)); + activity.dispatchKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0)); + } } diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index fd2038e780..effd12aea6 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -1844,6 +1844,14 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); 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")); + return true; } else if (command.equals("inputbox")) { String title = "Input"; String defString = "";