mirror of
https://github.com/citron-neo/emulator.git
synced 2026-07-05 15:21:57 -07:00
feat: add inline keyboard text replacement functionality and improve cursor handling
This commit is contained in:
@@ -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.
|
||||
|
||||
+14
@@ -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 {
|
||||
|
||||
@@ -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<InputOverlayDrawableDpad> = HashSet()
|
||||
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = 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) {
|
||||
|
||||
@@ -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<s32>(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);
|
||||
|
||||
@@ -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<s32>(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);
|
||||
@@ -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,
|
||||
@@ -205,8 +220,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 +235,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<std::size_t>(m_current_cursor_position), submitted_text);
|
||||
m_current_cursor_position += static_cast<s32>(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<int>(m_current_text.size()));
|
||||
m_current_cursor_position);
|
||||
}
|
||||
|
||||
void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
|
||||
@@ -238,20 +278,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<s32>(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<s32>(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<std::size_t>(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<int>(m_current_text.size()));
|
||||
m_current_cursor_position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ public:
|
||||
|
||||
void SubmitInlineKeyboardText(std::u16string submitted_text);
|
||||
|
||||
void ReplaceInlineKeyboardText(std::u16string submitted_text, s32 cursor_position);
|
||||
|
||||
void SubmitInlineKeyboardInput(int key_code);
|
||||
|
||||
private:
|
||||
@@ -50,14 +52,15 @@ private:
|
||||
|
||||
void SubmitNormalText(const ResultData& result) const;
|
||||
|
||||
Core::Frontend::KeyboardInitializeParameters parameters{};
|
||||
mutable Core::Frontend::KeyboardInitializeParameters parameters{};
|
||||
|
||||
mutable SubmitNormalCallback submit_normal_callback;
|
||||
mutable SubmitInlineCallback submit_inline_callback;
|
||||
|
||||
private:
|
||||
mutable bool m_is_inline_active{};
|
||||
std::u16string m_current_text;
|
||||
mutable std::u16string m_current_text;
|
||||
mutable s32 m_current_cursor_position{};
|
||||
};
|
||||
|
||||
// Should be called in JNI_Load
|
||||
|
||||
Reference in New Issue
Block a user