[Android] Fix swkbd inline keyboard initial text sync and cursor position

When the game opens an inline keyboard with pre-filled text (e.g. a
previously saved Mii name), the text was stored in m_current_text on
the C++ side but never communicated to the Android IME. As a result,
the Android keyboard field appeared empty, backspace did nothing, and
typing replaced the existing text instead of appending to it.

The root cause: the game sends InlineTextChanged() with the pre-filled
text in the same Calc request as appear(), before ShowInlineKeyboard()
is called. m_current_text was not updated by InlineTextChanged, and
ShowInlineKeyboard was using the stale parameters.initial_text (set
during initialization before the text was known).

Fix on the C++ side:
- Initialize m_current_text from parameters.initial_text in
  InitializeKeyboard so each session starts clean.
- Update m_current_text in InlineTextChanged so the latest game text
  is always reflected.
- Pass m_current_text (not parameters.initial_text) as initial_text
  to Kotlin when calling executeInline.

Fix on the Kotlin side:
- Pre-populate imeEditable with the received initial text.
- Call Selection.setSelection to place the cursor at the end so that
  backspace and new input work correctly from the start.
This commit is contained in:
CookieTraces 2026-04-29 10:11:17 +02:00
parent 0cc0db0a65
commit c0804a4e86
4 changed files with 25 additions and 5 deletions

View file

@ -20,6 +20,7 @@ import java.io.Serializable
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.applets.keyboard.ui.KeyboardDialogFragment
import org.yuzu.yuzu_emu.overlay.InputOverlay
@Keep
object SoftwareKeyboard {
@ -37,6 +38,7 @@ object SoftwareKeyboard {
val emulationActivity = NativeLibrary.sEmulationActivity.get()
val overlayView = emulationActivity!!.findViewById<View>(R.id.surface_input_overlay)
(overlayView as? InputOverlay)?.resetImeBuffer(config.initial_text ?: "")
overlayView.requestFocus()
val im =
overlayView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager

View file

@ -19,6 +19,7 @@ import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.InputType
import android.text.Selection
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
@ -57,6 +58,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
private val imeEditable = Editable.Factory.getInstance().newEditable("")
private var pendingInitialText: String = ""
private var inEditMode = false
private var gamelessMode = false
@ -85,15 +87,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
override fun onCheckIsTextEditor(): Boolean = true
fun resetImeBuffer(initialText: String = "") {
pendingInitialText = initialText
}
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
imeEditable.clear()
if (pendingInitialText.isNotEmpty()) {
imeEditable.append(pendingInitialText)
}
pendingInitialText = ""
Selection.setSelection(imeEditable, imeEditable.length)
outAttrs.inputType =
InputType.TYPE_CLASS_TEXT or
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or EditorInfo.IME_ACTION_DONE
outAttrs.initialSelStart = 0
outAttrs.initialSelEnd = 0
outAttrs.initialSelStart = imeEditable.length
outAttrs.initialSelEnd = imeEditable.length
return object : BaseInputConnection(this, true) {
override fun getEditable(): Editable = imeEditable

View file

@ -109,6 +109,7 @@ void AndroidKeyboard::InitializeKeyboard(
}
parameters = std::move(initialize_parameters);
m_current_text = parameters.initial_text;
LOG_INFO(Frontend,
"\nKeyboardInitializeParameters:"
@ -185,9 +186,13 @@ void AndroidKeyboard::ShowInlineKeyboard(
// Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber.
m_is_inline_active = true;
std::thread([&] {
// Pass m_current_text as initial_text so Kotlin receives any text set via InlineTextChanged
// before this call (e.g. game pre-fills the field in the same Calc request as appear).
std::thread([&, current_text = m_current_text] {
Core::Frontend::KeyboardInitializeParameters p = parameters;
p.initial_text = current_text;
GetEnvForThread()->CallStaticVoidMethod(s_software_keyboard_class, s_swkbd_execute_inline,
ToJKeyboardParams(parameters));
ToJKeyboardParams(p));
}).join();
}
@ -207,6 +212,8 @@ void AndroidKeyboard::InlineTextChanged(
"\ncursor_position={}",
Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position);
m_current_text = text_parameters.input_text;
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString,
text_parameters.input_text, text_parameters.cursor_position);
}

View file

@ -57,7 +57,7 @@ private:
private:
mutable bool m_is_inline_active{};
std::u16string m_current_text;
mutable std::u16string m_current_text;
};
// Should be called in JNI_Load