diff --git a/src/android/app/src/main/java/org/citron/citron_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citron/citron_emu/NativeLibrary.kt index a676682fe1..809292eea8 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/NativeLibrary.kt @@ -342,6 +342,13 @@ object NativeLibrary { */ external fun submitInlineKeyboardText(text: String?) + /** + * Replaces inline keyboard text with the IME editor contents. + * @param text Current text from the Android IME editor. + * @param cursor_position Cursor position in the editor text. + */ + external fun replaceInlineKeyboardText(text: String?, cursor_position: Int) + /** * Submits inline keyboard input. Used to indicate keys pressed that are not text. * @param key_code Android Key Code associated with the keyboard input. diff --git a/src/android/app/src/main/java/org/citron/citron_emu/applets/keyboard/SoftwareKeyboard.kt b/src/android/app/src/main/java/org/citron/citron_emu/applets/keyboard/SoftwareKeyboard.kt index 2661c8082b..7fd376a4df 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/applets/keyboard/SoftwareKeyboard.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/applets/keyboard/SoftwareKeyboard.kt @@ -25,6 +25,9 @@ object SoftwareKeyboard { lateinit var data: KeyboardData val dataLock = Object() + @Volatile + private var inlineConfig: KeyboardConfig? = null + private fun executeNormalImpl(config: KeyboardConfig) { val activity = NativeLibrary.sEmulationActivity.get() ?: return @@ -40,6 +43,8 @@ object SoftwareKeyboard { val previousVisibility = overlayView.visibility val previousAlpha = overlayView.alpha + inlineConfig = config + // Make sure the overlay can receive input overlayView.visibility = View.VISIBLE if (previousVisibility != View.VISIBLE) { @@ -84,10 +89,19 @@ object SoftwareKeyboard { overlayView.visibility = previousVisibility overlayView.alpha = previousAlpha NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_BACK) + clearInlineConfig() } }, delayMs) } + fun getInlineInitialText(): String = inlineConfig?.initial_text.orEmpty() + + fun getInlineInitialCursorPosition(): Int = inlineConfig?.initial_cursor_position ?: 0 + + fun clearInlineConfig() { + inlineConfig = null + } + @JvmStatic fun executeNormal(config: KeyboardConfig): KeyboardData { NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { diff --git a/src/android/app/src/main/java/org/citron/citron_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/citron/citron_emu/overlay/InputOverlay.kt index 8bbd6169fa..c6b8045352 100644 --- a/src/android/app/src/main/java/org/citron/citron_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/citron/citron_emu/overlay/InputOverlay.kt @@ -13,19 +13,28 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.VectorDrawable import android.os.Build +import android.text.Editable +import android.text.InputType +import android.text.Selection import android.util.AttributeSet import android.view.HapticFeedbackConstants +import android.view.KeyEvent import android.view.MotionEvent import android.view.SurfaceView import android.view.View import android.view.View.OnTouchListener import android.view.WindowInsets +import android.view.inputmethod.BaseInputConnection +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection import androidx.core.content.ContextCompat import androidx.window.layout.WindowMetricsCalculator import kotlin.math.max import kotlin.math.min -import org.citron.citron_emu.features.input.NativeInput +import org.citron.citron_emu.NativeLibrary import org.citron.citron_emu.R +import org.citron.citron_emu.applets.keyboard.SoftwareKeyboard +import org.citron.citron_emu.features.input.NativeInput import org.citron.citron_emu.features.input.model.NativeAnalog import org.citron.citron_emu.features.input.model.NativeButton import org.citron.citron_emu.features.input.model.NpadStyleIndex @@ -36,13 +45,6 @@ import org.citron.citron_emu.overlay.model.OverlayControlData import org.citron.citron_emu.overlay.model.OverlayLayout import org.citron.citron_emu.utils.NativeConfig -import android.view.inputmethod.BaseInputConnection -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputConnection -import org.citron.citron_emu.NativeLibrary -import android.view.KeyEvent -import android.text.Editable -import android.text.InputType /** * Draws the interactive input overlay on top of the * [SurfaceView] that is rendering emulation. @@ -54,6 +56,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : private val overlayDpads: MutableSet = HashSet() private val overlayJoysticks: MutableSet = HashSet() private val keyboardEditable = Editable.Factory.getInstance().newEditable("") + private var keyboardLastSubmittedText = "" private var inEditMode = false private var buttonBeingConfigured: InputOverlayDrawableButton? = null @@ -89,35 +92,96 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : requestFocus() } - // Support for Android virtual keyboard (Software Keyboard) + + // Support for Android virtual keyboard (Software Keyboard) override fun onCheckIsTextEditor(): Boolean = true override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection { + val initialText = SoftwareKeyboard.getInlineInitialText() + val initialCursorPosition = + SoftwareKeyboard.getInlineInitialCursorPosition().coerceIn(0, initialText.length) + keyboardEditable.clear() + keyboardEditable.append(initialText) + Selection.setSelection(keyboardEditable, initialCursorPosition) + keyboardLastSubmittedText = initialText 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 = initialCursorPosition + outAttrs.initialSelEnd = initialCursorPosition return object : BaseInputConnection(this, true) { override fun getEditable(): Editable = keyboardEditable override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean { - if (!text.isNullOrEmpty()) { - forwardTextToNative(text) + if (text == null) { + return true } - return super.commitText(text, newCursorPosition) + + val submittedText = text.toString() + val newlineIndex = submittedText.indexOf('\n') + if (newlineIndex >= 0) { + val prefix = submittedText.substring(0, newlineIndex) + if (prefix.isNotEmpty()) { + super.commitText(prefix, newCursorPosition) + submitEditableTextToNative() + } + NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER) + SoftwareKeyboard.clearInlineConfig() + return true + } + + val result = super.commitText(text, newCursorPosition) + submitEditableTextToNative() + return result + } + + override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean { + if (text == null) { + return true + } + + val submittedText = text.toString() + val newlineIndex = submittedText.indexOf('\n') + if (newlineIndex >= 0) { + val prefix = submittedText.substring(0, newlineIndex) + if (prefix.isNotEmpty()) { + super.setComposingText(prefix, newCursorPosition) + submitEditableTextToNative() + } + NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER) + SoftwareKeyboard.clearInlineConfig() + return true + } + + val result = super.setComposingText(text, newCursorPosition) + submitEditableTextToNative() + return result + } + + override fun finishComposingText(): Boolean { + val result = super.finishComposingText() + submitEditableTextToNative() + return result } override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean { - repeat(beforeLength.coerceAtLeast(0)) { - NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_DEL) - } - return super.deleteSurroundingText(beforeLength, afterLength) + val result = super.deleteSurroundingText(beforeLength, afterLength) + submitEditableTextToNative() + return result + } + + override fun deleteSurroundingTextInCodePoints( + beforeLength: Int, + afterLength: Int + ): Boolean { + val result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength) + submitEditableTextToNative() + return result } override fun sendKeyEvent(event: KeyEvent): Boolean { @@ -125,13 +189,19 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : when (event.keyCode) { KeyEvent.KEYCODE_BACK, - KeyEvent.KEYCODE_DEL, - KeyEvent.KEYCODE_ENTER, - KeyEvent.KEYCODE_ESCAPE -> NativeLibrary.submitInlineKeyboardInput(event.keyCode) + KeyEvent.KEYCODE_ESCAPE -> { + NativeLibrary.submitInlineKeyboardInput(event.keyCode) + SoftwareKeyboard.clearInlineConfig() + } + KeyEvent.KEYCODE_ENTER -> { + NativeLibrary.submitInlineKeyboardInput(event.keyCode) + SoftwareKeyboard.clearInlineConfig() + } + KeyEvent.KEYCODE_DEL -> deleteSurroundingText(1, 0) else -> { val unicode = event.unicodeChar if (unicode != 0) { - NativeLibrary.submitInlineKeyboardText(unicode.toChar().toString()) + commitText(unicode.toChar().toString(), 1) } } } @@ -140,28 +210,21 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : override fun performEditorAction(actionCode: Int): Boolean { NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER) + SoftwareKeyboard.clearInlineConfig() return true } } } - private fun forwardTextToNative(text: CharSequence) { - val buffer = StringBuilder() - text.forEach { char -> - when (char) { - '\n' -> { - if (buffer.isNotEmpty()) { - NativeLibrary.submitInlineKeyboardText(buffer.toString()) - buffer.clear() - } - NativeLibrary.submitInlineKeyboardInput(KeyEvent.KEYCODE_ENTER) - } - else -> buffer.append(char) - } - } - if (buffer.isNotEmpty()) { - NativeLibrary.submitInlineKeyboardText(buffer.toString()) + private fun submitEditableTextToNative() { + val text = keyboardEditable.toString() + if (text == keyboardLastSubmittedText) { + return } + + keyboardLastSubmittedText = text + val cursorPosition = Selection.getSelectionEnd(keyboardEditable).coerceIn(0, text.length) + NativeLibrary.replaceInlineKeyboardText(text, cursorPosition) } override fun draw(canvas: Canvas) { diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 9bb1c05887..5ca7be7f3b 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -735,6 +735,14 @@ void Java_org_citron_citron_1emu_NativeLibrary_submitInlineKeyboardText(JNIEnv* EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardText(input); } +void Java_org_citron_citron_1emu_NativeLibrary_replaceInlineKeyboardText(JNIEnv* env, jclass clazz, + jstring j_text, + jint j_cursor_position) { + const std::u16string input = Common::UTF8ToUTF16(Common::Android::GetJString(env, j_text)); + EmulationSession::GetInstance().SoftwareKeyboard()->ReplaceInlineKeyboardText( + input, static_cast(j_cursor_position)); +} + void Java_org_citron_citron_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env, jclass clazz, jint j_key_code) { EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code); diff --git a/src/citron/applets/qt_software_keyboard.cpp b/src/citron/applets/qt_software_keyboard.cpp index 615bbcb9fd..54639d0332 100644 --- a/src/citron/applets/qt_software_keyboard.cpp +++ b/src/citron/applets/qt_software_keyboard.cpp @@ -1622,7 +1622,7 @@ void QtSoftwareKeyboard::ShowTextCheckDialog( } void QtSoftwareKeyboard::ShowInlineKeyboard( - Core::Frontend::InlineAppearParameters appear_parameters) const { + Core::Frontend::InlineAppearParameters appear_parameters) { LOG_INFO(Service_AM, "\nInlineAppearParameters:" "\nmax_text_length={}" @@ -1651,8 +1651,7 @@ void QtSoftwareKeyboard::HideInlineKeyboard() const { emit MainWindowHideInlineKeyboard(); } -void QtSoftwareKeyboard::InlineTextChanged( - Core::Frontend::InlineTextParameters text_parameters) const { +void QtSoftwareKeyboard::InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) { LOG_INFO(Service_AM, "\nInlineTextParameters:" "\ninput_text={}" diff --git a/src/citron/applets/qt_software_keyboard.h b/src/citron/applets/qt_software_keyboard.h index 7e2fdf09ea..366d91af74 100644 --- a/src/citron/applets/qt_software_keyboard.h +++ b/src/citron/applets/qt_software_keyboard.h @@ -247,12 +247,11 @@ public: void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result, std::u16string text_check_message) const override; - void ShowInlineKeyboard( - Core::Frontend::InlineAppearParameters appear_parameters) const override; + void ShowInlineKeyboard(Core::Frontend::InlineAppearParameters appear_parameters) override; void HideInlineKeyboard() const override; - void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override; + void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) override; void ExitKeyboard() const override; diff --git a/src/common/android/applets/software_keyboard.cpp b/src/common/android/applets/software_keyboard.cpp index a2b93a54f4..79b8917590 100644 --- a/src/common/android/applets/software_keyboard.cpp +++ b/src/common/android/applets/software_keyboard.cpp @@ -21,6 +21,19 @@ static jmethodID s_swkbd_execute_inline; namespace Common::Android::SoftwareKeyboard { +static s32 ClampCursorPosition(s32 cursor_position, std::size_t text_size) { + if (cursor_position < 0) { + return 0; + } + + const auto max_position = static_cast(text_size); + if (cursor_position > max_position) { + return max_position; + } + + return cursor_position; +} + static jobject ToJKeyboardParams(const Core::Frontend::KeyboardInitializeParameters& config) { JNIEnv* env = GetEnvForThread(); jobject object = env->AllocObject(s_keyboard_config_class); @@ -155,7 +168,7 @@ void AndroidKeyboard::ShowTextCheckDialog( } void AndroidKeyboard::ShowInlineKeyboard( - Core::Frontend::InlineAppearParameters appear_parameters) const { + Core::Frontend::InlineAppearParameters appear_parameters) { LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the inline software keyboard."); @@ -182,6 +195,8 @@ void AndroidKeyboard::ShowInlineKeyboard( // Pivot to a new thread, as we cannot call GetEnvForThread() from a Fiber. m_current_text = parameters.initial_text; + m_current_cursor_position = + ClampCursorPosition(parameters.initial_cursor_position, m_current_text.size()); m_is_inline_active = true; std::thread([&] { GetEnvForThread()->CallStaticVoidMethod(s_software_keyboard_class, s_swkbd_execute_inline, @@ -194,8 +209,7 @@ void AndroidKeyboard::HideInlineKeyboard() const { "(STUBBED) called, backend requested to hide the inline software keyboard."); } -void AndroidKeyboard::InlineTextChanged( - Core::Frontend::InlineTextParameters text_parameters) const { +void AndroidKeyboard::InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) { LOG_WARNING(Frontend, "(STUBBED) called, backend requested to change the inline keyboard text."); @@ -205,8 +219,14 @@ void AndroidKeyboard::InlineTextChanged( "\ncursor_position={}", Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position); + m_current_text = text_parameters.input_text; + m_current_cursor_position = + ClampCursorPosition(text_parameters.cursor_position, m_current_text.size()); + parameters.initial_text = m_current_text; + parameters.initial_cursor_position = m_current_cursor_position; + submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, - text_parameters.input_text, text_parameters.cursor_position); + m_current_text, m_current_cursor_position); } void AndroidKeyboard::ExitKeyboard() const { @@ -214,14 +234,33 @@ void AndroidKeyboard::ExitKeyboard() const { } void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) { + if (!m_is_inline_active || submitted_text.empty()) { + return; + } + + m_current_cursor_position = + ClampCursorPosition(m_current_cursor_position, m_current_text.size()); + m_current_text.insert(static_cast(m_current_cursor_position), submitted_text); + m_current_cursor_position += static_cast(submitted_text.size()); + parameters.initial_text = m_current_text; + parameters.initial_cursor_position = m_current_cursor_position; + + submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text, + m_current_cursor_position); +} + +void AndroidKeyboard::ReplaceInlineKeyboardText(std::u16string submitted_text, s32 cursor_position) { if (!m_is_inline_active) { return; } - m_current_text += submitted_text; + m_current_text = std::move(submitted_text); + m_current_cursor_position = ClampCursorPosition(cursor_position, m_current_text.size()); + parameters.initial_text = m_current_text; + parameters.initial_cursor_position = m_current_cursor_position; submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text, - static_cast(m_current_text.size())); + m_current_cursor_position); } void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) { @@ -238,20 +277,26 @@ void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) { case KEYCODE_ENTER: m_is_inline_active = false; submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::DecidedEnter, m_current_text, - static_cast(m_current_text.size())); + m_current_cursor_position); break; case KEYCODE_BACK: case KEYCODE_ESCAPE: m_is_inline_active = false; submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::DecidedCancel, m_current_text, - static_cast(m_current_text.size())); + m_current_cursor_position); break; case KEYCODE_DEL: - if (m_current_text.empty()) { + m_current_cursor_position = + ClampCursorPosition(m_current_cursor_position, m_current_text.size()); + if (m_current_cursor_position <= 0 || m_current_text.empty()) { return; } + --m_current_cursor_position; + m_current_text.erase(static_cast(m_current_cursor_position), 1); + parameters.initial_text = m_current_text; + parameters.initial_cursor_position = m_current_cursor_position; submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text, - static_cast(m_current_text.size())); + m_current_cursor_position); break; } } diff --git a/src/common/android/applets/software_keyboard.h b/src/common/android/applets/software_keyboard.h index 9fd09d27c5..9ae5469867 100644 --- a/src/common/android/applets/software_keyboard.h +++ b/src/common/android/applets/software_keyboard.h @@ -27,17 +27,18 @@ public: void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result, std::u16string text_check_message) const override; - void ShowInlineKeyboard( - Core::Frontend::InlineAppearParameters appear_parameters) const override; + void ShowInlineKeyboard(Core::Frontend::InlineAppearParameters appear_parameters) override; void HideInlineKeyboard() const override; - void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) const override; + void InlineTextChanged(Core::Frontend::InlineTextParameters text_parameters) override; void ExitKeyboard() const override; void SubmitInlineKeyboardText(std::u16string submitted_text); + void ReplaceInlineKeyboardText(std::u16string submitted_text, s32 cursor_position); + void SubmitInlineKeyboardInput(int key_code); private: @@ -56,8 +57,9 @@ private: mutable SubmitInlineCallback submit_inline_callback; private: - mutable bool m_is_inline_active{}; + bool m_is_inline_active{}; std::u16string m_current_text; + s32 m_current_cursor_position{}; }; // Should be called in JNI_Load diff --git a/src/core/frontend/applets/software_keyboard.cpp b/src/core/frontend/applets/software_keyboard.cpp index 3ae34cc88f..42cb54f465 100644 --- a/src/core/frontend/applets/software_keyboard.cpp +++ b/src/core/frontend/applets/software_keyboard.cpp @@ -75,8 +75,7 @@ void DefaultSoftwareKeyboardApplet::ShowTextCheckDialog( LOG_WARNING(Service_AM, "(STUBBED) called, backend requested to show the text check dialog."); } -void DefaultSoftwareKeyboardApplet::ShowInlineKeyboard( - InlineAppearParameters appear_parameters) const { +void DefaultSoftwareKeyboardApplet::ShowInlineKeyboard(InlineAppearParameters appear_parameters) { LOG_WARNING(Service_AM, "(STUBBED) called, backend requested to show the inline software keyboard."); @@ -109,7 +108,7 @@ void DefaultSoftwareKeyboardApplet::HideInlineKeyboard() const { "(STUBBED) called, backend requested to hide the inline software keyboard."); } -void DefaultSoftwareKeyboardApplet::InlineTextChanged(InlineTextParameters text_parameters) const { +void DefaultSoftwareKeyboardApplet::InlineTextChanged(InlineTextParameters text_parameters) { LOG_WARNING(Service_AM, "(STUBBED) called, backend requested to change the inline keyboard text."); diff --git a/src/core/frontend/applets/software_keyboard.h b/src/core/frontend/applets/software_keyboard.h index a32a98e4cf..27c96fedbe 100644 --- a/src/core/frontend/applets/software_keyboard.h +++ b/src/core/frontend/applets/software_keyboard.h @@ -72,11 +72,11 @@ public: virtual void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result, std::u16string text_check_message) const = 0; - virtual void ShowInlineKeyboard(InlineAppearParameters appear_parameters) const = 0; + virtual void ShowInlineKeyboard(InlineAppearParameters appear_parameters) = 0; virtual void HideInlineKeyboard() const = 0; - virtual void InlineTextChanged(InlineTextParameters text_parameters) const = 0; + virtual void InlineTextChanged(InlineTextParameters text_parameters) = 0; virtual void ExitKeyboard() const = 0; }; @@ -96,11 +96,11 @@ public: void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result, std::u16string text_check_message) const override; - void ShowInlineKeyboard(InlineAppearParameters appear_parameters) const override; + void ShowInlineKeyboard(InlineAppearParameters appear_parameters) override; void HideInlineKeyboard() const override; - void InlineTextChanged(InlineTextParameters text_parameters) const override; + void InlineTextChanged(InlineTextParameters text_parameters) override; void ExitKeyboard() const override;